Use whenever the task is to run tests in GitHub Actions: a test workflow on pull requests, matrix builds across OS and runtime versions, splitting a suite across parallel runners, service containers such as Postgres or Redis, uploading JUnit reports, and trigger filters.
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
The canonical home for this skill is testland/github-actions-test-jobs
Test workflows are YAML files in .github/workflows/ that run jobs on
trigger events (gha). This skill sets up the idiomatic patterns -
matrix builds, sharding, service containers, JUnit reporting, trigger
filtering, concurrency, and secrets. Start with the minimal pattern in
Step 1.
.github/workflows/test.yml triggered on pull_request
and push to main; check out the repo and set up the runtime
(Step 1).npm ci then
npm test).fail-fast: false for multi-target signal (Step 2), and shard
large suites across parallel jobs (Step 3).if: always() -
references/services-and-reporting.md.schedule / workflow_dispatch
(Step 7); add a concurrency group so superseded pushes cancel
(Step 8).secrets.* and reference them from step env
(Step 9).# .github/workflows/test.yml
name: test
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with: { node-version: '24' }
- run: npm ci
- run: npm testThe minimal pattern: trigger on PR + push-to-main, install deps, run tests.
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [22, 24]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with: { node-version: ${{ matrix.node }} }
- run: npm ci
- run: npm testPer workflow syntax, when fail-fast is true GitHub "will
cancel all in-progress and queued jobs in the matrix if any job in the
matrix fails", and "This property defaults to true". Set it to false
explicitly so every OS and runtime reports. Matrix size is OS × Node =
3 × 2 = 6 jobs; per limits, "A job matrix can generate a
maximum of 256 jobs per workflow run."
To let one experimental target fail without cancelling the rest, mark
it with continue-on-error: ${{ matrix.experimental }}, which per
workflow syntax "applies to a single job".
Runner pinning, per-language runtime matrices (Node / Python / Java / .NET), OS-specific quirks (path separators, line endings, case sensitivity, shells), per-OS conditional steps and tests, and the tiered cost-management cadence are in references/os-matrix.md.
For large suites:
jobs:
test:
strategy:
matrix:
shard: [1, 2, 3, 4]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: npx jest --shard=${{ matrix.shard }}/44 parallel jobs, each running 1/4 of the test suite. Per
jest-cli, --shard takes shardIndex/shardCount, both
"1-based, positive numbers". Wall-clock time drops; billed minutes stay
about the same plus each job's setup time. Playwright (--shard=1/4)
and pytest-xdist splitting follow the same matrix shape.
Wiring the service containers the tests depend on (PostgreSQL, Redis) and publishing JUnit XML as artifacts plus PR-check summaries are in references/services-and-reporting.md.
GitHub Actions doesn't ship native test-retry; use the framework's
retry mechanism (e.g., Playwright's retries config) or a wrapper
action such as nick-fields/retry, which takes
timeout_minutes (or timeout_seconds), max_attempts and command:
- uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 2
command: npm testUse sparingly - retries hide flake. Prefer
flaky-test-quarantine (in the qa-flake-triage plugin).
on:
pull_request:
paths:
- 'src/**'
- 'tests/**'
- 'package.json'
push:
branches: [main]
paths-ignore:
- 'docs/**'
- '*.md'
schedule:
- cron: '0 4 * * *' # daily 4am UTC
workflow_dispatch: # manual trigger
inputs:
target_browser:
description: 'Browser to test'
type: choice
options: [chrome, firefox, safari]
default: chromePath filters skip workflows when only docs change - saves CI
budget. Per workflow syntax, "Use the paths-ignore filter when
you only want to exclude file path patterns"; to include and exclude in
one list, use paths with ! prefixes.
Per events, "By default, scheduled workflows run in UTC", "The shortest interval you can run scheduled workflows is once every 5 minutes", "Scheduled workflows run on the latest commit on the default branch", and the event "can be delayed during periods of high loads". Don't schedule a nightly run at minute 0 if timing matters; pick an odd minute.
Per workflow syntax, the filter decides "if a workflow should run" from the changed files. A workflow that doesn't run posts no status for its jobs, so if a branch rule requires one of those jobs, a PR that only touches docs waits for a check that never arrives. Keep path filters off workflows whose jobs are required checks, or filter inside the job instead.
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: trueWhen a PR receives multiple pushes, the older runs cancel -
saves CI cost on superseded commits. Per workflow syntax, a
new run only replaces a pending one by default; "To also cancel any
currently running job or workflow in the same concurrency group,
specify cancel-in-progress: true". Don't use this group on main
pushes that deploy: a cancelled deploy can leave an environment half
updated.
env:
CI: true
steps:
- run: npm test
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}Secrets configured in repo settings; never committed.
Run the workflow once before merging - push the branch or trigger it
manually via workflow_dispatch (or dry-run locally with act).
Verify: the matrix expands into the expected job count (e.g. OS × Node =
3 × 2 = 6 jobs) and each job uploads its JUnit artifact. If a job fails on
a missing secret, confirm NPM_TOKEN / TEST_DATABASE_URL exist under
repo Settings -> Secrets and variables -> Actions, add any that are
missing, and re-run the job.
A Node service needs PR tests on Linux + macOS, plus a nightly full run against PostgreSQL.
test.yml triggers on pull_request and push to main,
with schedule: cron '0 4 * * *' for the nightly run.test job runs a matrix of os: [ubuntu-latest, macos-latest] × node: [20, 22] with fail-fast: false - 4
jobs, and no target cancels another.integration job (Linux only) declares a
postgres:15 service with a pg_isready healthcheck and
passes DATABASE_URL into npm test.test-results/junit.xml; actions/upload-artifact@v7
with if: always() keeps the report even when tests fail, and
dorny/test-reporter@v3 renders it in the PR check summary.concurrency group keyed on the ref cancels superseded runs,
so rapid pushes don't pile up billable minutes.Result: PR authors get fast cross-platform signal, the nightly catches DB-integration regressions, and failed runs still surface their JUnit report.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
fail-fast: true on matrix | First failure cancels all; lose multi-target signal. | fail-fast: false (Step 2). |
No concurrency group | PRs with rapid pushes pile CI runs. | Add concurrency cancel (Step 8). |
if: always() everywhere | Some steps shouldn't run on failure (deploy). | Selective if: always() for upload steps only (services-and-reporting). |
| Hardcoded secrets in YAML | Secret leak; revocation needed. | secrets.X references (Step 9). |
| Massive single workflow file | Hard to navigate; merge conflicts. | Split per concern (test.yml, deploy.yml, lint.yml). |
Missing actions/checkout step | Job can't access repo files. | First step always (Step 1). |
No timeout-minutes on the test job | A hung test burns up to the 360-minute default. | timeout-minutes: 20 (or the suite's p99 plus margin). |
| Path filter on a workflow that runs a required check | The check never reports; docs-only PRs stay blocked. | Filter inside the job, or keep the workflow unfiltered (Step 7). |
main or nightly, Linux on PRs.timeout-minutes defaults to 360. Set a much lower
timeout-minutes on test jobs so a hung test fails in minutes, not
hours.dorny/test-reporter..github/workflows/.fail-fast default, continue-on-error,
paths-ignore, concurrency and cancel-in-progress,
timeout-minutes default, Linux-only service containers.schedule (UTC, 5-minute minimum, default branch,
delays) and workflow_dispatch.--shard format.nick-fields/retry inputs.gitlab-ci-test-jobs,
jenkinsfile-test-stages -
per-platform alternatives.ci-test-job-conventions - cross-CI conventions; CircleCI patterns
live in its references/circleci.md.junit-xml-analysis - downstream JUnit XML parser.flaky-test-quarantine - preferred over retries.testland/qa
aa16752
Canonical home
since Jul 23, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.