GitHub Actions
Vidha approved a PR she shouldn't have — not because she's careless, but because catching a missing semicolon by eyeball, every single time, was never a fair job for a human.
Sahil's PR touches four files, four different kinds of changes, and somewhere in the third file there's a stray unused variable that the project's lint rules explicitly forbid. Vidha reviews it, reads the actual logic carefully, approves it — and doesn't catch the lint violation, because nobody catches every stylistic detail in a four-file diff by eye, every time, forever. It merges. A week later someone else copies the same bad pattern nearby, because it's now sitting in main looking approved and normal.
Nobody did anything wrong here, exactly. The actual failure is that "run the linter" was a step that existed only in people's heads, optional, easy to forget under any kind of time pressure — the same shape of problem as the group chat from last chapter, just for machine-checkable rules instead of decisions.
The fix isn't a better habit. It's removing the habit from the equation.
GitHub Actions runs code on GitHub's own machines, triggered automatically by things that happen in the repo — a push, a PR being opened, a schedule, a manual button. No one has to remember to run anything locally, because the check runs itself, on every single push, unconditionally.
└─ workflow (a YAML file in
.github/workflows/)└─ job (runs on a fresh virtual machine)
└─ step (a shell command, or a reusable `uses:` action)
The workflow that closes the exact gap from Sahil's PR
# .github/workflows/lint.yml
name: lint
on:
push:
branches: [main]
pull_request:
jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # clone the repo onto the runner
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
Commit this file once, and from then on every PR — Sahil's, Ishaan's, anyone's — gets an automatic, unskippable lint pass. If the unused-variable rule would have failed here, this workflow fails loudly, right on the PR, before Vidha even opens the "Files changed" tab. Her review time goes back to what only a human can actually judge — does this logic make sense — instead of also being a manual style-linter.
Reading the YAML like a sentence
| Key | Meaning |
|---|---|
name: | What shows up in the Actions tab |
on: | The trigger — push, pull_request, schedule, workflow_dispatch (a manual button), issues, release |
jobs: | One or more; run in parallel unless chained with needs: |
runs-on: | The runner's operating system |
uses: | A reusable, prebuilt action |
run: | A raw shell command |
with: | Inputs passed to an action |
Triggers worth knowing well
on:
push:
branches: [main, release/*]
paths: ["src/**", "package.json"] # only fire if these paths changed
pull_request:
types: [opened, synchronize, reopened]
schedule:
- cron: "0 3 * * 1" # every Monday, 3 AM UTC
workflow_dispatch: # a manual "Run workflow" button
inputs:
environment:
type: choice
options: [staging, production]
Matrix builds — the same check, many environments at once
Sahil's eventually going to want the site tested against more than one Node version, since not everyone on the club's laptops has the same one installed:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${{ matrix.node }} }
- run: npm test
That's six jobs from one definition, running in parallel — every combination of OS and Node version shows up as its own status check on the PR.
Secrets — the values that never appear in a log
Once deployment (next chapter) needs an API key or a token, it goes in Settings → Secrets and variables → Actions, and is referenced as ${{ secrets.MY_SECRET }} — encrypted at rest, never printed to logs even if a workflow tries, and by default not exposed at all to workflows triggered from a fork's PR (which matters, because Ishaan's fork from Chapter 11 could otherwise have been a way to exfiltrate a club secret just by opening a PR).
Why Actions themselves are restricted, not just what they run
A workflow step that does uses: some-action@v3 is running someone else's code, on the club's own repo, with access to whatever secrets that job has. Accelerate restricts allowed Actions to GitHub-owned ones plus verified publishers — and pins every one to an exact commit SHA rather than a movable tag, since a tag like @v4 can be silently repointed by its maintainer (or an attacker who compromises their account) to different code than what was originally reviewed:
# Avoid — a movable tag, could point anywhere tomorrow
- uses: some-verified/action@v3
# Correct — this exact commit, forever, regardless of what the tag later points to
- uses: some-verified/action@a1b2c3d4e5f6789012345678901234567890abcd
@v4?v4, every workflow trusting @v4 silently starts running the new code with zero warning. A commit hash, per Chapter 3, is a fingerprint of exact content — it cannot be redirected to mean something else.
Lint runs automatically now. But "automatic checking" is only half the story Sahil actually needs solved — his real job, the one written right there on the dashboard as "CI/CD — Pages deploy," is getting the website itself onto the internet without anyone manually uploading files at 11 PM before an event.