diff --git a/.github/AGENTS.md b/.github/AGENTS.md index cd629b4a..28f457ff 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -10,7 +10,9 @@ 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. - 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 9a3c1a5b..2ae694b1 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -5,9 +5,15 @@ 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 fine-grained personal access token +# (`WEEKLY_AUTOMATION_TOKEN`) pushes and opens the pull request instead, and +# `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: write - pull-requests: write + contents: read concurrency: group: update-weekly-npm-downloads @@ -17,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: @@ -50,41 +57,50 @@ jobs: - name: Commit and push snapshot if: steps.snapshot.outputs.changed == 'true' env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.WEEKLY_AUTOMATION_TOKEN }} shell: bash run: | 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' env: - GH_TOKEN: ${{ github.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 c6d8db03..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. 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