sn sysnerdunderstand it from the kernel up
← curriculum map
GitHub Actions
Tool · ~45 min · 5 labs
map / The stack / GitHub Actions
The stack Tool ~45 min requires: Gitrequires: Docker

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.

Before you start. You'll need a GitHub repo. To run workflows fully locally, install act (needs Docker) — otherwise push and watch the Actions tab.
Q1Every job starts on a fresh runnerwarm-up

A workflow job runs on a clean machine that's destroyed afterward. Nothing persists unless you make it.

Task

Add a workflow that prints the runner's identity, then run it.

Verify it yourself
verify
$ act -j hello   # or: push and read the Actions tab

The log shows a fresh Ubuntu runner and a non-root runner user — a clean room every time.

Reveal solution
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
Q2Steps share state; jobs do notcore

Steps in one job share the runner's filesystem. Separate jobs get separate runners — so state does not carry over without artifacts.

Task

Write a file in one step, read it in the next (works); try across two jobs (fails).

Verify it yourself
verify
$ act   # observe: within-job read succeeds, cross-job read fails

The intra-job read works; the cross-job read errors — proving job isolation. Use upload/download-artifact to pass data between jobs.

Reveal solution
solution
# job A: step1 writes state.txt, step2 cats it -> works
# job B: cats state.txt -> file not found (fresh runner)
Q3Secrets are injected and maskedcore

Secrets are provided to the runner at runtime and automatically masked in logs — they're never in your repo.

Task

Add a repo secret, echo it in a step, and confirm it prints as ***.

Verify it yourself
verify
# read the step log

The value is replaced with *** in the log. Note: masking is not encryption — never print secrets on purpose.

Reveal solution
solution
# Settings > Secrets > Actions > New secret: TOKEN
# step:  run: echo "token is ${{ secrets.TOKEN }}"   -> prints ***
Sponsored

Reach engineers who read the man page

Native, contextual, no tracking — this is how the curriculum stays free.

Q4Self-hosted runner on your own machinecore

You don't need GitHub's compute — register your own Ubuntu box as a runner. (Fits the whole ethos: your machine, your labs.)

Task

Register a self-hosted runner and target it with runs-on: self-hosted.

Verify it yourself
verify
# the job runs on your machine; check its hostname in the log

The job executes on your host — the runner is just an agent you control.

Reveal solution
solution
# Settings > Actions > Runners > New self-hosted runner
# follow the ./config.sh + ./run.sh steps
# workflow:  runs-on: self-hosted
Q5Break-and-fix — where are my files?debug

A fresh runner has none of your code until you check it out. The most common first-pipeline failure is a missing checkout step.

Task

Watch a build fail because the repo isn't there, then fix it.

Verify it yourself
verify
$ act   # passes once checkout is added

Adding actions/checkout@v4 as the first step puts your code on the runner. Obvious once you know runners start empty.

Reveal solution
solution
$ steps:
$   - uses: actions/checkout@v4
$   - run: ls -la && make build
What you just built

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.