Claude
Skills
Sign in
Back

github-actions-pipelines

Included with Lifetime
$97 forever

Debugs and authors GitHub Actions workflows — OIDC federation to AWS/GCP/Azure, GITHUB_TOKEN permissions hardening, reusable workflows vs composite actions, deploy concurrency, caching, the path-filter/required-check trap, and pull_request_target security. Use when working with GitHub Actions, `.github/workflows/`, OIDC to cloud providers, `pull_request_target`, branch protection required checks, reusable workflows, or CI/CD pipelines that deploy to AWS/GCP/DigitalOcean.

Cloud & DevOps

What this skill does


# GitHub Actions Pipelines

## When to invoke

**Symptoms:**

- `Not authorized to perform: sts:AssumeRoleWithWebIdentity` from a GitHub Actions job that's "supposed to use OIDC."
- `Error: google-github-actions/auth failed with: failed to generate Google Cloud federated token`.
- A required status check is stuck "Expected — Waiting for status to be reported" on PRs that touched unrelated paths.
- Secrets are `null` / empty in a workflow triggered by a fork PR.
- A reusable workflow can't see the caller's secrets.
- Two deploys to the same environment race each other and the older one wins.
- `actions/cache` reports a hit but the build still re-installs everything.
- A workflow runs untrusted PR code with `pull_request_target` and has secrets — security audit needs a verdict.

**The trap this prevents:** treating GitHub Actions as "just YAML." The privilege model, trigger semantics, and branch-protection interactions have non-obvious failure modes that look like "the action is broken" but are actually misconfiguration.

## Cross-cutting rules

These apply to every section below.

1. **Pin third-party actions to a commit SHA, not a floating tag.** See [supply chain](#supply-chain) for the format. First-party `actions/*` / `aws-actions/*` / `google-github-actions/*` can use major-version tags; everything else pins by SHA.
2. **Default `permissions:` to least-privilege.** Add `permissions: contents: read` at the workflow root and elevate per-job only what's needed. A repo's "default workflow permissions" setting can be `read` or `read-and-write` org-wide — don't rely on it; be explicit.
3. **Never check out and execute fork code from `pull_request_target`.** See the [pull_request_target rule](#pull_request-vs-pull_request_target).
4. **Verify current action versions before recommending YAML.** First-party actions (`aws-actions/*`, `google-github-actions/*`, `actions/*`) ship breaking major versions on their own cadence and training data lags. Before writing YAML, query the action's README via Context7 (`/aws-actions/configure-aws-credentials`, `/actions/cache`, etc.) to confirm the current major and any protocol changes.
5. **Skipped jobs are not passing jobs.** A required check that's skipped (via `paths:`, `if:`, or matrix-exclude) reports nothing to branch protection. See [path-filter trap](#the-path-filter--required-check-trap).

## OIDC federation to cloud

Use OIDC instead of long-lived access keys whenever possible. Three pieces must agree, or the assume-role call fails:

1. The workflow has `permissions: id-token: write` on the job (or workflow). Without it, no OIDC token is minted.
2. The cloud trust policy's audience (`aud`) matches what the action sends (`sts.amazonaws.com` for AWS).
3. The cloud trust policy's subject (`sub`) condition matches the actual workflow context.

### AWS — `aws-actions/configure-aws-credentials`

Verify the current major via Context7 (`/aws-actions/configure-aws-credentials`) before pasting — the action follows semver and major bumps have changed the role-assumption protocol in the past. The shape below is stable across recent majors:

```yaml
permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gh-deploy
          aws-region: us-east-1
      - run: aws sts get-caller-identity
```

Trust policy (the IAM role's `AssumeRolePolicyDocument`):

```json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
      },
      "StringLike": {
        "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
      }
    }
  }]
}
```

**`sub` claim formats** (mix these up and you get AssumeRoleWithWebIdentity denied):

| Workflow context | `sub` value |
|---|---|
| Push to `main` | `repo:ORG/REPO:ref:refs/heads/main` |
| Tag push | `repo:ORG/REPO:ref:refs/tags/v1.2.3` |
| Pull request | `repo:ORG/REPO:pull_request` |
| Environment `prod` | `repo:ORG/REPO:environment:prod` |
| Any context (wildcard) | `repo:ORG/REPO:*` — use `StringLike`, not `StringEquals` |

Prefer environment-scoped subjects for deploys — they pair with [environment protection rules](#environments--protection-rules) for human approval.

**AWS-side prerequisites:**

- `arn:aws:iam::<acct>:oidc-provider/token.actions.githubusercontent.com` must exist in the account (one-time setup; v4+ of the action no longer requires a thumbprint list — AWS handles it).
- The role must have a permissions policy attached, not just the trust policy. Trust policy ≠ permissions.

### GCP — `google-github-actions/auth`

```yaml
permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/123/locations/global/workloadIdentityPools/gh/providers/gh-provider
          service_account: [email protected]
      - uses: google-github-actions/setup-gcloud@v2
      - run: gcloud auth list
```

The Workload Identity Pool provider must have an attribute condition that matches the GitHub OIDC claim, and the service account must grant `roles/iam.workloadIdentityUser` to the pool's principalSet. Details belong in the `gcp-iam` skill; this skill only validates the GitHub side.

### Azure — `azure/login`

```yaml
permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
```

Federated credentials on the Entra app registration must include the subject (`repo:ORG/REPO:ref:refs/heads/main` or env form) and audience `api://AzureADTokenExchange`.

## GITHUB_TOKEN permissions model

Every workflow run gets a scoped `GITHUB_TOKEN`. Its default permissions are controlled at three levels (most-specific wins):

1. Org / repo setting "Default workflow permissions" — either `read` or `read-and-write`.
2. Workflow-level `permissions:` block.
3. Job-level `permissions:` block.

**Recommended pattern:**

```yaml
permissions: {}   # explicit empty — nothing by default

jobs:
  build:
    permissions:
      contents: read
  release:
    permissions:
      contents: write     # to create tags / releases
      id-token: write     # to mint OIDC for signing
```

Available scopes (most common): `actions`, `attestations`, `checks`, `contents`, `deployments`, `id-token`, `issues`, `packages`, `pages`, `pull-requests`, `security-events`, `statuses`. Each is `read | write | none`.

**Watch for:** `permissions: read-all` is convenient but grants read on every scope including `pull-requests` (PR contents can be sensitive) and `security-events` (vuln data). Prefer explicit per-scope.

**Fork PR caveat:** for workflows triggered by `pull_request` from a fork, `GITHUB_TOKEN` is read-only regardless of the `permissions:` block, and repo secrets are not exposed.

## pull_request vs pull_request_target

| Trigger | Runs in context of | Secrets available | Default checkout |
|---|---|---|---|
| `pull_request` | PR head (fork or branch) | No (on fork PRs) | PR head SHA |
| `pull_request_target` | Base repo at base ref | **Yes** | Base ref (NOT PR head) |

`pull_request_target` exists for safe automation on PR metadata — labeling, commenting, triaging — using base-repo code with secrets. The killer rule:

> **Do not check out PR head and execute its code under `pull_request_target`.**

The attacke

Related in Cloud & DevOps