RizTech Academy logo
RizTech Academy
CI/CDLesson 3 of 635 min

A pipeline that builds and tests

The heart of CI is the part that runs on every single change: build the app and run the tests, and stop if anything fails. Get this right and broken code never merges; get it wrong (flaky, slow, or skipping the real checks) and the pipeline becomes noise people ignore. This lesson builds the CI half of the greetings pipeline properly — installing, testing, building the image — and covers the practices that keep it fast and trustworthy.

The CI job, step by step

Here is the greetings CI job again, and this time we treat each step as a deliberate choice:

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4          # 1. get the code
      - uses: actions/setup-node@v4        # 2. install the runtime
        with:
          node-version: 20
          cache: npm
      - run: npm ci                        # 3. install dependencies (from the lockfile)
      - run: npm test                      # 4. run the tests (the gate)
      - run: docker build -t greetings:${{ github.sha }} .   # 5. build the image
  • Checkout — bring the repository onto the clean runner.
  • Set up the runtime — install the exact language version (node-version: 20), pinned for reproducibility. cache: npm caches downloaded packages between runs so installs are fast.
  • Install dependencies — npm ci, not npm install. npm ci installs exactly what the lockfile specifies, deleting any existing node_modules first — so CI runs the same dependency versions every time. This reproducibility is the whole reason CI is trustworthy; npm install could quietly resolve different versions.
  • Test — npm test runs the suite. This is the gate: if a test fails, this step fails, the job fails, and nothing after it runs. The greetings tests (verified: two passing tests hitting /health and /) run here on every change.
  • Build the image — confirm the Dockerfile still builds a working image, tagged with the commit SHA (a unique, traceable tag — the deploy lesson uses it).

That is a complete CI pipeline: every push and PR gets a clean checkout, the exact dependencies, the tests, and an image build — and any failure stops it.

Tag images by commit, not latest

Notice greetings:${{ github.sha }} — the image is tagged with the commit SHA, not latest. This matters more than it looks:

  • latest is ambiguous — it means "the most recent build", which changes constantly, so you can never be sure which code a latest image contains. Deploying latest and rolling back becomes guesswork.
  • A SHA (or version) tag is exact and traceable — greetings:a91a983 is this specific commit, forever. You know precisely what is in it, you can deploy an exact version, and you can roll back to a known prior one.

${{ github.sha }} is a GitHub Actions context — a variable the runner fills in (here, the commit's hash). Actions provides many (github.ref, github.actor, etc.); the SHA is the one you use to tag images uniquely. Tagging by commit is a small habit with a big payoff: traceable, rollback-able deployments (the deployment lesson depends on it).

Keep CI fast and trustworthy

The QA course taught this and it applies fully here: a CI pipeline is only valuable if people trust it and it runs fast enough to run on every change. The habits:

  • Fail fast and clearly. Order steps so cheap checks (lint, unit tests) run before expensive ones (a full image build, integration tests), so a common failure is caught in seconds, not minutes. A failed step's log must show what failed.
  • Cache dependencies (cache: npm) and build layers so repeat runs are quick. A slow pipeline gets skipped or worked around.
  • No flaky tests. A test that fails randomly trains people to ignore red — the single most corrosive thing in CI (the QA course's central warning). Fix or quarantine flaky tests immediately.
  • Make failures actionable — clear logs, uploaded reports/artifacts on failure, so a red build is diagnosable, not a dead end.
  • Run the real checks. A pipeline that "passes" because it does not actually run the tests, or tests nothing meaningful, is worse than none — it gives false confidence.

The goal is a pipeline that is fast, always meaningful when it goes red, and therefore believed — so that a green tick genuinely means "this change is safe to merge".

Make it a required check

The final step ties back to the Git module: configure branch protection so the CI check is required — a pull request cannot merge while the pipeline is red. This is what turns CI from advisory into a real gate: not discipline, but a rule that broken code cannot reach main. Combined with the pipeline running on every PR, this means every change that lands on main has been built and tested on a clean machine — which is the entire promise of continuous integration.

Check your work

The CI job: checkout → setup runtime (pinned version, cache: npm) → npm ci (exact lockfile versions — reproducibility; not npm install) → npm test (the gate — fail stops everything) → build the image. The greetings tests (2 passing) run on every change.

Tag by commit, not latest: greetings:${{ github.sha }} is exact and traceable (know what's in it, deploy/roll back precisely); latest is ambiguous (changes constantly — rollback becomes guesswork). ${{ github.sha }} is a GitHub Actions context variable.

Fast & trustworthy: fail fast (cheap checks first) with clear logs; cache deps/layers; no flaky tests (they train people to ignore red); make failures actionable (logs/artifacts); run the real checks (a pass-that-tests-nothing is worse than none).

Make it required: branch protection makes the CI check required so a red pipeline blocks merge — a rule, not discipline. Every change on main has then been built and tested on a clean machine.

Practice

  1. Write a CI job that checks out, installs with npm ci, tests, and builds the image; explain each step.
  2. Explain why npm ci is used instead of npm install in CI.
  3. Explain why tagging an image with the commit SHA beats tagging it latest, for deploys and rollbacks.
  4. Reorder a pipeline so the cheapest checks fail first, and explain the benefit.
  5. Explain why a flaky test is more damaging in CI than a missing test.
  6. Explain what making the CI check "required" does and why it is the real gate on main.

Official documentation

Next: adding a deployment stage.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship