CI Change Checklist
A short, mandatory checklist for any PR that modifies
.github/workflows/* or action.yml. The recurring failure mode of CI
changes is "passes review, breaks the moment it runs against main" — this
list eliminates the most common preventable causes.
Pre-merge checklist
Tick each item before requesting review:
- [ ] YAML parses. Run
python3 -c "import yaml; yaml.safe_load(open('<file>'))"against every changed file. - [ ] actionlint clean. Run
actionlint .github/workflows/*.yml(the Workflow Lint job runs this on every PR — green = sufficient).action.ymlis YAML-validated separately because it is a composite-action schema, not a workflow schema. - [ ] All
uses:are SHA-pinned or./. Major-version floats (@v4,@main) are rejected by the Workflow Lint job's SHA-pin step. - [ ] Dogfood path (
uses: ./): if you reference the action in this same workflow, sanity-check that emptygithub.action_refis handled. The fallback exists inaction.yml— don't break it. - [ ]
set -euo pipefailat the top of every multi-linerun:block. Without it, a failedgh run download --patternreturns 0 and tanks downstream steps silently. - [ ] Optional asset patterns (
gh release download --pattern "..."): if the asset may be absent on older releases, swallow the non-zero exit and[ -f ... ]-guard the consumer. - [ ] Egress allow-list (workflows using
step-security/harden-runnerwithegress-policy: block): every host the engine and yourrun:blocks reach must be listed inallowed-endpoints. Typo → silent block → opaque failure mode. - [ ] Permissions are minimum-privilege. Workflow-level:
contents: read. Job-level: only what the job actually needs. Audit:contents: writerequires either a published-asset use case or a justifying comment in the workflow. - [ ] No co-authorship trailers in any commit message produced by the workflow. The repo policy is sole-author Riley Ghramm.
- [ ] Manual dispatch tested when changing workflows that already had a
workflow_dispatch:trigger. Trigger one, observe the run, link it in the PR description.
What the Workflow Lint job already enforces
The .github/workflows/workflow-lint.yml job runs on every PR that touches
workflow files:
- YAML syntax — Python
yaml.safe_load. - actionlint — full static analysis via the official 1.7.7 release.
- SHA-pin discipline — rejects
@v<major>/@main/@master/@latest.
You do not need to run these locally — the PR check is authoritative — but running them locally catches issues before pushing.
Local validation script
# YAML syntax
for f in .github/workflows/*.yml action.yml; do
python3 -c "import yaml,sys; yaml.safe_load(open('$f'))" || echo "FAIL: $f"
done
# SHA-pin scan
grep -rEn '^\s*-?\s*uses:\s*[^./].*@(v[0-9]+(\.[0-9]+)?|main|master|latest)\s*$' \
.github/workflows/ action.yml || echo "OK: all SHA-pinned"
# actionlint (one-time install)
go install github.com/rhysd/actionlint/cmd/[email protected]
actionlint .github/workflows/*.yml
Deliberate exceptions
actions/checkout@<sha>is the canonical pin and always allowed.step-security/harden-runner@<sha>is mandatory on hardened jobs.- A
# v<major.minor.patch>comment next to the SHA is encouraged for human readability; the SHA is what enforces integrity.
When a workflow is acceptable to land "broken"
Almost never. The two narrow cases:
- You are explicitly fixing the workflow. Land the fix as a PR; the
first run on
mainproves it. Add a follow-up PR if regressions appear. - The workflow is being deleted. A deletion PR removes the file; no future runs, no breakage to verify.
In every other case, prefer to land workflow changes in their own PR with a linked manual-dispatch run that proves green. CI changes batched into a feature PR have a 4× higher rate of post-merge surprises in this repo's historical data.