GitHub Actions Quality Gates: Check Before You Deploy
Learn CI quality gates from a real workflow: typecheck, lint, tests and build, job dependencies with needs, fail-fast order, and branch protection.
By Matthews WongReviewed on
Key takeaway
A CI quality gate is a check whose failure stops the pipeline before the next stage. In GitHub Actions, steps run in order and stop at the first failure, a job with needs is skipped when the job it needs fails, and branch protection can require checks before a merge. Cheap checks go first so a bad commit fails fast.
You will be able to
- Order typecheck, lint, tests and build into a pipeline where each gate protects the next.
- Use needs so a failing job skips everything downstream, and explain what a skipped job means.
- Say what this site's gates cover, what they do not, and where branch protection fits.
Step through the diagram
Press Play, or step with the arrows. Every step is written out below the diagram.
How a failing check stops a deploy
A push runs typecheck, then lint, then the build, then the deploy. A failure at the first gate skips every later job, and a check that CI never runs protects nothing.
Keys: ← → step · Home / End · K play or pause
One chain: push, typecheck, lint, build, deploy. Each job waits for the one before it. The two faded boxes at the bottom are the gaps we come back to.
The push triggers the workflow. Typecheck and lint are steps of the verify job, so they run in order: lint only starts if typecheck passed.
Predict the next step Typecheck reports an error. What happens to the build and deploy jobs? (They run anyway · They are skipped · Only deploy is skipped)The type error fails the step, so the verify job fails. A job that needs it is skipped, and so is every job that needs that one. Nothing is built or deployed.
Fix the error and push again. Typecheck passes, lint passes, and only then does the build job start. The deploy waits for the build in the same way.
needs: verifyThis repo has an npm test script, but the workflow never calls it. A failing test would not stop the deploy. The gate has a gap exactly where no job runs.
Predict the next step A branch rule requires a check, but that check ends up skipped. Can the pull request merge? (No, skipped counts as failed · Yes, skipped counts as satisfied)Required status checks are a GitHub setting, not workflow code. With one turned on, a pull request cannot merge until the check passes. A skipped check counts as satisfied.
What is a quality gate?
A quality gate is a check that must pass before the pipeline continues. The idea is simple: decide what must be true of every commit, run those checks automatically, and make failure stop the line. The typical candidates are a typecheck, a linter, automated tests and a build.
Read more (1)Show less
The point is not ceremony. A check you run by memory gets skipped on the day you are in a hurry, which is the day you need it most. A gate does not forget.
How do steps and needs turn checks into gates?
Inside one job, steps run in order. Unless a condition says otherwise, a step runs only if the previous steps succeeded, because the default condition is success(). The runner decides pass or fail from the command's exit code, so a failing typecheck or linter is enough to stop the job.
Read more (1)Show less
Between jobs, needs names the jobs that must finish first. If a job fails or is skipped, every job that needs it is skipped too, unless a condition such as always() overrides that. That is what makes a chain of jobs a gate: build needs verify, deploy needs build.
Which checks go first, and why fail fast?
Put the cheapest, most informative checks first. A typecheck or linter reports a mistake long before a full build would, so a bad commit is rejected early and the slow, expensive jobs never start. Failing fast saves runner time and, more usefully, gets the signal to you sooner.
Read more (1)Show less
A matrix adds a related switch. With fail-fast on, a failure in a matrix job that is not allowed to fail cancels the matrix jobs still in progress or queued. This site has no matrix, but the rule is the same: stop spending time on a run that cannot succeed.
Where do this site's gates stop?
The verify job runs npm ci, then a typecheck, then the linter. The build job needs verify and the deploy job needs the build job, so no image is built from a commit whose typecheck or lint failed. Next's own build deliberately skips typechecking and linting, because they already ran once, and the config says to remove that skip if verify ever stops gating the build.
Read more (1)Show less
One honest gap and one extra gate. The gap: the repo has an npm test script, but neither workflow calls it, so tests are not a gate. The extra: the build also triggers the prebuild script, which runs the site's parity, placement, import and message-key checks, so those do gate the image. A separate smoke workflow builds the site and runs browser tests on pull requests, but the deploy does not wait for it.
What does branch protection add?
A workflow reports a result; branch protection is what makes the result binding. With required status checks on, all required checks must pass before changes can be merged into the protected branch. A check with a successful, skipped or neutral status counts as satisfied, so a gate that is skipped by accident is a gate that is open.
Read more (1)Show less
Two details trip people up. Job names should be unique across workflows, since a reused name can give ambiguous results and block merges. And this deploy workflow runs on pushes to main, not on pull requests, so to gate a pull request you need a workflow that runs on the pull_request event. The repo files cannot show whether branch protection is enabled; that lives in the repository settings.
The real config behind this lesson
Excerpts from the repository that builds and deploys this website, quoted as they are.
- run: npm ci
- run: npx tsc --noEmit
- run: npm run lint
build-and-push:
name: Build & push image
needs: verifyThe whole gate. Three steps run in order, and a failure in any of them fails the verify job. The build job names it in needs, so a failed verify skips the build and, through the build, the deploy.
// Run `npx tsc --noEmit` yourself before pushing, as CLAUDE.md already says.
// If the `verify` job is ever removed or stops gating the build, these two
// lines must come out in the same change.
typescript: { ignoreBuildErrors: true },
eslint: { ignoreDuringBuilds: true },A deliberate trade: the Docker build does not typecheck or lint again, because the gate already did. The comment is the safety valve, naming the exact condition under which these two lines must go. A gate you depend on should always be written down like this.
"prebuild": "node scripts/generate-blog-sources.mjs --check && node scripts/game/check-placements.mjs && node scripts/game/check-imports.mjs && node scripts/check-message-keys.mjs",npm runs a script named prebuild automatically before build. Every check is joined with &&, so the first one that exits non-zero stops the chain and fails the build. That makes it a gate inside the build step, with no workflow change.
pull_request:The one line that makes this workflow a pull-request check. The deploy workflow has no such trigger, which is why a gate for pull requests needs its own workflow or its own trigger.
Mistakes that cost real time
A check job nobody waits for
needs is what orders jobs. If the build job does not name verify, nothing makes it wait for verify, and a failed check no longer stops it. Whenever you add a gate, also add it to the needs of the job it should protect.
Turning checks off downstream without a gate upstream
This site's build skips typecheck and lint because verify runs them. Remove verify, or let it stop gating the build, and type errors ship silently. The config comment names this coupling; copy that habit whenever one check replaces another.
Tests exist but CI never runs them
A script in package.json protects nothing until a job calls it. Here npm test is not in the verify job, so a failing test does not stop a deploy. List what your pipeline actually runs, then compare it with what your package.json says.
A required check that is skipped
Branch protection treats a skipped check as satisfied. If a path filter or an upstream failure skips the required job, the merge button turns green. Make sure the required check runs on every pull request, or accept that a skip means pass.
| What it catches | In this site's deploy gate | |
|---|---|---|
| Typecheck | Wrong types and broken contracts | Yes, verify job |
| Lint | Suspicious code and style rules | Yes, verify job |
| Build | Code that cannot compile or bundle | Yes, build job (prebuild checks first) |
| Tests | Behaviour that changed by accident | No, npm test is not called |
Do it on your own machine
Any small repo with GitHub Actions works. You will build a two-job gate, break it on purpose, and watch the second job get skipped.
Run the checks locally first, in the order you want CI to run them. Fix anything that fails before you push.
npx tsc --noEmit && npm run lintCreate .github/workflows/gate.yml with a verify job and a build job that needs it.
name: gate on: push: branches: [main] pull_request: jobs: verify: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm - run: npm ci - run: npx tsc --noEmit - run: npm run lint build: needs: verify 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 buildPush it and confirm both jobs pass. Then add a deliberate type error, such as assigning a string to a number, and push again.
const count: number = 'three';Open the run. The verify job should be red and the build job should show as skipped, not run. Fix the error and push to turn it green.
Optional: in the repository settings, add a branch protection rule for main that requires the verify check, then open a pull request and see the merge blocked while it fails.
Three questions before you move on
Pick an answer to see why it is right or wrong. Nothing is saved or graded, so guess freely.
Answer in your head, then check
1.Which jobs must pass before this site's deploy job can start?Show the answer
Answer:Build and push, which itself needs verify (typecheck and lint). The deploy needs the build job, so a failed verify skips both.
2.Why does the Docker build set ignoreBuildErrors and ignoreDuringBuilds?Show the answer
Answer:Typecheck and lint already ran once in the verify job, so repeating them inside the image build only costs time. The comment names the exit condition: if verify stops gating the build, remove both lines.
3.What does branch protection add that a workflow alone does not?Show the answer
Answer:It makes a check binding: with required status checks on, the pull request cannot merge until the check passes. Without it, a red check is only a warning.
Related deep dives on this site
Sources
Primary sources checked on the research date. Follow them for the current version.