Pipeline stages and artefacts
Order stages for the fastest feedback, then promote one immutable artefact through every environment.
Stages
A pipeline is a sequence of stages, and each stage exists to catch a class of problem as early and as cheaply as possible. A failing stage fails the build; it does not report and continue.
| Stage | Question it answers | Typical duration |
|---|---|---|
| Lint / format | Is the code consistent and obviously wrong anywhere? | Seconds |
| Build | Does it compile and do the dependencies resolve? | Under a minute |
| Test | Does it still do what it is supposed to do? | Minutes |
| Package | Can we produce the deployable artefact? | Minutes |
| Deploy | Can we put it where users will reach it? | Minutes |
# cheap feedback first, expensive stages later
stages: [lint, build, test, package, deploy]
lint:
script: [npm ci, npm run lint]
build:
script: [npm ci, npm run build]
test:
script: [npm test]- Fail fast: a two-second lint failure should never wait behind a ten-minute test suite.
- Stages after the first failure are skipped, so a broken commit costs seconds instead of the whole pipeline.
- If a stage is genuinely slow, parallelise the work inside it rather than reordering the pipeline.
Artefacts
Build once, then promote the exact same artefact through every environment. Rebuilding for staging and again for production means you never tested what you shipped.
# identify the artefact by the commit, not by "latest"
docker build -t registry.example.com/app:$GIT_SHA .
docker push registry.example.com/app:$GIT_SHA
# deploy that exact image to staging, then the same one to production
kubectl set image deployment/app app=registry.example.com/app:$GIT_SHA- Tag container images with the commit SHA. Keep
latestas a convenience for humans, never as the deploy target. - Artefacts should be immutable: if the content changes, the version changes.
- Store test reports, coverage and logs as artefacts too. When a build fails at 2am, that output is the only evidence you will have.
💡
The artefact is the contract between build and deploy. If the deploy step is allowed to rebuild from source, it will eventually ship something the tests never saw, so make that a review point.
Keeping the pipeline fast
# cache dependency downloads between runs
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- node_modules/
- .cache/pip
# run the slow suite in parallel shards
test:
parallel: 4
script: [npm test -- --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL]| Symptom | Usual fix |
|---|---|
| Every run reinstalls dependencies | Cache the package manager directory, keyed on the lockfile hash |
| One slow test stage dominates | Shard tests across parallel jobs |
| Runs wait for a scarce runner | Use a hosted runner pool or scale your own agents |
| Random failures | Remove the real network and clock from the test; flakiness is a bug, not noise |
FAQ
How long should a pipeline take?
Under ten minutes for a normal change is the common target, because that is roughly the point where developers stop waiting and switch context. Push expensive work to later stages once the early feedback is fast.
Should the pipeline build on every branch?
Lint and test on every push, but publish only for main and version tags. That keeps runner cost and registry noise proportional to what might actually be released.
Related
A GitHub Actions workflow Gates, secrets and deploys
Last refreshed 2026-09-18.