fix(ci): least-privilege permissions + fork-safe concurrency keys

- evals.yml / evals-periodic.yml evals jobs: explicit contents:read +
  packages:read (container-image pull) and persist-credentials:false —
  the jobs that execute PR-authored code with three provider API keys
  ran on the repo-default token grant with the token written into
  .git/config
- permissions blocks for the 4 workflows that had none (skill-docs,
  make-pdf-gate, windows-free-tests, windows-setup-e2e)
- fork-safe concurrency keys: actionlint, skill-docs, make-pdf-gate,
  windows-setup-e2e switch from head_ref to PR-number keying — a bare
  branch name carries no fork prefix, so same-name branches from two
  forks shared one group and cancelled each other's runs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-29 04:39:36 +00:00
co-authored by Claude Fable 5
parent 9fbd0700ff
commit e2904be7a4
7 changed files with 46 additions and 7 deletions
+4 -4
View File
@@ -6,11 +6,11 @@ on:
branches: [main] branches: [main]
pull_request: pull_request:
# Cancel superseded runs for the same branch (matches evals.yml, # PR-number keyed (run_id fallback for push): a bare branch name carries no
# windows-free-tests.yml, etc.). head_ref is set on pull_request; ref_name is # fork prefix, so same-name branches from two forks would share one group and
# the fallback for push so a rapid push series doesn't pile up stale lint runs. # cancel each other's runs (same rationale as free-tests.yml).
concurrency: concurrency:
group: actionlint-${{ github.head_ref || github.ref_name }} group: actionlint-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
# Lint needs nothing from the token; the job runs a third-party image with # Lint needs nothing from the token; the job runs a third-party image with
+6
View File
@@ -68,6 +68,11 @@ jobs:
evals: evals:
runs-on: ubicloud-standard-8 runs-on: ubicloud-standard-8
needs: build-image needs: build-image
# Least privilege (mirrors evals.yml): read-only contents, packages:read
# for the container-image pull.
permissions:
contents: read
packages: read
container: container:
image: ${{ needs.build-image.outputs.image-tag }} image: ${{ needs.build-image.outputs.image-tag }}
credentials: credentials:
@@ -110,6 +115,7 @@ jobs:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false
- name: Fix bun temp - name: Fix bun temp
run: | run: |
+10
View File
@@ -89,6 +89,13 @@ jobs:
runs-on: ${{ matrix.suite.runner || 'ubicloud-standard-8' }} runs-on: ${{ matrix.suite.runner || 'ubicloud-standard-8' }}
needs: build-image needs: build-image
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
# Least privilege for the job that executes PR-authored code with three
# provider API keys in env: read-only contents, packages:read for the
# container-image pull below. Without this block the job ran on the
# repo-default token grant.
permissions:
contents: read
packages: read
container: container:
image: ${{ needs.build-image.outputs.image-tag }} image: ${{ needs.build-image.outputs.image-tag }}
credentials: credentials:
@@ -176,6 +183,9 @@ jobs:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
# Don't write the token into .git/config — this job runs
# PR-authored code; nothing in it pushes.
persist-credentials: false
# Bun creates root-owned temp dirs during Docker build. GH Actions runs as # Bun creates root-owned temp dirs during Docker build. GH Actions runs as
# runner user with HOME=/github/home. Redirect bun's cache to a writable dir. # runner user with HOME=/github/home. Redirect bun's cache to a writable dir.
+7 -1
View File
@@ -16,9 +16,15 @@ on:
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: make-pdf-gate-${{ github.head_ref || github.run_id }} # PR-number keyed: head_ref carries no fork prefix, so same-name branches
# from two forks would share one group and cancel each other's runs.
group: make-pdf-gate-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
# Build + test only — no token writes.
permissions:
contents: read
jobs: jobs:
gate: gate:
strategy: strategy:
+8 -1
View File
@@ -10,9 +10,16 @@ on:
# windows-free-tests.yml, etc.). head_ref is set on pull_request; ref_name is # windows-free-tests.yml, etc.). head_ref is set on pull_request; ref_name is
# the fallback for push so a rapid push series doesn't pile up stale runs. # the fallback for push so a rapid push series doesn't pile up stale runs.
concurrency: concurrency:
group: skill-docs-${{ github.head_ref || github.ref_name }} # PR-number keyed (run_id fallback for push/dispatch): a bare branch name
# carries no fork prefix, so same-name branches from two forks would share
# one group and cancel each other's runs (same rationale as free-tests.yml).
group: skill-docs-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
# The job only reads the checkout and runs the generator — no token writes.
permissions:
contents: read
jobs: jobs:
check-freshness: check-freshness:
runs-on: ubicloud-standard-2 runs-on: ubicloud-standard-2
+4
View File
@@ -31,6 +31,10 @@ concurrency:
group: windows-free-${{ github.event.pull_request.number || github.run_id }} group: windows-free-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
# Test-only lane — no token writes.
permissions:
contents: read
jobs: jobs:
windows-free-tests: windows-free-tests:
# Ubicloud Windows runner (same provider as the Linux evals workflow). # Ubicloud Windows runner (same provider as the Linux evals workflow).
+7 -1
View File
@@ -26,9 +26,15 @@ on:
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: windows-setup-e2e-${{ github.head_ref || github.run_id }} # PR-number keyed: head_ref carries no fork prefix, so same-name branches
# from two forks would share one group and cancel each other's runs.
group: windows-setup-e2e-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
# Install-path exercise only — no token writes.
permissions:
contents: read
jobs: jobs:
windows-setup: windows-setup:
runs-on: windows-latest runs-on: windows-latest