diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 08b4b10980..a3fde9e658 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -32,8 +32,18 @@ jobs: # via `workflow_dispatch` with the relevant commit SHA. runs-on: ubuntu-latest permissions: - contents: write - pull-requests: write + # `id-token: write` is required so we can mint a GitHub Actions + # OIDC token and exchange it for a scoped GitHub App installation + # token via gh-sts. The gh-sts token (not the default GITHUB_TOKEN) + # is used for everything that actually writes to the repo — + # creating the backport branch, pushing the signed commit, and + # opening the PR. The default GITHUB_TOKEN is only used for + # read-only lookups (gh api / gh pr list) and for posting issue + # comments on the source PR, so read perms suffice for contents + # and pull-requests; only `issues: write` is needed for comments. + id-token: write + contents: read + pull-requests: read issues: write steps: - name: Resolve commit SHA @@ -593,6 +603,21 @@ jobs: ;; esac + - name: Mint scoped GitHub token via gh-sts + # The default `GITHUB_TOKEN` cannot call `createCommitOnBranch` + # (the push step below) under our org's GHA token policy. We + # exchange the workflow's OIDC token for a scoped GitHub App + # installation token instead. The token is revoked automatically + # at the end of the job by the action's post step. + if: | + steps.cherry-pick.outputs.status == 'clean' || + (steps.cherry-pick.outputs.status == 'conflict' && steps.ai-resolve.outputs.resolved == 'true') + id: gh-sts + uses: vercel/gh-sts-action@c30f0b7a16e0766c4ffbc0d210b54d0e75053fd2 # main as of 2026-05-14 + with: + repos: vercel/workflow + permissions: '{"contents":"write","pull_requests":"write"}' + - name: Push backport branch via GitHub API # Branch protection on this repo requires verified signatures on # every ref (an enterprise-level ruleset matching `~ALL`). A normal @@ -602,7 +627,7 @@ jobs: # mutation, which signs commits automatically with GitHub's # internal key (the same way commits made via the web UI are # signed). The resulting commit is attributed to the token owner - # (`github-actions[bot]`), not the original author. + # (the gh-sts App installation), not the original author. if: | steps.cherry-pick.outputs.status == 'clean' || (steps.cherry-pick.outputs.status == 'conflict' && steps.ai-resolve.outputs.resolved == 'true') @@ -612,7 +637,7 @@ jobs: BRANCH: ${{ steps.existing-pr.outputs.branch }} SHA: ${{ steps.resolve.outputs.sha }} with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ steps.gh-sts.outputs.token }} script: | const { execFileSync } = require('node:child_process'); @@ -810,7 +835,9 @@ jobs: if: steps.push-branch.outputs.pushed == 'true' id: backport-pr env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Reuse the gh-sts token from the push step so the branch + # commit and the PR are attributed to the same identity. + GITHUB_TOKEN: ${{ steps.gh-sts.outputs.token }} SHA: ${{ steps.resolve.outputs.sha }} PR_NUMBER: ${{ steps.pr-lookup.outputs.pr_number }} PR_TITLE: ${{ steps.pr-lookup.outputs.pr_title }} diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 0e5bd24798..960b1a7543 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -615,7 +615,11 @@ jobs: timeout-minutes: 5 permissions: - contents: write + # `id-token: write` is required to mint a scoped GitHub App + # installation token via gh-sts (used to push a GitHub-signed + # commit to `gh-pages`). `contents: read` is for actions/checkout. + id-token: write + contents: read steps: - uses: actions/checkout@v4 @@ -637,10 +641,56 @@ jobs: --commit "${{ github.sha }}" \ --branch "${{ github.ref_name }}" - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v4 + - name: Mint scoped GitHub token via gh-sts + # The `gh-pages` branch is subject to the enterprise "require + # signed commits" ruleset (targets ~ALL refs, no bypass actors), + # which rejects a normal `git push` of an unsigned commit. We + # use `createCommitOnBranch` instead so GitHub signs the commit + # automatically; that mutation requires a scoped installation + # token because the default `GITHUB_TOKEN` cannot call it under + # our org's GHA token policy. + id: gh-sts + uses: vercel/gh-sts-action@c30f0b7a16e0766c4ffbc0d210b54d0e75053fd2 # main as of 2026-05-14 + with: + repos: vercel/workflow + permissions: '{"contents":"write"}' + + - name: Publish to gh-pages via createCommitOnBranch + uses: actions/github-script@v7 + env: + DEPLOY_SHA: ${{ github.sha }} with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./docs-data - destination_dir: ci - keep_files: true + github-token: ${{ steps.gh-sts.outputs.token }} + script: | + const fs = require('node:fs'); + const contents = fs + .readFileSync('docs-data/benchmark-results.json') + .toString('base64'); + const { data: ref } = await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'heads/gh-pages', + }); + const result = await github.graphql( + `mutation($input: CreateCommitOnBranchInput!) { + createCommitOnBranch(input: $input) { + commit { oid url } + } + }`, + { + input: { + branch: { + repositoryNameWithOwner: `${context.repo.owner}/${context.repo.repo}`, + branchName: 'gh-pages', + }, + expectedHeadOid: ref.object.sha, + message: { headline: `deploy: ${process.env.DEPLOY_SHA}` }, + fileChanges: { + additions: [{ path: 'ci/benchmark-results.json', contents }], + }, + }, + }, + ); + core.info( + `Pushed benchmark results as ${result.createCommitOnBranch.commit.oid} (${result.createCommitOnBranch.commit.url})`, + ); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 49357598af..ac7838b7eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,9 +21,14 @@ jobs: name: Release runs-on: ubuntu-latest permissions: - contents: write - pull-requests: write + # `id-token: write` is required so we can mint a GitHub Actions + # OIDC token and exchange it for a scoped GitHub App installation + # token via gh-sts. The gh-sts token (not the default GITHUB_TOKEN) + # is used by changesets/action to update the "Version Packages" PR + # and to create the GitHub Release, so we only need `contents: + # read` here for the checkout step. id-token: write + contents: read env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} @@ -51,6 +56,18 @@ jobs: - name: Install Dependencies run: pnpm install --frozen-lockfile + - name: Mint scoped GitHub token via gh-sts + # The default `GITHUB_TOKEN` cannot call `createCommitOnBranch` + # under our org's GHA token policy. changesets/action with + # `commitMode: github-api` uses that mutation to update the + # "Version Packages" PR, so we mint a scoped installation token + # instead. The token is revoked at the end of the job. + id: gh-sts + uses: vercel/gh-sts-action@c30f0b7a16e0766c4ffbc0d210b54d0e75053fd2 # main as of 2026-05-14 + with: + repos: vercel/workflow + permissions: '{"contents":"write","pull_requests":"write"}' + - name: Create Release Pull Request or Publish to npm id: changesets uses: changesets/action@v1 @@ -62,12 +79,12 @@ jobs: # Use GitHub API for GPG-signed commits (required by branch rules). commitMode: github-api env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.gh-sts.outputs.token }} - name: Create GitHub Release if: steps.changesets.outputs.published == 'true' env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.gh-sts.outputs.token }} PUBLISHED_PACKAGES: ${{ steps.changesets.outputs.publishedPackages }} run: | # Generate release notes (PUBLISHED_PACKAGES filters to only include packages from this release) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b6b6095985..e11aa40fe4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -936,7 +936,11 @@ jobs: timeout-minutes: 5 permissions: - contents: write + # `id-token: write` is required to mint a scoped GitHub App + # installation token via gh-sts (used to push a GitHub-signed + # commit to `gh-pages`). `contents: read` is for actions/checkout. + id-token: write + contents: read steps: - uses: actions/checkout@v4 @@ -960,10 +964,56 @@ jobs: --commit "${{ github.sha }}" \ --branch "${{ github.ref_name }}" - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v4 + - name: Mint scoped GitHub token via gh-sts + # The `gh-pages` branch is subject to the enterprise "require + # signed commits" ruleset (targets ~ALL refs, no bypass actors), + # which rejects a normal `git push` of an unsigned commit. We + # use `createCommitOnBranch` instead so GitHub signs the commit + # automatically; that mutation requires a scoped installation + # token because the default `GITHUB_TOKEN` cannot call it under + # our org's GHA token policy. + id: gh-sts + uses: vercel/gh-sts-action@c30f0b7a16e0766c4ffbc0d210b54d0e75053fd2 # main as of 2026-05-14 + with: + repos: vercel/workflow + permissions: '{"contents":"write"}' + + - name: Publish to gh-pages via createCommitOnBranch + uses: actions/github-script@v7 + env: + DEPLOY_SHA: ${{ github.sha }} with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./docs-data - destination_dir: ci - keep_files: true + github-token: ${{ steps.gh-sts.outputs.token }} + script: | + const fs = require('node:fs'); + const contents = fs + .readFileSync('docs-data/e2e-results.json') + .toString('base64'); + const { data: ref } = await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'heads/gh-pages', + }); + const result = await github.graphql( + `mutation($input: CreateCommitOnBranchInput!) { + createCommitOnBranch(input: $input) { + commit { oid url } + } + }`, + { + input: { + branch: { + repositoryNameWithOwner: `${context.repo.owner}/${context.repo.repo}`, + branchName: 'gh-pages', + }, + expectedHeadOid: ref.object.sha, + message: { headline: `deploy: ${process.env.DEPLOY_SHA}` }, + fileChanges: { + additions: [{ path: 'ci/e2e-results.json', contents }], + }, + }, + }, + ); + core.info( + `Pushed E2E results as ${result.createCommitOnBranch.commit.oid} (${result.createCommitOnBranch.commit.url})`, + );