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.

StageQuestion it answersTypical duration
Lint / formatIs the code consistent and obviously wrong anywhere?Seconds
BuildDoes it compile and do the dependencies resolve?Under a minute
TestDoes it still do what it is supposed to do?Minutes
PackageCan we produce the deployable artefact?Minutes
DeployCan 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 latest as 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]
SymptomUsual fix
Every run reinstalls dependenciesCache the package manager directory, keyed on the lockfile hash
One slow test stage dominatesShard tests across parallel jobs
Runs wait for a scarce runnerUse a hosted runner pool or scale your own agents
Random failuresRemove 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.

A GitHub Actions workflow Gates, secrets and deploys

Last refreshed 2026-09-18.