Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
id: ca-157
title: '`actions/cache` save silently skipped on `pull_request_target`, `issue_comment`, and fork `workflow_run` — cache token now read-only'
category: caching-artifacts
severity: silent-failure
tags:
- pull_request_target
- issue_comment
- workflow_run
- fork
- cache
- read-only
- cache-poisoning
- breaking-change
- silent-failure
patterns:
- regex: 'Cache save is skipped.*read.only|read-only cache token|cache.*write.*not.*permitted'
flags: 'i'
- regex: 'Warning: Cache not saved.*untrusted trigger|cache.*token.*read.only'
flags: 'i'
- regex: 'Cache hit.*but.*save.*skipped|save.*denied.*read.only.*token'
flags: 'i'
error_messages:
- 'Warning: Cache not saved: cache token is read-only for untrusted triggers'
- 'Cache save is skipped because the GITHUB_TOKEN does not have cache write permission'
- 'Warning: Cache not saved — read-only cache token issued for this trigger'
- 'actions/cache: cache save skipped (read-only token)'
root_cause: |
On June 26, 2026, GitHub began issuing read-only cache tokens when BOTH of these
conditions are true:

1. The triggering event is "untrusted" — meaning someone outside the repository's
collaborator set can fire the event. Untrusted triggers include:
- `pull_request_target` (can be triggered by fork PR authors)
- `issue_comment` (any user with read access can comment)
- Fork pull-request `workflow_run` cascades (the downstream workflow runs with
default-branch context but was triggered by a fork's PR)

2. The workflow's execution context AND cache scope come from the default-branch SHA.

Prior to this change, all triggers received read-write cache tokens. This allowed a
"cache poisoning" attack: an external actor could trigger an untrusted workflow, write
a malicious cache entry keyed to the default branch, and then a trusted workflow (e.g.,
`push` or `schedule`) would restore those poisoned entries and execute attacker-controlled
code with the repository's full secrets and identity.

When the read-only restriction applies:
- `actions/cache` steps that call the save action log a warning and complete without
saving. **The job does not fail.** This is a silent failure mode.
- `actions/cache/restore` (read-only restore steps) are unaffected.
- Triggers that keep full read-write cache access: `push`, `schedule`,
`workflow_dispatch`, `repository_dispatch`, `delete`, `registry_package`,
`page_build`, `pull_request` (non-default-branch scope), `release`.

Common symptom: After this change ships, workflows using `pull_request_target` or
`issue_comment` that previously populated the cache now show persistent cache misses
on subsequent runs, with no error — just degraded build performance and slow CI.
fix: |
Move cache SAVES to a trusted trigger workflow (`push`, `schedule`, or
`workflow_dispatch`). Untrusted trigger workflows should only RESTORE cache.

Split pattern:
1. Create a `cache-warm.yml` workflow triggered by `push` that saves the cache.
2. Modify the `pull_request_target` / `issue_comment` workflow to use
`actions/cache/restore` instead of `actions/cache` (the combined save+restore action).

If the PR/issue workflow must also build artifacts that were previously cached, restore
from what the trusted workflow wrote. If there is no warm cache entry yet, accept the
cold build on those runs.
fix_code:
- language: yaml
label: 'Trusted workflow: save cache on push to default branch'
code: |
# .github/workflows/cache-warm.yml
name: Warm cache
on:
push:
branches: [main]
jobs:
warm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/cache/save@v4
with:
key: deps-${{ hashFiles('package-lock.json') }}
path: node_modules
- run: npm ci
- language: yaml
label: 'Untrusted workflow: restore cache only (no save)'
code: |
# .github/workflows/pr-check.yml
name: PR check
on: pull_request_target

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/cache/restore@v4 # restore only, not the combined save+restore
with:
key: deps-${{ hashFiles('package-lock.json') }}
path: node_modules
- run: npm test # may be a cold run if cache isn't warm yet
- language: yaml
label: 'Wrong pattern: actions/cache on pull_request_target silently does nothing on save'
code: |
# This pattern silently skips the cache save step — no error, just no cache written
on: pull_request_target
jobs:
check:
steps:
- uses: actions/cache@v4 # save step is silently skipped — use cache/restore@v4 instead
with:
key: deps-${{ hashFiles('package-lock.json') }}
path: node_modules
prevention:
- 'Use `actions/cache/restore@v4` (not `actions/cache@v4`) in untrusted-trigger workflows'
- 'Create a separate cache-warm workflow triggered by `push` that saves dependencies'
- 'Monitor cache hit rates in pull_request_target workflows — a sudden drop after June 26, 2026 is this change'
- 'Never rely on cache saves inside `issue_comment` or `pull_request_target` workflows for correctness'
- 'This is a security improvement — do not attempt to work around the read-only restriction'
docs:
- url: 'https://github.blog/changelog/2026-06-26-read-only-actions-cache-for-untrusted-triggers/'
label: 'GitHub Changelog: Read-only Actions cache for untrusted triggers (Jun 26, 2026)'
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows'
label: 'GitHub Docs: Caching dependencies to speed up workflows'
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
id: pa-126
title: 'Copilot CLI in GitHub Actions fails — `copilot-requests: write` permission missing from GITHUB_TOKEN'
category: permissions-auth
severity: error
tags:
- copilot-cli
- github-token
- copilot-requests
- permissions
- GITHUB_TOKEN
- breaking-change
patterns:
- regex: 'copilot.requests.*write.*permission|permission.*copilot.requests'
flags: 'i'
- regex: 'Resource not accessible by integration'
flags: 'i'
- regex: 'Missing required permission.*copilot|Insufficient permissions.*copilot.requests'
flags: 'i'
- regex: 'Allow use of Copilot CLI billed to the organization.*not enabled|copilot.*org.*policy.*disabled'
flags: 'i'
error_messages:
- 'Resource not accessible by integration'
- 'Missing required permission: copilot-requests: write'
- 'Error: The GITHUB_TOKEN does not have the required copilot-requests: write permission'
- 'copilot: authentication error — ensure the workflow grants copilot-requests: write'
- 'Copilot CLI request failed: 403 Forbidden — organization policy "Allow use of Copilot CLI billed to the organization" is not enabled'
root_cause: |
On July 2, 2026, GitHub announced that Copilot CLI can authenticate in GitHub Actions
using the built-in GITHUB_TOKEN instead of a personal access token (PAT). This eliminates
the need to manage long-lived PATs for Copilot CLI automation.

However, two conditions must be met for GITHUB_TOKEN authentication to succeed:

1. **`copilot-requests: write` permission** — the workflow's `permissions:` block must
explicitly grant this permission. GITHUB_TOKEN permissions default to a minimal set;
`copilot-requests: write` is NOT included by default. Without it, any Copilot CLI
invocation results in "Resource not accessible by integration" (HTTP 403).

2. **Organization policy enabled** — the organization must have "Allow use of Copilot CLI
billed to the organization" turned on (this IS enabled by default for orgs where the
"Copilot CLI" policy is active, but may be toggled off by an admin). Without this policy,
Copilot CLI requests are rejected regardless of the GITHUB_TOKEN permissions.

Teams migrating from the PAT-based approach, or setting up Copilot CLI automation for the
first time, commonly omit the `copilot-requests: write` permission from their workflow file,
because it is not a well-known or widely documented GITHUB_TOKEN permission.
fix: |
Add `copilot-requests: write` to the workflow's `permissions:` block. Keep other
permissions minimal per least-privilege principle.

If the error persists after fixing permissions, confirm the org-level policy is enabled:
Organization Settings → Copilot → Policies → "Allow use of Copilot CLI billed to the
organization" must be checked.

Also ensure you are on a recent version of Copilot CLI:
npm install -g @github/copilot # reinstall latest
# or
copilot update # update in place
fix_code:
- language: yaml
label: 'Add copilot-requests: write to workflow permissions'
code: |
name: Copilot CLI workflow
on: [push]

permissions:
contents: read
pull-requests: write
copilot-requests: write # Required for GITHUB_TOKEN auth with Copilot CLI

jobs:
copilot:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install Copilot CLI
run: npm install -g @github/copilot
- name: Run Copilot
run: copilot --yolo -p "Summarize the changes in this commit"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- language: yaml
label: 'Scoped permissions at job level (preferred over workflow level)'
code: |
name: Copilot CLI workflow
on: [push]

jobs:
copilot:
runs-on: ubuntu-latest
permissions:
contents: read
copilot-requests: write # Scoped to this job only
steps:
- uses: actions/checkout@v6
- name: Run Copilot CLI
run: copilot --yolo -p "Review this change"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
prevention:
- 'Always declare `copilot-requests: write` when using Copilot CLI with GITHUB_TOKEN'
- 'Scope permissions at job level rather than workflow level to limit blast radius'
- 'Use `copilot update` or reinstall before wiring GITHUB_TOKEN auth — old CLI versions do not support it'
- 'Confirm the org policy "Allow use of Copilot CLI billed to the organization" is enabled before deploying'
- 'Note: GITHUB_TOKEN scope is limited to the current repository — cross-repo Copilot CLI calls still require a PAT'
- 'Use `--yolo` flag in non-interactive environments to suppress interactive prompts'
docs:
- url: 'https://github.blog/changelog/2026-07-02-copilot-cli-no-longer-needs-a-personal-access-token-in-github-actions/'
label: 'GitHub Changelog: Copilot CLI no longer needs a PAT in GitHub Actions (Jul 2, 2026)'
- url: 'https://docs.github.com/en/copilot/how-tos/copilot-cli/use-copilot-cli-in-actions'
label: 'GitHub Docs: Using Copilot CLI in GitHub Actions with GITHUB_TOKEN'
- url: 'https://github.github.com/gh-aw/reference/permissions/'
label: 'GitHub Agentic Workflows: Permissions Reference'
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
id: re-522
title: 'Jobs queue indefinitely or fail when org admin disables standard hosted runner labels (`ubuntu-latest`, `windows-latest`, `macos-latest`)'
category: runner-environment
severity: error
tags:
- ubuntu-latest
- windows-latest
- macos-latest
- runner-labels
- org-policy
- runner-groups
- queued
- no-runner-found
- enterprise
- breaking-change
patterns:
- regex: 'No runner matching the specified labels was found.*ubuntu-latest|No hosted runner found.*ubuntu-latest'
flags: 'i'
- regex: 'No runner.*labels.*\[ubuntu-latest\]|Waiting for a runner to pick up this job'
flags: 'i'
- regex: 'Standard hosted runners.*disabled|default runner labels.*disabled.*organization'
flags: 'i'
- regex: 'runs-on.*ubuntu-latest.*queued.*\d+.*minutes'
flags: 'i'
error_messages:
- 'No runner matching the specified labels was found: ubuntu-latest'
- 'No hosted runner found for labels: windows-latest'
- 'Waiting for a runner to pick up this job... (standard hosted runners are disabled for this organization)'
- 'Error: This request was attempting to send a job to a runner, but no runner was found with the specified labels'
root_cause: |
On June 25, 2026, GitHub released "More control over your GitHub-hosted runners"
(Team and Enterprise plans). Organization admins can now DISABLE standard hosted
runner labels (`ubuntu-latest`, `ubuntu-24.04`, `windows-latest`, `macos-latest`,
etc.) across the entire organization.

When an org admin disables standard hosted runners, all workflows that use
`runs-on: ubuntu-latest` (or any standard label) will either:
- **Queue indefinitely** — the job sits in "Queued" state waiting for a runner that
no longer accepts standard-label jobs. It will eventually time out (6 hours default).
- **Fail immediately** with "No runner matching the specified labels was found"
depending on how the org enforced the restriction.

The intent is to force teams to use runner groups with explicit group-name routing,
allowing org admins to enforce runner policies (network configuration, concurrency
limits, macOS access controls, cost management).

Common scenarios where this causes unexpected breakage:
- Developer forks a repo from an org with disabled standard labels → their fork's
workflows also fail if org inheritance applies.
- Third-party GitHub App workflows that assume `ubuntu-latest` availability.
- Reusable workflows called from outside the org that reference standard labels.
- Newly onboarded repos that don't know about the org's runner-group requirement.
fix: |
If you control the workflow, update `runs-on:` to reference a runner group name
instead of a standard label. The runner group name is specific to your organization
(e.g., `runs-on: [self-hosted, org-default]` or `runs-on: group-name`).

If you do NOT control the workflow (e.g., a reusable workflow or third-party action),
you have two options:
1. Ask the org admin to re-enable standard hosted runners, or create a runner group
that accepts standard-label jobs as a passthrough.
2. Copy/fork the workflow and replace `runs-on` values.

If you are the org admin who enabled this and are receiving reports, check:
- Settings → Actions → Runner groups → verify a group is accessible to affected repos
- Settings → Actions → Hosted runners → confirm "Standard hosted runners" toggle status
fix_code:
- language: yaml
label: 'Replace standard label with org runner group name'
code: |
# Before (standard label — now blocked if org disabled standard runners)
jobs:
build:
runs-on: ubuntu-latest

# After (explicit runner group or org-managed runner label)
jobs:
build:
runs-on: [self-hosted, linux, x64] # or your org's runner group name
# OR using runner groups:
# runs-on:
# group: my-org-runner-group
# labels: ubuntu-latest
- language: yaml
label: 'Use runner group name routing (recommended for Enterprise)'
code: |
jobs:
build:
runs-on:
group: production-runners # runner group name defined in org settings
labels: ubuntu-latest # optional: further filter within group
- language: yaml
label: 'Audit which workflows use standard labels (shell script)'
code: |
# Find all workflows using standard labels in a repo
grep -rn 'runs-on:.*ubuntu-latest\|runs-on:.*windows-latest\|runs-on:.*macos-latest' \
.github/workflows/ \
--include="*.yml" --include="*.yaml"
prevention:
- 'Enterprise/Team org members: check organization runner policy before assuming `ubuntu-latest` is available'
- 'When creating new workflows in a managed org, prefer runner group routing over standard labels'
- 'Org admins: document which runner group name to use in your CONTRIBUTING.md or workflow templates'
- 'Use repository-level workflow templates that pre-configure the correct `runs-on` values for your org'
- 'Standard hosted runner label disabling requires Team or Enterprise plan — not available on free/pro orgs'
docs:
- url: 'https://github.blog/changelog/2026-06-25-more-control-over-your-github-hosted-runners/'
label: 'GitHub Changelog: More control over your GitHub-hosted runners (Jun 25, 2026)'
- url: 'https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners'
label: 'GitHub Docs: About GitHub-hosted runners'
- url: 'https://docs.github.com/en/actions/using-github-hosted-runners/managing-larger-runners/controlling-access-to-larger-runners'
label: 'GitHub Docs: Controlling access to larger runners (runner groups)'
Loading