A Guide to CI/CD Workflows with GitHub Actions
Júlia Tomé de Sousa
QA Automation Engineer | Cypress | Mobile testing with Maestro | API testing
GitHub Actions is a critical tool for automating workflows such as code deployment and repository management. In my article, I will dive deep into the importance of GitHub Actions, based on insights from the Udemy Course This knowledge is especially crucial for Quality Assurance (QA) professionals, as it enables them to ensure seamless integration, testing, and deployment processes, all of which are vital for maintaining high software quality.
Understanding CI/CD pipelines through GitHub Actions allows QA teams to automate testing, build more reliable workflows, and reduce manual intervention. These skills help QA professionals improve code quality, catch bugs earlier, and ultimately deliver better software faster.
Creating Your First GitHub Actions Workflow
To start automating tasks with GitHub Actions, you’ll create a YAML file in your repository that defines your workflow. The file should follow YAML syntax, where indentation is crucial for the correct interpretation of jobs, steps, and events.
Example: First Workflow
name: First Workflow
# When should this workflow run?
on:
workflow_dispatch: # Allows manual triggering of the workflow
jobs:
firstjob:
runs-on: ubuntu-latest
steps:
- name: Print greeting
run: echo "Hello World!"
Workflow Components:
Each component in GitHub Actions is designed to offer flexibility and power, enabling developers to automate their software development processes efficiently.
1. Event Triggers
In GitHub Actions, the on keyword defines when a workflow should be triggered. There are a variety of supported events, both repository-related and general:
on:
push:
branches:
- main
- 'dev-*'
paths-ignore:
- '.github/workflows/*'
on:
pull_request:
types: [opened, edited, closed]
Other Events:
These events allow more flexibility in workflow execution, outside of regular repository updates.
on:
workflow_dispatch:
inputs:
browser:
description: 'Browser to run tests on'
required: true
default: 'chrome'
type: choice
options:
- 'chrome'
- 'firefox'
on:
schedule:
- cron: '00 08 * * *'
For more events and details, refer to GitHub Events Documentation
2. Jobs
A job defines the tasks that will be performed. Each job can have multiple steps, and jobs can be run in parallel or sequentially. Here's how you can set up jobs:
Example:
jobs:
test:
runs-on: ubuntu-latest
Example of steps:
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
Installing dependencies can take time, especially in large projects. You can cache dependencies to reuse them in future runs, reducing the time spent reinstalling. The actions/cache action stores files (e.g., node_modules), so if your dependencies haven’t changed, they’ll be reused.
Here’s a quick setup:
- name: Cache NPM dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: deps-node-module-${{ hashFiles('**/package-lock.json') }}
- name: Cache Yarn dependencies
uses: actions/cache@v2
with:
path: |
node_modules
.yarn/cache
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
By caching, you skip redundant installations, speeding up workflows, especially when dependencies don’t change frequently. You should repeat this step across all jobs that need the same dependencies.
2. Environment Variables & Secrets:
jobs:
tests:
API_KEY: ${{ secrets.API_KEY }}
In your project code, you can access these variables using the relevant environment variable method. For example, in Node.js, you'd use process.env.
Use .env files for local development, and load them using libraries like dotenv in Node.js projects:
require('dotenv').config()
console.log(process.env.MY_VARIABLE)
Best Practices:
3. Using the needs Keyword for Job Dependencies:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Build the project
run: echo "Building the project"
test:
runs-on: ubuntu-latest
needs: build # Waits for 'build' job to complete
steps:
- name: Run tests
run: echo "Running tests"
deploy:
runs-on: ubuntu-latest
needs: [build, test] # Waits for both 'build' and 'test' jobs to complete
steps:
- name: Deploy the project
run: echo "Deploying the project"
4. Jobs and Docker Containers in GitHub Actions
In GitHub Actions, Docker containers allow you to define a fully controlled environment for running your jobs, making them ideal for creating reproducible execution environments. Containers bundle the code with dependencies, ensuring that jobs run consistently across different systems.
Key Concepts:
Using Docker and service containers enhances job isolation, maintains consistent results, and avoids affecting critical systems during tasks like testing.
领英推荐
3. Cancelling and Skipping Workflows
Workflows can be automatically cancelled if jobs fail or they can be manually cancelled via the GitHub UI. You can also use concurrency control to cancel redundant workflows:
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
steps:
- name: Run tests
run: npm run test
continue-on-error: true #Even if the tests fail, the workflow will proceed with the next steps.
4. Artifacts and Job Outputs
Artifacts are files or folders created during the execution of a job that you want to save or share between jobs. Common use cases include logs, test results, or build files (e.g., app binaries, package files). Artifacts help ensure continuity when you're running multiple jobs or workflows.
Job outputs are simple values that can be passed between jobs. This is especially useful when one job produces information or data needed by another job in the same workflow.
Conditionals in GitHub Actions
In GitHub Actions, conditional expressions like failure(), success(), always(), and cancelled() are used to control whether or not certain steps or jobs run based on the outcome of previous steps or jobs. Here's an explanation of each condition and how they are typically used:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Install dependencies
run: npm ci
- name: Run tests
id: run-tests
run: npm run test
- name: Upload test report if tests fail
if: failure() # This condition checks if the previous step failed
uses: actions/upload-artifact@v3
with:
name: test-report
path: test.json
Explanation:
This will ensure that the test report is only uploaded when the tests fail, keeping your workflow efficient.
5. Matrix Strategy in GitHub Actions
The matrix strategy is a powerful feature that allows you to run jobs with multiple configurations in parallel, making it ideal for testing across different environments (e.g., operating systems, and programming language versions). Instead of duplicating jobs for each configuration, the matrix allows you to define multiple variations that run simultaneously.
Key Points:
jobs:
test:
strategy:
fail-fast: false
matrix:
node-version: [12, 14, 16]
os: [ubuntu-latest, windows-latest]
include:
- node-version: 18
os: ubuntu-latest
exclude:
- node-version: 12
os: windows-latest
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
6. Reusable Workflows, Passing Inputs, Secrets, and Outputs
Reusable workflows in GitHub Actions allow you to centralize common logic in one workflow and use it in multiple repositories or workflows. This makes it easier to maintain and update automation processes.
Key Concepts:
Reusable Workflow Example:
name: Reusable Deploy
on:
workflow_call:
inputs:
artifact-name:
description: The name of the deployable artifact files
required: false
default: dist
outputs:
result:
description: The result of the deployment
value: ${{ jobs.deploy.outputs.outcome }}
jobs:
deploy:
runs-on: ubuntu-latest
outputs:
outcome: ${{ steps.set-result.outputs.step-result }}
steps:
- name: Get Code
uses: actions/download-artifact@v3
with:
name: ${{ inputs.artifact-name }}
- name: Output information
run: echo "Deploying & uploading..."
- name: Set result output
id: set-result
run: echo "step-result=success" >> $GITHUB_OUTPUT
Using the Reusable Workflow:
jobs:
deploy:
uses: ./.github/workflows/reusable.yml
with:
artifact-name: dist-files
secrets:
some-secret: ${{ secrets.some-secret }}
print-deploy-result:
needs: deploy
runs-on: ubuntu-latest
steps:
- name: Print deploy output
run: echo "${{ needs.deploy.outputs.result }}"
7. Custom GitHub Actions
Custom Actions allow you to simplify complex workflows by combining multiple steps into a single reusable unit. Instead of writing numerous detailed steps in your workflow file, you can build custom actions tailored to solve specific workflow problems, especially when existing community actions don’t meet your needs.
Key Types of Custom Actions:
1. JavaScript Actions:
- These are actions written in Node.js, allowing you to execute JavaScript code and include any npm package you need.
- They are easy to create and use within your workflows, offering flexibility for many common tasks.
- Example: Build a Node.js script to automate a task like testing or deployment without defining each step in your YAML file.
2. Docker Actions:
- Use Docker to create custom actions with specific configurations and dependencies.
- You write a Dockerfile to define the environment and logic, which allows for complex tasks in any language, with the flexibility of Docker.
- Example: Perform multi-step tasks with specialized tools not natively available in GitHub runners.
3. Composite Actions:
- These actions combine multiple steps (using run and uses) in a single action, making them reusable across multiple workflows.
- You can use existing actions and scripts within composite actions, reducing redundancy in your workflows.
- Example: Combine several commonly used steps, such as checking out the code, setting up dependencies, and running tests, into one reusable action.
Custom actions streamline your workflows by packaging logic and functionality into reusable units, giving you greater control over your CI/CD pipeline.
To wrap up the article, here's a practical example of connecting GitHub Actions with Cypress Cloud for running parallel tests across multiple environments. We will cover two use cases: one where all tests are executed in parallel, and another where you can selectively run tests.
Step 1: Create a Reusable Workflow File
Create a new YAML file in your repository under .github/workflows/reusable.yml. This workflow will handle the common steps like checking out the code and installing dependencies.
name: Reusable Setup Workflow
on:
workflow_call:
jobs:
setup:
runs-on: ubuntu-latest
steps:
# Step 1: Checkout code
- name: Checkout ??
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
# Step 2: Setup Node.js and cache Yarn dependencies
- uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
cache-dependency-path: '**/yarn.lock'
# Step 3: Install Yarn dependencies
- name: Install dependencies ??
run: yarn install --frozen-lockfile
All Tests WorkFlow
In the main workflow, we will reuse the setup workflow and integrate Cypress Cloud to run the tests in parallel on multiple containers.
name: Run All Cypress Tests in Parallel
on:
push:
branches:
- main
workflow_dispatch:
schedule:
- cron: '00 08 * * *'
jobs:
cypress-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
container: [1, 2, 3, 4] # Run across 4 containers
env:
CI_ID: ${{github.repository}}-${{github.run_id}}-${{github.run_attempt}}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_CLOUD_RECORD_KEY }}
steps:
# Reuse the setup workflow to avoid repetition
- name: Setup Workflow
uses: ./.github/workflows/reusable.yml
# Run Cypress tests in parallel on Chrome
- name: Run Cypress on Google Chrome ??
uses: cypress-io/github-action@v6
with:
parallel: true
record: true
browser: chrome
group: "parallel-tests"
ci-build-id: ${{ env.CI_ID }}
config: retries=1
Selective Cypress Test Workflow
This GitHub Actions workflow is designed to run Cypress tests selectively based on user input. The user can manually trigger the workflow and select a specific test file or set of tests to run from a predefined list of Cypress specs.
name: Run Selective Cypress Tests
on:
workflow_dispatch:
inputs:
spec:
description: 'Select which Cypress spec to run'
required: true
default: 'cypress/e2e/**/*.cy.js'
type: choice
options:
- 'cypress/e2e/spec1/**/*.cy.js'
- 'cypress/e2e/spec2.cy.js'
- 'cypress/e2e/**/*.cy.js'
jobs:
selective-tests:
runs-on: ubuntu-latest
env:
CI_ID: ${{github.repository}}-${{github.run_id}}-${{github.run_attempt}}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_CLOUD_RECORD_KEY }}
CYPRESS_SPEC: ${{ github.event.inputs.spec }}
steps:
# Step 1: Reuse the setup workflow
- name: Setup Workflow
uses: ./.github/workflows/reusable.yml
# Step 2: Run specific Cypress tests
- name: Run Selected Cypress Tests ??
uses: cypress-io/github-action@v6
with:
spec: ${{ env.CYPRESS_SPEC }}
browser: chrome
record: true
ci-build-id: ${{ env.CI_ID }}
config: retries=1
Devops Engineer at Tech Mahindra
1 个月Useful content for beginners ??
Analista de teste/QA
1 个月Muito bom
Senior Test Automation Engineer | Quality Assurance | Quality Engineer
1 个月This is essential knowledge for anyone serious about test automation and looking to scale their projects, driving real impact on product development. Congratulations on the great article!