🐙Testfile

GitLab CI

The repository ships a GitLab CI template — the GitLab counterpart of the GitHub Action — so running a Testfile in a pipeline is a single include:

include:
  - remote: https://raw.githubusercontent.com/testfile-dev/testfile/main/gitlab/testfile.gitlab-ci.yml

That adds a testfile job to the pipeline: it runs testfile doctor and then testfile start against your repository’s Testfile, in a node:22 container. The job fails when tests fail; the JUnit report it writes feeds GitLab’s own test reporting, so failed tests appear in the merge request widget and the pipeline’s Tests tab — that is the GitLab shape of what annotations, the job summary and the per-test commit statuses do on GitHub. The recorded run is kept as a job artifact.

Variables

The job is configured with TESTFILE_* variables — set them on the job, the pipeline, or in a rules: entry:

testfile:
  variables:
    TESTFILE_FILTER_TAGS: fast
    TESTFILE_FAIL_FAST: "true"
VariableDefaultDescription
TESTFILE_PATH.Testfile or directory containing one.
TESTFILE_FILTER / TESTFILE_FILTER_NAME / TESTFILE_FILTER_TAGS / TESTFILE_FILTER_MATRIX–Same as the -f/-n/-t/-m CLI filters.
TESTFILE_CHANGEDfalseRun only tests whose inputs match files changed against the base branch (--changed).
TESTFILE_CHANGED_SINCEMR target branchBase branch for changed. Defaults to $CI_MERGE_REQUEST_TARGET_BRANCH_NAME, so on merge request pipelines the diff is against the MR’s target branch. The job fetches it itself.
TESTFILE_FAIL_FASTfalseAbort the whole run at the first failure.
TESTFILE_MAX_PARALLEL–Global cap on concurrently running tests.
TESTFILE_REPORTER / TESTFILE_OUTPUTjunit / junit.xmlMachine-readable results. The defaults are what GitLab’s test reporting reads; changing them means overriding the job’s artifacts to match.
TESTFILE_NODE_IMAGEnode:22Image the job runs in. Needs bash, git and npm — any node:* image has all three.
TESTFILE_DOCTORtrueRun testfile doctor before the tests: every missing tool, engine or taken port that would fail the run anyway fails early, in one readable report.
TESTFILE_VARIANTS–What tells this run apart from the other legs of a matrix, as key=value pairs separated by commas or newlines (e.g. node=22; whitespace is stripped, so values cannot contain spaces). Recorded in run.yaml; merging needs it.
TESTFILE_LABELS–Extra labels to record, as key=value pairs separated by commas or newlines (e.g. tier=nightly, owner=infra). Merged with the automatic ones; a key you set yourself wins.
TESTFILE_AUTO_LABELStrueLabel the run with the GitLab context — see below.

The template also defines a hidden .testfile job; extends: .testfile derives more jobs from it (a nightly one, one per package) without touching the default testfile job.

What a CI run is labelled with

Every run the job records is labelled with where it came from, so a history collected from many pipelines can be narrowed down afterwards (testfile serve filters by label). The keys match what the GitHub Action records, so runs from both forges sit in one history:

KeyValue
triggerhow the pipeline started: manual (a web, api or trigger pipeline), schedule, push, merge_request, or GitLab’s own name for any other source
branchthe branch the run used — on a merge request the source branch
basemerge requests only: the target branch
mrmerge requests only: the merge request number (pr on GitHub)
tagtag pipelines only, instead of branch
actorthe GitLab username that started the pipeline
repothe project path (group/project)
jobthe job name — with parallel: matrix:, GitLab includes the leg’s values, so the legs stay distinguishable
shathe short commit sha
ci-runthe pipeline id, to get from a recorded run back to its pipeline

A label is only recorded when the context supplies it, so a run never carries an empty one. Your own TESTFILE_LABELS win over the automatic ones — setting branch=release replaces the derived value rather than clashing with it. Set TESTFILE_AUTO_LABELS: "false" to record only your own.

Examples

Only the fast tests on merge requests, everything nightly, the full suite on the default branch — rules: picks the variables per pipeline:

testfile:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      variables:
        TESTFILE_FILTER_TAGS: fast
        TESTFILE_FAIL_FAST: "true"
    - if: $CI_PIPELINE_SOURCE == "schedule"
      variables:
        TESTFILE_FILTER_TAGS: nightly
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Only the tests a merge request could have affected, diffed against the MR’s target branch (note GIT_DEPTH: "0" — change detection diffs against the base branch, which a shallow clone doesn’t reach):

testfile:
  variables:
    GIT_DEPTH: "0"
    TESTFILE_CHANGED: "true"

Tests without inputs always run, so a suite adopts this incrementally: declare inputs on the expensive tests first.

More than one leg — GitLab’s parallel: matrix: runs the job once per combination, and TESTFILE_VARIANTS is what tells the recorded runs apart when testfile merge combines them (job artifacts are per job, so nothing overwrites):

testfile:
  parallel:
    matrix:
      - TESTFILE_NODE_IMAGE: ["node:20", "node:22"]
  variables:
    TESTFILE_VARIANTS: node=$TESTFILE_NODE_IMAGE

Container services

Unlike GitHub’s runners, a GitLab job does not come with a container engine: the engine selection needs a docker client in the job’s image and a daemon it can reach. On a runner that allows docker-in-docker, that is one service and one download:

testfile:
  services:
    - docker:dind
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
  before_script:
    - curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-27.5.1.tgz | tar -xz --strip-components=1 -C /usr/local/bin docker/docker

On a shell executor or a runner whose host docker socket is mounted, the client alone is enough. Testfiles whose services are plain processes need none of this.

Bringing CI runs home

Every run of the job keeps the recorded run folder (.testfile/runs/<id>) as a job artifact, for two weeks by default (expire_in on the job changes that). testfile gitlab sync downloads the artifacts of the latest pipelines and imports them into your local run history, where testfile runs, inspect run, diff, --flaky and the TUI’s runs/tests views treat them like local runs:

export GITLAB_TOKEN=...              # a token with read_api
testfile gitlab sync group/project   # import the latest pipelines
testfile runs                        # CI runs are now part of the history

It looks for a job named testfile — the template’s default — and --job <name> selects another (a job derived with extends: .testfile, say). --latest <n> sets how many pipelines to consider, --ref <branch> narrows to one branch, and --host https://gitlab.example.com points at a self-hosted instance — the template itself has no GitLab.com dependency beyond where it is included from, so it works there unchanged. A manually downloaded artifact zip imports with testfile archive import; see sharing runs for the underlying commands and the S3 variant.

Without the template

The template is a convenience, not a requirement — a Testfile suite is one command, so a job you write yourself stays three lines. The ci/ folder has that spelled out for GitLab and the other CI systems, including what to archive so the runs stay inspectable later.