Building a CI Pipeline
A CI pipeline takes "it works on my machine" and turns it into "it works on a clean VM, every time." The job runs on a runner that starts empty: it checks out your source, installs the toolchain, restores dependencies, builds, runs tests, and exits non-zero the moment any gate fails. The shape is identical across languages — only the setup action changes.
The discipline that makes CI worth running is that a red build blocks the merge. A pipeline that always goes green because failures are swallowed is theatre. Every step below exists to make a real failure stop the run, and to make the run a required check on the branch.
Checkout
The runner has no idea what repository triggered it until you check it out. actions/checkout@v4 is the first step in almost every code job; skip it and every later step fails because there are no files on disk. It also configures the credentials the rest of the workflow uses for Git operations.
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm test
Toolchain Setup
The language setup-* actions — setup-node, setup-python, setup-go — install the requested runtime and, crucially, wire up dependency caching with a single cache: option. Pinning an explicit version here is what keeps the runner's behavior reproducible instead of drifting with whatever the image ships this month.
Prefer the built-in cache: over hand-rolling actions/cache; the setup action already knows the lockfile path and the cache key for its ecosystem, so you get correct invalidation for free.
Install and Build
Use a deterministic install. npm ci installs exactly what the lockfile specifies and fails if package-lock.json is out of sync; npm install can mutate the lockfile and quietly paper over dependency drift. The equivalents are pip install -r requirements.txt with hashes, or go mod download. The point is that the lockfile, not the installer's resolution, is authoritative.
Test Gates
A test step that exits non-zero fails the job, and any downstream job that declared needs: on it is skipped rather than run against a broken build. That chaining is the gate: build feeds test feeds deploy, and a failure upstream stops everything below it.
Resist the urge to make a flaky step green with || true or continue-on-error: true. Those turn a gate into a decoration — the run reports success while the thing it was supposed to verify is broken.
Status Checks and Branch Protection
A passing workflow only blocks bad merges if you make it a required status check in the branch protection rules for main. Without that, a pull request with a red build can still be merged. Splitting lint, type-check, and test into separate parallel jobs keeps the pipeline fast and makes each required check independently legible in the PR.
- Omitting
actions/checkoutand then debugging why the build cannot find a single source file — the runner starts with an empty workspace. - Running
npm installinstead ofnpm ciin CI, which can rewrite the lockfile on the runner and hide the dependency drift CI exists to catch. - Adding
|| trueorcontinue-on-error: trueto a failing test step to force the run green, which silently disables the gate. - Never marking the build job as a required status check, so pull requests merge with a red CI run.
- Reinstalling dependencies from scratch on every run with no cache, adding minutes of avoidable wall-clock time per build.
- Start every code job with
actions/checkout@v4before any step that touches the source. - Use the language's
setup-*action with its built-incache:option instead of configuringactions/cacheby hand. - Run a deterministic install —
npm ci, hash-lockedpip install, orgo mod download— so the lockfile is the source of truth. - Make the build and test jobs required status checks in branch protection so red builds cannot merge.
- Parallelize lint, type-check, and test into separate jobs to keep total pipeline time short.
script stages with needs for the DAGCircleCI checkout step plus orbs for toolchain setupJenkins checkout scm and declarative stagesAzure Pipelines checkout and tasksKnowledge Check
Why is npm ci preferred over npm install in a CI job?
- It installs exactly what the lockfile specifies and fails on drift, instead of mutating it
- It is the only install command the npm registry will accept from a hosted GitHub-managed runner
- It skips installing the listed dev dependencies to keep the install step faster
- It automatically caches and restores
node_modulesbetween runs with no further config
What happens if you forget actions/checkout in a build job?
- The job runs on an empty workspace, so later steps cannot find any source files
- GitHub clones the source into the workspace automatically before the first step runs
- Only the README at the repo root is staged; all other tracked files must be fetched by hand
- The workflow fails to parse immediately and the job reports a YAML syntax error
What does adding continue-on-error: true to a test step do to the CI gate?
- It lets the job report success even when the tests failed, disabling the gate
- It re-runs the failed step up to three times until the tests finally pass
- It pauses and blocks the merge until a reviewer approves the failing tests
- It only annotates the logs and has no effect on the job's final outcome
How does a workflow become a check that actually blocks a merge?
- By configuring it as a required status check in the branch protection rules
- By naming the job
requiredin the workflow file so GitHub treats it as a gate - Any workflow that passes on the head commit blocks the merge by default
- By setting
fail-fast: trueon the job so a failure stops the merge
You got correct