From faa94baed01e8abedd8759dc58c5454bb06c5e1e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 13:01:38 +0000 Subject: [PATCH 1/5] ci(weekly): open the downloads pull request as a GitHub App A push or pull request made with `GITHUB_TOKEN` leaves the pull request's `pull_request` workflows waiting for maintainer approval, so the weekly downloads pull request never reports its required checks on its own. Mint a GitHub App installation token for the push and the pull request instead, and drop the workflow's own `GITHUB_TOKEN` to read-only. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014fc28pKY9wf4DFssSzoNRN --- .github/AGENTS.md | 2 ++ .github/workflows/weekly.yml | 28 ++++++++++++++++++++++------ docs/README.md | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.github/AGENTS.md b/.github/AGENTS.md index cd629b4a..94e24530 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -12,6 +12,8 @@ These are the same durable learnings the root [`AGENTS.md`](../AGENTS.md) record - The weekly npm downloads workflow reuses `automation/weekly-npm-downloads`; merge `origin/main` into that branch before committing a new snapshot so an open automation PR does not drift behind `main`. +- The weekly npm downloads workflow pushes and opens its PR with a GitHub App installation token (`vars.AUTOMATION_APP_CLIENT_ID`, `secrets.AUTOMATION_APP_PRIVATE_KEY`), never `GITHUB_TOKEN`: a push or PR made with `GITHUB_TOKEN` leaves that PR's `pull_request` workflows at `action_required`, so every required check waits for a maintainer to approve the runs. Keep the workflow's own `GITHUB_TOKEN` read-only. + - Cloudflare Workers Builds watch paths are represented by [`.github/cloudflare/docs-watch-paths.json`](../.github/cloudflare/docs-watch-paths.json). Keep the desired list a superset of every docs-build input, including both packages, `.node-version`, `.nvmrc`, and the existing docs/build inputs. Include the gate and configuration policy paths so a policy change forces a real Cloudflare build; the gate uses the union of the immutable merge-base and head policies so a PR cannot hide changes by narrowing its own list. Cloudflare `*` spans `/`, and the owner must sync and verify both live triggers through the documented MCP checklist or dashboard. - `pr.yml` keeps its privileged comment job on `workflow_run`, so GitHub always runs that job from the default branch. Its separate `pull_request` smoke job has read-only permissions and a fake GitHub client; it exercises the PR version's artifact round trip, TypeScript import, binding, and rendering without making API calls. Keep the smoke job free of write permissions and secrets. Its success does not prove production token delivery, so observe the first natural post-merge benchmark comment after changing the privileged job. diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index 9a3c1a5b..148c96a8 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -5,9 +5,12 @@ on: schedule: - cron: "17 3 * * 1" +# Pushes and pull requests made with `GITHUB_TOKEN` leave the pull request's +# `pull_request` workflows waiting for maintainer approval, so the required +# checks never report on their own. A GitHub App installation token pushes +# and opens the pull request instead, and `GITHUB_TOKEN` stays read-only. permissions: - contents: write - pull-requests: write + contents: read concurrency: group: update-weekly-npm-downloads @@ -47,17 +50,30 @@ jobs: echo "changed=true" >> "$GITHUB_OUTPUT" fi + - name: Create app token + if: steps.snapshot.outputs.changed == 'true' + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.AUTOMATION_APP_CLIENT_ID }} + private-key: ${{ secrets.AUTOMATION_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + - name: Commit and push snapshot if: steps.snapshot.outputs.changed == 'true' env: - GH_TOKEN: ${{ github.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} shell: bash run: | snapshot="$RUNNER_TEMP/npm-weekly-downloads.json" cp "$SNAPSHOT_PATH" "$snapshot" git restore -- "$SNAPSHOT_PATH" - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + bot="$APP_SLUG[bot]" + bot_id="$(gh api "/users/$bot" --jq .id)" + git config user.name "$bot" + git config user.email "$bot_id+$bot@users.noreply.github.com" gh auth setup-git if [[ "$(git rev-parse --is-shallow-repository)" == true ]]; then git fetch --unshallow --no-tags origin @@ -84,7 +100,7 @@ jobs: - name: Open or update pull request if: steps.snapshot.outputs.changed == 'true' env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} shell: bash run: | pr_url="$(gh pr list --head "$UPDATE_BRANCH" --base main --state open --json url --jq '.[0].url')" diff --git a/docs/README.md b/docs/README.md index c6d8db03..90661e51 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,7 +21,7 @@ The generator removes stale `docs/.generated/bundle-sizes.json`. It runs each pa The generator does not compile packages. Root install/prepare provides `dist`. After editing package source, run `pnpm build` or `pnpm --filter build` before direct docs measurement. -The homepage reads weekly npm totals from [`src/content/npm-weekly-downloads.json`](./src/content/npm-weekly-downloads.json). The `weekly / npm-downloads` workflow fetches and validates npm's latest seven-day totals each Monday, then opens or updates a pull request containing the snapshot. Documentation builds read only the checked-in file and never depend on npm's API. +The homepage reads weekly npm totals from [`src/content/npm-weekly-downloads.json`](./src/content/npm-weekly-downloads.json). The `weekly / npm-downloads` workflow fetches and validates npm's latest seven-day totals each Monday, then opens or updates a pull request containing the snapshot. It pushes and opens that pull request as a GitHub App, so the pull request's required checks run without a maintainer approving them; the repository needs the App's client ID in the `AUTOMATION_APP_CLIENT_ID` variable and its private key in the `AUTOMATION_APP_PRIVATE_KEY` secret. Documentation builds read only the checked-in file and never depend on npm's API. ## Markdown mirrors From b9afc4398bcc8ee581b8c721b5a88d5eba7fabff Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 13:27:52 +0000 Subject: [PATCH 2/5] ci(weekly): use a personal access token instead of a GitHub App Replace the GitHub App token with an `AUTOMATION_TOKEN` fine-grained personal access token, so the setup is a single repository secret. It still keeps pushes and the pull request off `GITHUB_TOKEN`, so the weekly pull request's checks run without approval. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014fc28pKY9wf4DFssSzoNRN --- .github/AGENTS.md | 2 +- .github/workflows/weekly.yml | 26 +++++++------------------- docs/README.md | 2 +- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/.github/AGENTS.md b/.github/AGENTS.md index 94e24530..e6d04737 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -12,7 +12,7 @@ These are the same durable learnings the root [`AGENTS.md`](../AGENTS.md) record - The weekly npm downloads workflow reuses `automation/weekly-npm-downloads`; merge `origin/main` into that branch before committing a new snapshot so an open automation PR does not drift behind `main`. -- The weekly npm downloads workflow pushes and opens its PR with a GitHub App installation token (`vars.AUTOMATION_APP_CLIENT_ID`, `secrets.AUTOMATION_APP_PRIVATE_KEY`), never `GITHUB_TOKEN`: a push or PR made with `GITHUB_TOKEN` leaves that PR's `pull_request` workflows at `action_required`, so every required check waits for a maintainer to approve the runs. Keep the workflow's own `GITHUB_TOKEN` read-only. +- The weekly npm downloads workflow pushes and opens its PR with the `AUTOMATION_TOKEN` fine-grained personal access token (Contents and Pull requests read/write on this repository only), never `GITHUB_TOKEN`: a push or PR made with `GITHUB_TOKEN` leaves that PR's `pull_request` workflows at `action_required`, so every required check waits for a maintainer to approve the runs. Keep the workflow's own `GITHUB_TOKEN` read-only. - Cloudflare Workers Builds watch paths are represented by [`.github/cloudflare/docs-watch-paths.json`](../.github/cloudflare/docs-watch-paths.json). Keep the desired list a superset of every docs-build input, including both packages, `.node-version`, `.nvmrc`, and the existing docs/build inputs. Include the gate and configuration policy paths so a policy change forces a real Cloudflare build; the gate uses the union of the immutable merge-base and head policies so a PR cannot hide changes by narrowing its own list. Cloudflare `*` spans `/`, and the owner must sync and verify both live triggers through the documented MCP checklist or dashboard. diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index 148c96a8..c6a9cec0 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -7,8 +7,9 @@ on: # Pushes and pull requests made with `GITHUB_TOKEN` leave the pull request's # `pull_request` workflows waiting for maintainer approval, so the required -# checks never report on their own. A GitHub App installation token pushes -# and opens the pull request instead, and `GITHUB_TOKEN` stays read-only. +# checks never report on their own. A fine-grained personal access token +# (`AUTOMATION_TOKEN`) pushes and opens the pull request instead, and +# `GITHUB_TOKEN` stays read-only. permissions: contents: read @@ -50,30 +51,17 @@ jobs: echo "changed=true" >> "$GITHUB_OUTPUT" fi - - name: Create app token - if: steps.snapshot.outputs.changed == 'true' - id: app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ vars.AUTOMATION_APP_CLIENT_ID }} - private-key: ${{ secrets.AUTOMATION_APP_PRIVATE_KEY }} - permission-contents: write - permission-pull-requests: write - - name: Commit and push snapshot if: steps.snapshot.outputs.changed == 'true' env: - APP_SLUG: ${{ steps.app-token.outputs.app-slug }} - GH_TOKEN: ${{ steps.app-token.outputs.token }} + GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} shell: bash run: | snapshot="$RUNNER_TEMP/npm-weekly-downloads.json" cp "$SNAPSHOT_PATH" "$snapshot" git restore -- "$SNAPSHOT_PATH" - bot="$APP_SLUG[bot]" - bot_id="$(gh api "/users/$bot" --jq .id)" - git config user.name "$bot" - git config user.email "$bot_id+$bot@users.noreply.github.com" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" gh auth setup-git if [[ "$(git rev-parse --is-shallow-repository)" == true ]]; then git fetch --unshallow --no-tags origin @@ -100,7 +88,7 @@ jobs: - name: Open or update pull request if: steps.snapshot.outputs.changed == 'true' env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} + GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} shell: bash run: | pr_url="$(gh pr list --head "$UPDATE_BRANCH" --base main --state open --json url --jq '.[0].url')" diff --git a/docs/README.md b/docs/README.md index 90661e51..e74b1d45 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,7 +21,7 @@ The generator removes stale `docs/.generated/bundle-sizes.json`. It runs each pa The generator does not compile packages. Root install/prepare provides `dist`. After editing package source, run `pnpm build` or `pnpm --filter build` before direct docs measurement. -The homepage reads weekly npm totals from [`src/content/npm-weekly-downloads.json`](./src/content/npm-weekly-downloads.json). The `weekly / npm-downloads` workflow fetches and validates npm's latest seven-day totals each Monday, then opens or updates a pull request containing the snapshot. It pushes and opens that pull request as a GitHub App, so the pull request's required checks run without a maintainer approving them; the repository needs the App's client ID in the `AUTOMATION_APP_CLIENT_ID` variable and its private key in the `AUTOMATION_APP_PRIVATE_KEY` secret. Documentation builds read only the checked-in file and never depend on npm's API. +The homepage reads weekly npm totals from [`src/content/npm-weekly-downloads.json`](./src/content/npm-weekly-downloads.json). The `weekly / npm-downloads` workflow fetches and validates npm's latest seven-day totals each Monday, then opens or updates a pull request containing the snapshot. It pushes and opens that pull request with the `AUTOMATION_TOKEN` Actions secret, a fine-grained personal access token limited to this repository with Contents and Pull requests read/write, so the pull request's required checks run without a maintainer approving them. Renew the token before it expires, or the weekly run fails. Documentation builds read only the checked-in file and never depend on npm's API. ## Markdown mirrors From 56bd88c7e735b3b4ab015432c7c841597daec198 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 09:56:19 +0000 Subject: [PATCH 3/5] ci(weekly): rename the token secret to WEEKLY_AUTOMATION_TOKEN Match the name the secret was created under in the repository settings. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014fc28pKY9wf4DFssSzoNRN --- .github/AGENTS.md | 2 +- .github/workflows/weekly.yml | 6 +++--- docs/README.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/AGENTS.md b/.github/AGENTS.md index e6d04737..d36efaea 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -12,7 +12,7 @@ These are the same durable learnings the root [`AGENTS.md`](../AGENTS.md) record - The weekly npm downloads workflow reuses `automation/weekly-npm-downloads`; merge `origin/main` into that branch before committing a new snapshot so an open automation PR does not drift behind `main`. -- The weekly npm downloads workflow pushes and opens its PR with the `AUTOMATION_TOKEN` fine-grained personal access token (Contents and Pull requests read/write on this repository only), never `GITHUB_TOKEN`: a push or PR made with `GITHUB_TOKEN` leaves that PR's `pull_request` workflows at `action_required`, so every required check waits for a maintainer to approve the runs. Keep the workflow's own `GITHUB_TOKEN` read-only. +- The weekly npm downloads workflow pushes and opens its PR with the `WEEKLY_AUTOMATION_TOKEN` fine-grained personal access token (Contents and Pull requests read/write on this repository only), never `GITHUB_TOKEN`: a push or PR made with `GITHUB_TOKEN` leaves that PR's `pull_request` workflows at `action_required`, so every required check waits for a maintainer to approve the runs. Keep the workflow's own `GITHUB_TOKEN` read-only. - Cloudflare Workers Builds watch paths are represented by [`.github/cloudflare/docs-watch-paths.json`](../.github/cloudflare/docs-watch-paths.json). Keep the desired list a superset of every docs-build input, including both packages, `.node-version`, `.nvmrc`, and the existing docs/build inputs. Include the gate and configuration policy paths so a policy change forces a real Cloudflare build; the gate uses the union of the immutable merge-base and head policies so a PR cannot hide changes by narrowing its own list. Cloudflare `*` spans `/`, and the owner must sync and verify both live triggers through the documented MCP checklist or dashboard. diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index c6a9cec0..28ef67fc 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -8,7 +8,7 @@ on: # Pushes and pull requests made with `GITHUB_TOKEN` leave the pull request's # `pull_request` workflows waiting for maintainer approval, so the required # checks never report on their own. A fine-grained personal access token -# (`AUTOMATION_TOKEN`) pushes and opens the pull request instead, and +# (`WEEKLY_AUTOMATION_TOKEN`) pushes and opens the pull request instead, and # `GITHUB_TOKEN` stays read-only. permissions: contents: read @@ -54,7 +54,7 @@ jobs: - name: Commit and push snapshot if: steps.snapshot.outputs.changed == 'true' env: - GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} + GH_TOKEN: ${{ secrets.WEEKLY_AUTOMATION_TOKEN }} shell: bash run: | snapshot="$RUNNER_TEMP/npm-weekly-downloads.json" @@ -88,7 +88,7 @@ jobs: - name: Open or update pull request if: steps.snapshot.outputs.changed == 'true' env: - GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} + GH_TOKEN: ${{ secrets.WEEKLY_AUTOMATION_TOKEN }} shell: bash run: | pr_url="$(gh pr list --head "$UPDATE_BRANCH" --base main --state open --json url --jq '.[0].url')" diff --git a/docs/README.md b/docs/README.md index e74b1d45..32c1644c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,7 +21,7 @@ The generator removes stale `docs/.generated/bundle-sizes.json`. It runs each pa The generator does not compile packages. Root install/prepare provides `dist`. After editing package source, run `pnpm build` or `pnpm --filter build` before direct docs measurement. -The homepage reads weekly npm totals from [`src/content/npm-weekly-downloads.json`](./src/content/npm-weekly-downloads.json). The `weekly / npm-downloads` workflow fetches and validates npm's latest seven-day totals each Monday, then opens or updates a pull request containing the snapshot. It pushes and opens that pull request with the `AUTOMATION_TOKEN` Actions secret, a fine-grained personal access token limited to this repository with Contents and Pull requests read/write, so the pull request's required checks run without a maintainer approving them. Renew the token before it expires, or the weekly run fails. Documentation builds read only the checked-in file and never depend on npm's API. +The homepage reads weekly npm totals from [`src/content/npm-weekly-downloads.json`](./src/content/npm-weekly-downloads.json). The `weekly / npm-downloads` workflow fetches and validates npm's latest seven-day totals each Monday, then opens or updates a pull request containing the snapshot. It pushes and opens that pull request with the `WEEKLY_AUTOMATION_TOKEN` Actions secret, a fine-grained personal access token limited to this repository with Contents and Pull requests read/write, so the pull request's required checks run without a maintainer approving them. Renew the token before it expires, or the weekly run fails. Documentation builds read only the checked-in file and never depend on npm's API. ## Markdown mirrors From 627b74a855f98cf29d210ddadd90d0c280537609 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 10:06:34 +0000 Subject: [PATCH 4/5] ci(weekly): read the token from a main-only environment A repository secret is readable by any branch: a write-access user can dispatch the workflow from a branch whose copy prints the token, and the token acts as the owner, who bypasses the default-branch ruleset. Run the job in the `weekly-automation` environment so the token is only released to `main`. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014fc28pKY9wf4DFssSzoNRN --- .github/AGENTS.md | 2 +- .github/workflows/weekly.yml | 5 ++++- docs/README.md | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/AGENTS.md b/.github/AGENTS.md index d36efaea..4f01c98c 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -12,7 +12,7 @@ These are the same durable learnings the root [`AGENTS.md`](../AGENTS.md) record - The weekly npm downloads workflow reuses `automation/weekly-npm-downloads`; merge `origin/main` into that branch before committing a new snapshot so an open automation PR does not drift behind `main`. -- The weekly npm downloads workflow pushes and opens its PR with the `WEEKLY_AUTOMATION_TOKEN` fine-grained personal access token (Contents and Pull requests read/write on this repository only), never `GITHUB_TOKEN`: a push or PR made with `GITHUB_TOKEN` leaves that PR's `pull_request` workflows at `action_required`, so every required check waits for a maintainer to approve the runs. Keep the workflow's own `GITHUB_TOKEN` read-only. +- The weekly npm downloads workflow pushes and opens its PR with the `WEEKLY_AUTOMATION_TOKEN` fine-grained personal access token (Contents and Pull requests read/write on this repository only), never `GITHUB_TOKEN`: a push or PR made with `GITHUB_TOKEN` leaves that PR's `pull_request` workflows at `action_required`, so every required check waits for a maintainer to approve the runs. Keep the workflow's own `GITHUB_TOKEN` read-only. The token is an owner credential, and the owner bypasses the default-branch ruleset, so keep it an environment secret of `weekly-automation` (deployment branches: `main` only), never a repository secret: any branch can dispatch the workflow or add a new one, and the dispatched branch supplies the workflow definition, so a guard inside the workflow does not protect a repository secret. - Cloudflare Workers Builds watch paths are represented by [`.github/cloudflare/docs-watch-paths.json`](../.github/cloudflare/docs-watch-paths.json). Keep the desired list a superset of every docs-build input, including both packages, `.node-version`, `.nvmrc`, and the existing docs/build inputs. Include the gate and configuration policy paths so a policy change forces a real Cloudflare build; the gate uses the union of the immutable merge-base and head policies so a PR cannot hide changes by narrowing its own list. Cloudflare `*` spans `/`, and the owner must sync and verify both live triggers through the documented MCP checklist or dashboard. diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index 28ef67fc..e88479ab 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -9,7 +9,9 @@ on: # `pull_request` workflows waiting for maintainer approval, so the required # checks never report on their own. A fine-grained personal access token # (`WEEKLY_AUTOMATION_TOKEN`) pushes and opens the pull request instead, and -# `GITHUB_TOKEN` stays read-only. +# `GITHUB_TOKEN` stays read-only. The token lives in the `weekly-automation` +# environment, which only `main` may deploy to, so a run dispatched from +# another branch cannot read it. permissions: contents: read @@ -21,6 +23,7 @@ jobs: npm-downloads: name: npm-downloads if: github.repository == 'joe-bell/cva' + environment: weekly-automation runs-on: ubuntu-latest timeout-minutes: 10 env: diff --git a/docs/README.md b/docs/README.md index 32c1644c..0c52d3fd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,7 +21,7 @@ The generator removes stale `docs/.generated/bundle-sizes.json`. It runs each pa The generator does not compile packages. Root install/prepare provides `dist`. After editing package source, run `pnpm build` or `pnpm --filter build` before direct docs measurement. -The homepage reads weekly npm totals from [`src/content/npm-weekly-downloads.json`](./src/content/npm-weekly-downloads.json). The `weekly / npm-downloads` workflow fetches and validates npm's latest seven-day totals each Monday, then opens or updates a pull request containing the snapshot. It pushes and opens that pull request with the `WEEKLY_AUTOMATION_TOKEN` Actions secret, a fine-grained personal access token limited to this repository with Contents and Pull requests read/write, so the pull request's required checks run without a maintainer approving them. Renew the token before it expires, or the weekly run fails. Documentation builds read only the checked-in file and never depend on npm's API. +The homepage reads weekly npm totals from [`src/content/npm-weekly-downloads.json`](./src/content/npm-weekly-downloads.json). The `weekly / npm-downloads` workflow fetches and validates npm's latest seven-day totals each Monday, then opens or updates a pull request containing the snapshot. It pushes and opens that pull request with `WEEKLY_AUTOMATION_TOKEN`, a fine-grained personal access token limited to this repository with Contents and Pull requests read/write, so the pull request's required checks run without a maintainer approving them. The token is a secret of the `weekly-automation` environment, which only `main` may deploy to, so workflows on other branches cannot read it. Renew the token before it expires, or the weekly run fails. Documentation builds read only the checked-in file and never depend on npm's API. ## Markdown mirrors From 01bcf2a9d2d5500784dce4850b21164f4aff6468 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 10:15:56 +0000 Subject: [PATCH 5/5] ci(weekly): rebuild the update branch from main without hooks Merging `main` into the update branch made the token push a merge commit that carries `main`'s workflow-file changes, which a token without the Workflows permission cannot write. Checking that unprotected branch out also ran whatever a collaborator pushed to it, hooks included, while the token was exposed. Rebuild the branch from `origin/main` plus the snapshot commit on every run and force-push it with a lease, skip the push when the remote already matches, and unset `core.hooksPath` before using the token. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014fc28pKY9wf4DFssSzoNRN --- .github/AGENTS.md | 2 +- .github/workflows/weekly.yml | 39 ++++++++++++++++++++++-------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/.github/AGENTS.md b/.github/AGENTS.md index 4f01c98c..28f457ff 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -10,7 +10,7 @@ These are the same durable learnings the root [`AGENTS.md`](../AGENTS.md) record - The required `benchmark` context must always exist, even when benchmark work is skipped. In `ci.yml`, use the PR base-to-head three-dot range so base-only package changes do not run benchmarks, the push before-to-after two-dot range with the all-zero initial-base fallback, and fail closed on an unexpected event or unavailable range; condition only dependency installation, baselines, benchmarking, the push-only summary, and PR artifact on that output. -- The weekly npm downloads workflow reuses `automation/weekly-npm-downloads`; merge `origin/main` into that branch before committing a new snapshot so an open automation PR does not drift behind `main`. +- The weekly npm downloads workflow reuses `automation/weekly-npm-downloads`, but rebuilds it from `origin/main` plus one snapshot commit on every run and force-pushes with a lease, so an open automation PR never drifts behind `main`. Never merge `main` into it or check it out: the merge commit would carry `main`'s workflow-file changes, which the token's missing Workflows permission rejects, and checking out the unprotected branch would run whatever a collaborator pushed there (hooks included) while the token is exposed. Keep `core.hooksPath` unset in that step for the same reason. - The weekly npm downloads workflow pushes and opens its PR with the `WEEKLY_AUTOMATION_TOKEN` fine-grained personal access token (Contents and Pull requests read/write on this repository only), never `GITHUB_TOKEN`: a push or PR made with `GITHUB_TOKEN` leaves that PR's `pull_request` workflows at `action_required`, so every required check waits for a maintainer to approve the runs. Keep the workflow's own `GITHUB_TOKEN` read-only. The token is an owner credential, and the owner bypasses the default-branch ruleset, so keep it an environment secret of `weekly-automation` (deployment branches: `main` only), never a repository secret: any branch can dispatch the workflow or add a new one, and the dispatched branch supplies the workflow definition, so a guard inside the workflow does not protect a repository secret. diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index e88479ab..2ae694b1 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -63,30 +63,39 @@ jobs: snapshot="$RUNNER_TEMP/npm-weekly-downloads.json" cp "$SNAPSHOT_PATH" "$snapshot" git restore -- "$SNAPSHOT_PATH" + # `pnpm install` above points `core.hooksPath` at `.github/hooks`. + # Drop it so no repository hook runs while the token is exposed. + git config --unset-all core.hooksPath || true git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" gh auth setup-git - if [[ "$(git rev-parse --is-shallow-repository)" == true ]]; then - git fetch --unshallow --no-tags origin - fi - git fetch --no-tags origin main:refs/remotes/origin/main - - if git ls-remote --exit-code --heads origin "$UPDATE_BRANCH" >/dev/null; then - git fetch --no-tags origin "$UPDATE_BRANCH:refs/remotes/origin/$UPDATE_BRANCH" - git switch --create "$UPDATE_BRANCH" --track "origin/$UPDATE_BRANCH" - git merge --no-commit --no-ff origin/main - else - git switch --create "$UPDATE_BRANCH" origin/main - fi + # Rebuild the update branch from `main` on every run instead of + # merging `main` into it: the branch is never checked out, so its + # contents never run here, and the only new commit is the snapshot, + # so the push never carries workflow-file changes that the token + # has no permission to write. + git fetch --no-tags --depth=1 origin main:refs/remotes/origin/main + git switch --create "$UPDATE_BRANCH" origin/main cp "$snapshot" "$SNAPSHOT_PATH" git add "$SNAPSHOT_PATH" - if git diff --cached --quiet && ! git rev-parse --quiet --verify MERGE_HEAD >/dev/null; then + if git diff --cached --quiet; then exit 0 fi - git commit -m "chore: update weekly npm downloads" - git push --set-upstream origin "$UPDATE_BRANCH" + + remote_sha="" + if git ls-remote --exit-code --heads origin "$UPDATE_BRANCH" >/dev/null; then + git fetch --no-tags --depth=1 origin \ + "refs/heads/$UPDATE_BRANCH:refs/remotes/origin/$UPDATE_BRANCH" + remote_sha="$(git rev-parse "origin/$UPDATE_BRANCH")" + if git diff --quiet "origin/$UPDATE_BRANCH" HEAD; then + exit 0 + fi + fi + git push \ + --force-with-lease="refs/heads/$UPDATE_BRANCH:$remote_sha" \ + origin "HEAD:refs/heads/$UPDATE_BRANCH" - name: Open or update pull request if: steps.snapshot.outputs.changed == 'true'