Lesson 2 · CI/CD & GitHub Actions fundamentals

GitHub Actions anatomy

Workflow → job → step → action, and the events that start it all.

Your win: read any workflow file — its trigger, jobs, steps, and actions — and know what a runner is. This is the grammar the other 98 workflows in the repo are written in.

The four-level hierarchy

GitHub Actions is the CI/CD system built into GitHub. Everything is YAML in .github/workflows/, and it nests four levels deep:1

Workflow (a .yml file; runs on an event) └─ Job (runs on ONE runner; jobs run in parallel unless `needs:`) └─ Step (run: a shell command OR uses: an action) └─ Action (a reusable unit a step calls)
on: pull_request           # the trigger (event)
jobs:
  test:
    runs-on: ubuntu-latest    # the runner
    steps:
      - uses: actions/checkout@v4   # a step calling an action
      - run: go test ./...          # a step running a shell command

Events — what starts a workflow

EventFires when
pushcommits land on a branch
pull_requesta PR is opened/updated (or labeled)
workflow_dispatcha human clicks "Run workflow" (manual)
schedulea cron timer
repository_dispatchan API/other-workflow signal
workflow_callanother workflow invokes it (reusable)

You met all the important ones in Lesson 1's chain: push (cuts the tag), schedule (the 2-hourly cron), repository_dispatch (build → deploy), and workflow_dispatch (the manual prod deploy).

Jobs, steps, runners — the details that bite

Jobs don't share a disk — steps do Because each job gets a fresh machine, files built in one job aren't visible in another unless you pass them explicitly (artifacts, a cache, or a registry). Steps within a job do share the filesystem. This trips up everyone once: "my build job made the binary, why can't the deploy job see it?"
Anchor — the repo's grammar & a twist Every file in .github/workflows/ (99 of them) is this same structure. But this repo does something clever with runs-on:: instead of hard-coding a runner, jobs read a computed label — runs-on: ${{ fromJson(needs.requirements.outputs.runners)['<job>'] }}. A runners composite action decides which self-hosted runner size each job should use (Lesson 7). And a requirements job even decides which jobs run at all (Lesson 3). So the workflow grammar is standard; the repo layers dynamic routing on top.
Read this next

GitHub — Workflows + Events + Workflow syntax

The workflow/job/step model, the full events list, and the YAML syntax reference.

docs.github.com — Workflows
Events · Workflow syntax

Check yourself (from memory)

Q1. The correct nesting is…

A workflow has jobs, a job has steps, a step may call an action. Each job runs on a runner.

Q2. By default, jobs in a workflow…

Parallel, each on a fresh machine; needs: sequences them. Steps within a job share the disk.

Q3. A manual "Run workflow" button is the event…

workflow_dispatch = manual trigger — how this repo's prod deploys are started.
The GitHub Actions hierarchy, events, and the job-vs-step disk rule.
recall, then click to reveal
HIERARCHY: WORKFLOW (.yml on an event) → JOB (one runner; parallel by default, needs: to sequence) → STEP (run: shell OR uses: an action) → ACTION (reusable unit). RUNNER = the machine (runs-on:) — GitHub-hosted or SELF-HOSTED. EVENTS: push, pull_request (incl. labeled), workflow_dispatch (manual), schedule (cron), repository_dispatch (workflow→workflow), workflow_call (reusable). KEY: jobs get FRESH machines (no shared disk — pass files via artifacts/cache/registry); steps in a job SHARE the disk. Repo twist: runs-on: is computed dynamically, and a requirements job decides which jobs even run.
Want to see how outputs pass between jobs, or the difference between an action and a step? Ask me.

1. GitHub — Workflows.