GitHub Actions CI Workflow
CI is a machine that runs your checks every time code moves, without being asked and without getting bored. That last part is the point. You will run the tests before merging for about two weeks. Then you'll be busy one afternoon, and the agent will say the tests pass, and you'll take its word for it, and that is the merge that breaks production. The robot does not get busy. It runs everything, every push, forever, and it does not accept "it worked locally" as evidence.
This matters double when an agent writes most of your code. Agents produce plausible code quickly, and plausible is exactly the quality that slips past a tired human reviewer. A CI run is a checkpoint the agent cannot charm its way through: the lint either passes or it doesn't, the build either compiles or it doesn't. Green checkmarks on the PR are the difference between "the agent says it's done" and "it's done."
The template is a working workflow file plus a short spec around it. The spec matters because the file alone isn't the whole job: a CI that runs but doesn't block merging is a smoke alarm with the battery out. The branch protection step at the end is what gives it teeth.
Prerequisites
- A GitHub account with your project pushed to a repo.
- A test script and lint script runnable from the command line (`npm test`, `npm run lint` or equivalent). If you don't have these yet, that is the first thing to ask your agent for.
# Task: CI pipeline with GitHub Actions
Create `.github/workflows/ci.yml` from the base below, adapted to this
project. Then walk me through enabling branch protection; that part is
manual and mine.
## Project facts
- Runtime: [Node 20 / Python 3.12 / other]
- Package manager: [pnpm / npm / yarn / pip+uv]
- Commands: install [..], lint [..], test [..], build [..]
(If any of these don't exist as scripts yet, create them first.)
## The workflow
```yaml
name: CI
on:
push:
branches: [main]
pull_request: # this is the one that matters: checks run on
# every PR before anything reaches main
concurrency:
# New push to the same PR cancels the already-outdated run.
# Saves minutes; free-tier minutes are finite.
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
checks:
runs-on: ubuntu-latest
timeout-minutes: 10 # a hung job should fail, not sit for six hours
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20 # pin it; match local and production
cache: npm # cache dependencies between runs
- name: Install
run: npm ci # ci, not install: uses the lockfile exactly,
# fails loudly if lockfile and package.json disagree
- name: Lint
run: npm run lint
- name: Test
run: npm test
- name: Build
run: npm run build # "tests pass but the build is broken" is a
# real and popular way to fail. Build in CI.
```
## Order of jobs, and why it is one job
Lint, test, build run in sequence in a single job. Parallel jobs are faster
but cost separate setup time each and complicate the file. Start sequential.
Split only when the run is slow enough to annoy you, and measure first.
## Branch protection (manual, do this in the GitHub UI)
Settings → Branches → Add branch ruleset for `main`:
1. Require a pull request before merging.
2. Require status checks to pass: select the `checks` job.
3. Require branches to be up to date before merging.
Without this, CI is advisory. With it, red X means the merge button is gray,
for you, for the agent, for everyone.
## Verification
- [ ] Push a branch with a deliberate lint error; PR shows a red X
- [ ] Fix it; PR goes green
- [ ] Try to merge a red PR; GitHub refuses
- [ ] Full run takes under [5] minutes (if not, tell me before optimizing)
## Rules for the agent
- Never disable, skip, or comment out a failing check to make CI green.
A failing check is information. Fix the cause or bring it to me.
- Never commit secrets to the workflow file. CI secrets go in
Settings → Secrets and are referenced as `${{ secrets.NAME }}`.Adaptation notes:
- pnpm: swap
cache: npmfor the pnpm setup action andpnpm install --frozen-lockfile. Python: setup-python, cache pip, then ruff and pytest in place of lint and test. The skeleton survives the translation. - TypeScript projects should add a typecheck step (
tsc --noEmit) between lint and test. It is the cheapest check per bug found. - If tests need a database, add a Postgres service container to the job rather than mocking everything into meaninglessness. Ask your agent for that variant by name.
- The mistake: treating a flaky test as CI noise and adding retries until it passes. A test that fails one run in five is telling you about a race condition. Retries are how you unsubscribe from that information while keeping the bug.
- Merge queues, deploy jobs, coverage gates: all real, all later. Get red-blocks-merge working first. It is 80% of the value at 10% of the YAML.