On this page10 sections
ConceptsIntermediate8-10 min reference

CI/CD for Testers

The CI/CD vocabulary, GitHub Actions patterns, and parallelisation tricks you'll use when wiring tests into a real pipeline.

CI/CD Concepts

TermMeaning
CI (Continuous Integration)Build and test automatically on every push / PR.
Continuous DeliveryEvery passing build is deployable to production. Deploy is one click away.
Continuous DeploymentEvery passing build goes to production automatically. No human in the loop.
PipelineThe end-to-end sequence: lint → build → test → deploy.
Stage / JobA logical step in the pipeline. Jobs can run in parallel; stages run sequentially.
TriggerWhat starts the pipeline — push, PR, schedule (cron), manual dispatch.
ArtifactA file produced by a job — test reports, screenshots, build outputs.
EnvironmentA target deployment — dev / staging / production. May have its own secrets and approval gates.

GitHub Actions Basics

A workflow lives at .github/workflows/<name>.yml and runs as one or more jobs, each made of steps.

Minimal example

name: CI
on: [push, pull_request]
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm test

Triggers

on:
  push:
    branches: [main]
    paths-ignore: ["docs/**", "*.md"]
 
  pull_request:
    branches: [main]
 
  schedule:
    - cron: "0 6 * * *"          # daily at 06:00 UTC
 
  workflow_dispatch:              # manual button in the Actions tab
    inputs:
      environment:
        type: choice
        options: [staging, production]
        default: staging

Runners

runs-on: ubuntu-latest         # fastest, cheapest
runs-on: macos-latest          # for iOS/Safari/Xcode
runs-on: windows-latest        # for Edge/Windows-only tooling
runs-on: self-hosted           # your own machine — for hardware needs

Setting up languages

# Node
- uses: actions/setup-node@v4
  with: { node-version: 20, cache: npm }
 
# Java
- uses: actions/setup-java@v4
  with: { java-version: 21, distribution: temurin, cache: maven }
 
# Python
- uses: actions/setup-python@v5
  with: { python-version: "3.12", cache: pip }

Caching

actions/setup-node (and similar) handles dependency caching automatically when you pass cache: npm. For arbitrary caches, use actions/cache:

- uses: actions/cache@v4
  with:
    path: |
      ~/.cache/Cypress
      node_modules
    key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}
    restore-keys: ${{ runner.os }}-deps-

Environment variables and secrets

jobs:
  e2e:
    env:
      NODE_ENV: test                          # job-level
    steps:
      - run: npm test
        env:
          API_TOKEN: ${{ secrets.API_TOKEN }} # step-level — secrets only injected here
          BASE_URL: https://staging.example.com

Define secrets at Settings → Secrets and variables → Actions in the repo. Treat them as write-only — they don't appear in logs.

Running Tests in CI

# Unit tests (Jest / Vitest)
- run: npm test
- run: npx vitest run --coverage
 
# Cypress headless
- run: npx cypress run
 
# Playwright
- run: npx playwright install --with-deps    # browsers + system deps
- run: npx playwright test
 
# pytest
- run: python -m pytest tests/ --junitxml=results.xml -v
 
# JVM (Maven / Gradle)
- run: mvn test
- run: ./gradlew test

Always prefer npm ci over npm install in CI — it's faster, deterministic, and respects the lockfile.

Cypress in GitHub Actions

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - uses: cypress-io/github-action@v6
        with:
          browser: chrome
          start: npm start
          wait-on: "http://localhost:3000"
          wait-on-timeout: 120
          spec: cypress/e2e/smoke/**/*.cy.ts
        env:
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

The cypress-io/github-action handles install, cache, browser binary, and the run itself. start: boots the dev server, wait-on: blocks until the URL responds.

Cypress Cloud (parallel + dashboard)

strategy:
  matrix:
    containers: [1, 2, 3, 4]
steps:
  - uses: cypress-io/github-action@v6
    with:
      record: true
      parallel: true
      group: "smoke"
      tag: "${{ github.event_name }}"
    env:
      CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

record: true requires a Cypress Cloud project. parallel: true lets the cloud orchestrator distribute specs across the matrix containers.

Playwright in GitHub Actions

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
 
      - run: npm ci
      - run: npx playwright install --with-deps
 
      - run: npx playwright test --reporter=html
 
      - if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report
          retention-days: 7

Sharding for parallel runs

strategy:
  fail-fast: false
  matrix:
    shard: [1/4, 2/4, 3/4, 4/4]
steps:
  - run: npx playwright test --shard=${{ matrix.shard }}
 
  - if: always()
    uses: actions/upload-artifact@v4
    with:
      name: blob-report-${{ strategy.job-index }}
      path: blob-report

Then merge in a separate job:

merge-reports:
  if: always()
  needs: test
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
    - run: npm ci
 
    - uses: actions/download-artifact@v4
      with:
        path: all-blob-reports
        pattern: blob-report-*
        merge-multiple: true
 
    - run: npx playwright merge-reports --reporter html ./all-blob-reports
 
    - uses: actions/upload-artifact@v4
      with:
        name: html-report
        path: playwright-report

Cross-browser matrix

strategy:
  matrix:
    browser: [chromium, firefox, webkit]
steps:
  - run: npx playwright test --project=${{ matrix.browser }}

Test Reports & Artifacts

Uploading

- if: always()                        # run even if previous step failed
  uses: actions/upload-artifact@v4
  with:
    name: test-results
    path: |
      cypress/screenshots
      cypress/videos
      coverage
      playwright-report
    retention-days: 14
    if-no-files-found: ignore

if: always() is critical — by default failed steps cancel the rest of the job.

JUnit XML for the test summary

Most runners can emit JUnit XML; GitHub's UI and many third-party actions parse it.

- run: pytest --junitxml=results.xml
 
- if: always()
  uses: dorny/test-reporter@v1
  with:
    name: pytest results
    path: results.xml
    reporter: java-junit

HTML reports via GitHub Pages

For browseable reports (Allure, Playwright HTML), publish to GitHub Pages on main:

- if: github.ref == 'refs/heads/main'
  uses: peaceiris/actions-gh-pages@v3
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    publish_dir: ./playwright-report
    destination_dir: reports/${{ github.run_id }}

Comment results on the PR

- uses: actions/github-script@v7
  if: github.event_name == 'pull_request'
  with:
    script: |
      const passed = ${{ steps.test.outputs.passed }};
      const failed = ${{ steps.test.outputs.failed }};
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: `✅ ${passed} passed · ❌ ${failed} failed`,
      });

Parallel Execution

Matrix — many configurations of the same job

strategy:
  fail-fast: false                       # don't cancel other matrix entries on first fail
  matrix:
    node-version: [18, 20, 22]
    os: [ubuntu-latest, windows-latest]
    exclude:
      - { node-version: 18, os: windows-latest }   # skip a combo
 
jobs:
  test:
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/setup-node@v4
        with: { node-version: ${{ matrix.node-version }} }
      - run: npm test

Splitting by spec / shard

The two universal options:

  1. By file — divide spec files across N runners. Works with any framework. Requires a deterministic file list.
  2. By shard — Playwright's built-in --shard=i/N and Cypress Cloud's parallelisation handle this for you.

Merging artifacts from many shards

Each shard uploads its own artifact (e.g. blob-report-1, blob-report-2, …). A final job downloads them all (pattern: blob-report-*, merge-multiple: true) and merges into one report.

Pipeline Stages Pattern

A typical fast → slow flow:

lint  →  build  →  unit  →  integration  →  e2e  →  deploy
 ~30s     ~1m     ~30s        ~2m         ~10m+    ~1m
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run lint
 
  build:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - uses: actions/upload-artifact@v4
        with: { name: build, path: dist }
 
  unit:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test
 
  e2e:
    needs: [build, unit]
    runs-on: ubuntu-latest
    strategy:
      matrix: { shard: [1/4, 2/4, 3/4, 4/4] }
    steps:
      - uses: actions/download-artifact@v4
        with: { name: build, path: dist }
      - run: npx playwright test --shard=${{ matrix.shard }}
 
  deploy:
    needs: e2e
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - run: ./scripts/deploy.sh

needs: makes a job wait for the previous one. Independent fast jobs (lint, unit) run in parallel; later jobs gate on the ones that actually depend on them.

Environment Management

Per-environment secrets and protection rules

deploy-staging:
  environment: staging
  runs-on: ubuntu-latest
  steps:
    - run: ./deploy.sh
      env:
        DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}
 
deploy-prod:
  needs: deploy-staging
  environment: production           # GitHub will require manual approval
  runs-on: ubuntu-latest
  steps:
    - run: ./deploy.sh
      env:
        DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}

Configure environments at Settings → Environments: required reviewers, wait timers, deployment branches, and per-environment secrets.

Preview deployments

Vercel, Netlify, Cloudflare Pages, and Render all create a preview URL per PR automatically. Make your tests target it:

- name: Wait for Vercel preview
  uses: patrickedqvist/wait-for-vercel-preview@v1.3.1
  id: wait
  with:
    token: ${{ secrets.GITHUB_TOKEN }}
    max_timeout: 600
 
- run: npx playwright test
  env:
    BASE_URL: ${{ steps.wait.outputs.url }}

Test data / DB seeding

Run a migration / seed step before tests, parameterised per environment:

- run: npm run db:migrate
  env: { DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }} }
 
- run: npm run db:seed:test
  env: { DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }} }
 
- run: npm test

Cleanup

- name: Clean up test data
  if: always()
  run: npm run db:reset:test
  env: { DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }} }

Notifications & Monitoring

Slack on failure

- name: Notify Slack on failure
  if: failure()
  uses: 8398a7/action-slack@v3
  with:
    status: failure
    fields: repo,message,commit,author,ref,workflow,job
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Status badges

![Tests](https://github.com/me/repo/actions/workflows/test.yml/badge.svg)
![Coverage](https://codecov.io/gh/me/repo/branch/main/graph/badge.svg)

Branch protection — require tests before merge

Settings → Branches → Branch protection rules → main:

  • Require status checks to pass — pick the workflow names that gate merges.
  • Require branches to be up to date — re-run CI on the latest base.
  • Require linear history (optional).
  • Restrict who can push.

Re-run failed jobs only

In the Actions UI for any failed run: Re-run failed jobs — runs only the failed ones, reuses successful artifacts. Saves time on flaky integration tests.

Manual dispatch with inputs

on:
  workflow_dispatch:
    inputs:
      filter:
        description: "Test name pattern"
        type: string
        default: ""
      env:
        type: choice
        options: [staging, production]
        default: staging
 
jobs:
  smoke:
    runs-on: ubuntu-latest
    steps:
      - run: |
          npx playwright test \
            --grep="${{ inputs.filter }}" \
            --project=${{ inputs.env }}

Lets you re-run the suite against a specific environment with a click — no code change needed.