---
name: github-actions-templates
description: "Authoring GitHub Actions workflows: triggers, matrices, caching, concurrency, least-privilege permissions, SHA pinning, OIDC, reusable workflows, composite actions, Xcode runners. Use when writing or hardening a workflow file."
metadata:
  risk: safe
  source: multi-agent-pipeline
  date_added: "2026-09-25"
  not-for: ci-cd-pipelines
---

# GitHub Actions Authoring

This skill covers the YAML itself: how a workflow is triggered, how jobs are
wired, and how it is kept fast and safe. Deciding what a delivery pipeline
should contain (stages, promotion, rollback) is `ci-cd-pipelines`; building the
container image is `docker-expert`.

## Workflow skeleton

Start every workflow from the same frame and add to it:

```yaml
name: products-api

on:
  pull_request:
    paths: ["services/products/**", ".github/workflows/products-api.yml"]
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

defaults:
  run:
    shell: bash
    working-directory: services/products

jobs:
  test:
    runs-on: ubuntu-24.04
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@<full-40-char-sha> # v4.2.2
        with:
          persist-credentials: false
      - uses: actions/setup-node@<full-40-char-sha> # v4.1.0
        with:
          node-version-file: services/products/.nvmrc
          cache: npm
          cache-dependency-path: services/products/package-lock.json
      - run: npm ci
      - run: npm test
```

Why each piece is there:

- `permissions: contents: read` at the top makes the token read-only; jobs that
  need more ask for it themselves.
- `concurrency` cancels superseded PR runs but lets every push to `main` finish.
- `timeout-minutes` stops a hung job before it burns the default six hours.
- `paths` keeps unrelated changes from running this workflow. A required status
  check on a path-filtered workflow blocks PRs that never trigger it; gate on
  an always-running summary job instead.

## Security rules

1. **Pin third-party actions to a full commit SHA** and keep the tag in a
   comment. Tags are mutable. Dependabot (`package-ecosystem: github-actions`)
   updates SHA pins.
2. **Grant permissions per job.** Typical needs:

   | Job does | Needs |
   |----------|-------|
   | comment on a PR | `pull-requests: write` |
   | push a tag or release | `contents: write` |
   | publish to GHCR | `packages: write` |
   | OIDC to a cloud | `id-token: write` |
   | attestations | `id-token: write`, `attestations: write` |

3. **Never interpolate untrusted input into `run:`.** A PR title or branch name
   can contain shell. Pass it through `env:` and quote it:

   ```yaml
   - env:
       TITLE: ${{ github.event.pull_request.title }}
     run: echo "Checking: $TITLE"
   ```

4. **`pull_request_target` runs with secrets on fork PRs.** Do not check out
   and execute the PR head under it. Use `pull_request` for untrusted code and
   a separate `workflow_run` job for privileged follow-ups.
5. **Prefer OIDC to long-lived cloud keys.** The runner exchanges a short-lived
   token scoped to repo, branch or environment.

   ```yaml
   deploy:
     permissions:
       id-token: write
       contents: read
     environment: production
     steps:
       - uses: aws-actions/configure-aws-credentials@<sha> # v4
         with:
           role-to-assume: arn:aws:iam::123456789012:role/products-deploy
           aws-region: eu-central-1
   ```

   The cloud trust policy should match the `sub` claim, for example
   `repo:acme/shop:environment:production`, not just the repository.
6. **Keep secrets in environments** when only the deploy job needs them, so
   test jobs cannot read them.

## Matrices

```yaml
strategy:
  fail-fast: false
  matrix:
    node: [20, 22]
    os: [ubuntu-24.04]
    include:
      - node: 22
        os: macos-15
    exclude:
      - node: 20
        os: macos-15
```

- `fail-fast: false` shows every failing combination instead of the first.
- `max-parallel` protects shared resources such as a test database.
- A dynamic matrix comes from a previous job's output with `fromJSON(...)`,
  useful for "test only the packages that changed" in a monorepo.

## Caching

- Use the setup action's built-in `cache:` first (npm, pnpm, yarn, pip, poetry,
  gradle, maven, go).
- Otherwise `actions/cache` with a key that hashes the lockfile and a
  `restore-keys` prefix for partial hits.
- Caches are scoped per branch with fallback to the default branch; a PR can
  read `main`'s cache but not write to it.
- Docker layers: `docker/build-push-action` with `cache-from: type=gha` and
  `cache-to: type=gha,mode=max`.

## Environments and approvals

An `environment:` on a job applies its protection rules: required reviewers,
wait timer, allowed branches, and environment-scoped secrets and variables.
Model staging and production as separate environments and let the production
job `needs:` the staging one.

## Provenance

```yaml
permissions:
  id-token: write
  attestations: write
  contents: read
steps:
  - uses: actions/attest-build-provenance@<sha>
    with:
      subject-path: dist/products-api.tar.gz
```

Consumers verify with `gh attestation verify <file> --repo acme/shop`.

## Debugging a workflow

- Re-run with debug logging (`ACTIONS_STEP_DEBUG` secret or the re-run option).
- Lint locally with `actionlint`; it catches expression and shell errors.
- `act` runs many workflows locally, but not macOS jobs or OIDC.
- Expressions in `if:` without `${{ }}` are evaluated anyway; a string that
  starts with `!` must be wrapped in `${{ }}`.

## Review checklist

- Top-level `permissions` is read-only; every write scope is job-level and used.
- Every non-GitHub action is SHA-pinned.
- No `${{ github.event.* }}` inside a `run:` script body.
- Each job has `timeout-minutes`; PR runs cancel on new pushes.
- Cloud access goes through OIDC with a subject-restricted trust policy.
- Deploy jobs use an environment with reviewers.

## References

- `references/patterns.md`: reusable workflows versus composite actions, passing outputs and artifacts between jobs, and Xcode builds on macOS runners. Load it when the task reaches those topics.
