ES

Your first DevOps pipeline: from push to production

Leo G.7 minLeer en español

Roller conveyor belts crossing in an automated warehouse.
Roller conveyor belts crossing in an automated warehouse.

Somebody merged mid-afternoon and production broke. The worst part is not that: it is that the test which would have caught it already existed. Nobody ran it, because running it depended on somebody remembering, and that afternoon nobody did.

That is the problem automation solves: not writing better tests, but taking the remembering out of the loop. Below is what is worth running on its own every time you push code, in what order —which matters more than it sounds— what is better left out, and how all of it reaches production without the deployment being an event. It takes an afternoon, and the minimal version is ten lines.

What a pipeline is, in one line

If you have never set one up: a pipeline is a file in your repository saying which commands to run when something happens — for example, when somebody pushes code or opens a proposed change.

It is not a program you install. It is a list of steps executed by somebody else’s server, called a runner, on a freshly cloned copy of your project. That clean copy is the whole point: if something works on your machine but was never declared in the repository, this is where it falls over. The file is written in YAML, a text format where indentation defines the structure, and it lives versioned alongside the code.

Running those steps on every change is called continuous integration, or CI. When the result also gets published automatically, you add continuous delivery, or CD.

The order matters more than the contents

Here is the part almost nobody mentions, and the one that makes the most difference. Most people build the pipeline by putting the steps in whatever order occurred to them. The right order is fastest to slowest, and it is not an aesthetic choice.

A pipeline that takes eighteen minutes to tell you a semicolon is missing teaches the team to ignore it. People merge without waiting, check the result the next day, and the pipeline becomes decoration. Whereas one that tells you “this does not even compile” in forty seconds gets consulted, because it answers quickly.

The order that works, with typical timings:

Step What it catches How long
Formatting and style spacing, quotes, unused imports seconds
Type checking 80% of the silly mistakes ~1 min
Unit tests the logic, with no database or network 1-2 min
Build what only breaks when you compile 2-4 min
Integration tests the system together, for real 5-15 min

The rule behind all of it is called failing early: each step runs only if the previous one passed. If type checking fails, there is no point spending ten minutes on integration tests — that code is not going anywhere anyway.

A complete example

This is a real, entire pipeline for GitHub Actions. Other services change the names, not the ideas.

name: CI
# Runs on pushes to main and on every proposed change targeting main.
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # `cache: npm` keeps dependencies between runs. Without it, every
      # run downloads them again: usually the slowest step of all.
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      # `npm ci` installs exactly what the lockfile says. `npm install`
      # can bump versions without telling you, and then the pipeline
      # stops being reproducible.
      - run: npm ci

      # Fastest to slowest: each step runs only if the previous passed.
      - run: npm run lint
      - run: npm run check
      - run: npm test
      - run: npm run build

If that looks like too little, it is fine: it is close to everything a small project needs. What the file does not show, and has to be configured once in the repository, is the part that makes it binding: blocking the merge when the pipeline fails. Without that, it is a suggestion.

One prerequisite worth saying out loud: those four commands have to work on a freshly cloned copy, with no manual steps first. If they work on your machine because of an environment variable you loaded three months ago, they will fail on the runner — we covered that here.

What this costs

The question that stops people before they start, and the short answer is that for a small project, nothing.

On GitHub Actions, public repositories do not pay for minutes. Private ones come with a free monthly allowance that is more than enough for a personal project: the pipeline above uses about five minutes per run, so the allowance covers hundreds of runs a month. GitLab and the rest have comparable schemes.

What does cost money, when it does, is missing caching and slow steps running where they should not. Keeping dependencies between runs —that cache: npm line in the example— and not sending the full suite on every push are the two things that keep usage low. Optimise, as always, only once the number starts to bother you.

What does not belong in the pipeline

As important as what to put in:

  • Tests against real external services. If your pipeline calls a third party’s API, it inherits that third party’s outages. One day it fails with nobody having touched anything, and that is where distrust starts.
  • Anything slower than someone is willing to wait for. If the full suite is forty minutes, it does not go on every push: it goes once a night, or before deploying.
  • Secrets written into the file. The YAML is versioned and visible. Keys go in the service’s secret store and come in as variables.
  • Steps that fail often for reasons that are not the code. This one is the worst, and it deserves its own paragraph.

A step that fails one time in five for no reason —a test that depends on the clock, a flaky download— is worse than not having that step at all. It teaches the team that red means nothing, and the day red does mean something, nobody will look. If a step is unreliable, fix it or remove it; do not leave it “to look at later”.

From green to production

The other half is deployment. The idea is the same: it should not depend on somebody remembering the steps.

What triggers it. The simplest thing that works is that a merge to the main branch, with the pipeline green, deploys on its own. No button, no deployment window, no forbidden Fridays.

An intermediate environment. Before production it is worth having a test environment —usually called staging— that mirrors the real one and gets changes first. It is for seeing the change working against data resembling the real thing, which is different from seeing the tests go green.

Separating deploying from enabling. A feature flag is a switch that leaves the new code shipped but turned off, and turns it on when you decide. It sounds like extra complication until the first time you switch something broken off in ten seconds, instead of rushing out a fix.

And the hard rule: if you do not know how to go back, that is not automated deployment, it is an automated leap into the void. Before automating the way out, you need to know how long the way back takes, have it tested, and make sure it does not depend on one particular person.

The minimum that already pays

You need none of the above to start gaining. If you are going to do one thing, make it this: have the tests run on their own on every proposed change, and block the merge if they fail.

It is ten lines of configuration and half an hour of work, and it changes the question the team asks itself. You stop debating whether somebody ran the tests, because it is no longer something anybody can forget: either they are green, or it does not merge.

Comments