Exporting pipelines
testfile export translates a Testfile into a pipeline that runs the same
tests without the runner: a GitHub Actions workflow, a GitLab CI
configuration, a Tekton Pipeline, or a plain bash script. Sequences,
parallel groups, matrices, services, conditions, retries and hooks all come
along β as native jobs, matrices and sidecars where the target has them, as
generated shell where it does not.
testfile export github # writes .github/workflows/testfile.yaml
testfile export gitlab # writes .gitlab-ci.yml
testfile export tekton # writes .tekton/testfile-pipeline.yaml
testfile export bash # writes testfile-ci.sh (executable)
When you can run the runner in CI, prefer it: the GitHub
Action, the GitLab template and the
Tekton Task keep the pipeline a single job and record the run β
run.yaml, per-test logs, service logs, artifacts, caching, the viewer.
The export is for the cases where that is not wanted: a pipeline that must
not depend on Node.js or on the runner, a migration away from (or toward)
Testfile, or a readable answer to βwhat would this suite look like as a
workflow?β. An exported pipeline runs the tests and fails when they fail β
it does not write run.yaml, collect logs or artifacts, or use the
result cache.
Each generated file starts with a Generated by testfile export header.
Re-run the command after editing the Testfile instead of editing the
output; the command refuses to overwrite a file that lacks the header
(that is somebodyβs hand-written work) unless --force is given.
Options
testfile export <target> [path] [options]
| Option | Description |
|---|---|
-f, --filter <value> | Export only tests matching by name/path, tag, or key:value matrix β the same filters start takes. (repeatable) |
-n, --filter-name <name-or-path> | Only tests whose path contains this. (repeatable) |
-t, --filter-tags <tags> | Only tests tagged with any of these comma-separated tags. (repeatable) |
-m, --filter-matrix <key:value> | Only matrix instances with this value. (repeatable) |
-l, --label <key=value> | Write a TESTFILE_LABEL_<KEY> variable into the pipeline (workflow env on GitHub, variables on GitLab, step env on Tekton, an export in the script). (repeatable) |
-o, --output <file> | Where to write the pipeline (defaults per target, next to the Testfile), or - for stdout. |
--image <image> | Container image for GitLab jobs and Tekton steps (Tekton default: node:22; GitLab: none unless given). |
--runs-on <label> | GitHub Actions runner label (default ubuntu-latest). |
--force | Overwrite a file that was not generated by this command. |
--failed, --changed and --shard do not apply here: they select
against a recorded run or a git checkout at run time, which an exported
pipeline does not have.
How a suite becomes a pipeline
bash is the closest translation, because nothing has to be cut into
jobs: one script runs sequences in order, parallel groups as background
processes (needs becomes dependency waves), expands matrices, starts and
stops services exactly where the Testfile declares them, and prints a
pass/fail/skipped summary. Exit code 0 only when nothing (hard) failed.
GitHub Actions and GitLab CI are cut into jobs the way a human would write them:
- Every branch of a parallel group becomes its own job, and a matrix
becomes one job with a native matrix (
strategy.matrix/parallel: matrix). - CI jobs run on fresh machines, so the leaves of an enclosing sequence
that run before a parallel group (your
npm ci) are repeated as the first steps of every branch job. Tests after the group become a job withneedson all branch jobs. - Branches connected through
needsshare workspace state under the runner, which separate machines cannot provide β such branches are merged into a single job, in dependency order (this repositoryβsbuild-clichain is one job in the export). - Services declared on the document or on groups start once per job, in a
first βStart servicesβ step, and stop in a final
always()step; services on a test start and stop inside that testβs step.
Tekton gets one task per test, with runAfter edges for sequences and
needs. All tasks share the source workspace (a PVC, cloned by a
git-clone task like the Tekton Taskβs pipeline), so unlike
the job-based exports nothing has to be repeated: a sequenceβs earlier
tasks leave their state on the workspace. Container services become
sidecars of the tasks that declared them; process services run inside
the stepβs shell.
Testfile templates are rewritten rather than resolved: ${{ env.X || d }}
becomes the shellβs ${X:-d}, ${{ ports.web }} the pinned port number,
${{ matrix.node }} either the baked value or the TESTFILE_MATRIX_NODE
variable the native matrix feeds. if conditions are evaluated at run
time, in shell, with TESTFILE_OS/TESTFILE_ARCH detected by the script.
What each target supports
Legend: β supported Β· ~ supported with the noted caveat Β· β not exported (a note in the generated header says so where it matters).
| Feature | bash | GitHub | GitLab | Tekton |
|---|---|---|---|---|
command / script / shell | β | β | β | β |
sequence | β | ~ steps in a job; leading tests repeat per branch job | ~ same as GitHub | β runAfter over a shared workspace |
parallel (nested too) | β background waves | β one job per branch | β | β parallel tasks |
needs | β waves + result gates | ~ merged into one job (shared state) | ~ same as GitHub | β runAfter |
matrix (incl. exclude/include) | β expanded | β native strategy.matrix | β parallel: matrix, one entry per combination | ~ native matrix.params for plain cross products on a single test; otherwise expanded |
maxParallel | β noted | ~ only on a matrix (max-parallel) | β noted | β noted |
foreach / include | β expanded at export time | β | β | β |
if conditions | β at run time | β | β | β |
env / envFile / ports | β (random ports pinned at export) | β | β | β |
secrets | ~ taken from the environment | β wired as ${{ secrets.NAME }} | ~ define them as CI/CD variables | ~ provide them to the pod |
Process services + readiness (http/tcp/log/exec) | β | β | β | β in-step |
| Container services | β via podman/docker (TESTFILE_ENGINE) | β same | ~ same; the runner must be able to run containers (shell executor or docker:dind) | β sidecars; readiness by http/tcp only, once containers β |
Service needs | β started in dependency order | β | β | β |
shared services | ~ one instance per declaring scope, noted | ~ | ~ | ~ |
setup / teardown | β where declared | ~ group hooks run once per job | ~ same | ~ once per task |
retry | β | β | β | β |
continueOnError | β soft failure in the summary | β continue-on-error | β allow_failure / soft step | β onError: continue |
timeout | ~ via the timeout command where installed | ~ same | ~ same | ~ same |
container on a test | β engine CLI | β | ~ needs a container-capable runner | β becomes the stepβs image |
Run recording (run.yaml, logs), artifacts | β by design | β | β | β |
Result caching (inputs) | β tests always run | β | β | β |
--failed / --changed / --shard / watch mode | β | β | β | β |
Env isolation / forwardEnv / secret masking | β tests see the CI jobβs environment | β | β | β |
Semantics that shift slightly in every export, all of them consequences of running without the runner:
- Random ports are pinned.
ports: {web: random}becomes a fixed number (from 24100 up), one per declaring test β fine in CI, where every job has the machine to itself. - The environment is the CI jobβs. The runner starts tests in an
isolated environment; an exported test inherits whatever the job has,
so
forwardEnvis unnecessary and secret values are not masked. - An unset
${{ env.X }}expands to empty instead of failing the run the way the runnerβs strict template resolution does. - Group-scoped services and hooks run once per job/task (GitHub, GitLab, Tekton), not once per group β a consequence of cutting one tree into independent jobs. The bash export keeps the exact scoping.
sharedservices are not pooled across the tests that declare them; each scope starts its own instance.
Examples
Only the fast tests, labelled, to stdout:
testfile export github -t fast -l tier=smoke -o -
One matrix leg for a debugging pipeline:
testfile export gitlab -m node:22
A Tekton pipeline whose steps run on your build image:
testfile export tekton --image registry.example.com/build:latest
kubectl apply -f .tekton/testfile-pipeline.yaml
The generated pipeline needs the same prerequisites the Taskβs pipeline
does: the git-clone catalog task and a PVC-backed source workspace β
see Tekton for both.
Run the bash export anywhere a shell is:
testfile export bash && ./testfile-ci.sh
The scripts assume bash, and curl where ready: http is used;
container services additionally need podman or docker (pick one with
TESTFILE_ENGINE, default docker).