From "it works on my machine" to automation

Picture a team without automation. Joost finishes a change to the inventory script after two weeks on his own branch, tests it on his laptop, and merges. Amara's branch, also two weeks old, touched the same module. The merge conflicts take an afternoon; the combined code fails on the jump host because it has a different Python version and a library Joost had installed by hand. Nobody is sure which version is running in production, because Mei copied it there over SSH (Secure Shell) last month.

Each problem has the same root: integration, testing and deployment are manual, rare and different every time. CI/CD (continuous integration and continuous delivery or deployment) replaces them with an automated, repeatable process that runs on every change, on a clean machine, in exactly the same way. The ideas are introduced on the Cloud page DevOps and CI/CD; this topic shows how to build it with Git and GitHub.

Continuous integration

Continuous integration (CI) has two halves, and both matter:

  • Integrate often. Everyone merges small changes into the shared main branch at least daily, through short-lived branches and pull requests (see Branching strategies and releases). Merging a day's work is easy; merging two weeks of work by two people is the afternoon of conflicts above.
  • Verify every change automatically. Every push and pull request triggers a build and the tests on a clean machine. A failure shows up minutes after the change, while the author still remembers what they did, and the team treats a red main branch as the most urgent thing to fix.

A CI server that runs tests on branches that are merged once a month is not continuous integration; it is automated testing of long-lived branches. The integration is the point.

Continuous delivery and continuous deployment

The Cloud page defines continuous delivery and continuous deployment: both extend CI so that every change that passes is automatically packaged and deployed to test environments; they differ only in whether the last step to production waits for a human decision (delivery) or happens automatically (deployment).

One nuance helps when talking to operations and service management colleagues: deploying is not the same as releasing. A deployment puts a new version on the servers or devices; a release makes new functionality available to users. With feature flags, dark launches or a planned switch-over, code can be deployed days before it is released. In ITIL terms these are two practices, release management and deployment management; see Deploying is not releasing. Continuous deployment automates deployments; when users see a change can still be a business decision.

Pipeline stages

A pipeline is divided into stages. A pipeline stage is a named phase of a pipeline, such as build, test or deploy, that groups related work; a stage starts only when the stages it depends on have succeeded, and a failure stops the pipeline so that later stages never see the broken change. Inside a stage, independent jobs (unit tests and a linter, for instance) can run in parallel.

A commit triggers the build, test and package stages, which form continuous integration. Packaging produces one versioned artifact. The same artifact is then deployed first to staging and then to production, which is continuous delivery: build once, deploy many. Commit Build Test Package artifact v1.4.0 Deploy: staging Deploy: production CI: on every change CD: the same artifact, promoted a failed stage stops the change here
Build once, deploy many: production receives exactly the artifact that was tested in staging.
Stage What happens Typical tools
Build Install dependencies, compile, check that it starts pip install, npm ci, go build, docker build
Test Linters, unit tests, integration tests, security scans (see Testing and quality in pipelines) pytest, yamllint, CodeQL
Package Produce the versioned artifact and store it Wheel, zip, container image; a registry
Deploy Put the artifact into an environment, then verify it (see Deployment pipelines and GitOps) Scripts, Ansible, Helm, Argo CD

Builds and build artifacts

A build is the automated process that turns source code at one specific commit into something that can be run or installed: resolving dependencies, compiling where the language needs it, and assembling the result. In CI it runs on a fresh machine for every change, so it cannot depend on something that happens to be installed on one person's laptop.

The output is a build artifact: the versioned file or image a build produces (a Python wheel, a zip, a container image, a rendered set of device configurations), stored unchanged in an artifact repository or registry so that exactly the same thing can be tested and later deployed. Good artifacts are:

  • Versioned and traceable: the name or tag says which commit it came from.
  • Immutable: once published, version 1.4.0 never changes. A fix is version 1.4.1.
  • Promoted, not rebuilt: the artifact that passed staging is the one that goes to production. A rebuild could pick up a newer dependency and is, strictly, untested.
  • Environment-neutral: settings that differ per environment (URLs, credentials) are supplied at deploy time, not baked into the artifact.

Git gives a build everything it needs to name its artifact. git describe --tags returns the nearest tag, the number of commits since it and the abbreviated commit hash:

git init -q netbox-sync && cd netbox-sync
printf 'print("syncing")\n' > sync.py
git add sync.py && git commit -q -m "Add sync script"
git tag -a v1.4.0 -m "Release 1.4.0"
printf '# netbox-sync\n' > README.md
git add README.md && git commit -q -m "Add README"

# The build names its artifact after the exact commit it came from
VERSION=$(git describe --tags)
echo "$VERSION"
tar -czf "netbox-sync-$VERSION.tar.gz" sync.py README.md
ls
v1.4.0-1-g0afeaf0
README.md
netbox-sync-v1.4.0-1-g0afeaf0.tar.gz
sync.py

v1.4.0-1-g0afeaf0 reads as "one commit after the tag v1.4.0, at commit 0afeaf0" (the g stands for Git). Anyone holding this file can find the exact source with git show 0afeaf0. Built on the tagged commit itself, the same command returns just v1.4.0. Tags and version numbers are covered in Branching strategies and releases.

Pipeline as code

Pipeline as code means defining a pipeline in a text file stored in the same repository as the code it builds, instead of clicking it together in a web interface. The pipeline is then versioned, reviewed in pull requests and branched together with the code: a branch that adds a new test can add the step that runs it, and the history shows who changed the deployment and why. It is the same idea as infrastructure as code, applied to the delivery process.

Platform Pipeline file Format
GitHub Actions .github/workflows/*.yml YAML
GitLab CI/CD .gitlab-ci.yml YAML
Azure Pipelines usually azure-pipelines.yml YAML
Jenkins Jenkinsfile Groovy-based syntax

The platforms are compared on Other CI/CD platforms; the next page, GitHub Actions, builds a complete workflow.

YAML basics

Most pipeline files are written in YAML (YAML Ain't Markup Language), a human-readable data format that expresses the same structures as JSON (JavaScript Object Notation) (mappings, lists, strings, numbers, booleans) using indentation instead of braces, and allows # comments. The Networking topic shows it in network automation; here are the parts you meet in pipelines, with the JSON that the same file means:

# netbox-sync.yml: settings for a small sync tool
name: netbox-sync
version: "1.4"            # quoted: text, not the number 1.4
enabled: true
retries: 3
sites:
  - name: Amsterdam
    prefix: 192.0.2.0/24
  - name: Rotterdam
    prefix: 198.51.100.0/24
banner: |
  Managed by netbox-sync.
  Do not edit by hand.
defaults: &defaults
  timeout: 30
  verify_tls: true
staging:
  <<: *defaults
  url: https://staging.netbox.example.com
production:
  <<: *defaults
  timeout: 60
  url: https://netbox.example.com
{
  "name": "netbox-sync",
  "version": "1.4",
  "enabled": true,
  "retries": 3,
  "sites": [
    {
      "name": "Amsterdam",
      "prefix": "192.0.2.0/24"
    },
    {
      "name": "Rotterdam",
      "prefix": "198.51.100.0/24"
    }
  ],
  "banner": "Managed by netbox-sync.\nDo not edit by hand.\n",
  "defaults": {
    "timeout": 30,
    "verify_tls": true
  },
  "staging": {
    "timeout": 30,
    "verify_tls": true,
    "url": "https://staging.netbox.example.com"
  },
  "production": {
    "timeout": 60,
    "verify_tls": true,
    "url": "https://netbox.example.com"
  }
}
  • Mappings: key: value pairs; the space after the colon is required. Nesting is shown by indentation with spaces; tabs are not allowed.
  • Lists: each item starts with - at the same indentation. An item can itself be a mapping, as in sites.
  • Strings usually need no quotes. Quote them when the value would otherwise be read as something else: "1.4" stays text, 1.4 becomes a number; "yes" and "on" stay text, while some parsers turn them into booleans.
  • Multi-line strings: | keeps the line breaks, which is what you want for a shell script in a pipeline step. (> folds the lines into one.)
  • Anchors and aliases: &defaults names a block, *defaults refers to it, and <<: *defaults merges its keys into a mapping, where keys written next to it override them (timeout: 60 in production). Use them sparingly; they save repetition but make a file harder to read.

Measuring delivery: DORA metrics

How do you know whether all this helps? The DORA metrics are the delivery performance measures from the DevOps Research and Assessment (DORA) program, five measures: three for throughput (change lead time, deployment frequency and failed deployment recovery time) and two for instability (change fail rate and deployment rework rate, added in 2024). They are explained on the Cloud page under Measuring delivery.

A pipeline is where most of these numbers come from: it knows when a commit was made, when it was deployed and whether the deployment had to be rolled back. The research's main finding is what makes CI/CD worth the effort: teams that deploy small changes often are not less stable, they tend to be more stable, because each change is small, tested and easy to undo. Use the metrics to see whether a team improves, not to rank teams.

What makes a good pipeline

  • Fast. Feedback within minutes keeps people waiting for it instead of switching to other work. Run the quickest checks first, run independent jobs in parallel, cache dependencies.
  • Reliable. A red pipeline must mean a real problem. Flaky tests that fail at random teach people to press "re-run" and ignore failures; fix or quarantine them.
  • Reproducible. Pinned dependency versions, a pinned runner or container image, the same steps every time. Running the pipeline again on the same commit gives the same result.
  • Secure. The pipeline can deploy to production, so it is a prime target. Least privilege for its tokens, secrets in the platform's secret store, third-party actions pinned (see GitHub Actions and Securing repositories).
  • Visible. Everyone can see the status of main, and a failure notifies the person who caused it.

CI/CD and change enablement

In organizations that follow ITIL, every change to production goes through change enablement. That does not have to mean a meeting for every deployment. A well-built pipeline is exactly the evidence a change authority needs: every change is peer-reviewed in a pull request, tested the same way, deployed by the same automated procedure, and can be rolled back. Many organizations therefore classify deployments through such a pipeline as standard changes: pre-authorized, low-risk and repeatable, with the pipeline's log as the record. Larger or riskier changes still go through normal change assessment, often with an approval step in the pipeline itself. See also Releases, changes and CI/CD.

Common anti-patterns

  • Long-lived branches with a CI server: the tests run, but nothing is integrated until the painful merge.
  • A red main branch that stays red. Once a failure is normal, new failures go unnoticed. Fix or revert first.
  • Rebuilding per environment: a separate build for staging and production, so production runs something nobody tested.
  • Manual steps in the middle: "then Mei copies the file to the server". Every manual step is a place where the process differs between runs. An approval is fine; a manual procedure is not.
  • Skipping the pipeline for urgent fixes. An emergency fix is exactly the change you most want tested. Make the pipeline fast enough that nobody is tempted.
  • Secrets in the pipeline file, or one all-powerful token shared by every job.
  • A hand-built build server that nobody dares to update, with tools installed years ago. Define the build environment in code too: a pinned runner image or container.