@@ -256,24 +256,19 @@ jobs:
256256 name : Regenerate the shard-timings dataset
257257 runs-on : ubuntu-latest
258258 timeout-minutes : 30
259+ # Reads only. The push, the PR and its label go out on the fleet App token
260+ # the write step mints (see "WHOSE IDENTITY OPENS THE PR" at that step),
261+ # never on this run's own `GITHUB_TOKEN`.
259262 permissions :
260- # Declaring any `permissions:` block drops every scope not listed, so all
261- # three are spelled even though only two are writes.
262- contents : write # push the bot branch
263- pull-requests : write # open the PR and add its label
263+ contents : read # the checkout, and `git show HEAD:` of the committed dataset
264264 actions : read # list runs, read jobs, download run-summary artifacts
265265 steps :
266+ # No persisted credential: the one push goes out on the fleet token
267+ # (`gh auth setup-git` in the write step), never on `GITHUB_TOKEN`.
266268 - name : Checkout repository
267269 uses : actions/checkout@v7
268270 with :
269- # A PAT when one exists, the Actions token otherwise — the same choice
270- # cut-rc.yml makes, for the same reason, and the difference is stated
271- # in the PR body rather than left to be discovered: a PR opened with
272- # the Actions `GITHUB_TOKEN` starts NO workflow runs (GitHub's
273- # recursion guard), so with the fallback credential the refresh PR
274- # arrives with no CI of its own until someone pushes to it or reopens
275- # it. With the PAT its checks run immediately.
276- token : ${{ secrets.RELEASE_PUSH_TOKEN || github.token }}
271+ persist-credentials : false
277272
278273 - name : Setup Node.js
279274 uses : actions/setup-node@v7
@@ -710,7 +705,6 @@ jobs:
710705 RUN_COUNT : ${{ steps.generate.outputs.run_count }}
711706 HEAD_SHA : ${{ steps.generate.outputs.head_sha }}
712707 PARTITIONER_EXIT : ${{ steps.bins.outputs.partitioner_exit }}
713- USED_PAT : ${{ secrets.RELEASE_PUSH_TOKEN != '' }}
714708 run : |
715709 set -euo pipefail
716710
@@ -907,14 +901,9 @@ jobs:
907901 echo "reports and stops, because both are decisions."
908902 echo
909903 fi
910- if [ "$USED_PAT" != "true" ]; then
911- echo "## No checks will start on this PR by themselves"
912- echo
913- echo "It was opened with the Actions \`GITHUB_TOKEN\`, and GitHub's recursion guard means a"
914- echo "PR opened that way triggers no workflow runs. Push any commit to the branch, or close"
915- echo "and reopen the PR, to start CI."
916- echo
917- fi
904+ echo "Pushed and opened as the fleet App (\`objectstack-fleet[bot]\`, #25936), so this PR's"
905+ echo "own events start CI: no push or reopen is needed to get its checks."
906+ echo
918907 # ⛔ No closing keyword is emitted here, in any form. GitHub's
919908 # parser matches `fixes`/`closes`/`resolves` plus a number and
920909 # ignores every negation around them, so even a sentence saying a
@@ -924,6 +913,64 @@ jobs:
924913 } > "$RUNNER_TEMP/pr-body.md"
925914 echo "Composed a $(wc -l < "$RUNNER_TEMP/pr-body.md")-line pull request body."
926915
916+ # WHOSE IDENTITY OPENS THE PR (#25936)
917+ # ------------------------------------
918+ # GitHub starts no workflow run for an event raised by a run's own
919+ # `GITHUB_TOKEN` (its recursion guard). This lane used to push and open
920+ # with that token whenever no PAT was configured, and none was: every
921+ # refresh PR arrived with no CI and sat until a seat pushed to it (#24467,
922+ # 2026-10-19, ten days; #25960, 2026-10-26, started only by a relay
923+ # `pr_update_branch`). A refresh can move pin 3c's slice count, so a PR
924+ # nobody's CI reads is exactly the one that needs reading.
925+ #
926+ # So the acts that leave the runner are done as the fleet App
927+ # (`objectstack-fleet[bot]`), whose events DO start workflows: the two
928+ # steps below mint a token from the organization variable
929+ # `OS_FLEET_APP_ID` and secret `OS_FLEET_PRIVATE_KEY` with
930+ # `actions/create-github-app-token`, the pair and action
931+ # fleet-write.yml mints with, and the one ui/'s refresh already took
932+ # (objectui#13362). The token is narrowed to this repository and to
933+ # contents (push the bot branch) + pull-requests (open the PR, add its
934+ # label), exists only in the write step's `env:`, and is revoked by the
935+ # action's post step. No `workflows` grant: the branch is cut from the
936+ # checked-out `main` and adds one commit that touches only the dataset,
937+ # so the push carries no workflow-file change.
938+ #
939+ # ⛔ No fallback to `GITHUB_TOKEN` and no PAT. A missing credential is a
940+ # red run that names it, never a quiet return to a PR with no CI.
941+ # ⛔ A `pull_request` run mints nothing and never reads the App secret:
942+ # its write step is the dry run, and nothing it does leaves the
943+ # runner.
944+ - name : Check the fleet App credential is visible to this repository
945+ id : credential
946+ if : steps.compare.outputs.changed == 'true' && github.event_name != 'pull_request'
947+ env :
948+ HAS_APP_ID : ${{ vars.OS_FLEET_APP_ID != '' }}
949+ HAS_PRIVATE_KEY : ${{ secrets.OS_FLEET_PRIVATE_KEY != '' }}
950+ run : |
951+ # Read as booleans, never printed: a red that names the missing half
952+ # beats create-github-app-token's own error, which does not.
953+ if [ "$HAS_APP_ID" != 'true' ] || [ "$HAS_PRIVATE_KEY" != 'true' ]; then
954+ echo "::error title=Shard timings refresh::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). Nothing was pushed (#25936)."
955+ exit 1
956+ fi
957+ # The token's one repository, by name: the action takes names
958+ # without the owner, and an empty list would widen it to every
959+ # repository the installation covers.
960+ echo "repository=${GITHUB_REPOSITORY#*/}" >> "$GITHUB_OUTPUT"
961+
962+ - name : Mint the fleet App token, narrowed to this repository
963+ id : fleet
964+ if : steps.compare.outputs.changed == 'true' && github.event_name != 'pull_request'
965+ uses : actions/create-github-app-token@v3
966+ with :
967+ app-id : ${{ vars.OS_FLEET_APP_ID }}
968+ private-key : ${{ secrets.OS_FLEET_PRIVATE_KEY }}
969+ owner : ${{ github.repository_owner }}
970+ repositories : ${{ steps.credential.outputs.repository }}
971+ permission-contents : write # push the bot branch
972+ permission-pull-requests : write # open the PR and add its label
973+
927974 # The WRITE — and, on a `pull_request` run of this lane, its rehearsal.
928975 #
929976 # ONE STEP, ONE `env:`, ONE SCRIPT, ON EVERY EVENT (#18341). The write and
@@ -956,7 +1003,11 @@ jobs:
9561003 # refuses any other spelling, and refuses 'false' on a `pull_request`
9571004 # run, so no edit to this line can make a pull request push.
9581005 DRY_RUN : ${{ github.event_name == 'pull_request' }}
959- GH_TOKEN : ${{ secrets.RELEASE_PUSH_TOKEN || github.token }}
1006+ # The fleet App token on every live run. A dry run mints none, so it
1007+ # holds the run's own read-only token as a stand-in that `outward`
1008+ # never hands to a network act; a live run with no minted token is
1009+ # empty here and the guard below reds it.
1010+ GH_TOKEN : ${{ github.event_name == 'pull_request' && github.token || steps.fleet.outputs.token }}
9601011 # Every variable the script expands that the runner does not provide.
9611012 # The commit message is the dataset's provenance and names all four:
9621013 # ⛔ never drop one from the message to get a green run.
@@ -1004,8 +1055,11 @@ jobs:
10041055 }
10051056
10061057 BRANCH="claude/shard-timings-refresh-$RUN_ID"
1007- git config user.name 'github-actions[bot]'
1008- git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
1058+ # The checkout persisted no credential; this points git's helper at
1059+ # GH_TOKEN, the fleet token, for the one push below.
1060+ outward gh auth setup-git
1061+ git config user.name 'objectstack-fleet[bot]'
1062+ git config user.email '332303061+objectstack-fleet[bot]@users.noreply.github.com'
10091063 git switch -c "$BRANCH"
10101064 git add scripts/test-shard-timings.json
10111065 # Separate -m flags rather than one embedded newline: a YAML-indented
0 commit comments