GitHub Actions
CI is one idea: run my commands in a fresh, reproducible environment on every change. A runner is an ephemeral VM/container — the Docker and Linux primitives you already know, on a trigger.
act (needs Docker) — otherwise push and watch the Actions tab.A workflow job runs on a clean machine that's destroyed afterward. Nothing persists unless you make it.
Add a workflow that prints the runner's identity, then run it.
$ act -j hello # or: push and read the Actions tabThe log shows a fresh Ubuntu runner and a non-root runner user — a clean room every time.
Reveal solution
$ mkdir -p .github/workflows $ cat > .github/workflows/ci.yml <<EOF name: ci on: [push] jobs: hello: runs-on: ubuntu-latest steps: - run: uname -a && whoami EOF $ git add . && git commit -m ci && git push
Steps in one job share the runner's filesystem. Separate jobs get separate runners — so state does not carry over without artifacts.
Write a file in one step, read it in the next (works); try across two jobs (fails).
$ act # observe: within-job read succeeds, cross-job read failsThe intra-job read works; the cross-job read errors — proving job isolation. Use upload/download-artifact to pass data between jobs.
Reveal solution
# job A: step1 writes state.txt, step2 cats it -> works # job B: cats state.txt -> file not found (fresh runner)
Secrets are provided to the runner at runtime and automatically masked in logs — they're never in your repo.
Add a repo secret, echo it in a step, and confirm it prints as ***.
# read the step logThe value is replaced with *** in the log. Note: masking is not encryption — never print secrets on purpose.
Reveal solution
# Settings > Secrets > Actions > New secret: TOKEN # step: run: echo "token is ${{ secrets.TOKEN }}" -> prints ***
Reach engineers who read the man page
Native, contextual, no tracking — this is how the curriculum stays free.
You don't need GitHub's compute — register your own Ubuntu box as a runner. (Fits the whole ethos: your machine, your labs.)
Register a self-hosted runner and target it with runs-on: self-hosted.
# the job runs on your machine; check its hostname in the logThe job executes on your host — the runner is just an agent you control.
Reveal solution
# Settings > Actions > Runners > New self-hosted runner # follow the ./config.sh + ./run.sh steps # workflow: runs-on: self-hosted
A fresh runner has none of your code until you check it out. The most common first-pipeline failure is a missing checkout step.
Watch a build fail because the repo isn't there, then fix it.
$ act # passes once checkout is addedAdding actions/checkout@v4 as the first step puts your code on the runner. Obvious once you know runners start empty.
Reveal solution
$ steps: $ - uses: actions/checkout@v4 $ - run: ls -la && make build
A runner is an ephemeral Docker/Linux box on a trigger; jobs are isolated; secrets are injected and masked. CI stops being YAML voodoo once you see it as "run my commands in a clean room." Ansible and Terraform slot into these pipelines next.