Testland
Browse all skills & agents

ci-test-job-conventions

Pure-reference for cross-CI test workflow conventions - when to shard (and how many shards), retry policy (which failures are safe to retry), flake-quarantine integration, artifact retention, per-trigger cadence (per-PR vs per-merge vs nightly), concurrency-cancel patterns, per-job timeouts, secret management, and cross-CI portability. Use as the team's reference for CI test-workflow design across GitHub Actions / GitLab CI / Jenkins / CircleCI; per-CI reporting and per-language reporter / cache-key lookups live in references/, as do the CircleCI test-config patterns (.circleci/config.yml workflows, test splitting, orbs, contexts - references/circleci.md).

Install with skills.sh (any agent)

npx skills add testland/qa --skill ci-test-job-conventions
View source

ci-test-job-conventions

Overview

Per-CI platform skills (GitHub Actions / GitLab CI / Jenkins / CircleCI) cover how to express workflows. This skill covers what to express - the cross-platform conventions that apply regardless of CI tool.

When to use

  • Designing the CI test workflow for a new repo or service.
  • Auditing an existing CI config for anti-patterns - retries that mask flake, no concurrency-cancel, artifacts retained forever.
  • PR review of a workflow / .gitlab-ci.yml / Jenkinsfile change.
  • Standardizing test-job conventions across teams on different CI tools.

How to use this reference

  1. Size the suite, pick the shard count from the sharding matrix (§1) and set the per-job timeout for each job type (§8).
  2. Set the retry policy - which failure classes are safe to retry vs. quarantine as flake (§2, §3).
  3. Add concurrency-cancel so a new push cancels the superseded run (§6).
  4. Tier the per-trigger cadence - what runs per-PR vs per-merge vs nightly vs pre-release (§5).
  5. Emit JUnit XML and set artifact retention - the reporting and cache lookup tables live in references/junit-and-cache-lookups.md; retention bands in §4.
  6. Guard secrets with the CI secret store, never in-repo (§7).

Then re-check the workflow against the conventions below.

§1 - When to shard

Sharding splits a test suite across N parallel jobs. Decision matrix:

Suite total runtimeSharding recommendation
< 2 minNone. Overhead exceeds benefit.
2-10 minOptional. Shard if PR feedback time matters.
10-30 min2-4 shards.
> 30 min4-8 shards. Investigate the suite - may be too big.
> 60 min8+ shards + investigate suite refactoring (per e2e-suite-budget).

Sharding cost-equivalent is N parallel × ~runtime/N - same total CPU-time, faster wall-clock.

§2 - Retry policy

Distinguish retry classes:

Failure classRetry?Pattern
Runner died / system failureYes (1-2x)CI platform's retry-on-runner-failure.
Network timeout to dependencyYes (1x)Test framework retry; flag for analysis.
Test flake (passed on retry)NoMark + quarantine via flaky-test-quarantine.
Test consistently failsNoReal bug; investigate.

Rule: Maximum 1 framework-level retry. More retries hide flake.

§3 - Flake quarantine integration

Failed-on-first-run-passed-on-retry tests are flake. Pattern:

  1. Test fails → CI marks @flaky tag.
  2. Continued failures over N runs → quarantine (move to separate suite that doesn't gate).
  3. Periodic review of quarantined tests → fix or delete.

Per flaky-test-quarantine (in the qa-flake-triage plugin) for the workflow.

§4 - Artifact lifecycle

# Recommended retention per artifact type
test-results:        14 days   # short-term debugging
coverage-reports:    30 days   # trend analysis
e2e-screenshots:     30 days   # failure debugging
performance-traces:  90 days   # historical analysis
deployment-logs:     90 days   # audit / compliance

Per-CI:

CIRetention default
GitHub Actions90 days; configurable per artifact via retention-days
GitLab CIPer-job expire_in:; 30 days project default
JenkinsConfigured via buildDiscarder(logRotator(...))
CircleCI30 days; non-configurable on free tier

Don't retain forever - storage cost.

§5 - Per-trigger filtering

Per-PR (push to PR branch):
  - Smoke tests.
  - Lint + unit tests.
  - Per-changed-files coverage gate.

Per-merge to main:
  - Full unit + integration suite.
  - Smoke E2E.
  - Coverage trend tracking.

Per-deploy to staging:
  - Smoke E2E against staging.
  - Synthetic monitor smoke.

Nightly scheduled:
  - Full E2E across browsers.
  - Full security scans (axe, OWASP).
  - Mutation testing (per `stryker-mutation`).

Pre-release tag:
  - Cross-platform matrix (per `mobile-device-matrix-toolkit`).
  - Cross-browser matrix (per `playwright-testing` references/browser-matrix.md).
  - Manual UAT sign-off.

Manual / on-demand:
  - Specific debug runs.
  - Performance / load tests.

Tier the cadence to balance feedback latency vs cost.

§6 - Concurrency control

When PRs receive rapid pushes:

CIPattern
GitHub Actionsconcurrency: group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true
GitLab CIinterruptible: true per job
JenkinsdisableConcurrentBuilds() in pipeline options
CircleCIauto-cancel-redundant-workflows in project settings

The pattern: cancel superseded runs. Saves CI cost on stale commits.

§7 - Secret management

Never:
- Commit credentials to .yml / Jenkinsfile
- Use secrets in pull_request from forks
- Use `set -x` in scripts that handle secrets

Always:
- CI platform's secret store
- Mask in logs (`echo "::add-mask::$VALUE"` for GHA)
- Rotate on schedule
- Scope per-job (job-level env > workflow-level env > global)

§8 - Per-job timeouts

Job typeRecommended timeout
Lint5 min
Unit tests10 min
Integration tests20 min
E2E (per-browser)30 min
E2E (full matrix)60 min
Deploy30 min
Performance / load60 min

Hard timeouts prevent runaway jobs from consuming runners.

§9 - Cross-CI portability

If the team needs CI portability (multiple CIs in use, or anticipates migration):

  • Encapsulate logic in shell scripts (scripts/test.sh, scripts/build.sh); CI calls the script.
  • Use standard env vars (CI=true, CI_BRANCH, CI_COMMIT_SHA); abstract per-CI vars.
  • Containerize the build (Docker image with all deps); CI just runs the container.

The goal: .github/workflows/test.yml, .gitlab-ci.yml, and Jenkinsfile are thin wrappers calling the same scripts.

Worked example: a 25-minute Playwright E2E suite on GitHub Actions

Walk the How-to-use steps for a suite that runs 25 minutes end-to-end.

  1. Shard + timeout. 25 min falls in the 10-30 min band (§1), so split into 4 shards - roughly 6-7 min wall-clock each - and set a 30 min hard timeout per shard job (the E2E per-browser row in §8).
  2. Retry. Allow at most 1 framework-level retry for a network timeout to a dependency (§2); a test that only passes on that retry is flake, not a pass - tag it @flaky and quarantine it (§3) rather than retrying further.
  3. Concurrency. Set concurrency: group: ${{ github.workflow }}-${{ github.head_ref }} with cancel-in-progress: true (§6) so a fresh push to the PR cancels the superseded run.
  4. Cadence. Run a smoke subset per-PR, the full 4-shard E2E per-merge to main, and the full cross-browser E2E nightly (§5).
  5. Reporting + artifacts. Emit JUnit XML via dorny/test-reporter so results feed junit-xml-analysis (references/junit-and-cache-lookups.md), and upload failure screenshots with retention-days: 30 - the e2e-screenshots band in §4.

Deep references

The per-CI reporting mechanism and the per-language reporter and dependency-cache lookups live in one companion reference so this file stays a decision surface:

  • Reporting + cache lookups - JUnit XML support per CI, default reporters per language, and per-language cache-key recommendations: references/junit-and-cache-lookups.md.
  • CircleCI test configs - .circleci/config.yml workflows, executors, timing-based test splitting, orbs, insights, contexts: references/circleci.md.

References

  • github-actions-test-jobs, gitlab-ci-test-jobs, jenkinsfile-test-stages - per-CI implementation skills.
  • CircleCI patterns: references/circleci.md.
  • flaky-test-quarantine - flake handling.
  • junit-xml-analysis - JUnit XML parser.
  • e2e-suite-budget - when to refactor instead of shard more.
  • Reporting + cache lookups (per-CI JUnit, per-language reporters + cache keys): references/junit-and-cache-lookups.md.

CircleCI test configs

Deep reference for ci-test-job-conventions SKILL.md. Configures CircleCI test workflows - .circleci/config.yml with workflows, jobs, executors, parallelism (test splitting), orbs (reusable shared config), insights for analytics, contexts for per-team secrets. Consult for CircleCI-hosted CI when the team values its parallelism + insights features.

Overview

Configuration lives at .circleci/config.yml:

  • Executors - where jobs run (Docker, machine, macOS).
  • Jobs - individual units of work.
  • Workflows - orchestrate jobs (sequential / parallel).
  • Orbs - reusable, shareable config packages.

CircleCI's differentiator is test splitting - automatic parallelization based on per-test timing.

When to use

  • Project is on CircleCI.
  • Test suite is large enough for parallel splitting to matter (>5 min single-instance runtime).
  • The team values orbs (per-tool reusable config).

Step 1 - Basic test config

# .circleci/config.yml
version: 2.1

jobs:
  test:
    docker:
      - image: cimg/node:22.0
    steps:
      - checkout
      - run: npm ci
      - run: npm test

workflows:
  test:
    jobs:
      - test

cimg/node:22.0 is CircleCI's pre-warmed Node image (faster startup than node:22).

Step 2 - Parallelism + test splitting

jobs:
  test:
    docker:
      - image: cimg/node:22.0
    parallelism: 4
    steps:
      - checkout
      - run: npm ci
      - run:
          name: Test (split by timing)
          command: |
            TESTS=$(circleci tests glob "tests/**/*.spec.ts")
            echo "$TESTS" | circleci tests split --split-by=timings | xargs npx jest
      - store_test_results:
          path: reports/junit

circleci tests split --split-by=timings reads prior run timings and distributes tests evenly across 4 parallel containers.

Step 3 - Multiple executors

executors:
  node-22:
    docker:
      - image: cimg/node:22.0
  node-20:
    docker:
      - image: cimg/node:20.0
  with-postgres:
    docker:
      - image: cimg/node:22.0
      - image: cimg/postgres:15.0
        environment:
          POSTGRES_PASSWORD: test

jobs:
  unit:
    executor: node-22
    steps:
      - checkout
      - run: npm test

  integration:
    executor: with-postgres
    environment:
      DATABASE_URL: postgres://postgres:test@localhost:5432/postgres
    steps:
      - checkout
      - run: npm run test:integration

  unit-on-node-20:
    executor: node-20
    steps:
      - checkout
      - run: npm test

The second container in the executor (postgres) is reachable as localhost from the primary container.

Step 4 - Workflow orchestration

workflows:
  test-and-deploy:
    jobs:
      - lint
      - unit:
          requires: [lint]
      - integration:
          requires: [unit]
      - e2e:
          requires: [integration]
      - deploy:
          requires: [e2e]
          filters:
            branches:
              only: main

requires: builds the DAG; filters: restricts when jobs run.

Step 5 - Orbs (reusable config)

version: 2.1

orbs:
  node: circleci/node@5.2.0
  codecov: codecov/codecov@4.2.0

jobs:
  test:
    docker:
      - image: cimg/node:22.0
    steps:
      - checkout
      - node/install-packages
      - run: npm test -- --coverage
      - codecov/upload:
          file: coverage/lcov.info

workflows:
  test:
    jobs: [test]

Orbs encapsulate common patterns (npm install, codecov upload, slack notify, etc.). Browse at circleci.com/developer/orbs.

Step 6 - Test results + artifacts

- run:
    name: Test
    command: npm test -- --reporters=default --reporters=jest-junit
    environment:
      JEST_JUNIT_OUTPUT_FILE: reports/junit/junit.xml

- store_test_results:
    path: reports/junit/

- store_artifacts:
    path: coverage/
    destination: coverage

store_test_results: parses JUnit XML and renders results in the CircleCI UI (Insights tab tracks flakes / slow tests over time).

store_artifacts: uploads files; viewable via the build's Artifacts tab.

Step 7 - Insights (CircleCI's analytics)

CircleCI Insights tracks per-test metrics:

  • Flaky tests (passing then failing on retry).
  • Slowest tests.
  • Per-job duration trends.
  • Pipeline duration.

Available via the project's Insights tab; no extra config needed (uses the data from store_test_results).

Step 8 - Contexts (shared secrets)

workflows:
  test:
    jobs:
      - integration:
          context: test-database-credentials

Contexts are project-organization-level credential stores - secrets shared across multiple projects without re-entering. Configured in Org Settings.

Step 9 - Conditional / parameterized

parameters:
  run-e2e:
    type: boolean
    default: false

jobs:
  e2e:
    when: << pipeline.parameters.run-e2e >>
    steps:
      - run: npx playwright test

# Trigger from API with:
# {"parameters": {"run-e2e": true}}

Useful for opt-in expensive tests (cross-browser, full regression).

Anti-patterns

Anti-patternWhy it failsFix
parallelism: N without test splittingAll N containers run all tests; wasted parallelism.circleci tests split (Step 2).
Hardcoded credentials in config.ymlSecret leak.Contexts or env vars (Step 8).
version: 2 (deprecated)Lacks features; orbs / parameters not available.Always version: 2.1 (Step 1).
Not using orbs for common patternsBoilerplate per project.Orbs (Step 5).
Missing store_test_resultsInsights doesn't populate; flake tracking missing.Always store results (Step 6).
One mega-job for unit + integration + E2ENo parallelism; slow.Workflow with parallel jobs (Step 4).

Limitations

  • CircleCI-specific. Migration to other CI requires rewrite.
  • Test splitting needs prior run data. First run doesn't benefit; subsequent runs do.
  • Cost model. Free tier limited; paid plans charge per minute / per resource class.
  • Per-orb update cadence varies. Some orbs are stale.

References

  • CircleCI docs at circleci.com/docs/.
  • github-actions-test-jobs, gitlab-ci-test-jobs, jenkinsfile-test-stages - alternatives.
  • ci-test-job-conventions SKILL.md - the cross-CI conventions this reference belongs to.

CI JUnit and cache-key lookup tables

View source (opens in new window)

CI JUnit and cache-key lookup tables

Deep reference for ci-test-job-conventions SKILL.md. Consult by CI platform and language once the workflow's shard / retry / cadence decisions are made - these tables carry no design decisions, only the concrete mechanism per platform and per language.

JUnit XML reporting (cross-CI standard)

Every modern CI accepts JUnit XML via either a native plugin or a third-party action:

CIJUnit XML support
GitHub Actionsdorny/test-reporter action
GitLab CIartifacts.reports.junit: (native)
Jenkinsjunit '...' (JUnit Plugin; native)
CircleCIstore_test_results: (native; feeds Insights)

Always emit JUnit XML; the same output feeds every CI's reporting and the downstream junit-xml-analysis parser (in the qa-test-reporting plugin).

Per-language standard reporters

LanguageDefault reporterJUnit XML output
JavaScript (Jest)defaultjest-junit (separate package)
TypeScript(same as JS)(same)
Python (pytest)pytestpytest --junitxml=reports/junit.xml
Java (Maven)Surefiretarget/surefire-reports/*.xml (default)
Java (Gradle)Gradle Testbuild/test-results/test/*.xml (default)
.NETdotnet test--logger "junit;LogFilePath=..."
Gogo testgotestsum --junitfile=junit.xml
Ruby (RSpec)RSpec--format RspecJunitFormatter --out junit.xml

The same JUnit XML feeds every CI's reporting + downstream analysis tools.

Per-language cache strategies

Per-language cache key recommendations:

  • Node: cache key on package-lock.json hash
  • Python: cache key on requirements.txt / poetry.lock hash
  • Java (Maven): cache key on pom.xml hash; cache ~/.m2
  • Java (Gradle): cache ~/.gradle
  • Go: cache ~/go/pkg/mod on go.sum hash
  • Rust: cache ~/.cargo on Cargo.lock hash

Benefits: repeat installs are sub-second vs 30s-2min cold. Trade-off: cache eviction when key changes; extra config to manage.