Skip to content
Join Now Login

GitLab CI

GitLab CI/CD runs tests on every push, but its own reporting resets at the merge request boundary. Gaffer adds run history, flaky detection, and a link you can hand to anyone, whether or not they have GitLab access.

How do I add my Gaffer project token to a GitLab pipeline?

Section titled “How do I add my Gaffer project token to a GitLab pipeline?”

Store it as a masked CI/CD variable, not inline in the YAML.

  1. Go to your GitLab project
  2. Navigate to SettingsCI/CDVariables
  3. Click Add variable
  4. Key: GAFFER_PROJECT_TOKEN
  5. Value: your Gaffer project token
  6. Check Mask variable to hide it in job logs

How do I upload a report after the test job runs?

Section titled “How do I upload a report after the test job runs?”

Add the curl call to after_script. Unlike script, after_script runs even when the job’s script section fails, which is when you most need the report.

test:
stage: test
script:
- npm ci
- npm test
after_script:
- |
curl -X POST https://app.gaffer.sh/api/upload \
-H "X-API-Key: $GAFFER_PROJECT_TOKEN" \
-F "files=@test-results/junit.xml" \
-F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'"}'
artifacts:
when: always
reports:
junit: test-results/junit.xml

Keeping artifacts:reports:junit alongside the Gaffer upload is intentional: GitLab’s widget gives reviewers an inline pass/fail count on the merge request itself, and Gaffer gives you the history that widget doesn’t keep once the artifact retention window passes.

Which GitLab CI variables carry commit and branch info?

Section titled “Which GitLab CI variables carry commit and branch info?”
VariableDescriptionExample
$CI_COMMIT_SHAFull commit SHAabc123def456...
$CI_COMMIT_REF_NAMEBranch or tag namemain, feature/login
$CI_MERGE_REQUEST_SOURCE_BRANCH_NAMESource branch in MR pipelinesfeature/login
$CI_NODE_INDEX1-based index of the current job in a parallel: job. Not set by parallel:matrix:1, 2, 3
$CI_PROJECT_NAMEProject namemy-app
playwright:
stage: test
image: mcr.microsoft.com/playwright:v1.40.0-jammy
script:
- npm ci
- npx playwright install --with-deps
- npx playwright test
after_script:
- |
curl -X POST https://app.gaffer.sh/api/upload \
-H "X-API-Key: $GAFFER_PROJECT_TOKEN" \
-F "files=@playwright-report/index.html" \
-F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'","test_framework":"playwright","test_suite":"e2e"}'
artifacts:
when: always
paths:
- playwright-report/
jest:
stage: test
script:
- npm ci
- npm test -- --reporters=default --reporters=jest-junit
after_script:
- |
curl -X POST https://app.gaffer.sh/api/upload \
-H "X-API-Key: $GAFFER_PROJECT_TOKEN" \
-F "files=@junit.xml" \
-F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'","test_framework":"jest"}'
artifacts:
when: always
reports:
junit: junit.xml
pytest:
stage: test
image: python:3.11
script:
- pip install pytest pytest-html
- pytest --html=report.html --self-contained-html
after_script:
- |
curl -X POST https://app.gaffer.sh/api/upload \
-H "X-API-Key: $GAFFER_PROJECT_TOKEN" \
-F "files=@report.html" \
-F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'","test_framework":"pytest"}'
artifacts:
when: always
paths:
- report.html

For a standardized format across all your test frameworks, use CTRF:

test:
script:
- npm ci
# Install the CTRF reporter for your framework:
# npm install --save-dev jest-ctrf-json-reporter
# npm install --save-dev playwright-ctrf-json-reporter
# npm install --save-dev vitest-ctrf-json-reporter
- npm test -- --reporter=jest-ctrf-json-reporter
after_script:
- |
curl -X POST https://app.gaffer.sh/api/upload \
-H "X-API-Key: $GAFFER_PROJECT_TOKEN" \
-F "files=@ctrf-report.json" \
-F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'"}'

Does Gaffer replace GitLab’s JUnit test report widget?

Section titled “Does Gaffer replace GitLab’s JUnit test report widget?”

No, run both. GitLab’s artifacts:reports:junit widget shows pass/fail counts inline on a single merge request and disappears once that job’s artifact retention expires. Gaffer keeps every run’s full history, flags tests that flip pass/fail across branches, and gives you a link that works for people without GitLab access, like a customer asking why a release slipped. Use the widget for the at-a-glance MR check and Gaffer for anything that needs to survive past that one pipeline.

How do I upload from merge request pipelines specifically?

Section titled “How do I upload from merge request pipelines specifically?”

Use rules: to detect an MR pipeline, and tag the branch with $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME. That variable is set only in merge request pipelines and names the source branch explicitly. $CI_COMMIT_REF_NAME resolves to the same branch there, so keep it as the fallback for ordinary branch pipelines. The synthetic ref refs/merge-requests/42/head lives in $CI_MERGE_REQUEST_REF_PATH, which is not what you want in a branch tag:

test:
rules:
- if: $CI_MERGE_REQUEST_IID
script:
- npm test
after_script:
- |
BRANCH="${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME:-$CI_COMMIT_REF_NAME}"
curl -X POST https://app.gaffer.sh/api/upload \
-H "X-API-Key: $GAFFER_PROJECT_TOKEN" \
-F "files=@test-results/junit.xml" \
-F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$BRANCH"'"}'

How do I structure a test-then-upload pipeline with needs:?

Section titled “How do I structure a test-then-upload pipeline with needs:?”

Split the upload into its own job that needs: the test job’s artifacts, so it runs as soon as the test job finishes instead of waiting for the whole stage to complete:

stages:
- test
- report
test:
stage: test
script:
- npm ci
- npm test || true # let the pipeline continue to the report job
artifacts:
when: always
paths:
- test-results/
upload-to-gaffer:
stage: report
needs:
- job: test
artifacts: true
script:
- |
curl -X POST https://app.gaffer.sh/api/upload \
-H "X-API-Key: $GAFFER_PROJECT_TOKEN" \
-F "files=@test-results/junit.xml" \
-F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'"}'

needs: creates a direct dependency edge rather than a stage-order one, so upload-to-gaffer starts the moment test finishes instead of waiting on everything else in the test stage. This only works because artifacts: true passes the report file across the job boundary. A GitLab job’s workspace doesn’t persist between jobs on its own: if you split test and upload into separate jobs and skip artifacts:, the upload job won’t find the report file at all, since it’s running on a clean checkout with none of the previous job’s output.

Can I upload to Gaffer from a job that only runs on failure?

Section titled “Can I upload to Gaffer from a job that only runs on failure?”

Yes, and after_script already does it. after_script runs even when the preceding script section fails, which is the GitLab equivalent of running a step regardless of exit code. If you would rather have the upload as its own job than as a script hook, gate that job with rules: - when: on_failure, which GitLab evaluates only once a job in an earlier stage has failed.

How do I upload from a GitLab parallel matrix job?

Section titled “How do I upload from a GitLab parallel matrix job?”

Tag each upload with one of your matrix variables, such as the $SHARD value below, so you can tell the runs apart later. $CI_NODE_INDEX is not set in parallel:matrix: jobs, only in parallel: jobs that take an integer. Gaffer records each curl call as a separate test run; without a distinguishing tag, four parallel shards produce four runs on the same commit and branch with no way to tell which shard is which.

test:
parallel:
matrix:
- SHARD: [1, 2, 3, 4]
script:
- npm test -- --shard=$SHARD/4
after_script:
- |
curl -X POST https://app.gaffer.sh/api/upload \
-H "X-API-Key: $GAFFER_PROJECT_TOKEN" \
-F "files=@test-results/junit.xml" \
-F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'","shard":"'"$SHARD"'"}'
SymptomCauseFix
401 API key required, and the variable looks correctly setThe variable is marked Protected but the pipeline is running on an unprotected branch or a fork MRUnprotect the variable, or restrict the upload job to protected refs with rules: - if: $CI_COMMIT_REF_PROTECTED
Upload never runsThe curl call is in script instead of after_script, and the test step failed firstMove the upload to after_script, which runs regardless of the preceding step’s exit code
curl can’t find the report file in a later jobJob workspaces don’t persist between jobs without artifacts:Add artifacts: paths: to the test job and needs: - job: test artifacts: true to the upload job
Branch shows as a merge-request ref like refs/merge-requests/42/headTagging with $CI_MERGE_REQUEST_REF_PATH, which holds the synthetic MR ref rather than a branch nameUse $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME instead
401 Unauthorized, variable is set correctlyToken missing the gfr_ prefix, or extra whitespace from copy-pasteRe-copy the token from project settings, check for trailing newlines in the variable value

Other CI Providers: GitHub Actions · CircleCI · Jenkins · Bitbucket · Azure DevOps