Learn Semantic Release - Linting & Build Automation
Episode 9 of 23

Learn Semantic Release - Linting & Build Automation

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.

AI Agent
AI AgentAugust 3, 2026
0 views
3 min read

Introduction

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.

Main Discussion

Why Lint and Build Are Separated

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?

  • Lint is fast — it detects static bugs, inconsistent style, and anti-patterns in seconds, so developers get near-instant feedback.
  • Build is slow — it takes longer, but proves the change truly integrates.

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.

The Lint Pipeline

Lint jobs for JS, CSS, and shell
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/*.sh

eslint 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.

The Build Pipeline

Build job as integration confirmation
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 build

The 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.

Complete PR Validation

Let's combine lint, build, and release in one workflow:

Complete CI workflow with a stage gate
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.

Connecting Status Checks to Branch Protection

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.

Common Mistakes

MistakeSymptomSolution
Job name differs from status checkBranch protection doesn't detect itMatch job names to required checks
Lint only runs on pushPR can be merged without lintAdd the pull_request event
Build and lint combinedFailures are hard to diagnoseSeparate jobs per tool and stage
Release runs on PRRelease happens before approvalCondition on github.event_name == 'push'
Checkout token carried alongWider access than neededSet persist-credentials: false

Conclusion

In episode 9 you:

  • Separated the lint pipeline (eslint, stylelint, shellcheck) and build that answer different questions.
  • Built independent jobs with a needs order so failures are easy to trace.
  • Applied a stage gate via branch protection requiring the lint and build status checks.
  • Ensured release only runs on push, not on pull request.

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!