diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml index 544f694fd..c3fd5ae47 100644 --- a/.github/workflows/actionlint.yml +++ b/.github/workflows/actionlint.yml @@ -6,11 +6,11 @@ on: branches: [main] pull_request: -# Cancel superseded runs for the same branch (matches evals.yml, -# 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 lint runs. +# PR-number keyed (run_id fallback for push): 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). concurrency: - group: actionlint-${{ github.head_ref || github.ref_name }} + group: actionlint-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true # Lint needs nothing from the token; the job runs a third-party image with diff --git a/.github/workflows/evals-periodic.yml b/.github/workflows/evals-periodic.yml index 00510d640..19f3efe51 100644 --- a/.github/workflows/evals-periodic.yml +++ b/.github/workflows/evals-periodic.yml @@ -68,6 +68,11 @@ jobs: evals: runs-on: ubicloud-standard-8 needs: build-image + # Least privilege (mirrors evals.yml): read-only contents, packages:read + # for the container-image pull. + permissions: + contents: read + packages: read container: image: ${{ needs.build-image.outputs.image-tag }} credentials: @@ -110,6 +115,7 @@ jobs: - uses: actions/checkout@v7 with: fetch-depth: 0 + persist-credentials: false - name: Fix bun temp run: | diff --git a/.github/workflows/evals.yml b/.github/workflows/evals.yml index 3aab49708..40ad34811 100644 --- a/.github/workflows/evals.yml +++ b/.github/workflows/evals.yml @@ -89,6 +89,13 @@ jobs: runs-on: ${{ matrix.suite.runner || 'ubicloud-standard-8' }} needs: build-image 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: image: ${{ needs.build-image.outputs.image-tag }} credentials: @@ -176,6 +183,9 @@ jobs: - uses: actions/checkout@v7 with: 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 # runner user with HOME=/github/home. Redirect bun's cache to a writable dir. diff --git a/.github/workflows/make-pdf-gate.yml b/.github/workflows/make-pdf-gate.yml index 1259a951e..ddbd184f1 100644 --- a/.github/workflows/make-pdf-gate.yml +++ b/.github/workflows/make-pdf-gate.yml @@ -16,9 +16,15 @@ on: workflow_dispatch: 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 +# Build + test only — no token writes. +permissions: + contents: read + jobs: gate: strategy: diff --git a/.github/workflows/skill-docs.yml b/.github/workflows/skill-docs.yml index 47ba5f36b..173f0467f 100644 --- a/.github/workflows/skill-docs.yml +++ b/.github/workflows/skill-docs.yml @@ -10,9 +10,16 @@ on: # 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. 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 +# The job only reads the checkout and runs the generator — no token writes. +permissions: + contents: read + jobs: check-freshness: runs-on: ubicloud-standard-2 diff --git a/.github/workflows/windows-free-tests.yml b/.github/workflows/windows-free-tests.yml index 67e5cd0b8..ab4cb9381 100644 --- a/.github/workflows/windows-free-tests.yml +++ b/.github/workflows/windows-free-tests.yml @@ -31,6 +31,10 @@ concurrency: group: windows-free-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true +# Test-only lane — no token writes. +permissions: + contents: read + jobs: windows-free-tests: # Ubicloud Windows runner (same provider as the Linux evals workflow). diff --git a/.github/workflows/windows-setup-e2e.yml b/.github/workflows/windows-setup-e2e.yml index 7d2014a2f..a9590b796 100644 --- a/.github/workflows/windows-setup-e2e.yml +++ b/.github/workflows/windows-setup-e2e.yml @@ -26,9 +26,15 @@ on: workflow_dispatch: 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 +# Install-path exercise only — no token writes. +permissions: + contents: read + jobs: windows-setup: runs-on: windows-latest