NestJS course Β· Module 9: Deployment and Infrastructure

CI/CD - the legions' automatic forge

6 min read
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      - main

The 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:latest

The 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/checkout fetches the code, actions/setup-node fixes a Node.js version identical for every run,
  • npm ci installs exactly what is in package-lock.json; npm install may 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)
81

Spotted 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. 1. CI/CD stands for:

  2. 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:

Useful articles