Module 1: From Your Machine To The Pipeline
3. What Continuous Integration is
Description
By the end of this lesson you'll be able to define Continuous Integration (CI) precisely, distinguish it from what it isn't, and recognize its three pillars. Lesson 2 left you with the problem in the flesh —"it works on my machine", the same test giving two verdicts depending on the environment—; this lesson puts a name and a shape to the solution. In one sentence: Continuous Integration is running your test suite automatically, in a clean and reproducible environment, on a shared neutral machine, every time someone pushes a change to the repository. Every word of that sentence carries weight, and we're going to take it apart piece by piece, because understanding what makes the integration automatic, what makes the environment clean, and what makes the machine neutral is understanding why CI closes exactly the cracks a local green leaves open.
You'll also see where the name comes from —"integrate" isn't a decorative word, it describes a concrete act: pulling everyone's work together, over and over, and verifying it each time—; what CI is not, so you don't ask of it what it doesn't give; and which platform we'll use as canonical in the guide —GitHub Actions— along with its equivalents. And you'll see, honestly presented, how the CI log would read running our Reservo suite: the same pytest you already run, wrapped by a runner's stages.
Connection to the module: this is the module's hinge lesson. Lesson 1 posed the why; lesson 2 showed the problem; this one defines the solution. What follows leans on this definition: lesson 4 explains why it's worth having that automatic environment catch the failure as early as possible (the feedback loop); lesson 5 opens the pipeline from the inside (its stages); lesson 6 says what it protects (the green branch). Here you still don't write the YAML —that's module 2—; here you understand what the thing that YAML is going to describe is.
A building's water system
Think of it this way. In an apartment building, everyone could get their own water: go down to the street with buckets, fill them at a public tap, carry them up, and trust that the water is clean because "it's never made me sick". It would work… until it makes someone sick, and by then they've already drunk it. No one would know if the water is good until someone gets ill. The verification —"is it clean?"— would be manual, occasional, and late.
That's why buildings don't work that way. They have a central system: water enters at a single point, passes automatically through a filter and an analysis before reaching the taps, and it does so every time it flows, not when someone remembers to check. The system is shared: it's not "Ana's water" and "Beto's water", it's the building's water, the same for everyone, verified at a neutral point everything passes through. If the analysis detects something, it shuts the tap before the dirty water reaches a glass. No one has to remember anything; the system doesn't get tired or forget.
CI is that central system for code. Without it, each developer "gets their own water": runs the tests on their machine, when they remember, and trusts their local green —until it "makes someone sick" on another machine or in production—. With CI, the code passes automatically through the suite, at a neutral and shared point that all changes pass through, every time someone pushes something. And if the suite detects a problem, CI "shuts the tap" —marks the change red— before the broken code reaches the main branch. The water system's three adjectives —automatic, shared, every-time— are exactly the three pillars of CI.
Continuous Integration: running the suite automatically (no one has to remember), in a clean and reproducible environment (not your machine, with its assumptions), at a neutral and shared point (the same for the whole team), on every change that's pushed (not every now and then).
The three pillars, one by one
The definition is dense on purpose. Each pillar closes one of the cracks lesson 1 named in a local green. It's worth seeing them separately.
Pillar 1: automatic — closes the forgetting crack
A suite that depends on a human remembering to run it, sooner or later, doesn't get run. Not out of carelessness: out of hurry, out of a Friday at six, out of "it's a one-line change, surely nothing breaks". CI removes the human from the equation: the suite triggers on its own when an event happens —typically a push to a branch or the opening of a pull request—. There's no "did you run the tests?" in review, because the answer is always yes, the machine ran them. The word "continuous" in the name lives here: integration isn't an event you schedule, it's something that happens continuously, on every change, without friction.
Pillar 2: clean and reproducible environment — closes the environment crack
This is the pillar that directly answers lesson 2. The CI runner starts empty: a freshly created machine, without your shell variables, without the packages you installed once and forgot, with a version of Python the project declares explicitly. On that clean base, it installs only what the project says it needs, and runs the suite there. That's why CI is lesson 2's "case B" made routine: the neutral machine that discovers the invisible assumptions. "Reproducible" is the other half: since the environment is built from a written declaration —which version, which dependencies—, two CI runs start from the same point, and anyone can reconstruct that environment to reproduce a failure (the art of module 3).
Pillar 3: neutral and shared — closes the isolation crack
CI doesn't run on anyone's machine: it runs on a machine belonging to no one and everyone, a point all changes from all developers pass through. That makes it a shared truth. When CI says "green", it's not "green for Ana"; it's "green in the common environment", and that verdict holds for the whole team. When it says "red", no one can reply "well it works on mine" and close the case, because CI isn't "anyone's": it's the neutral stage. This pillar is the one that turns testing from an individual act into a team guarantee, and it's the basis of what lesson 6 will call "the always-green main branch".
Where the name comes from: integrate
It's worth understanding the word, because it explains the historical "why". Integrating is pulling different people's work together into a single body of code that works. In teams of the past —and in the ones that still don't do CI— everyone worked for weeks on their branch, isolated, and at the end came the dreaded "integration day": everyone pulled their code together all at once, and mountains of conflicts and breakages appeared that no one had seen coming, because each piece had been tested alone but never together. That day could cost days of pain.
The idea of Continuous Integration was: instead of integrating all at once every few weeks, integrate often —ideally several times a day— and verify each integration automatically with the suite. If you pull everyone's work together many times a day and a machine runs the tests at each merge, problems appear one at a time, small, the same day they were created, instead of accumulating into an avalanche. "Continuous" is the opposite of "every now and then"; "integration" is the act of pulling together and verifying. CI, then, isn't just "running tests on a server": it's a discipline of integrating often and verifying each time, and the test pipeline is the tool that makes it possible.
What Continuous Integration is NOT
Defining something by what it isn't keeps you from asking of it what it doesn't give. CI is not:
- It doesn't write your tests. CI runs the suite you already have; it doesn't invent it. If your suite doesn't test the pro discount, CI won't test it either —it will only run, faithfully, the tests that exist—. Writing good tests is the fundamentals guide; CI inherits your suite's quality, it doesn't create it.
- It's not a "correct code" seal. A green CI means "the tests that exist passed in the clean environment", not "the code has no bugs". A bug in a case no test touches passes CI without breaking a sweat. CI is only as good as your suite.
- It's not deployment. Running the tests on every change (CI) is different from sending the verified code to production (CD). They're separate things, and lesson 7 distinguishes them. This guide focuses on the CI of the tests.
- It's not a specific platform. GitHub Actions is one way of doing CI, the one we'll use; but CI is a concept, not a product. GitLab CI, CircleCI, Jenkins, Buildkite, and others do the same with different syntax. Learn the concept and the platform will be a detail.
The canonical platform: GitHub Actions (and its cousins)
In this guide we use GitHub Actions as the CI platform, for two practical reasons: it's the most widespread today, and it lives inside your GitHub repository, so you don't have to go set up a separate server. In module 2 you'll write its configuration file —a YAML that lives in .github/workflows/— and read its logs.
But it helps to know, from the start, that the concept is portable. All these platforms do the same —start a clean machine, install, run your suite, report green or red—; only the syntax of the file that describes it changes:
| Platform | Where the config lives | Typical file name |
|---|---|---|
| GitHub Actions (ours) | In the repo, .github/workflows/ | ci.yml |
| GitLab CI/CD | In the repo, root | .gitlab-ci.yml |
| CircleCI | In the repo, .circleci/ | config.yml |
| Jenkins | In the repo or on the server | Jenkinsfile |
What you learn here —what a pipeline is, its stages, what it protects, how to read its log— translates to any of them. We chose GitHub Actions to have one concrete syntax to touch, not because CI depends on it.
Worked example: how the CI log would read
Here a bit of honesty is in order, because it's the rule of this guide. We run Reservo's pytest for real locally, and its output is the one you've already seen: real, measured. A GitHub Actions CI log, on the other hand, we show as honest content —"this is how it looks, this is how it would read"—, because running a real CI needs a GitHub runner we don't have here. We won't make up numbers for you: the heart of the log is exactly the pytest output we did run; what surrounds it is the wrapper a runner puts on it.
The key idea, and why CI isn't scary, is this: CI runs the same command you run. When on your machine you type python3 -m pytest and see 12 passed, CI types that same command on its clean machine. Its log is your pytest output, wrapped in a few lines that say which stage is running. Here's how it would read, for our green Reservo suite:
Run python -m pytest
============================= test session starts ==============================
platform linux -- Python 3.12.7, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/runner/work/reservo/reservo
collected 12 items
test_pricing.py .... [ 33%]
test_refunds.py ..... [ 75%]
test_scheduling.py ... [100%]
============================== 12 passed in 0.05s ==============================
What to expect, line by line. The first line, Run python -m pytest, is the runner's: it tells you which command it's running in this stage (in GitHub's real log it appears as a collapsible step you can open). Everything below is pure pytest, identical to your local output, with two revealing differences: platform linux instead of darwin —the runner runs on Linux, not your Mac— and Python 3.12.7 instead of your 3.14.0 —the runner uses the version the project declared—. Those two differences are, no more and no less, two of lesson 2's environment leaks, now controlled: CI doesn't depend on your Mac or your version, it runs in a declared and explicit environment. And the path rootdir: /home/runner/work/... confirms to you that this didn't run on your machine, but on a clean runner belonging to no one.
Now, what would happen if the code were broken —the pro discount wrong, as in lesson 2—? CI would run the same command and its log would end like this:
Run python -m pytest
...
FAILED test_pricing.py::test_price_by_tier_and_hours[pro-3h] - assert 7500 == 6000
============================== 1 failed, 11 passed in 0.05s ===============================
Error: Process completed with exit code 1.
Notice the last line: Error: Process completed with exit code 1. That's the minimal magic of CI, and it's the topic of lesson 5: pytest ended with exit code 1 (red), the runner read that 1, and marks the whole pipeline as failed. On your machine, you ignored that 1; CI turns it into a red check that blocks the merge. CI doesn't understand tests or discounts: it understands a number —0 or 1— that pytest hands it when it finishes. Everything else is plumbing around that number.
Common mistakes
Believing CI runs "something different" from what you run. What happens: someone treats CI as a magical black box that does a mysterious, different, and stricter verification than their local pytest, and so they fear it or distrust its results. Why it happens: CI comes wrapped in logs and configuration, so it seems like something else. How to spot it: if you couldn't say which exact command your CI runs, you're treating it as magic. How to fix it: remember that CI runs the same command as you —python -m pytest—, only on a clean machine. The only different thing is the environment, not the verification. When CI and you differ, the difference is in the environment (lesson 2 and module 3), not in a "secret pytest".
Asking CI to guarantee that the code is correct. What happens: someone sees the green check and concludes "CI approved it, the code is fine", and lowers their guard about the quality of their suite. Why it happens: a big, reassuring green feels like a total guarantee. How to spot it: if you trust CI's green more than you'd trust your own suite run locally, you're fooling yourself —it's the same suite—. How to fix it: CI's green is worth exactly what your tests are worth. If your suite doesn't cover a case, CI doesn't either. CI guarantees "these tests pass in a clean environment", not "the code is correct". Raising the suite's quality is the fundamentals guide; putting coverage gates on CI is module 6 of this one.
Confusing the platform with the concept. What happens: someone learns GitHub Actions and believes that "CI" is GitHub Actions, so they feel lost if the team uses GitLab or Jenkins. Why it happens: you learn with a concrete tool and confuse it with the idea. How to spot it: if you think changing platforms means relearning CI from scratch, you confused the syntax with the concept. How to fix it: separate the two layers. The concept —clean machine, install, run the suite, report with an exit code— is identical across all. The file's syntax changes. Learn the concept well in this module and GitHub Actions in module 2, and translating to another platform will be reading its documentation one afternoon.
Exercises
Exercise 1 — Match the pillar with the crack. Lesson 1 named three cracks of a local green: forgetting, environment, and isolation. Match each one with the CI pillar that closes it (automatic / clean environment / neutral and shared) and explain the match in one sentence.
See solution
- Forgetting → automatic. The pillar of running the suite on its own on every push/PR closes the forgetting crack: no one has to remember, because the machine doesn't forget.
- Environment → clean and reproducible environment. The runner that starts empty, with declared version and dependencies, closes the environment crack: it runs lesson 2's "case B" as routine, bringing invisible assumptions to light.
- Isolation → neutral and shared. The point all changes pass through, which isn't anyone's machine, closes the isolation crack: its green holds for the team, not just for whoever ran it.
The lesson: CI isn't a monolithic trick; it's three mechanisms, each answering a different crack. Understanding which pillar closes which crack is understanding why CI resolves "it works on my machine" and doesn't just hide it.
Exercise 2 — Diagnose from the log. You look at a teammate's CI log and see this header: platform linux -- Python 3.10.6, pytest-9.1.1. On your machine, the suite passes with Python 3.14.0. CI is red with an error that a certain function doesn't exist. Without seeing more, (a) what's the most likely environment leak? (b) why didn't your local green warn you? (c) what closes this crack going forward, and in which module is it covered?
See solution
- (a) The Python version. CI runs on
3.10.6and you on3.14.0. A function that exists in 3.14 and not in 3.10 (or that changed) explains that it fails over there and not here. The header gives it away: theplatform ... -- Python Xline is exactly the datum that reveals this leak. - (b) Because your environment had the "good" version. Your
3.14has the function; you never exercised the old-version case. Your green only proves "passes on 3.14", not "passes on 3.10". It's a pure case of "it works on my machine" due to version difference. - (c) Declaring and controlling the version closes it —and, better still, testing across several at once with the matrix, which is module 4 of this guide—. Reproducing the failure by installing that version locally is module 3.
The lesson: the CI log isn't just a verdict, it's a diagnosis. Its header tells you the exact environment where it failed, and comparing that environment with yours usually points straight to the leak.
Exercise 3 — Translate between platforms. A team uses GitLab CI, not GitHub Actions. A teammate says: "then what we learned about CI is useless to us, it's all different". Argue why they're wrong, naming three things that are identical between the two platforms and one that changes.
See solution
They're wrong because they confuse the syntax with the concept. Three identical things between GitHub Actions and GitLab CI (and any other CI platform):
- The model: a clean machine starts, installs the declared dependencies, runs your suite, and reports green or red. It's the same in both.
- The trigger: the suite runs automatically on a repository event (a push, a merge request / pull request). Same pillar.
- The verdict signal: both read the exit code of the test process (0 = green, 1 = red) to decide if the pipeline passes. Same mechanism (lesson 5).
What changes is the syntax and location of the configuration file: GitHub Actions uses a YAML in .github/workflows/; GitLab CI uses .gitlab-ci.yml in the root, with different keywords. What changes is the "how it's written", not the "what it does".
The lesson: changing CI platform is learning a dialect, not a new language. What this module teaches you —the concept— is the same across all; what module 2 teaches you —the GitHub Actions YAML— is the dialect we chose.
Summary and next step
In this lesson you put a name and a shape to the solution of lesson 2's problem. Continuous Integration is running your suite automatically, in a clean and reproducible environment, on a neutral and shared machine, on every change pushed to the repository. Its three pillars close, one by one, the three cracks of a local green: automatic closes forgetting, clean environment closes the environment difference, and the neutral machine closes isolation. The name isn't decorative: "integrate" is pulling everyone's work together, and "continuous" is doing it often and verifying it each time, so problems appear small and on time instead of in an avalanche.
You saw what CI is not —it doesn't write your tests, doesn't guarantee correctness, isn't deployment, isn't a platform— and which one we'll use as canonical: GitHub Actions, with GitLab CI and others as cousins that do the same with different syntax. And you saw, honestly presented, how the CI log would read running our suite: the same pytest you already run, on platform linux -- Python 3.12.7, ending with the verdict the runner reads from the exit code.
Before moving on you should be able to: define CI in one sentence with its three pillars; explain where the word "integration" comes from; name two things CI is not; and say why CI runs "the same command as you".
What's next is understanding why it's so worth it for that automatic environment to catch the failure as early as possible. In lesson 4 you'll see the feedback loop and the cost curve: why the same bug costs cents if you catch it as you write it and a fortune if it reaches the customer, and how CI shortens that loop by bringing the warning close to the moment when you still have the context warm.
Resources
- Understanding GitHub Actions — GitHub documentation — the official explanation of the concepts of the platform we'll use (workflow, job, runner, step). Read it as a conceptual panorama; it fits piece by piece with this lesson's three pillars.
- About GitHub-hosted runners — GitHub documentation — what exactly that "clean and neutral machine" of the second and third pillars is: an ephemeral machine GitHub creates empty for each run. Here you see why the runner doesn't inherit your shell variables.
- Continuous Integration — Martin Fowler — the reference essay on the discipline of integrating often and verifying each time, not just the tool. It's where the historical "why" of the name we saw in this lesson comes from.
- How to invoke pytest — pytest documentation — the reference for the
python -m pytestcommand CI runs for you. It confirms that CI runs nothing magic: the same command you already use, on another machine.