A one-line change from uses: vendor/action@v2 to a compromised action release can run before your application tests ever start. The uncomfortable part is that many workflow reviews still spend more time debating YAML formatting than identifying what that action can read, modify, or publish.

The OWASP CI/CD Top 10 names the risks, including CICD-SEC-8: Ungoverned Usage of 3rd Party Services, CICD-SEC-9: Improper Artifact Integrity Validation, and CICD-SEC-10: Insufficient Logging and Visibility. That list is useful as a threat model, but it is not yet a review process. A pull request needs questions a reviewer can answer from the workflow diff in roughly 10 minutes.

This worksheet focuses on three things that are visible in GitHub Actions YAML: every external action or downloaded tool, every boundary an artifact crosses, and every piece of evidence left after a release. The goal is not “make the workflow look secure.” The goal is to make a reviewer able to say exactly which revision built a deployable artifact, what code ran with credentials, and where to investigate if a release is wrong.

1. Review a workflow diff as a trust-boundary map

Start by marking three types of lines in the pull request: uses:, credential access, and artifact movement. This is faster than reading top to bottom because those lines define where the workflow trusts code, obtains authority, and turns build output into a release.

  • External code: every uses: owner/action@ref, container action, shell installer, curl download, and package-manager install.
  • Authority: permissions:, secrets.*, cloud login steps, deploy tokens, and repository write access.
  • Release material: uploads, downloads, archives, container tags, checksums, package publication, and deployment commands.
  • Evidence: step summaries, artifact metadata, release records, logs, and an identifiable commit SHA or image digest.

A useful decision rule is: review the combination, not the line in isolation. An unpinned action in a test job with read-only permissions deserves a fix. The same action immediately before a cloud deployment, with a token in its environment, is a release blocker until it is pinned, justified, or removed.

GitHub Actions is intentionally flexible: workflows can combine repository actions, marketplace actions, scripts, APIs, and packages. That flexibility means “we only use GitHub Actions” is not a supply-chain boundary. The boundary is the exact code and artifact revisions your workflow chooses to trust.

2. Find the three failures in this sample workflow

Here is a small release workflow that looks normal in a busy repository. It checks out code, installs Node.js, builds an archive, uploads it, and deploys it. A reviewer should flag more than one issue even if every command succeeds.

name: Release

on:
  push:
    tags: ["v*"]

permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - run: npm ci
      - run: npm run build
      - run: tar -czf app.tgz dist

      - uses: actions/upload-artifact@v4
        with:
          name: app
          path: app.tgz

      - uses: acme-deploy/release-action@v2
        with:
          artifact: app.tgz
          environment: production

First, four actions are trusted by mutable tags such as v4 and v2. The fictional acme-deploy action is especially important because it is closest to production deployment; replace it with the actual action from your repository when using this worksheet.

Second, app.tgz is created and handed to a deployment action with no recorded digest and no verification step. Third, the workflow produces no concise release evidence: there is no step summary containing the triggering commit, artifact SHA-256, deployment environment, or resulting image/package identifier. Standard job logs exist, but finding the relevant facts later means manually reconstructing a run.

3. Worksheet item one: govern every third-party action

For each changed uses: line, add one row to the PR review. Do this for actions from GitHub itself as well as external maintainers; “first party” may reduce organizational risk, but a tag is still a moving reference rather than an immutable revision.

Review field Question to answer in the PR Example answer
Action and purpose What capability does this code provide? docker/login-action authenticates a registry session.
Exact revision Is it pinned to a full commit SHA? owner/action@<40-character-commit-sha>
Authority Which token, secret, or cloud identity can it access? Registry credentials only; no repository write permission.
Update owner Who will update the pin when needed? Platform team via a dependency-update PR.

Replace a mutable reference such as acme-deploy/release-action@v2 with a reviewed full commit SHA. Keep a comment containing the human-friendly version if it helps reviewers:

- uses: acme-deploy/release-action@<full-commit-sha>
  # reviewed release: v2

The tradeoff is maintenance. SHA pins make it harder for an action maintainer to silently change what runs in your pipeline, but they also stop automatic feature and security updates. Solve that operationally: assign action updates to Dependabot or a scheduled maintenance PR, then review the changed SHA and release notes as code. Do not solve it by reverting to a floating tag.

Also separate action governance from permission governance. Pinning an action does not make permissions: contents: write necessary. Grant the minimum permissions at workflow or job scope, and keep deployment credentials out of build jobs that do not need them.

4. Worksheet item two: prove which artifact was deployed

Artifact integrity is not satisfied merely because a workflow uploads an artifact and later downloads one. Your review needs to establish a binding between a specific source revision, a specific artifact digest, and the deployment that consumed it.

Add a SHA-256 file when the archive is created, then verify it immediately before the deployment step. For the sample workflow, the mechanical baseline is:

- run: tar -czf app.tgz dist
- run: sha256sum app.tgz > app.tgz.sha256

- uses: actions/upload-artifact@<full-commit-sha>
  with:
    name: app
    path: |
      app.tgz
      app.tgz.sha256

- run: sha256sum --check app.tgz.sha256
- uses: acme-deploy/release-action@<full-commit-sha>
  with:
    artifact: app.tgz
    environment: production

This catches a mismatched, truncated, or accidentally substituted archive. It does not independently protect against an attacker who can alter both app.tgz and app.tgz.sha256 in the same trusted build context. That distinction is the part often skipped in checklist-only reviews.

Use this decision rule: if the artifact crosses a job, workflow, repository, or human approval boundary, require an integrity value created before the crossing and checked after it. For higher-risk production releases, also require a protected build identity and a verifiable provenance or signature mechanism appropriate to your package or container registry. A checksum generated after downloading an unknown artifact is inventory, not validation.

5. Worksheet item three: leave release evidence, not just raw logs

OWASP’s insufficient logging and visibility risk is not solved by saying “GitHub retains workflow logs.” Logs are verbose execution traces. A reviewer and incident responder need a short, durable answer to four questions: who triggered the run, which commit was built, what artifact digest was deployed, and where it was deployed.

Add a release evidence step after verification and before or after deployment, depending on whether you want to record intended versus completed deployment. GitHub Actions provides the GITHUB_STEP_SUMMARY file for a readable run summary.

- name: Record release evidence
  run: |
    {
      echo "## Release evidence"
      echo "- Commit: $GITHUB_SHA"
      echo "- Artifact: app.tgz"
      echo "- SHA-256: $(cut -d ' ' -f1 app.tgz.sha256)"
      echo "- Environment: production"
      echo "- Run ID: $GITHUB_RUN_ID"
    } >> "$GITHUB_STEP_SUMMARY"

Do not put secrets, authorization headers, or full cloud configuration in the summary. The point is correlation, not a debug dump. If your deployment produces a container image digest, package version, release URL, or Sentry release identifier, add that resulting identifier too. Sentry’s GitHub Actions documentation, for example, notes that a release can default to the GitHub SHA that triggered the workflow; that is useful only when teams consistently connect that SHA to the shipped artifact and deployment record.

Set an explicit review expectation: every production deployment job must emit one searchable record containing commit, artifact digest or immutable package identifier, target environment, and outcome. That requirement makes a post-release investigation a lookup instead of a forensic exercise across hundreds of log lines.

6. Put this 10-minute worksheet in every workflow PR this week

Start with the workflows that can publish packages, push container images, create releases, modify infrastructure, or deploy production. Do not begin by trying to remediate every historical workflow in one large security refactor; that creates noisy diffs and makes it harder to see a dangerous permission or action change.

  1. Search the repository for uses:, secrets., permissions:, upload-artifact, and deployment commands.
  2. For one production workflow, inventory every external action and replace mutable action tags with reviewed full commit SHAs.
  3. Add a SHA-256 generation and verification pair for the deployable archive, or record the immutable package/container digest used by deployment.
  4. Add a GITHUB_STEP_SUMMARY entry containing commit, artifact identity, environment, run ID, and outcome.
  5. Copy the table fields into your pull-request template and require an answer whenever workflow YAML changes.

The practical threshold is simple: a reviewer should be able to identify trusted external code, verify the deployed artifact, and reconstruct the release from one workflow run without guessing. If any answer depends on “the tag should still point to the same thing,” “the artifact came from this job somehow,” or “the logs probably have it,” the workflow has found its next security improvement.