We use cookies to enhance your experience on the site
CodeWorlds

CI/CD - the legions' automatic forge

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, @name:

  • 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.

Go to CodeWorlds