From c543446859b1ab7b3b29285edf122136402dbb68 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 12:20:15 +0000 Subject: [PATCH 1/2] ci(test-shard-timings-refresh): push and open the refresh pull request as the fleet App so it runs CI The weekly refresh pushed its branch and opened its pull request with the run's GITHUB_TOKEN, and GitHub starts no workflow for an event that token raised, so every refresh pull request arrived without CI and waited for a human push (objectui#13028 sat from 2026-10-19). The push and the pull request now go out on a fleet App token minted on the runner with actions/create-github-app-token from the organization variable OS_FLEET_APP_ID and secret OS_FLEET_PRIVATE_KEY, narrowed to this repository and to contents / pull-requests / workflows write. The job's own token drops to reads. A credential that is not visible to this repository is a red run that names it; there is no fallback to GITHUB_TOKEN. The pull-request leg mints nothing. Ruling: Sales on objectui#13349, option A. Claude-Session: https://claude.ai/code/session_01Cr2pnminvpXPZAG6sKgzgN Co-authored-by: Claude --- .../workflows/test-shard-timings-refresh.yml | 94 +++++++++++++++---- content/docs/guide/ci-cd-pipeline.md | 19 ++-- 2 files changed, 91 insertions(+), 22 deletions(-) diff --git a/.github/workflows/test-shard-timings-refresh.yml b/.github/workflows/test-shard-timings-refresh.yml index 7198fa4f89..df22e61911 100644 --- a/.github/workflows/test-shard-timings-refresh.yml +++ b/.github/workflows/test-shard-timings-refresh.yml @@ -36,16 +36,40 @@ # edit, not this workflow's. # ⛔ It never opens a pull request for a byte-identical file. # -# ## The cost it carries, stated +# ## Whose identity opens the pull request, and why (objectui#13349) # -# The pull request is opened with this run's `GITHUB_TOKEN`, and GitHub starts -# no workflow run for an event that token raised -- the release pull request's -# limitation, which objectui#5397's ruling chose to keep rather than add a -# personal access token. So the refresh pull request arrives with no CI; the -# seat or person who lands it pushes to its branch once (a merge of `main` is -# enough), and that push runs the full suite on the new split. The branch is -# this workflow's own and is overwritten by each refresh, the same posture -# `changeset-release.yml` takes with its version branch. +# GitHub starts no workflow run for an event raised by this run's +# `GITHUB_TOKEN`. Until objectui#13349 the refresh pushed and opened with that +# token, so every refresh pull request arrived with no CI and sat until a seat +# or person pushed to its branch: #13028 waited from 2026-10-19 to 2026-10-23 +# while the shards ran 1.3-1.8x apart on a dataset missing 764 files. +# +# So the two acts that leave the runner -- the push and the pull request -- are +# done as the fleet App (`objectstack-fleet[bot]`), whose events DO start +# workflows: `actions/create-github-app-token` mints a token on this runner +# from the organization variable `OS_FLEET_APP_ID` and the organization secret +# `OS_FLEET_PRIVATE_KEY`, the same pair and action objectstack's +# `fleet-write.yml` mints with. The token is narrowed to this one repository +# and to the three grants below, exists only in the steps that use it, and is +# revoked by the action's post step. The job's own `GITHUB_TOKEN` keeps only +# the reads the selection and the downloads need. +# +# ⛔ Never a personal access token: objectui#5397's ruling keeps one out of +# this repository's automation, and the fleet App is an installation, not +# a person. The App-token route here is objectui#13349's ruling (Sales, +# 2026-10-23), which reads #5397's rejection as scoped to the release +# pull request, where an in-workflow validation was the alternative; here +# the pull request's own full-suite run IS the validation. +# ⛔ No fallback to `GITHUB_TOKEN`. A missing credential is a red run that +# names it, never a quiet return to a pull request nobody's CI reads. +# +# The branch, `ci/test-shard-timings-refresh`, is this workflow's own and is +# overwritten by each refresh, the same posture `changeset-release.yml` takes +# with its version branch. A force-push moves it from last week's base to +# today's `main`, across whatever workflow files landed in between, and GitHub +# refuses an App token that moves a ref across a workflow-file change unless it +# carries `workflows: write` -- the reason for that third grant; this workflow +# writes no workflow file. # # ## Which runs it reads # @@ -71,7 +95,8 @@ # # A pull request that edits this workflow or the scripts it runs executes it # all -- the selection, the downloads, the regeneration -- and stops before -# the two acts that leave the runner: the push and the pull request. +# the two acts that leave the runner: the push and the pull request. It mints +# no fleet token either, so a pull request's run never reads the App secret. name: Test Shard Timings Refresh @@ -103,14 +128,19 @@ jobs: name: Regenerate the per-file test timings runs-on: ubuntu-latest timeout-minutes: 15 + # The push and the pull request go out on the fleet App token minted + # below, so this run's own token only reads. permissions: - contents: write # push this workflow's own branch - pull-requests: write # open the refresh pull request + contents: read # the checkout actions: read # list runs, read their jobs, download their timing artifacts steps: + # No persisted credential: the one push goes out on the fleet token + # (`gh auth setup-git` below), never on this run's `GITHUB_TOKEN`. - name: Checkout code uses: actions/checkout@v7 + with: + persist-credentials: false # The selector and the generator are node builtins only: no pnpm, no # install. @@ -158,18 +188,50 @@ jobs: git diff --stat -- scripts/test-shard-timings.json cat "$RUNNER_TEMP/summary.md" >> "$GITHUB_STEP_SUMMARY" + # Read as a boolean, never printed: a red run that names the missing + # credential beats `create-github-app-token`'s own error, which does not. + - name: Check the fleet App credential is visible to this repository + id: credential + if: steps.select.outputs.status == '0' && github.event_name != 'pull_request' + env: + HAS_APP_ID: ${{ vars.OS_FLEET_APP_ID != '' }} + HAS_PRIVATE_KEY: ${{ secrets.OS_FLEET_PRIVATE_KEY != '' }} + run: | + if [ "$HAS_APP_ID" != 'true' ] || [ "$HAS_PRIVATE_KEY" != 'true' ]; then + echo "::error title=Test shard timings::The fleet App credential is not visible to this repository (OS_FLEET_APP_ID set: $HAS_APP_ID; OS_FLEET_PRIVATE_KEY set: $HAS_PRIVATE_KEY). Both are organization-level; grant this repository access to them. Nothing was pushed (objectui#13349)." + exit 1 + fi + # The token's one repository, by name: the action takes names + # without the owner, and an empty list would widen it to every + # repository the installation covers. + echo "repository=${GITHUB_REPOSITORY#*/}" >> "$GITHUB_OUTPUT" + + - name: Mint the fleet App token, narrowed to this repository + id: fleet + if: steps.select.outputs.status == '0' && github.event_name != 'pull_request' + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ vars.OS_FLEET_APP_ID }} + private-key: ${{ secrets.OS_FLEET_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: ${{ steps.credential.outputs.repository }} + permission-contents: write # push this workflow's own branch + permission-pull-requests: write # open or update the refresh pull request + permission-workflows: write # the force-push crosses workflow files main changed (header) + - name: Open or update the refresh pull request if: steps.select.outputs.status == '0' && github.event_name != 'pull_request' env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.fleet.outputs.token }} BRANCH: ci/test-shard-timings-refresh run: | if git diff --quiet -- scripts/test-shard-timings.json; then echo "The regenerated dataset is byte-identical to the committed one: no pull request." exit 0 fi - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + gh auth setup-git + git config user.name 'objectstack-fleet[bot]' + git config user.email '332303061+objectstack-fleet[bot]@users.noreply.github.com' git switch -c "$BRANCH" git add scripts/test-shard-timings.json git commit -m 'chore(test): refresh the per-file test timings (objectui#12208)' @@ -179,7 +241,7 @@ jobs: echo cat "$RUNNER_TEMP/summary.md" echo - echo '**This pull request has no CI of its own yet.** It was opened with the Actions token, and GitHub starts no workflow for an event that token raised. Push to its branch once (a merge of `main` is enough): the dataset is in the root vitest config import closure, so that push runs the full suite on the new split.' + echo 'Pushed and opened as the fleet App (objectui#13349), so this pull request runs its own CI: the dataset is in the root vitest config import closure, so that run is the full suite on the new split.' } > "$RUNNER_TEMP/body.md" open=$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$BRANCH" --state open --json number --jq '.[0].number // empty') if [ -n "$open" ]; then diff --git a/content/docs/guide/ci-cd-pipeline.md b/content/docs/guide/ci-cd-pipeline.md index 78abc67b53..5b28ddf553 100644 --- a/content/docs/guide/ci-cd-pipeline.md +++ b/content/docs/guide/ci-cd-pipeline.md @@ -50,7 +50,7 @@ one has its own section below. | `merge-queue-head-patrol.yml` | Merge queue head patrol | Every 15 minutes (cron `7,22,37,52 * * * *`); manual | No — it gates no branch and blocks no queue, but it **goes red on a finding**: a merge-queue head with no `merge_group` build is a live repo-wide block | | `required-check-set-patrol.yml` | Required check set patrol | Daily (cron `23 5 * * *`); manual | No — it gates no branch and blocks no queue, but it **goes red on a finding**: a merge queue whose required set has lost `Type Check` validates nothing that a type error would fail | | `hook-selftests.yml` | Hook Self-Tests | PR / push touching `.claude/hooks/**` or the workflow | **Yes** | -| `test-shard-timings-refresh.yml` | Regenerate the per-file test timings | Weekly cron `47 6 * * 1`; manual; PR touching the workflow or the two scripts it loads | No — it is path-filtered, so it does not report on most PRs; **blocking when it does run**. Its scheduled run opens a pull request and gates nothing | +| `test-shard-timings-refresh.yml` | Regenerate the per-file test timings | Weekly cron `47 6 * * 1`; manual; PR touching the workflow or the two scripts it loads | No — it is path-filtered, so it does not report on most PRs; **blocking when it does run**. Its scheduled run opens a pull request as the fleet App, so that pull request runs CI, and gates nothing | The path filters explain most "why did nothing run on my PR?" questions: @@ -2993,11 +2993,17 @@ full suite on the new split before it lands. timings, or after a week of red — the selector exits 3, the job prints a warning naming the reason and stays green, and nothing is written. A NOT MEASURED that persists is the finding. -**The refresh pull request arrives without CI.** It is opened with the run's `GITHUB_TOKEN`, and -GitHub starts no workflow for an event that token raised — the same limitation the release pull -request carries, which [#5397](https://github.com/objectstack-ai/objectui/issues/5397)'s ruling -kept rather than add a personal access token. Whoever lands it pushes to its branch once (a merge -of `main` is enough). The branch, `ci/test-shard-timings-refresh`, is this workflow's own and is +**The refresh pull request runs its own CI.** GitHub starts no workflow for an event raised by a +run's `GITHUB_TOKEN`, so until [#13349](https://github.com/objectstack-ai/objectui/issues/13349) +every refresh pull request arrived without CI and waited for someone to push to its branch. The +push and the pull request now go out as the fleet App (`objectstack-fleet[bot]`): +`actions/create-github-app-token` mints a token on the runner from the organization variable +`OS_FLEET_APP_ID` and secret `OS_FLEET_PRIVATE_KEY`, narrowed to this repository and to +`contents`, `pull-requests` and `workflows` write (the force-push moves the branch across any +workflow files `main` changed since the last refresh), and revokes it when the job ends. The job's +own token only reads. ⛔ No personal access token ([#5397](https://github.com/objectstack-ai/objectui/issues/5397)), +and ⛔ no fallback to `GITHUB_TOKEN`: when the credential is not visible to this repository the run +goes red and says so. The branch, `ci/test-shard-timings-refresh`, is this workflow's own and is overwritten by each refresh, as `changeset-release.yml` does with its version branch. **A pull-request run writes nothing.** It runs the selection, the downloads and the regeneration, @@ -3452,6 +3458,7 @@ on: |-------------------|---------|---------| | `GITHUB_TOKEN` | All workflows | GitHub API access (automatic) | | `NPM_TOKEN` | `changeset-release.yml` | npm package publishing | +| `OS_FLEET_APP_ID` (variable), `OS_FLEET_PRIVATE_KEY` (secret) | `test-shard-timings-refresh.yml` | Mint the fleet App token the refresh pushes its branch and opens its pull request with, so that pull request runs CI. Both are **organization**-level. If either is not visible to this repository, the run fails red. | | `CROSS_REPO_ISSUE_TOKEN` | `cross-repo-issue-closer.yml` | Closing issues in sibling repositories. `GITHUB_TOKEN` cannot do this — it is scoped to the repository running the workflow. When absent the workflow reports instead of closing. | | `VERCEL_TOKEN` | `docs-deploy.yml` | Vercel API token for the Object Stack team, used by `vercel pull` and `vercel deploy`. It is stored as an **organization** secret. If it is missing, the run fails red. | | `TURBO_TOKEN` | Build workflows | Turbo remote cache authentication | From 93b598db0f65f6568b2dbbf33d118df28456898d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 12:44:25 +0000 Subject: [PATCH 2/2] ci(test-shard-timings-refresh): mint and push only from main The contract review on #13362 noted that nothing pinned a manual dispatch to main, so a dispatch on another branch would mint the fleet token and force-push the refresh branch from that branch's tree. The credential, mint and push steps now also require github.ref == refs/heads/main; a dispatch elsewhere regenerates and prints, and pushes nothing. Claude-Session: https://claude.ai/code/session_01Cr2pnminvpXPZAG6sKgzgN Co-authored-by: Claude --- .github/workflows/test-shard-timings-refresh.yml | 10 +++++++--- content/docs/guide/ci-cd-pipeline.md | 5 +++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-shard-timings-refresh.yml b/.github/workflows/test-shard-timings-refresh.yml index df22e61911..4d0ed36688 100644 --- a/.github/workflows/test-shard-timings-refresh.yml +++ b/.github/workflows/test-shard-timings-refresh.yml @@ -97,6 +97,10 @@ # all -- the selection, the downloads, the regeneration -- and stops before # the two acts that leave the runner: the push and the pull request. It mints # no fleet token either, so a pull request's run never reads the App secret. +# +# The same three steps run only from `main`: a manual dispatch on any other +# branch regenerates and prints, and pushes nothing, so the refresh branch is +# only ever built from `main`'s tree. name: Test Shard Timings Refresh @@ -192,7 +196,7 @@ jobs: # credential beats `create-github-app-token`'s own error, which does not. - name: Check the fleet App credential is visible to this repository id: credential - if: steps.select.outputs.status == '0' && github.event_name != 'pull_request' + if: steps.select.outputs.status == '0' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' env: HAS_APP_ID: ${{ vars.OS_FLEET_APP_ID != '' }} HAS_PRIVATE_KEY: ${{ secrets.OS_FLEET_PRIVATE_KEY != '' }} @@ -208,7 +212,7 @@ jobs: - name: Mint the fleet App token, narrowed to this repository id: fleet - if: steps.select.outputs.status == '0' && github.event_name != 'pull_request' + if: steps.select.outputs.status == '0' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' uses: actions/create-github-app-token@v3 with: app-id: ${{ vars.OS_FLEET_APP_ID }} @@ -220,7 +224,7 @@ jobs: permission-workflows: write # the force-push crosses workflow files main changed (header) - name: Open or update the refresh pull request - if: steps.select.outputs.status == '0' && github.event_name != 'pull_request' + if: steps.select.outputs.status == '0' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' env: GH_TOKEN: ${{ steps.fleet.outputs.token }} BRANCH: ci/test-shard-timings-refresh diff --git a/content/docs/guide/ci-cd-pipeline.md b/content/docs/guide/ci-cd-pipeline.md index 5b28ddf553..01759fadad 100644 --- a/content/docs/guide/ci-cd-pipeline.md +++ b/content/docs/guide/ci-cd-pipeline.md @@ -3006,8 +3006,9 @@ and ⛔ no fallback to `GITHUB_TOKEN`: when the credential is not visible to thi goes red and says so. The branch, `ci/test-shard-timings-refresh`, is this workflow's own and is overwritten by each refresh, as `changeset-release.yml` does with its version branch. -**A pull-request run writes nothing.** It runs the selection, the downloads and the regeneration, -and skips the push and the pull request. +**A pull-request run writes nothing**, and neither does a manual dispatch on any branch but `main`. +It runs the selection, the downloads and the regeneration, and skips the mint, the push and the +pull request. ### Merge Queue Head Patrol (`merge-queue-head-patrol.yml`)