Skip to content

Latest commit

 

History

History
 
 

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Workflows

How we write GitHub Actions workflows safely. Follow these when adding or editing anything in this directory.

Token permissions

  • Every workflow declares a top-level permissions: block, defaulting to contents: read. This is the floor. A compromised dependency or action in a build/test job cannot push code, publish packages, or comment on issues.
  • Escalate at the job level, never the top level. A job-level permissions: block fully replaces the default for that job, so grant the extra scope only to the specific job that needs it:
    • posts a PR comment → pull-requests: write
    • publishes packages → packages: write
    • pushes commits / creates releases → contents: write
    • authenticates via OIDC → id-token: write
  • New jobs start with no extra scope. Because the default is read-only, an under-scoped job fails loudly rather than running over-privileged. Add only the scope that fails.
  • Prefer a PAT, deploy key, or OIDC over widening GITHUB_TOKEN. Cross-repo dispatch and releases use dedicated credentials, so those jobs keep contents: read.

permissions: scopes the GITHUB_TOKEN only. It does not protect secrets and does not prevent code from running — it caps the damage of a stolen token, it does not make a job safe.

Untrusted input

  • Use pull_request, not pull_request_target. Fork PRs then run with a read-only token and no access to secrets. Only use pull_request_target when you fully control what it runs, and never check out and execute untrusted code under it.

  • Never interpolate ${{ github.event.* }} into a run: block. PR titles, branch names, and bodies are attacker-controlled and lead to shell injection. Pass them through env: and reference the variable instead:

    # Bad — injectable
    run: echo "${{ github.event.pull_request.title }}"
    
    # Good
    env:
      PR_TITLE: ${{ github.event.pull_request.title }}
    run: echo "$PR_TITLE"

Secrets

  • Expose a secret only to the job that uses it. Do not make secrets available to jobs that run untrusted code.
  • Prefer OIDC (id-token: write) over long-lived stored secrets where the provider supports it.

Supply chain

  • Pin every third-party action to a full commit SHA, with the version as a trailing comment:

    uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

    A tag or branch ref can be re-pointed at malicious code; a SHA cannot.

  • Install with a frozen lockfile (pnpm install --frozen-lockfile).

Checklist for a new workflow

  1. Top-level permissions: { contents: read }.
  2. Job-level overrides only where a job needs more.
  3. Trigger is pull_request unless pull_request_target is genuinely required and safe.
  4. No ${{ github.event.* }} inside run:.
  5. Third-party actions pinned to SHAs.
  6. Secrets scoped to the jobs that use them.