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.
A workflow begins by naming the events that wake it:
1name: CI
2
3on:
4 push:
5 branches: [main]
6 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: pull_request
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.A pipeline splits into jobs, and their order is not arbitrary:
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.
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 --coverageA step's shape is fixed:
, then - name: Checkout code
, then uses:
. 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:
fetches the code, actions/checkout@v4
installs Node, actions/setup-node@v4
pulls the dependencies, and finally yarn install --frozen-lockfile
runs the tests.yarn test --coverage
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.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.
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.
The check no longer depends on who runs it:
on: pull_request runs the workflow when a pull request is created; on: schedule runs at an appointed time, on: deployment on deployment,- name:, then uses:, then the action name; run: executes a shell command,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,.env, not in a comment, not in README.md,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.