AI Skill Report Card
Reviewing GitHub Actions
Quick Start15 / 15
When reviewing a workflow file, check it in this order:
- Syntax & structure - valid YAML, correct top-level keys (
on,jobs,steps) - Triggers -
on:matches intended events, no accidental broad triggers - Permissions - explicit
permissions:block, least privilege - Secrets & security - no hardcoded secrets, safe handling of untrusted input
- Actions versions - pinned to SHA or trusted tag, not
@master/@main - Job/step logic - correct
needs:,if:, matrix, working-directory - Script consistency - scripts called by workflow still match invoked args/paths
Bash# Quick local validation before deep review actionlint .github/workflows/*.yml yamllint .github/workflows/*.yml
Recommendation▾
Add a third example showing a clean/passing workflow to demonstrate a good outcome, not just violations
Workflow15 / 15
Progress:
- [ ] Validate YAML syntax (actionlint/yamllint)
- [ ] Check triggers (on:) for scope and safety
- [ ] Verify permissions block is least-privilege
- [ ] Audit secrets usage and untrusted-input handling
- [ ] Confirm third-party actions are pinned (SHA preferred)
- [ ] Trace job dependencies (needs, if conditions)
- [ ] Cross-check invoked scripts match current signatures/paths
- [ ] Check caching, concurrency, and timeout settings
- [ ] Confirm matrix builds cover intended combinations
- [ ] Flag redundant or dead steps
1. Syntax & structure
- Run
actionlint— catches invalid contexts, unknown keys, shellcheck issues inrun:blocks. - Confirm indentation matches YAML spec; no tabs.
2. Triggers
pull_requestvspull_request_target— flagpull_request_targetcombined with checkout of PR head (code injection risk).workflow_dispatchinputs should havetypeanddefaultset.- Avoid
on: pushwith no branch/path filters on large repos (wastes CI minutes).
3. Permissions
- Repo default should be
permissions: read-allor explicit per-jobpermissions:. - Flag jobs granting
write-allorcontents: writeunless justified (e.g., release, tag push). GITHUB_TOKENscope should be minimal per job, not just workflow-level.
4. Secrets & security
- No secret values in
run:echo/print statements. ${{ github.event.* }}values used directly inrun:shell commands — flag as injection risk; require env var indirection:YAML# Bad run: echo "Title: ${{ github.event.issue.title }}" # Good env: TITLE: ${{ github.event.issue.title }} run: echo "Title: $TITLE"- Forked-PR workflows must not expose secrets to untrusted code (
pull_requesttriggers don't get secrets by default — verify nopull_request_targetmisuse).
5. Action versions
- Third-party actions (
uses: owner/repo@ref) should pin to a full commit SHA, or at minimum a version tag — never@main/@master/@latest. - Official GitHub actions (
actions/checkout,actions/setup-node) can use major version tags (@v4) but flag outdated majors.
6. Job/step logic
- Check
needs:graph has no cycles and reflects real dependencies. if:conditions usingalways()/success()/failure()are used deliberately, not copy-pasted.continue-on-error: trueshould be justified — flag silent failures in critical jobs.timeout-minutesset on long-running jobs to avoid runaway costs.
7. Script consistency
- When a workflow calls a script (
run: ./scripts/deploy.sh $ARG1 $ARG2), verify the script's actual parameter count/order/names still match. - If script was refactored (renamed flags, changed exit codes), confirm workflow's error handling (
if: failure(), exit code checks) still aligns. - Check working-directory assumptions (
working-directory:orcdin script) match repo layout.
Recommendation▾
Include guidance on how to structure the final review output (e.g., severity levels, summary format) for consistency across reviews
Examples18 / 20
Example 1: Input:
YAMLon: pull_request_target jobs: build: steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - run: npm install && npm test
Output:
⚠️ CRITICAL: pull_request_target checks out untrusted PR head SHA.
This exposes secrets/write-permissions to attacker-controlled code.
Fix: use `pull_request` trigger instead, or split into a trusted workflow
that only runs after manual approval/label, and never checkout untrusted
ref with elevated permissions.
Example 2: Input:
YAML- uses: some-org/some-action@main - run: ./deploy.sh ${{ github.event.inputs.env }}
Output:
⚠️ Pin `some-org/some-action` to a commit SHA — @main is mutable and a
supply-chain risk.
⚠️ `${{ github.event.inputs.env }}` interpolated directly into shell —
route through env: block to avoid injection.
ℹ️ Verify deploy.sh still accepts a single positional `env` arg (last
refactor added a required --region flag on 2024-03-10 commit).
Recommendation▾
Consider adding a brief note on handling composite/reusable workflow files (workflow_call) review nuances
Best Practices
- Prefer reusable workflows (
workflow_call) over duplicated job logic across files. - Use
concurrency:groups to cancel stale runs on the same ref (saves CI minutes). - Cache dependencies (
actions/cacheor built-in caching in setup actions) but scope cache keys to lockfile hash. - Keep
run:blocks short; move complex logic into versioned scripts underscripts/for testability. - Add
actionlintandyamllintto a pre-commit hook or CI meta-check so review catches issues before human review. - When a script changes, search all workflow files for its invocation (
grep -rn "script-name" .github/workflows/) to catch stale callers.
Common Pitfalls
- Trusting
pull_request_target+ PR-head checkout — classic secret-exfiltration vector. - Using
@main/@masterfor third-party actions — untraceable supply-chain risk. - Workflow-level
permissions: write-all"just in case." - Interpolating
github.event.*directly intorun:shell strings. - Forgetting to update workflow when the invoked script's CLI signature changes (silent breakage until next run).
- Missing
timeout-minutes, leading to stuck jobs consuming billable minutes indefinitely. - Matrix builds with unintended combinations (e.g.,
exclude:missing after adding a new OS/version).