We use cookies to enhance your experience on the site
CodeWorlds

Advanced CI/CD - matrix builds, artifacts and cache

The tests pass on your machine. Not on your colleague's - a different Node version. Not on the server either, because somebody forgot to rebuild the image. Each of those costs half a day, and all three share one cause: the check depends on who ran it and where.

Continuous Integration removes that dependency: it is the automatic building and testing of code after every push. Not manual weekly deployment, not memory monitoring, not database backups - just one check, always identical, on a machine nobody configured to their own taste.

When to run

A workflow begins by naming the events that wake it:

1name: CI
2
3on:
4  push:
5    branches: [main]
6  pull_request:

on: pull_request
runs the workflow when a pull request is created - and that is the moment that matters most, because the check happens before the change enters the main branch.

on: push
reacts to code being pushed; the two are usually used together. Other triggers exist but do something else:
on: schedule
runs a workflow at an appointed time, and
on: deployment
on a deployment event. Neither fires when somebody opens a pull request.

The order of jobs

A pipeline splits into jobs, and their order is not arbitrary:

  1. Lint - a formatting check.
  2. Build - TypeScript compilation.
  3. Test - unit tests.
  4. Docker build & push - building and pushing the image.

The organising rule reads: cheapest and fastest first. Lint finishes in seconds, so there is no sense waiting five minutes for tests only to learn about a missing semicolon. Compilation precedes testing, because tests of uncompiled code will not run anyway. The Docker image is built last - it is the most expensive step and there is no point starting it if anything earlier failed.

Step by step

Each job is made of steps, and the first is always fetching the code:

1jobs:
2  test:
3    runs-on: ubuntu-latest
4    steps:
5      - name: Checkout code
6        uses: actions/checkout@v4
7
8      - name: Setup Node
9        uses: actions/setup-node@v4
10        with:
11          node-version: 20
12
13      - name: Install dependencies
14        run: yarn install --frozen-lockfile
15
16      - name: Tests
17        run: yarn test --coverage

A step's shape is fixed:

- name: Checkout code
, then
uses:
, then
actions/checkout@v4
.
name
is the label shown in the interface,
uses
names a ready-made action, and
run
a command to execute in the shell.

The order inside the test job is equally fixed:

actions/checkout@v4
fetches the code,
actions/setup-node@v4
installs Node,
yarn install --frozen-lockfile
pulls the dependencies, and finally
yarn test --coverage
runs the tests.

The

--frozen-lockfile
flag deserves attention: it installs exactly the versions from the lockfile and aborts if they do not match. Without it the pipeline could quietly install a newer dependency and test something other than what you will run in production.

Secrets

A pipeline needs passwords - for the image registry, the server, the test database. We keep them in Settings > Secrets and variables > Actions, in the repository's settings.

Not in a

.env
file in the repository, not in a commit comment, not in
README.md
- all those places are public to anyone with access to the code, and Git history cannot simply be wiped.

A stored secret enters the workflow by reference, never in the open:

1      - name: Log in to the registry
2        run: docker login -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_TOKEN }}

The value is substituted at run time and appears masked in the logs.

Three improvements

Finally, three things separating a pipeline that works from one you can bear to wait for.

Matrix runs the same job across several configurations at once:

1    strategy:
2      matrix:
3        node-version: [18, 20, 22]

Three Node versions, three parallel runs, one description. This is the answer to the problem from the start of the lesson - "it does not work on my colleague's machine" now surfaces at pull request time rather than a week later.

Cache remembers downloaded dependencies between runs. Without it every run fetches the same packages afresh; with it installation shrinks from minutes to seconds.

Artifacts are the files a job leaves behind - a coverage report, a built package, screenshots from tests. They outlive the run and can be downloaded from the interface, which is sometimes the only way to understand why something failed only on the CI machine, @name.

Summary

The check no longer depends on who runs it:

  • CI is the automatic building and testing of code after every push - not manual deployment, not memory monitoring, not backups,
  • on: pull_request
    runs the workflow when a pull request is created
    ;
    on: schedule
    runs at an appointed time,
    on: deployment
    on deployment,
  • job order: Lint → Build → Test → Docker build & push, following "cheapest first",
  • a step's shape:
    - name:
    , then
    uses:
    , then the action name;
    run:
    executes a shell command,
  • the test job in order:
    actions/checkout@v4
    ,
    actions/setup-node@v4
    ,
    yarn install --frozen-lockfile
    ,
    yarn test --coverage
    ,
  • --frozen-lockfile
    installs exactly the lockfile versions and aborts on a mismatch,
  • secrets belong in Settings > Secrets and variables > Actions - not in
    .env
    , not in a comment, not in
    README.md
    ,
  • matrix runs a job across several configurations in parallel, cache shortens dependency installation, artifacts preserve files after the run ends.

In the next lesson we will drop from numbers to a single request - you will meet distributed tracing. For now remember: a pipeline is one check, always performed the same way - and a matrix is what stops "it works on my machine" from being an argument.

Go to CodeWorlds