Module 1: From Your Machine To The Pipeline

1. Module introduction: the suite that only ran on your machine

Description

By the end of this lesson you'll have a clear name for a problem you've probably already lived through without labeling it, and the idea that solves it. The problem is this: your tests only protect the place where they run. You wrote a Reservo suite, you run it with pytest, you watch it go green, and you trust it —rightly, it's a good suite—. But that suite runs on your machine, when you remember to run it, with the version of Python you have, with the dependencies you installed, and with the environment variables you exported in your shell months ago. Every one of those words —"your", "you"— is a crack. The day your teammate clones the repo and runs the same suite, or the day that code reaches production, any of those cracks can turn your calm green into a red no one expected.

The idea that solves this has a name, and it's the heart of the whole guide: Continuous Integration (CI). In a single sentence: running your test suite automatically, in a clean and shared environment, on every change someone pushes to the repository. Not when you remember: always. Not on your machine: on a neutral machine that starts from scratch every time. Not just for you: for the whole team, as a common truth. That's the leap this guide makes: taking the Reservo suite from "it runs on my laptop" to "it runs on its own, in a pipeline, every time someone touches the code".

Connection to the module: this lesson is the map, not the territory. Here you don't write a CI workflow yet —that comes in module 2, on purpose—; here you install the problem and the concept, which are what make module 2 make sense. Lesson 2 puts you face to face with "it works on my machine": you'll watch the same test give two different verdicts depending on the environment, with real output. Lesson 3 defines what CI is exactly and what it isn't. Lesson 4 explains why a failure caught late costs more, and how CI shortens that loop. Lesson 5 opens up a pipeline from the inside: its stages —checkout, install, test, report— and the signal it reads to decide. Lesson 6 answers what CI protects: the always-green main branch. Lesson 7 distinguishes CI from CD in two minutes. And lesson 8, the mini-project, has you map your own manual Reservo flow to the stages of a pipeline.

A note about tooling, because it sets the tone for the whole module. We're going to use GitHub Actions as the CI platform throughout the guide, because it's the most widespread and the easiest to work with without leaving your repository. But in this module you will not write its YAML yet. The reason is the same one that made the fundamentals guide have you write an assert by hand before handing you pytest: if I give you the configuration before you understand the problem, you'll learn to copy a workflow without ever having understood what CI is or why you need it. And that confusion is expensive: it produces people who have a green pipeline and couldn't tell you what it's protecting. So first the problem, then the tool.

The rehearsal that only went well in your room

Think of it this way. You're a musician and you have a solo to play at a concert. You rehearse it in your room, with your guitar, your amp, your pedals, at your volume, and it comes out perfect. Ten times in a row, flawless. You go to sleep at ease: "I've got it".

The day arrives. You go up on stage. The guitar is the same, but the amp is a different one, the stage has an echo your room didn't have, the monitor sends the sound back to you half a second late, and there's a cable that hums a hum you never heard at home. The solo that came out perfect now comes out crooked —not because you didn't know it, but because you never played it under these conditions. You rehearsed in one environment, and you play in another. And what worked in the first wasn't guaranteed in the second; you were only assuming it.

A professional musician knows this, and that's why they do a soundcheck: they arrive early, play on the real stage, with the real equipment, and discover the hum before the concert, when it can still be fixed. The soundcheck doesn't change how you play: it changes where you test that you know how to play. It moves the verification from your room —comfortable, familiar, deceptive— to the stage —the place where it really matters—.

CI is the soundcheck for your code. Your machine is your room: comfortable, familiar, full of things you installed once and never looked at again. The environment where your code is really going to run —your teammate's, the production server— is the stage. And CI is that neutral machine, which starts empty like a stage before soundcheck, installs only what the project declares it needs, and runs your suite there. If your suite passes in that clean environment, you have a real reason to trust. If it passes only in your room, you don't know whether you've got it or you got lucky with the amp.

A test passing on your machine proves it passes on your machine. CI moves the test to the stage: a clean, shared, reproducible environment, so that "passes" means "passes for anyone".

The guide's case: Reservo, now with a pipeline

The whole guide works on Reservo, the same coworking meeting-room booking system you already know from the sibling testing guides. If you're coming from them, you know it by heart; if you landed straight here, this is just enough of what you need.

Reservo is pure Python logic on top of the standard library: it has no database, no network, no web server. Its pieces are three models —Room, Member, Booking— and a handful of pure functions. These are the ones that will be with us most:

  • price_cents(room, member, hours) — how much to charge a member for booking a room. Exact arithmetic: price per hour, times hours, minus the pro tier discount (20%).
  • overlaps(a_start, a_end, b_start, b_end) — whether two time ranges [start, end) step on each other. Touching at the edge doesn't count as overlapping.
  • refund_cents(booking, price_paid_cents, now) — how much is refunded on cancellation, based on how early it is.

All money goes in cents, as an integer$25.00 is 2500—, never as a float, because floats accumulate tiny errors that make a total off by a cent. And every result is deterministic: the same input gives the same output, always. That purity is exactly what makes Reservo a perfect case for talking about CI: when the suite fails in module 2, you won't be able to blame "the database was slow"; the failure will be the environment's or the code's, which is exactly what we want to learn to tell apart.

The anchor numbers —the exact results we assert again and again— are the guide's "checksum":

ClaimCalculationResult (cents)
price_cents: basic, 3 h2500 × 37500
price_cents: pro, 3 h7500 − 20%6000
refund_cents: cancel 72 h ahead (≥ 48 h)100% of 60006000
refund_cents: cancel 36 h ahead (24–48 h)50% of 60003000
refund_cents: cancel 12 h ahead (< 24 h)0%0

These numbers already have their tests written —that's the premise of the guide: the tests already exist, the work is running them well, in CI—. Here you don't learn to write tests (that's testing-fundamentals-and-tdd-guide); you learn to take the suite you already have to a pipeline that runs it for you.

Worked example: this is what CI is going to run

Let's bring the idea down to earth with the real suite. We have the reservo/ package (the models and the functions) and three test files next to it:

reservo-ci/
├── reservo/
│   ├── __init__.py
│   ├── models.py        # Room, Member, Booking
│   ├── pricing.py       # price_cents
│   ├── scheduling.py    # overlaps, is_available
│   └── refunds.py       # refund_cents
├── test_pricing.py      # the four anchor prices
├── test_scheduling.py   # the boundaries of overlaps
└── test_refunds.py      # the five refund rows

The tests are tables of cases with parametrize, one per anchor number. For example, the pricing one:

# test_pricing.py
import pytest

from reservo.models import Room, Member
from reservo.pricing import price_cents

focus = Room(id="r-focus", name="Focus", capacity=1, hourly_cents=2500)


@pytest.mark.parametrize("tier, hours, expected", [
    ("basic", 3, 7500),   # 2500 * 3
    ("pro",   3, 6000),   # 7500 - 20%
    ("basic", 1, 2500),   # 2500 * 1
    ("pro",   1, 2000),   # 2500 - 20%
], ids=["basic-3h", "pro-3h", "basic-1h", "pro-1h"])
def test_price_by_tier_and_hours(tier, hours, expected):
    member = Member(id="m-1", name="Ana", tier=tier)
    assert price_cents(focus, member, hours) == expected

There's nothing new for you here: it's the suite you know how to write. What's interesting is what happens when you run it. This is the command you're going to type on your machine today —and, two modules from now, exactly the same command CI is going to type for you on its neutral machine.

What to expect. With Python 3.14.0 and pytest 9.1.1, running python3 -m pytest at the project root, this comes out:

============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
rootdir: /private/tmp/reservo-ci
collected 12 items

test_pricing.py ....                                                     [ 33%]
test_refunds.py .....                                                    [ 75%]
test_scheduling.py ...                                                   [100%]

============================== 12 passed in 0.04s ==============================

Read it slowly, because every piece comes back later. The header tells you which environment you ran in: Python 3.14.0, pytest-9.1.1. That detail, which looks decorative now, is the protagonist of module 3: when CI runs on Python 3.12 and you on 3.14, that line is the first clue as to why CI is red and you're green. Each dot (.) is a test that passed. And the last line, 12 passed in 0.04s, is the verdict: twelve claims about Reservo, all true, verified in four hundredths of a second.

Hold on to this image, because it's the starting point of everything: today, this green suite lives on your machine and nowhere else. If your teammate doesn't run it, they don't know it's green. If you forget to run it before pushing a change, no one runs it. And if your machine has something theirs doesn't, your green says nothing about theirs. The rest of the guide is closing those three cracks —forgetting, isolation, and environment difference— with a pipeline. And the first, the most treacherous, the environment difference, is the protagonist of the lesson that follows.

The guide's map

Eight modules, and each one leaves you a concrete capability around running your suite in CI. They're not in any random order: they go from understanding why to building the first pipeline to making it robust, fast, and with quality gates, and they end by assembling a complete pipeline for Reservo.

ModuleWhat it installsCapability you walk away with
1. From your machine to the pipeline: why CIThe "it works on my machine" problem, what CI is, the feedback loop, the stages of a pipeline, and what it protectsExplain why you need CI and what it does, without writing the workflow yet
2. Your first pipeline: pytest in CIThe first GitHub Actions workflow that runs pytest on every push: anatomy of the YAML (on, jobs, steps), reading the logWrite and read a pipeline that runs your suite on every change
3. Reproducing a CI failure locallyCI red, local green: the environment gap, pinned dependencies, deterministic installs, reproducing the failure on your machineClose the gap between "works on my machine" and the runner
4. The matrix: versions and environmentsRunning the suite across several Python versions and operating systems with strategy.matrix; when the matrix pays off and when it's noiseTest across several environments at once without duplicating work
5. Fast CI: caching and parallelismCaching dependencies, parallelizing with pytest-xdist, splitting the suite, the speed/cost trade-offHave a pipeline that protects without becoming a bottleneck
6. Quality gates: coverage thresholdsA coverage threshold that breaks the build, failing on a coverage drop, when a gate helps and when it gets in the waySet gates that raise the bar without becoming a fetish
7. Flaky tests in CIThe retry debate, quarantine, the failure that only happens in CI, why a flaky erodes trustHandle unstable tests without turning off your safety net
8. Project: a CI pipeline for ReservoThe whole process: the suite, the matrix, the coverage gate, caching and parallelism, and local parityAssemble and defend a complete end-to-end CI pipeline

Notice the shape of the arc. Module 1 gives you the why. Module 2, the real first pipeline. Module 3 closes the environment crack —the "it works on my machine" one—. Module 4 takes it to several environments at once. Module 5 makes it fast. Module 6 puts gates on it. Module 7 confronts the unstable tests. And module 8 pulls it all together in a real pipeline for Reservo.

The boundary: what's taught here and what's taught next door

This guide lives in an ecosystem of sibling guides about testing, and it has a clear rule about what's its job. It's the running your tests in CI/CD guide: not writing them, not diagnosing them in depth, not deploying your app. It helps to know from the start where to look for each thing, so you don't expect from here something that belongs to another guide or a later module:

TopicWhat's covered hereWhere the full treatment lives
How to WRITE and organize testsNothing: Reservo's tests already exist; the focus is running themtesting-fundamentals-and-tdd-guide, test-automation-framework-architecture-guide
The concrete GitHub Actions YAML workflowOnly the concept of a pipeline and its stages; the YAML arrives in module 2Module 2 of this same guide
The version matrix / CI speedNamed as part of the map, not developedModules 4 and 5 of this same guide
Diagnosing a failure in depth"Reproducing the CI failure" is touched in module 3, but deep diagnosis is separatetest-failure-diagnosis-guide
Deploying the app / CD to productionCD is named in lesson 7 to place it; deployment isn't taughttesting-backend-applications-guide grazes it; the focus here is test CI

The mechanical rule to remember it: if the question is "how do I run my suite automatically on every change?", it's this guide. If it's "how do I write a good test for this logic?", it's the fundamentals one. If it's "how do I deploy my app?", it's another. Keeping that boundary clear is what lets you learn to set up a pipeline without drowning in topics you don't need yet.

Common mistakes

Believing that "the tests pass" and "the tests pass for everyone" are the same thing. What happens: someone runs the suite on their machine, sees it green, and concludes "the code is tested, done". They push it. Their teammate clones the repo, runs the same suite, and sees red, because on their machine a dependency is missing, or the Python version is different, or an environment variable isn't set. Why it happens: the local verdict ("passes here") gets confused with a universal verdict ("passes anywhere"). How to spot it: ask yourself "would someone else, on another machine, get this same green?". If you can't answer with certainty, your green is local. How to fix it: it's literally the topic of the guide —running the suite in a clean and shared environment, that is, in CI—. This module installs the why; module 2 makes it real.

Thinking CI is "a tool you have to configure" instead of a concept. What happens: someone jumps straight to copying a YAML file from a tutorial, pastes it into their repo, sees a green check, and believes they "have CI now", without understanding what it's running or what it's protecting. When the pipeline turns red for an environment reason, they have no idea what's going on, because they never understood the mechanism. Why it happens: the visible part of CI is a config file, so it's tempting to treat it as a copy-and-paste chore. How to spot it: if you have a pipeline but couldn't explain in one sentence what problem it solves, you're missing the concept. How to fix it: this whole module. Understand the problem —"it works on my machine"— and the concept —running the suite automatically, cleanly, and shared— before touching the YAML in module 2.

Expecting the YAML in this module. What happens: someone arrives looking for "how do I write the workflow" and gets frustrated because this module doesn't give it. Why it happens: it's natural to want to go straight to the part you type. How to spot it: if you're expecting to see on: push and jobs: in this lesson, you're getting ahead of yourself. How to fix it: give yourself permission to understand first. Module 2 is dedicated entirely to the workflow, line by line, and it's going to pay off much more when you arrive with the problem and the concept already clear. The order is on purpose, just like in fundamentals an assert was written by hand before touching pytest.

Exercises

Exercise 1 — Find the three "you" words. Reread this sentence from the start: "that suite runs on your machine, when you remember to run it, with the version of Python you have". Without looking at the lesson, identify the three cracks those words hide —one per "you"— and say, for each, which part of CI closes it.

See solution

The three cracks are:

  1. "on your machine" → the environment crack. Your green only holds for your machine; on another it can be red. CI closes it by running the suite in a clean and neutral environment, the same for everyone (the topic of lessons 2 and 3).
  2. "when you remember" → the forgetting crack. A suite that depends on someone running it by hand sooner or later doesn't get run. CI closes it by running the suite automatically on every push/PR, without anyone having to remember (lesson 3).
  3. "the version of Python you have" → a concrete case of the environment crack: the version difference. Your 3.14 isn't your teammate's 3.12. CI closes it by declaring which version to use (and, in module 4, testing across several at once with the matrix).

The lesson: "it works on my machine" isn't a single failure, it's several distinct cracks —environment, forgetting, versions— and CI closes them with different mechanisms. Naming them separately is the first step to understanding what each part of a pipeline is solving.

Exercise 2 — What does a local green guarantee? You run python3 -m pytest on your machine and see 12 passed. For each of these claims, decide whether the local green guarantees it or only suggests it, and explain why in one sentence. (a) "The twelve tests pass on my machine, now." (b) "The twelve tests pass on my teammate's machine." (c) "The twelve tests will pass tomorrow on my machine." (d) "The code has no bugs."

See solution
  • (a) Guaranteed. It's exactly what the local green asserts: they passed, here, at this moment. No more, no less.
  • (b) Only suggested. Your machine and your teammate's can differ in Python version, dependencies, environment variables, or operating system. The local green says nothing firm about someone else's machine; to guarantee it you need to run in a common environment, i.e., CI.
  • (c) Only suggested. If some test depends on the clock, a date, or something that changes over time, tomorrow it could be red even though the code didn't change. Today's green doesn't guarantee tomorrow's on its own.
  • (d) Neither guaranteed nor well suggested. The green says the cases you tested pass, not that all cases pass. A bug in a case no test touches stays perfectly hidden behind a green. (This is the coverage lesson, in testing-fundamentals M6 and here in module 6.)

The lesson: a local green guarantees a single thing —"it passed here and now"— and the rest are assumptions of varying degree. A good part of this guide is turning assumption (b) into a guarantee, by moving the test to a shared environment.

Exercise 3 — Place each need where it belongs. For each sentence, decide whether you solve it with this module, with a later module of this guide, or with a sibling guide, and say which in one sentence. (a) "I want to understand why my green test broke for someone else." (b) "I want to write the workflow file that runs pytest on every push." (c) "I want to test my suite on Python 3.11, 3.12, and 3.13 at once." (d) "I want to learn to write a good test for refund_cents."

See solution
  • (a) This module (and 3). The "it works on my machine" phenomenon is exactly what this module installs, in lesson 2; reproducing and closing the gap in depth is module 3.
  • (b) A later module of this guide: module 2. The concrete YAML workflow —on, jobs, steps— is the topic of module 2. Here you'll only see the concept of a pipeline and its stages.
  • (c) A later module of this guide: module 4. Running the suite across several versions at once is the matrix, and it's developed in module 4. Here it's only named as part of the map.
  • (d) A sibling guide: testing-fundamentals-and-tdd-guide. Writing good tests belongs to the fundamentals guide. Here Reservo's tests already exist; the work is running them in CI.

The mechanical rule: "why does it break / how do I run it automatically?" is this guide; "how do I write the test?" is fundamentals; and within this guide, the concept is in module 1 and the mechanics (workflow, matrix, speed, gates) in the following modules.

Summary and next step

In this lesson you installed the problem that holds up the eight modules: your tests only protect the place where they run. Your Reservo suite lives today on your machine, runs when you remember, and its green only speaks about your environment. That leaves three cracks —forgetting, isolation, and environment difference— through which the most expensive bugs slip. The idea that closes them is Continuous Integration: running the suite automatically, in a clean and shared environment, on every change. It's the soundcheck for your code: it moves the verification from your comfortable room to the stage where it really matters.

You saw Reservo again —price_cents, overlaps, refund_cents, money in integer cents, deterministic results— and its suite really running locally: 12 passed in 0.04s. That green is the starting point. Everything that follows is taking it to a pipeline so it means "passes for anyone", not "passes for me".

Before moving on you should be able to: explain in one sentence what CI is; name the three cracks of a local green; say why Reservo's money goes in integer cents; and recite a couple of anchor numbers (basic 3 h → 7500, pro 3 h → 6000).

What's next is looking the first crack in the eye. In lesson 2 you'll see "it works on my machine" in the flesh: the same test, with the same code, giving two opposite verdicts —green for you, red in a clean environment— just because the environment changed. With real, measured output, not invented. It's the scene that makes everything else inevitable.

Resources