Skip to content

CI for the learner repo

Moduledep.05 · practice · ops · Pass 7 · 3 to 5 h
You build.github/workflows/platform.yml: jobs lint, unit, images, and kind-e2e (plus perf-gate if you did the optional load.02), next to craft.01’s ci.yml (which keeps commit-lint, native-tests, and course-check); craft.11 adds release.yml
ContractGitHub Actions workflow syntax; the CI recipe of DESIGN 5.13 (openlearn at contracts/VERSION)
Testscourse/tests/dep.05/ (check runs artifacts.py, which reads the workflow statically; section 4)
Needscraft.01 (the gate this extends), dep.04 (tilt ci is the kind job) · optional: load.02’s compare is the perf gate
Used byMS-prod and every later milestone run against what this pipeline keeps green; ops.06 (dependency upgrade) and ops.07 (perf regression) are graded by it going red
MilestoneMS-prod
Optional depthGitHub Actions security hardening (free), Continuous Delivery (Humble and Farley), ch. 5
  • Each job answers one question and has a fixed id, so branch protection can require it by name: does it lint, do the unit tests pass, do the images build, does the system deploy and pass its smoke milestone, is main slower than before.
  • Pin what runs: actions by release tag or commit sha, downloaded tools by release URL; permissions: contents: read by default.
  • The kind job builds the cluster from your deploy/kind/cluster.yaml and deploys with your tilt ci: CI and your laptop run the same loop.
  • The perf gate (optional, with load.02) runs on main only: each green run uploads its load report, the next run compares against it with load.02’s statistics, and the commit that regressed is the one that turns red.
  • Every job has a timeout; a hung cluster costs minutes, not six hours.
Terminal window
ol start dep.05 # records the start; there are no stubs
ol tests dep.05 # read the test catalog first
# write .github/workflows/platform.yml (section 4), then:
ol check dep.05 # reads the workflow; static only
git push # the real verdict: the run on GitHub
gh run list --workflow platform --limit 3

Your repo’s CI (craft.01) lints commit messages, runs a few native tests, and runs ol check --all --ci. Since then the system grew a Rust engine, a Go gateway, Dockerfiles, charts, a Tiltfile, and a load generator, and none of them is exercised when you push. A pull request can break the engine image (a moved COPY source), the chart (a renamed value), or TTFT (a lock in the scheduler) and still be green. Pass 7’s milestone is about running the system in production shape, and production shape includes a pipeline that refuses those changes.

JobQuestionFails when
lintis the code formatted and free of lint?ruff, gofmt, go vet, cargo fmt --check, cargo clippy -D warnings
unitdo the native tests pass?pytest, go test -race, cargo test, the C build
imagesdoes every Dockerfile build, as non-root?a docker build
kind-e2edoes the system deploy and serve?tilt ci, then ol milestone MS-prod --smoke
perf-gateis main slower than the last green main?{loadgen} compare base.json head.json --metric ttft_p95 --max-regress 5%

Jobs run in parallel unless needs: orders them: kind-e2e needs images (no point deploying images that do not build) and unit.

SymbolMeaning
uses: owner/action@refruns the code at ref of that repository with your job’s token and secrets
moving refa branch (main, master, stable): its code changes without you
pinned refa release tag (v4, v1.12.0) or a 40-hex commit sha

A workflow is code that runs with write access to your repository unless you say otherwise. Three habits close most of the gap: pin every action and downloaded tool, declare permissions: (read by default, a job asks for more), and pass secrets through env, never echo them (masking misses transformed values).

helm/kind-action creates a kind cluster on the runner from the config you pass. Pass deploy/kind/cluster.yaml: the same cluster name and NodePort mappings as on your laptop, so [deploy] in system.toml is right in CI too. Then tilt ci (dep.04) builds and deploys, and ol milestone MS-prod --smoke runs the milestone’s smoke steps from the openlearn commit your contracts/VERSION names (the craft.01 recipe). A throwaway API key is generated per run, masked, and stored in the Secret the gateway chart reads.

This job is optional: it needs load.02, an optional module, and the check reads it only when your workflow has it. A load report is noisy: two identical runs differ by a few percent. load.02’s compare decides whether a difference is a regression with a permutation test and a threshold (--max-regress 5%). The baseline must be a run of the same code path on the same kind of machine: the report of the last green run on main. So the job runs only on pushes to main, downloads that report (gh run download), compares, and uploads its own report for the next run. On a pull request there is no stable baseline, and a gate that compares against a different runner type blocks good changes at random.

A pull request changes go/gateway/route/route.go. With the reference needs: graph and typical durations:

Minutelintunitimageskind-e2eperf-gate
0startstartstartwaiting on images, unitskipped (not main)
2pass
6pass
9passstart
22pass (tilt ci 9 min, smoke 4 min)

Wall time 22 minutes; the jobs that can run in parallel do. If images had no needs: relationship, kind-e2e would start at minute 0 and fail on an image that does not build, after paying for the cluster.

After merge, the push to main runs perf-gate. The last green main run’s report says ttft_p95 = 410 ms; this run measures 445 ms:

445−410410=0.085=8.5%>5%\frac{445 - 410}{410} = 0.085 = 8.5\% > 5\%

and compare also finds the shift significant, so it exits 1 and the commit that introduced it is red on main. Had this run measured 418 ms (+2.0%), it would pass, and its report would become the next baseline.

A pin, checked by hand: uses: actions/checkout@v4 is a release tag (pinned); uses: dtolnay/rust-toolchain@stable is a branch (moving: rejected); curl .../tilt/master/scripts/install.sh | bash runs whatever that branch holds today (rejected); a release tarball URL with a version in it is pinned.

.github/workflows/platform.yml (the reference, abridged):

name: platform
on: {push: {branches: [main]}, pull_request: {}}
permissions: {contents: read}
jobs:
lint: {runs-on: ubuntu-latest, timeout-minutes: 10, steps: [...]} # ruff, gofmt, go vet, cargo fmt --check, clippy -D warnings
unit: {runs-on: ubuntu-latest, timeout-minutes: 20, steps: [...]} # make -C c, pytest, go test -race, cargo test
images: {runs-on: ubuntu-latest, timeout-minutes: 30, steps: [...]} # docker build -f deploy/docker/<part>.Dockerfile .
kind-e2e:
needs: [images, unit]
timeout-minutes: 45
steps:
- uses: helm/kind-action@v1.12.0
with: {cluster_name: forge, config: deploy/kind/cluster.yaml}
- run: tilt ci -f deploy/Tiltfile --context kind-forge --timeout 15m -- --topology=disaggregated
- run: | # openlearn at contracts/VERSION, as in craft.01
git -C .ol/openlearn checkout --quiet --detach "$SHA"
OL_COURSE_HOME="$PWD" .ol/openlearn/practice/bin/ol milestone MS-prod --smoke
perf-gate:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
timeout-minutes: 30
steps: # start the stack, run the loadgen, fetch the last report, then:
- run: go/bin/loadgen compare "$BASE" head.json --metric ttft_p95 --max-regress 5%
- uses: actions/upload-artifact@v4
with: {name: perf-report, path: head.json}

GitHub Actions cannot run inside ol check, so the check reads the workflow; MS-P0’s ci-status matcher is what reads the run on GitHub.

TestKINDChecksWhy it matters downstream
test_workflow_parses_and_triggersunitone mapping; on has push and pull_requestevery change is gated
test_required_jobsunitthe five job ids, each with runs-on, steps, and timeout-minutes at most 60branch protection requires them by name
test_actions_are_pinnedboundaryevery uses: has a version tag or a sha, no moving branch; no moving-branch script piped to a shellwhat runs is what you reviewed
test_lint_covers_every_languageunitruff, gofmt, go vet, cargo fmt --check, clippy -D warningscheap failures stay cheap
test_unit_runs_every_suiteunitpytest, go test -race, cargo test, make -C cyour own tests on every push
test_images_job_builds_every_dockerfileconformanceeach deploy/docker/*.Dockerfile is builta broken image fails its own pull request
test_kind_e2e_jobconformancekind from deploy/kind/cluster.yaml, tilt ci, ol milestone MS-prod --smoke at contracts/VERSION, needs: imagesthe deployed system on every pull request
test_perf_gate_runs_on_main_against_the_last_reportconformanceif the job exists: if: restricted to main; compare ... --metric ... --max-regress ...; the report uploadedregressions fail the commit that made them
test_least_privilege_and_no_echoed_secretsboundarytop-level permissions:; no step echoes a secreta compromised step can do less
PitfallSymptomCaught by
1. No permissions:, or echo ${{ secrets.X }} in a stepthe token can push to your repo; the secret is in the logtest_least_privilege_and_no_echoed_secrets
2. uses: some/action@main, or curl .../master/install.sh | basha change upstream changes your CI without a commit of yourstest_actions_are_pinned
3. go test without -racethe gateway’s data races pass CI and fail under loadtest_unit_runs_every_suite
4. A kind cluster from the action’s default configNodePorts 30080, 30090, 30320 are not mapped; MS-prod’s smoke cannot reach the gatewaytest_kind_e2e_job
5. No timeout-minutesa stuck tilt ci holds a runner for 6 hourstest_required_jobs
6. The perf gate on pull requests, or with no stored baselinerandom failures, or a gate that never compares anythingtest_perf_gate_runs_on_main_against_the_last_report
DirectionModuleHow it uses this
Backcraft.01the three gates of ci.yml and the openlearn-at-contracts/VERSION recipe
Backdep.04tilt ci is the kind job’s deploy step
Forwardcraft.11release.yml: tags, changelog, images pushed only from a release
Forwardops.06a dependency upgrade lands only with this pipeline green
Forwardops.07the perf gate’s red commit is what git bisect run hunts
Your pieceProduction equivalentWhat it addsWhere to look
tag-pinned actionssha-pinned actions with Dependabot or Renovate updatesimmutable refs that still get updates, by pull requestDependabot for actions
a kind cluster per runephemeral preview environments per pull requesta URL reviewers can clickArgo CD ApplicationSets, Vercel-style previews
a 60 s load run on maincontinuous benchmarking on dedicated runnersstable baselines, trend dashboardsBencher, Conbench
docker build onlySBOMs, signing, provenance (craft.18)know and prove what is in each imageSLSA, cosign