# What a CI/CD pipeline is (and why manual deployment costs more than it looks)
Table of Contents
In a lot of companies, releasing a website or an application is still a manual gesture. Someone opens a terminal, runs a sequence of commands learned by heart, waits, reloads the page and hopes. If the site shows up, everyone moves on. If it does not, the hunt begins: what changed, who touched what, how it looked before.
The problem is not the skill of the person typing. The problem is that a manual release is an unwritten procedure: slightly different every time, impossible to review beforehand, and tied to one person. When that person is on holiday, in a hurry or changes job, releasing becomes a risk.
A CI/CD pipeline answers that problem. It is the idea of writing down, once, what must happen when the code changes, and letting a machine do it the same way every time, checking before publishing.
Continuous integration: someone else checks your code
The first half of the acronym is CI, continuous integration. The idea is simple: every time someone saves work to the shared repository, a neutral machine downloads the project from scratch, installs everything it needs and runs the checks.
That “from scratch” is the part that matters. On a developer’s laptop the project also works thanks to things installed months ago and forgotten: a system library, an environment variable, a config file that was never committed. The CI machine has no such memory. If the project works there, it really works, and the industry’s favourite sentence, “it works on my machine”, stops being an opinion.
The practical benefit is when you find out. A test failing two minutes after you save costs five minutes, because you still remember what you were doing. The same failure found in production three weeks later costs a day, plus the phone call from the client.
Continuous delivery: from checked code to a live site
The second half is CD, and it is worth separating two meanings that often get mixed up.
Continuous delivery means the verified code is always ready to go to production, and publishing happens when somebody presses a button. Continuous deployment goes further: if every check passes, publishing happens on its own, with no human involved.
These are not skill levels, they are different choices: an online shop releasing twenty times a day benefits from automatic deployment, a firm updating a client’s management software once a month lives perfectly well with the button. When starting out, the button is the sensible default: you get almost all the value and keep control over when things change.
What a pipeline actually looks like
A pipeline is a text file, versioned alongside the code, describing the steps to run. Here is the minimal version with GitHub Actions, one of the most common systems:
name: CIon: push
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npm testIn plain English: on every push, start a clean Linux machine, download the code, install Node 22, install dependencies exactly as recorded in the lock file (npm ci, not npm install) and run the tests. If any command fails, the pipeline turns red and whoever made the change gets notified.
Pinning the Node version is not pedantry: it is the same reason it pays to declare the runtime version in the project. A pipeline that uses “the latest available version” will one day break on its own, with nobody having touched anything.
From there you grow it by adding publishing, which only runs if the tests passed and only from the main branch:
deploy: needs: test if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci && npm run build - run: ./deploy.sh env: DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}The two lines that matter are needs: test, which prevents unverified code from being published, and if:, which limits publishing to the main branch: work on a side branch gets checked but never goes live.
Same idea, different systems
The way you write it changes, the idea does not. The same pipeline on GitLab CI looks like this:
stages: - test - deploy
test: image: node:22 script: - npm ci - npm test
deploy: stage: deploy only: - main when: manual script: - ./deploy.shHere when: manual is the button described above: the work is prepared automatically, a person decides when it goes out. Forgejo and Gitea, which host code on your own servers, use a syntax nearly identical to GitHub Actions, and veterans like Jenkins have been doing this for twenty years. Picking the tool matters far less than deciding to have a written procedure at all.
One term to know: the machine running the steps is called a runner. It can be provided by the service, where you pay per minute, or be a machine of yours if the code must not leave the company network. In the second case it has to be installed, updated and isolated, because it executes code on every change to the project.
What is worth putting inside
The right order of steps goes from fastest to slowest, so a trivial mistake does not keep you waiting ten minutes to hear about it:
- Formal checks: formatting and static analysis, a few seconds.
- Automated tests: the core of the verification.
- Build: if the project does not compile, there is no point going further.
- Project specific checks: this is the part almost nobody exploits.
- Publishing.
Point 4 deserves an example, because it is where a pipeline stops being generic and becomes yours. A check is simply a command that exits with zero when all is well and something else when it is not:
#!/usr/bin/env bashset -euo pipefail
if grep -rn "console.log" src/; then echo "Leftover debug calls found in src/" exit 1fiThe same logic verifies things that are not code at all: oversized images committed by mistake, internal links pointing at pages that no longer exist, translation files out of sync between two languages, editorial rules for a blog. These are the errors nobody catches by re-reading, and a machine catches every time.
Secrets, the part most often got wrong
Publishing needs credentials: a provider token, an SSH key, a database password. Three rules, in order of importance.
Never in the repository. A token written into a versioned file stays in the history even after you delete it: removing it from the file does not remove it from earlier commits. When it happens, the only serious answer is to rotate that token.
In the CI system, with the protections on. The two settings to look for are masking, which stops the credential from appearing in job logs, and restriction to protected branches. Without those, the token is readable by anyone who can edit the pipeline file.
With least privilege. The token that publishes the site should be able to publish the site, not administer the whole account.
There is one last aspect people tend to ignore: a pipeline installs dependencies, and installing dependencies means running code written by strangers on a machine holding production credentials. That is why it pays to understand what happens when a package is compromised, and why the runner should be isolated from the rest of the infrastructure.
What it costs, honestly
A pipeline is not free: it has to be written, maintained, and every so often it breaks by itself because a token expires or a version changes. On tiny projects, with a single developer and a release every six months, it may not be worth it.
Then there is a cultural risk, and it is the serious one: a pipeline that has been red for weeks is not a pipeline, it is a broken traffic light. Once red becomes normal, people learn to ignore it and the checks stop meaning anything. Green must be the ordinary state: when a check produces too many false alarms, either fix it or remove it, but do not leave it blinking.
Finally, a pipeline verifies only what you told it to verify. It does not replace a restore you have actually tested, nor the ability to go back quickly when something slips through anyway.
In short
A CI/CD pipeline turns releasing from a memorised procedure into an executable document: continuous integration checks every change on a clean machine, continuous delivery takes what passed to production, with or without a button in between. It lives in a text file versioned with the code and works the same way on GitHub Actions, GitLab CI or a self-hosted system. As with infrastructure described in text files, the real gain is not speed: it is no longer entrusting to one person something the company does every week.
