# CI

## What it is

CI is short for continuous integration: a way of working in which everyone on a
team lands their changes on one shared line of code many times a week, and a
machine builds and tests the result each time so that a mistake surfaces while
its author still remembers making it. The full name, continuous integration, is
used as often as the abbreviation.

The word is used for two different things, and keeping them apart saves a lot
of confusion. The first is the practice, which is about people. Martin Fowler's
article on the subject, the source most teams end up quoting, describes it as a
set of habits: keep one mainline, automate the build, make the build test
itself, push to the mainline at least daily, build every push, fix a broken
build at once, keep the build fast, hide unfinished work rather than keeping it
on a branch, and let everyone see the state of the build. The article traces
the name to Extreme Programming, and notes that Grady Booch used the phrase
before that.

The second meaning is the machine. When someone says "the CI is red", they mean
the service that ran the build and reported a failure. GitHub Actions, GitLab
CI/CD and their peers all share one shape. A file in the repository describes
the work: `.github/workflows/*.yml` on GitHub, `.gitlab-ci.yml` at the root on
GitLab. An event starts it, such as a push, a pull or merge request, a schedule or a
person pressing a button. A runner, which is a machine set aside for the
purpose, checks out the commit, runs the commands in that file, and reports a
pass or a fail back against the commit.

A server on its own is not continuous integration. A team that keeps work on
branches for a month and has a server build each branch has automated its
builds, which is useful, but the integrating still happens all at once at the
end. The practice is the frequent merging; the machine is what makes frequent
merging safe enough to keep doing.

Continuous delivery and continuous deployment are the steps that come after.
Delivery keeps every green build ready to release, and deployment releases each
one automatically. Both depend on CI being in place first, which is why the
three are so often written together as CI/CD.

## Why it matters

The cost of merging two lines of work grows faster than the time they spend
apart. Two people who each changed the same module yesterday can sort out the
conflict in a few minutes, because both remember what they meant. Two people
who diverged six weeks ago are reconstructing intent from diffs. Merging small
pieces often keeps every conflict small, and that is most of the benefit before
any machine is involved.

The build on every push adds a second thing: a clean machine that agrees. "It
works on my laptop" is a claim about one laptop, with its own installed
libraries and leftover files. A runner that starts from a fresh checkout every
time either reproduces the result or shows where the laptop was special.

Speed is where the value lives. Fowler's article sets a target of about ten
minutes for the main build, and the reasoning holds up in practice. A failure
reported ten minutes after a push goes back to the person who caused it while
the change is still on their screen. A failure reported the next morning goes
to whoever happens to look, and costs an hour of archaeology first.

Visibility matters as well. When the state of the mainline is shown to
everyone, nobody starts new work on top of a broken build without knowing it,
and fixing that build becomes the team's first job rather than someone else's
problem.

CI also need not mean a hosted service. pre-commit runs a project's checks as
Git hooks before each commit or push, and its documentation shows the same set
run on a server with `pre-commit run --all-files`. Task gives each check a name
in one `Taskfile.yml`, so a person and a runner call the same command. The
checks behind this site follow that model. They run locally, where the pages
are written, and a full check runs before every push with no hosted build
service in between.

## What it looks like in practice

Roughly 4,000 hours were given back once a team stopped releasing by hand.
Getting software out had relied on manual steps that each person did slightly
differently, until
[automated build, test and deployment replaced them](/portfolio/streamlined-ci-cd-processes-saving-4-000-hours-42/).
The path from a change to production became a standard one rather than an
improvised one, and the releases became both faster and more reliable.

On a project with a long-lived database, the schema is described twice: once by
the initial setup and once by 1,022 migrations, numbered 036 through 1102.
[A CI job builds the database both ways on every change](/portfolio/kept-the-schema-honest-across-1022-migrations-99/),
once from the fresh schema and once with every migration replayed in order, and
compares the structure and the seeded data. Where the two builds differ, the
job goes red and prints what differs, so a fresh environment and a long-lived
one are the same database by check rather than by assumption.

CI can also keep two halves of one application in agreement. On another
project, the API description and the frontend's client are generated in a chain
that starts at the database, and the client alone is 44,076 lines of typed
TypeScript.
[A CI check holds what the frontend sends against what the API expects](/portfolio/built-a-request-schema-validation-contract-with-automated-61/)
and fails the build when they diverge. That is the result CI exists for: a
field that changes on one side only fails in a build, minutes after the change,
instead of reaching a user as an error in the browser.

## Where it goes wrong

Most CI setups decay rather than break. Each of these starts small and ends with
a build the team has learned to work around.

**The build takes longer than the work.** Once a push takes forty minutes to
come back, people stop waiting for it, batch several changes into one push and
push less often, which is the opposite of the practice. Measure where the time
goes before adding hardware. Cache dependencies, run the fast checks first, and
move the slow suites to a second stage or a schedule, so that the main build
stays near the ten-minute mark.

**The mainline stays red for a day.** A broken build that nobody fixes becomes
the normal state, and a second failure arrives unnoticed behind the first. The
rule that keeps this from happening is plain: whoever broke it fixes it now, or
reverts the change and fixes it on their own time. A green mainline is the
thing everyone else is building on.

**Branches live for weeks while the server builds each one.** Every branch
shows green, and the trouble arrives on the day they merge, all at once. This
is automated building without the integration. Merge to the mainline at least
daily, and keep unfinished features switched off rather than kept apart.

**The server checks one list and the laptop checks another.** Two lists of
commands kept by hand drift apart, and then a change passes locally and fails on
the server, or the reverse, and nobody trusts either. Give the checks one entry
point and call it from both places. With pre-commit, the server can run exactly
the hooks every developer runs:

```yaml
# .gitlab-ci.yml
check:
    image: python:3.13
    script:
        - pip install pre-commit
        - pre-commit run --all-files --show-diff-on-failure
```

**A flaky test teaches everyone to press retry.** A test that fails one run in
twenty for reasons unrelated to the change trains the team to rerun instead of
read. After that, a real failure gets the same treatment. Quarantine the test,
fix it or delete it, and treat a retry button that gets pressed often as a
finding in its own right.

## Further reading

- [Continuous Integration](https://martinfowler.com/articles/continuousIntegration.html)
  — Martin Fowler's long article: each practice, the ten-minute build, and
  where the name came from.
- [Understanding GitHub Actions](https://docs.github.com/en/actions/get-started/understand-github-actions)
  — workflows, events, jobs and runners as GitHub defines them.
- [GitLab CI/CD](https://docs.gitlab.com/ci/) — the `.gitlab-ci.yml` file,
  pipelines, stages, jobs and runners in GitLab's own terms.
- [pre-commit](https://pre-commit.com/) — checks declared once in a config
  file, run as Git hooks and again as a step on a CI server.
- [Task](https://taskfile.dev/) — a `Taskfile.yml` that gives each check one
  name, so a person and a runner type the same command.

<https://engineer.company/glossary/ci/>
