Skip to content

Latest commit

 

History

History
226 lines (172 loc) · 6.61 KB

File metadata and controls

226 lines (172 loc) · 6.61 KB

CI Review Example (GitHub Actions)

Goal

Review GitHub Actions workflow files for security posture, correctness, cost efficiency, and safe deployment patterns — without modifying any files.


Quick Reference

/gha-review

Reviews .github/workflows/*.yml files for security, cost, and correctness.

Expected output sections:

  • Findings — workflow name, file path, line number, and issue description
  • Security concerns — unpinned actions, missing permissions, secret exposure
  • Cost concerns — redundant jobs, missing caches, over-provisioned runners
  • Remediation — concrete YAML patches and actionlint commands

Detailed Example: Go Service CI Workflows

Prompt

/gha-review review all workflows in .github/workflows/ for security, cost, and correctness

Context

Workflow files:

.github/workflows/
  ci.yml          ← build, lint, test on pull_request and push
  deploy.yml      ← deploy to staging/production on push to main
  release.yml     ← create GitHub release on tag push

Example Output

## Findings

**F1 — Unpinned third-party actions (ci.yml:18, deploy.yml:12, release.yml:9)**

Multiple workflows use actions pinned to a mutable tag instead of a commit SHA:
```yaml
# Current (unsafe):
- uses: actions/checkout@v4
- uses: golangci/golangci-lint-action@v6
- uses: docker/build-push-action@v5

A compromised or force-pushed tag can silently replace the action code. Pin to the full commit SHA:

# Correct:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683        # v4.2.2
- uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897dab680ace5204f72  # v6.5.0
- uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83    # v5.4.0

Use pin-github-action or Renovate to manage SHA pins automatically.

  • Severity: High (supply-chain risk)

F2 — No top-level permissions block (ci.yml, deploy.yml, release.yml)

None of the workflows declare a permissions block. GitHub defaults to contents: read for most events, but GITHUB_TOKEN inherits the repository's default permission setting, which many organisations leave at read-write. Least-privilege requires explicit scoping:

# Add at the workflow level:
permissions:
  contents: read

# Override per-job where write is needed:
jobs:
  deploy:
    permissions:
      contents: read
      id-token: write   # for OIDC token exchange
      packages: write   # for GHCR push
  • Severity: High (over-privileged token)

F3 — No cache for Go modules (ci.yml:34)

The go test job downloads the full module cache on every run:

- name: Run tests
  run: go test ./...

With a 47-module dependency graph, cold-cache runs take ~90 seconds of network time. Add:

- uses: actions/setup-go@v5
  with:
    go-version-file: go.mod
    cache: true          # caches $GOPATH/pkg/mod and ~/.cache/go-build

actions/setup-go@v5 handles cache key derivation from go.sum automatically.

  • Severity: Medium (cost/speed)

F4 — No concurrency group on deploy.yml

deploy.yml triggers on every push to main. Rapid successive pushes will queue multiple deploy jobs targeting the same environment simultaneously, risking interleaved Helm releases or partial rollouts:

# Add at the workflow level or per-job:
concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: false   # false = queue, not cancel, for deploys

For CI jobs (ci.yml), cancel-in-progress: true is appropriate to save runner minutes. For deploy jobs, queuing (false) is safer than cancellation.

  • Severity: High (operational safety)

F5 — Secret referenced in run: step log (ci.yml:61)

- name: Integration test
  run: |
    echo "Connecting to ${{ secrets.DB_URL }}"
    go test ./integration/...

${{ secrets.* }} in a run: command will be masked in logs, but the value is passed as a shell argument and may appear in process listings or set -x output. Use an environment variable instead:

- name: Integration test
  env:
    DB_URL: ${{ secrets.DB_URL }}
  run: go test ./integration/...
  • Severity: Medium (secret hygiene)

F6 — actionlint not run in CI

No workflow runs actionlint to statically validate workflow syntax. Errors in YAML expressions (e.g. wrong context names, missing env: blocks) are only caught at runtime. Add a workflow lint step:

- name: Lint workflows
  uses: rhysd/actionlint@17bd74abb1b69c6cba85b2a98a67a7c8a82ca3db  # v1.7.7
  with:
    shellcheck: true

Or run locally before pushing:

brew install actionlint
actionlint .github/workflows/*.yml
  • Severity: Medium

Security Concerns

ID Issue Severity
F1 Unpinned third-party actions (3 workflows) High
F2 No permissions block — implicit over-privilege High
F5 Secret value interpolated in run: step Medium

Cost Concerns

ID Issue Severity
F3 No Go module cache — ~90s extra per CI run Medium
F4 No deploy concurrency group — wasted parallel runs High

Remediation

Priority order:

  1. Pin all actions to commit SHAs (F1) — run pin-github-action across all three workflow files, then commit the result to the repo.

  2. Add permissions block (F2) — add a workflow-level permissions: contents: read block to all three files; add per-job overrides for GHCR push and OIDC.

  3. Fix deploy concurrency (F4) — add concurrency: group with cancel-in-progress: false to deploy.yml.

  4. Add Go module cache (F3) — update ci.yml to use actions/setup-go@v5 with cache: true.

  5. Fix secret interpolation (F5) — move ${{ secrets.DB_URL }} to env: block in the integration test step.

  6. Add actionlint (F6) — add a workflow validation step or pre-push hook.

Validation commands:

# Install and run actionlint locally:
brew install actionlint
actionlint .github/workflows/*.yml

# Verify SHA pins resolve to the correct tags:
gh api /repos/actions/checkout/git/refs/tags/v4 --jq '.object.sha'

# Dry-run a workflow locally with act:
act pull_request --dry-run

---

## Notes

- `/gha-review` is read-only. No workflow files are modified.
- Always include the `actionlint` validation command in the remediation section
  — it catches expression and syntax errors that static review can miss.
- Supply-chain findings (F1) and over-privilege findings (F2) should always
  be listed first, regardless of the order they appear in the workflow files.
- For `cancel-in-progress`: use `true` for CI workflows (save minutes) and
  `false` for deploy workflows (prevent partial rollouts).