De-slop: remove every em-dash + banned words across all modules + capstone (#94)
Sync course wiki / sync-wiki (push) Successful in 4s

Co-authored-by: claude <claude@jpaul.io>
Co-committed-by: claude <claude@jpaul.io>
This commit was merged in pull request #94.
This commit is contained in:
2026-06-22 23:21:22 -04:00
committed by Claude (agent)
parent 513d7e7ac8
commit c098933f25
99 changed files with 1324 additions and 1315 deletions
+33 -33
View File
@@ -1,4 +1,4 @@
# Module 14 Continuous Integration
# Module 14: Continuous Integration
> **The AI writes code that looks right. CI checks whether it actually is: automatically, on every
> push, before anyone trusts it.** This module turns the tests you wrote in Module 13 into a gate
@@ -8,18 +8,18 @@
## Prerequisites
- **Module 8 Remotes and Hosting.** CI runs *on the forge*, triggered by pushes. You need a repo
pushed to a remote (any forge GitHub, GitLab, a self-hosted Forgejo/Gitea, whatever you set up
- **Module 8: Remotes and Hosting.** CI runs *on the forge*, triggered by pushes. You need a repo
pushed to a remote (any forge: GitHub, GitLab, a self-hosted Forgejo/Gitea, whatever you set up
in Module 8) for there to be anything to trigger.
- **Module 13 Testing in the AI Era.** CI is mostly "run the tests, automatically." You need tests
- **Module 13: Testing in the AI Era.** CI is mostly "run the tests, automatically." You need tests
to run. If you skipped writing them, this module's lab ships a small suite so you're not blocked,
but the real payoff is automating *your* tests.
- **Module 2 Version Control.** Pushes, commits, and the diff habit are the substrate CI sits on.
- **Module 2: Version Control.** Pushes, commits, and the diff habit are the substrate CI sits on.
You do **not** need Docker, secrets management, or your own runner yet those are Modules 16, 17,
You do **not** need Docker, secrets management, or your own runner yet; those are Modules 16, 17,
and 19. On a **SaaS forge** (GitHub, GitLab.com, Bitbucket, and the rest) this module uses the
forge's hosted runners, which require zero setup. **One honesty note for the self-host track:** a
self-hosted Forgejo/Gitea/GitLab CE has the CI *feature* but no hosted compute nothing actually
self-hosted Forgejo/Gitea/GitLab CE has the CI *feature* but no hosted compute; nothing actually
runs until you attach a runner, and that's Module 19. The workflow you write here is correct either
way and will run the moment a runner is registered; to watch it go green *now*, use a SaaS forge's
hosted runners, then come back and own the compute end-to-end in Module 19.
@@ -30,7 +30,7 @@ hosted runners, then come back and own the compute end-to-end in Module 19.
By the end of this module you can:
1. Explain what CI actually is automated checks bound to a trigger and why "on every push" is the
1. Explain what CI actually is, automated checks bound to a trigger, and why "on every push" is the
part that makes it valuable.
2. Write a forge-native CI workflow that checks out your code, installs its tools, and runs a linter
and your test suite.
@@ -73,9 +73,9 @@ Three properties make CI more than a glorified shell script:
Almost every CI configuration, on every forge, is the same four moves:
1. **Check out the code** onto the runner. The runner starts empty; first you put your repo on it.
2. **Set up the environment** install the language runtime, pin its version.
3. **Install the tools** the checks need the test runner, the linter.
4. **Run the checks** lint, then test. Any check that exits non-zero fails the whole run.
2. **Set up the environment**: install the language runtime, pin its version.
3. **Install the tools** the checks need: the test runner, the linter.
4. **Run the checks**: lint, then test. Any check that exits non-zero fails the whole run.
That last point is the load-bearing one. CI's entire enforcement mechanism is the **exit code**.
Every tool you'd run in a terminal returns 0 for success and non-zero for failure. `python -m
@@ -88,13 +88,13 @@ testing system; you're wiring the tools you already have to a trigger.
Three tiers of check, cheapest first, because a fast check that fails early saves you waiting on a
slow one:
- **Lint** — static checks that don't run your code: style, unused imports, obvious mistakes. Fast,
- **Lint.** Static checks that don't run your code: style, unused imports, obvious mistakes. Fast,
cheap, catches a surprising amount. We use a linter as the example here; the principle is
tool-agnostic.
- **Build** — does the code even assemble? For an interpreted language like our Python example
- **Build.** Does the code even assemble? For an interpreted language like our Python example
there's no compile step, so "build" often collapses into "does it import without erroring." For
compiled languages this is where a broken type or missing symbol gets caught.
- **Test** — the Module 13 suite. The expensive, high-value tier: it actually runs your code and
- **Test.** The Module 13 suite. The expensive, high-value tier: it actually runs your code and
checks behavior.
Order them cheap-to-expensive so the fast checks fail fast. There's no reason to spend two minutes
@@ -102,8 +102,8 @@ running the test suite if the linter would have rejected the push in three secon
### The worked example: a forge-native workflow
Here's a complete, real CI pipeline for the `tasks-app`. This is GitHub Actions YAML the most
common dialect, and our default example but **read it as a concept, not a product.** Every forge
Here's a complete, real CI pipeline for the `tasks-app`. This is GitHub Actions YAML, the most
common dialect and our default example, but **read it as a concept, not a product.** Every forge
has the exact same pipeline in its own dialect; the GitLab version is in the lab folder, and it's
the same five moves.
@@ -133,7 +133,7 @@ jobs:
```
Reading it top to bottom: `on:` is the trigger (push and pull request). `runs-on:` picks the clean
machine. The `steps:` are the four moves checkout, set up Python, install the tools, then the two
machine. The `steps:` are the four moves: checkout, set up Python, install the tools, then the two
checks. `uses:` pulls in a pre-built action (someone else's reusable step); `run:` is just a shell
command. The linter runs first because it's cheap; the tests run last because they're the
expensive, decisive check. Only the linter needs a `pip install` here; the tests run on Python's
@@ -151,7 +151,7 @@ When CI goes red, the skill is triage, and it's fast once you know the shape:
1. **Open the run.** The forge shows the job as a list of steps with a red X on the one that failed.
2. **The first red step is the cause.** Steps run in order and stop at the first failure; everything
after it is skipped, not broken. Don't get distracted by the skipped steps.
3. **Read that step's log.** It's the same output the tool prints in your terminal a failing
3. **Read that step's log.** It's the same output the tool prints in your terminal: a failing
`unittest` assertion, a `ruff` finding with a file and line number. CI didn't invent a new error
format; it's showing you the command's own output.
4. **Reproduce it locally.** The same command from the failed step (`python -m unittest` or
@@ -213,12 +213,12 @@ break it on purpose and watch CI catch it.
- The `tasks-app` from Modules 12, **pushed to a forge** (Module 8). Any forge works.
- The starter files in this module's `lab/`:
- `ci-starter.yml` the workflow (GitHub Actions flavor).
- `gitlab-ci-starter.yml` the same pipeline for GitLab, if that's your forge.
- `test_tasks.py` a small test suite (use your Module 13 tests instead if you have them).
- `ci-starter.yml`: the workflow (GitHub Actions flavor).
- `gitlab-ci-starter.yml`: the same pipeline for GitLab, if that's your forge.
- `test_tasks.py`: a small test suite (use your Module 13 tests instead if you have them).
- Python 3.10+ locally, and your agent. Examples use **Claude Code**; sub your own agent anywhere.
### Part A Run the checks locally first
### Part A: Run the checks locally first
Never push a workflow you haven't run by hand. CI just runs the same commands, so prove they work on
your machine first.
@@ -249,7 +249,7 @@ your machine first.
If both are clean locally, CI will be green. If not, fix it here; it's faster than waiting on a
runner. (Only the linter needs installing. The stdlib `unittest` runner ships with Python.)
### Part B Add the workflow and watch it pass
### Part B: Add the workflow and watch it pass
2. Direct the agent to put the workflow where your forge looks for it. Tell Claude Code which forge
you're on and let it pick the path:
@@ -277,7 +277,7 @@ your machine first.
prerequisites; the workflow is correct, it just has no compute until you attach a runner in
Module 19. Run this part on a SaaS forge to see green right now.)
### Part C Break it on purpose and watch CI catch it
### Part C: Break it on purpose and watch CI catch it
This is the whole point. You're going to ship the kind of plausible-but-wrong change AI produces,
and watch CI stop it.
@@ -336,7 +336,7 @@ the reviewer that caught a change you might have trusted.
The honest caveats, because a skeptical audience trusts the limits more than the pitch:
- **CI only catches what your checks check.** A green run means "the linter found nothing and the
tests passed" not "the code is correct." If the AI broke behavior you have no test for, CI is
tests passed," not "the code is correct." If the AI broke behavior you have no test for, CI is
cheerfully green while the bug ships. CI is exactly as good as your test suite (Module 13), and no
better. The flipped-comparison bug above got caught *because a test covered it.*
- **Green CI is not "reviewed."** It checks behavior, not design, intent, security, or whether the
@@ -344,7 +344,7 @@ The honest caveats, because a skeptical audience trusts the limits more than the
in Module 15; it sits alongside them. Treating a green check as sign-off is how plausible-wrong
code with no failing test sails straight through.
- **The clean machine is a feature that feels like a bug.** Sooner or later CI fails in a way you
can't reproduce locally a dependency you have installed but never declared, a file outside the
can't reproduce locally: a dependency you have installed but never declared, a file outside the
repo your code quietly reads, a path that only exists on your machine. That's not flakiness; it's
CI correctly catching that your code depends on something that isn't in the repo. Fix the
dependency, don't blame the runner. (Module 16's containers make local and CI environments
@@ -368,15 +368,15 @@ The honest caveats, because a skeptical audience trusts the limits more than the
- Your `tasks-app` has a committed CI workflow that runs a linter and your tests on every push, and
you've watched it go green on the forge.
- You pushed a plausible-but-wrong change and watched CI catch it found the failed step, read the
- You pushed a plausible-but-wrong change and watched CI catch it: found the failed step, read the
log, reproduced the failure locally, and fixed it.
- You can explain, in your own words, why CI specifically matters for AI-generated code (it checks
behavior, not appearance) and the one thing a green check does *not* tell you (that the code is
correct only that your checks passed).
correct; only that your checks passed).
- You can point at the same pipeline in two forge dialects and see it's the same five moves.
When pushing a change and *expecting* the gate to either bless it or stop it feels automatic when
you'd be uneasy merging code that hadn't been through CI you've got it. Module 15 adds the next
When pushing a change and *expecting* the gate to either bless it or stop it feels automatic, when
you'd be uneasy merging code that hadn't been through CI, you've got it. Module 15 adds the next
gates on the same pushes: scanning for vulnerable dependencies, leaked secrets, and the packages AI
hallucinates into existence.
@@ -392,10 +392,10 @@ Re-check at build time:
- [ ] **Runner labels.** Confirm `ubuntu-latest` (and any GitLab `image:` tag) still resolves to a
supported image; default runner OS versions roll forward.
- [ ] **Trigger and config syntax.** Verify the `on:` keys and overall workflow schema against the
forge's current docs Actions YAML keys do change.
forge's current docs; Actions YAML keys do change.
- [ ] **Forge UI labels.** The tab names in the lab ("Actions," "CI/CD," "Pipelines") and the
workflow file locations (`.github/workflows/`, `.gitlab-ci.yml`, `.forgejo/`, `.gitea/`) match
what the current forge versions actually use.
- [ ] **Tool names.** The example linter (`ruff`) is current, installable, and still behaves as
described or swap in the equivalent the rest of the course uses. (The test runner is Python's
standard-library `unittest`, which ships with Python no install, nothing to drift.)
described, or swap in the equivalent the rest of the course uses. (The test runner is Python's
standard-library `unittest`, which ships with Python; no install, nothing to drift.)