NestJS course Β· Module 9: Deployment and Infrastructure
CI/CD - the legions' automatic forge
In this lesson6
A manual deployment always looks the same: somebody logs on to the server, pulls the code, runs the build, restarts the application. It works - as long as that somebody remembers every step, performs them in the same order, and does not deploy at five o'clock on a Friday.
A legionary forge did not rely on the smith's memory. It had a settled order: the same steel, the same temperature, the same tempering, the same mark on every blade. That is why a sword forged in Gaul fitted a scabbard made in Rome. CI/CD is that order written down in a file.
What the abbreviation means
CI/CD stands for Continuous Integration / Continuous Deployment. Not "Container", not "Component", not "Code": both words refer to the process, not to what is being processed.
Each half solves a different problem. Continuous Integration is the automatic building and testing of code after every change - so that a fault comes to light within minutes, rather than a week later when nobody remembers whose change introduced it. Continuous Deployment is the automatic release of a change that has passed verification - so that the road from finished code to production does not depend on whether somebody has the time.
GitHub Actions
There are many tools; we shall stay with one. GitHub Actions automate build, test and deployment after each push and pull request (PR) - that is their whole role.
Three misunderstandings are worth clearing away at once. They are not used only for managing issues - that is a separate GitHub feature. They are not available only for private repositories; public ones use them free and without a minute limit. And they do not replace Docker - on the contrary, it is in a workflow step that you call docker build and docker push, so the one makes use of the other.
The configuration is a YAML file in the .github/workflows/ directory:
1# .github/workflows/deploy.yml
2name: Deploy
3
4on:
5 push:
6 branches:
7 - mainThe trigger, or when this should run
The fragment above is the trigger - the statement of the event that starts the workflow. It is written in four nestings, always in this order: on: opens the events section, push: names the particular event, branches: narrows it to chosen branches, and - main is an item in the list of those branches.
In YAML the indentation carries meaning - each further level moves two spaces to the right, and that is what builds the structure. The dash before main marks a list item, so further branches are added on the lines below.
There is more than one event. pull_request: will run the checks on every PR - and that is usually more valuable than push, because it catches a fault before the change reaches the main branch.
The pipeline's four stages
The real work happens in a job, and its steps have a settled order:
1jobs:
2 build:
3 runs-on: ubuntu-latest
4 steps:
5 - uses: actions/checkout@v4
6
7 - uses: actions/setup-node@v4
8 with:
9 node-version: 20
10
11 - run: npm ci
12 - run: npm test
13 - run: npm run build
14 - run: docker push registry.imperium.rome/legion-api:latestThe order of the stages is not a matter of taste: install dependencies (npm ci) β run linter and tests (npm test) β build the application (npm run build) β deploy to production (docker push).
One principle governs it: the cheaper the stage, the earlier it goes. A linter finishes in seconds, tests in minutes, an image build in a quarter of an hour. Were the build to run before the tests, every typo would cost the full build time - and end in failure regardless.
The two steps at the start are preparation rather than pipeline stages. actions/checkout fetches the repository's code onto the machine the workflow runs on - without it the directory is empty. actions/setup-node installs the stated version of Node.js and thereby settles the "it works on my machine" argument: the version is written in the file, so every run gets the same one.
Note npm ci rather than npm install. The first installs exactly what is recorded in package-lock.json and stops if that disagrees with package.json. The second may quietly raise a dependency's version - and then CI is testing something other than what you have locally.
When a stage fails
The steps run one after another, and the first failure stops the rest. This is the default behaviour and the desirable one: if the tests do not pass, building an image and pushing it to a registry would merely waste time, and at worst deploy a broken version.
Hence a practical division. Run the checks - install, linter, tests - on every push and every PR, on every branch. Restrict deployment to the main branch, through the branches: - main of the earlier section. Work on a feature is then verified from its first commit, while nothing reaches production until the change is merged.
Summary
The forge tempers every blade alike:
- CI/CD stands for Continuous Integration / Continuous Deployment - not Container, not Component, not Code,
- CI is automatic building and testing after every change, CD the automatic release of whatever passed verification,
- GitHub Actions automate build, test and deployment after each push and PR - they are not for issues alone, not reserved for private repositories, and do not replace Docker,
- the configuration lives in
.github/workflows/and is a YAML file whose structure is built by indentation, - the trigger syntax:
on:βpush:βbranches:β- main;pull_request:checks a change before it is merged, - the order of stages:
npm ciβnpm testβnpm run buildβdocker push, cheapest first, actions/checkoutfetches the code,actions/setup-nodefixes a Node.js version identical for every run,npm ciinstalls exactly what is inpackage-lock.json;npm installmay change versions quietly,- the first failed step stops the others - which is why checks run everywhere and deployment only from the main branch.
We shall return to CI/CD in the infrastructure module, with builds across several versions at once, artifacts and dependency caching. For now remember: a pipeline is not there to deploy faster. It is there to deploy the same way - and the speed follows.
Code for this lesson: .github/workflows/ci.yml
1# CI/CD Pipeline - Automated Cohortyard of the Empire
2
3name: Roman Empire CI/CD
4
5on:
6 push:
7 branches: [main, develop]
8 pull_request:
9 branches: [main]
10
11# Environment variables for the entire workflow
12env:
13 NODE_VERSION: '18'
14 REGISTRY: ghcr.io
15
16jobs:
17 # === JOB 1: Tests ===
18 test:
19 name: Run Tests
20 runs-on: ubuntu-latest
21
22 steps:
23 - name: Checkout code
24 uses: actions/checkout@v4
25
26 - name: Setup Node.js
27 uses: actions/setup-node@v4
28 with:
29 node-version: '18'
30 cache: 'npm'
31
32 - name: Install dependencies
33 run: npm ci
34
35 - name: Run linter
36 run: npm run lint
37
38 - name: Run unit tests with coverage
39 run: npm run test:cov
40
41 - name: Run E2E tests
42 run: npm run test:e2e
43
44 # === JOB 2: Build ===
45 build:
46 name: Build Application
47 needs: test
48 runs-on: ubuntu-latest
49
50 steps:
51 - uses: actions/checkout@v4
52
53 - name: Build Docker image
54 run: docker build -t roman-api:latest .
55
56 - name: Run container health check
57 run: |
58 docker run -d --name test-api -p 3000:3000 roman-api:latest
59 sleep 10
60 curl -f http://localhost:3000/health || exit 1
61 docker stop test-api
62
63 # === JOB 3: Deploy ===
64 deploy:
65 name: Deploy to Production
66 needs: build
67 runs-on: ubuntu-latest
68 if: github.ref == 'refs/heads/main'
69
70 steps:
71 - uses: actions/checkout@v4
72
73 - name: Deploy
74 run: echo "Deploying Roman Empire API..."
75
76 - name: Verify health
77 run: echo "Checking health endpoint..."
78
79# CI = Continuous Integration (tests on every push)
80# CD = Continuous Deployment (automatic deploy after merge)
81Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. CI/CD stands for:
2. GitHub Actions in the context of CI/CD:
Hands-on tasks in the game
- Code editor
Create .github/workflows/deploy.yml with steps: install, lint, test, build, deploy
- Vertical ordering
Arrange the CI/CD pipeline stages from first to last:
- Vertical ordering
Arrange the GitHub Actions workflow trigger syntax: