CI/CD·7 min read·

CI Stage Design: Lint, Test, Build, Artifact

Decide what belongs in each pipeline stage so mistakes fail early and builds stay fast and predictable.

NB

Netbay Engineering

Netbay Engineering

On this page

A pipeline is really a decision sequence: each stage should answer one question about your code, cheaply if possible, so you never pay the cost of a slow stage to learn what a fast one could have told you. Get the ordering and separation right and every run becomes faster, failures are easier to read, and the pipeline scales as your team and test suite grow.

The Failure-Cost Ordering Rule

The single most important design rule is: run the cheapest check that can catch a problem before the expensive one. A lint error costs seconds; a failing end-to-end test can cost several minutes of build time. If your end-to-end tests run before your linter, you waste that window on every style bug.

The canonical ordering, from first to last:

  1. **Lint and formatting** — no compile needed, seconds.
  2. **Type checks** — static, fast, catch a whole class of bugs.
  3. **Unit tests** — fast, isolated, run in parallel.
  4. **Build** — produce the artifact, needs the most CPU.
  5. **Integration / end-to-end tests** — slowest, run against the built artifact.
  6. **Publish artifact** — final, only if everything above passed.

What Belongs in Each Stage

Let us be concrete about responsibilities.

  • **Lint** enforces style and obvious errors. It should have zero network access and no dependency on other stages. Keep it fast by running only changed files when you can.
  • **Test** is where unit tests run in parallel across processors. Split them by module so a failure in one area does not block the others.
  • **Build** compiles or bundles into a deployable artifact. This is the first stage that needs real compute and the first that must run reproducibly.
  • **Artifact** is not a stage so much as an outcome. The build produces files that every later environment uses verbatim.

The trap most teams fall into is treating build and test as interchangeable. Testing *source* is fine for unit tests, but integration tests should run against the *artifact* you will actually deploy. Otherwise you validate a build nobody ships.

A Reference Stage Pipeline

Here is a concrete YAML sketch mapping the stages to jobs. Adjust the language and commands to your stack.

yaml
name: ci
on:
  push:
    branches: [main]
  pull_request:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint
        working-directory: web

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test -- --ci

  build:
    runs-on: ubuntu-latest
    needs: [lint, test]
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: web-dist
          path: web/dist

Notice needs: [lint, test] on the build job. That creates an ordering: lint and test run in parallel, and only when both are green does build start. This is exactly the failure-cost ordering in action — fast checks gate the expensive build.

lint seconds test parallel build most CPU artifact ships to envs cheap to expensive fail early one artifact never rebuild per environment built bytes are the truth

Keep Stages Independent and Hermetic

Each stage should run from a clean checkout and install its own dependencies. Do not let one job mutate shared state that another expects. The practical consequence: your pipeline becomes reproducible. A green build today will be green tomorrow because nothing leaks across stages.

This also makes retrying safer. If the build fails on an unrelated flaky test, you can rerun just that job instead of restarting the world, because jobs have no hidden dependencies on each other's side effects.

When to Merge or Split Stages

There is no rule that every stage gets its own job. Merge stages that share a single expensive setup and are unlikely to fail individually; you save redundant setup time. Split stages when they have different resource needs, different cadences, or when you want a failure of one to be independently retryable. For example, split integration tests into their own job that runs only on main, not on every pull request, if they are slow.

Takeaway

Design stages from cheap to expensive, keep each hermetic and independent, gate the expensive build behind the fast checks, and build the artifact once. Fail early, and every later stage trusts the ones before it.

To give your stages predictable compute on their own runner, you can create a Netbay VPS in seconds and scale it to your build needs — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance