Building separate lint and build pipelines as a quality gate before release. Wiring status checks into pull request validation and applying stage gates so that only code that passes lint and build is allowed to be merged into the main branches.

In episode 8 the commit analyzer determined when a release happens, but the code quality is decided much earlier. Semantic-release only bumps versions and publishes packages — it doesn't care whether the code is broken. If bad code makes it all the way to a release, the result is still a bad release.
This episode builds the quality gate: a lint pipeline that separates eslint, stylelint, and shellcheck from the build, wires them into pull request validation, and applies a stage gate so only code that passes can be merged.
Lint and build answer two different questions. Lint asks: is this code written correctly and consistently? Build asks: can this code be compiled into a working artifact?
By separating them, failures are easier to identify. A lint failure means a code problem; a build failure means an integration problem. If they're combined, you won't know which one to fix first.
name: Lint
on:
pull_request:
push:
branches: [main, staging]
jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run lint
stylelint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx stylelint "src/**/*.css"
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: shellcheck scripts/*.sheslint handles JavaScript and TypeScript, stylelint handles CSS, shellcheck checks shell scripts — each as an independent job. Independent jobs mean each tool's results can be viewed and analyzed on its own, and one failure doesn't hide the others.
jobs:
build:
runs-on: ubuntu-latest
needs: [eslint, stylelint, shellcheck]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run buildThe needs keyword ensures the build only runs after all three lint jobs pass. This saves runner time: there's no point building code that already clearly violates the rules.
Let's combine lint, build, and release in one workflow:
name: CI
on:
pull_request:
push:
branches: [main, staging]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run lint
- run: npx stylelint "src/**/*.css"
- run: shellcheck scripts/*.sh
build:
runs-on: ubuntu-latest
needs: [lint]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run build
release:
runs-on: ubuntu-latest
needs: [build]
if: github.event_name == 'push'
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- run: npm ci
- name: Semantic Release
run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Three layers: lint runs all the lint tools, build waits for lint and builds the artifact, release waits for build and only runs on a push to main or staging — not on a pull request. With persist-credentials: false, the checkout token isn't carried into subsequent steps.
Warning
To enforce the stage gate, enable branch protection on main
and staging in Settings > Branches, then "Require status checks to pass
before merging". The status check names must match the job names exactly,
e.g. lint and build, and those jobs must run on the pull request
event. If a job only runs on push, its status check won't appear on the
PR and merging won't be blocked.
A status check is the proof GitHub displays on the pull request page. When a job named lint fails, the PR shows a cross and can't be merged while branch protection is active. When everything is green, the merge button re-enables.
This is what a stage gate means: the only door for code into main and staging is a PR that passes every gate. Releases only happen from branches whose code is already validated.
Tip
Keep the lint jobs fast. A lint that takes more than two minutes usually signals scope that's too broad or caching that's off. A fast pipeline makes the stage gate feel light, and developers won't be tempted to find shortcuts.
| Mistake | Symptom | Solution |
|---|---|---|
| Job name differs from status check | Branch protection doesn't detect it | Match job names to required checks |
| Lint only runs on push | PR can be merged without lint | Add the pull_request event |
| Build and lint combined | Failures are hard to diagnose | Separate jobs per tool and stage |
| Release runs on PR | Release happens before approval | Condition on github.event_name == 'push' |
| Checkout token carried along | Wider access than needed | Set persist-credentials: false |
In episode 9 you:
needs order so failures are easy to trace.lint and build status checks.In episode 10 we'll use Semantic Release Dry Run & Verification — a full rehearsal without side effects to make sure the generated version and changelog are correct before the real release. See you in episode 10!