ci: regenerate the shard-timings dataset on a timer, and assemble a sliced package per run #3
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Regenerate scripts/test-shard-timings.json on a timer, and open the PR. | |
| # | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # WHY THIS EXISTS: THE BALANCING INPUT WAS THE ONLY PART OF THE LOOP WITH NO | |
| # CLOCK ON IT. (#16464) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # | |
| # scripts/test-shard-timings.json is the per-package duration dataset the Test | |
| # Core split is binned from. It is GENERATED — scripts/measure-test-shard- | |
| # timings.mjs turns a green run's six turbo summaries into it — but until now it | |
| # was generated only when a person remembered to. It went 13 days without a | |
| # refresh while ~700 test files were added, and the shard it mis-weighted was | |
| # killed by the job wall twelve times in one day and ejected from the merge queue | |
| # twice (#16173). | |
| # | |
| # The rot is one-directional and silent, which is what makes a timer the fix | |
| # rather than more discipline: suites only get slower, the table stays put, and | |
| # the shard that drifted heavy reads as balanced on paper right up to the moment | |
| # it is killed — and a killed shard produces NO measurement while the rollup | |
| # reads green. | |
| # | |
| # WHAT THIS WORKFLOW IS NOT ALLOWED TO DO, AND WHY EACH ONE IS LOAD-BEARING | |
| # ------------------------------------------------------------------------ | |
| # ⛔ It never hand-edits the dataset. Every byte it commits came out of the | |
| # generator. A hand-touched number wearing a generated file's `provenance` | |
| # is worse than a stale one: the staleness is at least visible in | |
| # `measuredAt`. | |
| # ⛔ It never touches `timeout-minutes`, the shard matrix, MAX_SHARD_OVER_MEAN, | |
| # MAX_MEASURED_OVER_PREDICTED or FILE_SHARDED_PACKAGES. The partitioner's | |
| # own header calls raising a bound "the one move that cannot be right", and | |
| # its pin 3c is built to red on exactly that. If an honest refresh breaches | |
| # a bound, that breach is the REPORT, not a problem to be tuned away — it is | |
| # quoted verbatim into the PR body and a human decides. See "THE PINS" below. | |
| # ⛔ It never opens a PR when the regenerated file is byte-identical. A weekly | |
| # no-op PR trains everyone to ignore this PR. | |
| # | |
| # HOW IT GETS ITS INPUTS, AND WHY IT RUNS HERE RATHER THAN IN AN AGENT WORKTREE | |
| # ---------------------------------------------------------------------------- | |
| # The documented refresh path downloads the six `test-core-run-summary-N-of-6` | |
| # artifacts of a green run. `GET /actions/artifacts/{id}/zip` redirects to | |
| # `productionresultssa*.blob.core.windows.net`, and every agent container's | |
| # egress policy answers 403 to CONNECT for that host — reproduced four ways, on | |
| # two artifacts across two runs, on the pre-signed URL as well as the API one, on | |
| # two separate days and containers (#16222, and again on #16173). So the | |
| # documented path cannot be walked from a dev seat at all. | |
| # | |
| # A GitHub-hosted runner reaches that host natively — it is the same host | |
| # `actions/download-artifact` uses. Running the refresh here is therefore not a | |
| # workaround for the denial, it is the removal of the channel from the loop: the | |
| # regeneration happens where the data already is, on a timer, and no seat needs | |
| # reachability it does not have. | |
| # | |
| # CHOOSING THE RUNS IS THE HARD PART — see scripts/ci/select-shard-timings-run.mjs | |
| # ------------------------------------------------------------------------------ | |
| # "The newest run" is wrong three different ways here (cancelled runs, cache | |
| # replays, expired artifacts), and a refresh built on the wrong run is worse than | |
| # no refresh because it stamps a fresh `measuredAt` on numbers nobody measured. | |
| # That script carries the argument and the self-test; this file only drives it. | |
| # | |
| # ⚠ AND IT IS "RUNS", PLURAL, WHICH THE DOCUMENTED PROCEDURE DOES NOT SAY. | |
| # Measured on this lane's first live run (34083991141): of the seven retained | |
| # green push runs on main, the BEST measured 52 of the 71 packages the committed | |
| # dataset holds, and the others measured 2, 3, 13, 18, 22 and 49. All seven were | |
| # partial cache replays. That follows from the cache design rather than from luck | |
| # — turbo's key is namespaced per shard and only main pushes write it, so a | |
| # package whose inputs have not changed is a HIT, and the generator refuses hits | |
| # rather than recording a replayed ~0.1s window as a suite's cost. "Download six | |
| # artifacts from any green run" therefore measures a SLICE of the workspace. | |
| # | |
| # So the regeneration step accumulates runs, each under its own `--run <id>` | |
| # group — the grouping #16473 added, which sums a sliced package's slices within | |
| # a run and medians the per-run sums across runs. A package seen in three of the | |
| # accumulated runs gets the median of three observations, which is the property | |
| # the dataset's own merge rule always claimed and could not previously deliver. | |
| # | |
| # ⚠ AND ACCUMULATION ALONE STILL FALLS SHORT, so the pass MERGES rather than | |
| # replaces. Measured on the same live runs: the union of all seven retained runs | |
| # reaches 57 of 71 packages and converges there, because 14 packages are cache | |
| # HITs in every one of them. Those are carried at their previous weights through | |
| # `--merge-into`, and the carry is sound for one specific reason — a cache HIT is | |
| # not missing data, it is POSITIVE EVIDENCE that the package's inputs are | |
| # unchanged since the run whose output was replayed, so its last measured weight | |
| # still describes it. Every carried package is named in the dataset's | |
| # `carriedOver` list, and a package absent for any OTHER reason is not carried at | |
| # all: it drops out and the coverage check names it as a refusal. The file's | |
| # invariant is preserved exactly — every number in it is a real measurement of | |
| # code as it stands, never an estimate. | |
| # | |
| # THE PINS, AND THE ONE THING A MACHINE MUST NOT DECIDE | |
| # ---------------------------------------------------- | |
| # partition-test-shards.mjs `--self-test` grades the dataset against the | |
| # acceptance bound. On a refresh that makes a package heavier than any split can | |
| # bin, it reds BY DESIGN and names the remedy (raise the slice count — never the | |
| # bound), and its pin 3c additionally demands a decision the day the CLI comes | |
| # back under the bound on its own. Both are judgement, so this workflow REPORTS | |
| # the verdict and never acts on it: the self-test is run on the refreshed file | |
| # and its output — pass or fail, verbatim — goes into the PR body. The PR is | |
| # opened either way, because a refusal to open it would lose the measurement, | |
| # which is the exact silent-rot failure this workflow exists to end. | |
| name: Shard Timings Refresh | |
| on: | |
| schedule: | |
| # Monday 05:30 UTC. After the nightly lanes, and early enough in the week | |
| # that the PR is in front of someone before the week's merge volume builds. | |
| - cron: '30 5 * * 1' | |
| workflow_dispatch: | |
| # Changes to this lane get exercised before they merge — the same posture | |
| # half-state-patrol.yml and required-set-patrol.yml keep, and the reason a | |
| # scheduled-only lane is not an option: `dispatch-gates.mjs` reds when a gate | |
| # family is reachable ONLY on a schedule, because a family no PR-time event | |
| # reaches appears on no card's gate list and is therefore graded by nobody | |
| # until the next sweep. | |
| # | |
| # On a `pull_request` run everything executes — selection, download, | |
| # regeneration, the coverage check, the byte comparison and the partitioner's | |
| # verdict — so the transport and the flags are proven on a real runner rather | |
| # than argued about. The WRITE is what is skipped: no branch is pushed, no PR | |
| # is opened, no label is written, and the body that would have been posted is | |
| # rendered to the run's step summary instead. | |
| pull_request: | |
| paths: | |
| - '.github/workflows/shard-timings-refresh.yml' | |
| - 'scripts/ci/select-shard-timings-run.mjs' | |
| - 'scripts/measure-test-shard-timings.mjs' | |
| # Read-only at the top level; the one job widens to exactly what it writes. | |
| permissions: | |
| contents: read | |
| # One refresh at a time. A second run while the first is mid-push would race on | |
| # the same bot branch; the newer inputs are the better ones, so the in-flight run | |
| # yields. | |
| concurrency: | |
| # Scoped BY REF, not one global group. Two scheduled refreshes cannot overlap | |
| # (they would race on the same bot branch, and the newer inputs are the better | |
| # ones, so the in-flight run yields) — but two PRs touching this lane are | |
| # unrelated runs, and a single global group would have each cancel the other's | |
| # dry run and report a cancellation as though the lane were busy. | |
| group: shard-timings-refresh-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| refresh: | |
| name: Regenerate the shard-timings dataset | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| # Declaring any `permissions:` block drops every scope not listed, so all | |
| # three are spelled even though only two are writes. | |
| contents: write # push the bot branch | |
| pull-requests: write # open the PR and add its label | |
| actions: read # list runs, read jobs, download run-summary artifacts | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| with: | |
| # A PAT when one exists, the Actions token otherwise — the same choice | |
| # cut-rc.yml makes, for the same reason, and the difference is stated | |
| # in the PR body rather than left to be discovered: a PR opened with | |
| # the Actions `GITHUB_TOKEN` starts NO workflow runs (GitHub's | |
| # recursion guard), so with the fallback credential the refresh PR | |
| # arrives with no CI of its own until someone pushes to it or reopens | |
| # it. With the PAT its checks run immediately. | |
| token: ${{ secrets.RELEASE_PUSH_TOKEN || github.token }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22' | |
| - name: Setup pnpm | |
| uses: ./.github/actions/setup-pnpm | |
| - name: Get pnpm store directory | |
| shell: bash | |
| run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV | |
| # Restore-only: scheduled runs read main's store cache; the per-push | |
| # workflows own saving it. Same shape as coverage-nightly.yml. | |
| - name: Restore pnpm cache | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: ${{ env.STORE_PATH }} | |
| key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pnpm-store-v3- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| # Verify the instruments BEFORE trusting their output. Both scripts carry a | |
| # battery floor, so this also catches the case where their assertions | |
| # stopped running — which would otherwise let a wrong dataset through a | |
| # green-looking pipeline. | |
| - name: Self-test the generator and the run selector | |
| run: | | |
| # A COLLECTOR, not a bare sequence. Under `bash -e` the first non-zero | |
| # exit aborts the step, so a plain `a && b` list leaves the second | |
| # self-test neither green nor red -- and this step exists precisely to | |
| # say which instrument is broken before the dataset is trusted. | |
| # ⛔ Never let the collector swallow the exit code: a green step over a | |
| # red self-test looks identical to success. | |
| failed="" | |
| run_self_test() { | |
| echo "-- $*" | |
| if "$@"; then | |
| echo "PASS $*" | |
| else | |
| echo "FAIL $*" | |
| failed="${failed} $*"$'\n' | |
| fi | |
| return 0 | |
| } | |
| run_self_test node scripts/measure-test-shard-timings.mjs --self-test | |
| run_self_test node scripts/ci/select-shard-timings-run.mjs --self-test | |
| if [ -n "$failed" ]; then | |
| echo "" | |
| echo "Shard-timings instrument self-tests — the following FAILED:" | |
| printf "%s" "$failed" | |
| exit 1 | |
| fi | |
| echo "Shard-timings instrument self-tests — both ran and passed" | |
| # The package list the coverage check judges against. `turbo ls` is | |
| # experimental, so the reader asserts its payload loudly rather than | |
| # defaulting around it (an empty list would make every package look deleted | |
| # and every coverage check pass). | |
| - name: List the workspace | |
| run: pnpm exec turbo ls --output=json > "$RUNNER_TEMP/turbo-ls.json" | |
| - name: Choose a green, uncensored, un-replayed run | |
| id: select | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| node scripts/ci/select-shard-timings-run.mjs --candidates --limit 15 \ | |
| > "$RUNNER_TEMP/candidates.json" | |
| echo "Eligible runs, newest first:" | |
| node -e ' | |
| const runs = JSON.parse(require("fs").readFileSync(process.env.RUNNER_TEMP + "/candidates.json", "utf8")); | |
| for (const r of runs) console.log(` ${r.run_id} ${r.created_at} ${r.head_sha.slice(0, 10)}`); | |
| ' | |
| # ACCUMULATE runs until the workspace is covered — do not look for one run | |
| # that covers it, because there is no such run. | |
| # | |
| # MEASURED on the first live run of this lane (run 34083991141), and it is | |
| # the fact that shapes this step: of the seven retained green push runs on | |
| # main, the BEST measured 52 of the 71 packages the committed dataset | |
| # holds, and the rest measured 2, 3, 13, 18, 22 and 49. Every one of them | |
| # was a partial cache replay. That is not bad luck, it is the cache design: | |
| # turbo's key is namespaced per shard and only main pushes write it, so a | |
| # package whose inputs have not changed is a HIT — and the generator | |
| # refuses hits rather than recording a replayed ~0.1s window as a suite's | |
| # cost. "Download six artifacts from any green run" therefore measures a | |
| # SLICE of the workspace, never all of it. | |
| # | |
| # So runs are accumulated. Each contributes its six summaries under its own | |
| # `--run <id>` group, which is exactly the grouping #16473 added: slices are | |
| # summed within a run and the per-run sums are medianed across runs, so a | |
| # package measured by three of these runs gets a median of three | |
| # observations rather than whichever run happened to be read last. Feeding | |
| # several runs WITHOUT that grouping is refused by the generator by name. | |
| # | |
| # The loop stops at the first accumulation that covers the workspace, so a | |
| # quiet week costs one run's download and a busy one costs a few. | |
| - name: Regenerate the dataset, accumulating runs until the workspace is covered | |
| id: generate | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| WORK="$RUNNER_TEMP/refresh" | |
| mkdir -p "$WORK" | |
| CHOSEN='' | |
| ACCEPTED=() | |
| RUN_COUNT=$(node -e 'console.log(JSON.parse(require("fs").readFileSync(process.env.RUNNER_TEMP + "/candidates.json", "utf8")).length)') | |
| for i in $(seq 0 $((RUN_COUNT - 1))); do | |
| RUN_ID=$(node -e 'const r=JSON.parse(require("fs").readFileSync(process.env.RUNNER_TEMP+"/candidates.json","utf8"))[Number(process.argv[1])];console.log(r.run_id)' "$i") | |
| echo "::group::Candidate run $RUN_ID" | |
| SUMDIR="$WORK/$RUN_ID" | |
| rm -rf "$SUMDIR"; mkdir -p "$SUMDIR" | |
| OK=1 | |
| for N in 1 2 3 4 5 6; do | |
| ART=$(node -e 'const r=JSON.parse(require("fs").readFileSync(process.env.RUNNER_TEMP+"/candidates.json","utf8"))[Number(process.argv[1])];console.log(r.artifact_ids[process.argv[2]])' "$i" "$N") | |
| # curl drops the Authorization header across the redirect to the | |
| # blob host, which is correct: that URL is pre-signed and the | |
| # header would be rejected. | |
| if ! curl -sSL --fail -H "Authorization: Bearer $GITHUB_TOKEN" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "https://api.github.com/repos/$GITHUB_REPOSITORY/actions/artifacts/$ART/zip" \ | |
| -o "$SUMDIR/$N.zip"; then | |
| echo "::warning::Could not download artifact $ART (shard $N) of run $RUN_ID; skipping this run." | |
| OK=0; break | |
| fi | |
| unzip -qo "$SUMDIR/$N.zip" -d "$SUMDIR/$N" | |
| done | |
| if [ "$OK" -ne 1 ]; then echo "::endgroup::"; continue; fi | |
| # `find` rather than a fixed-depth glob: `upload-artifact` with | |
| # `path: .turbo/runs/` puts the summaries at the artifact root, but a | |
| # shape assumption here would silently collect NOTHING and hand the | |
| # generator an empty argument list, which reads like a refusal for | |
| # the wrong reason. | |
| find "$SUMDIR" -name '*.json' -type f > "$SUMDIR/summaries.txt" | |
| SUMMARY_COUNT=$(wc -l < "$SUMDIR/summaries.txt") | |
| echo "Collected $SUMMARY_COUNT run summary file(s) from run $RUN_ID." | |
| if [ "$SUMMARY_COUNT" -eq 0 ]; then | |
| echo "::warning::Run $RUN_ID's artifacts contained no run-summary JSON; skipping this run." | |
| echo "::endgroup::"; continue | |
| fi | |
| ACCEPTED+=("$RUN_ID") | |
| # The argument list is REBUILT from the accepted set every round | |
| # rather than appended to, so a run the generator refuses can be | |
| # dropped cleanly instead of poisoning every later attempt. Each run | |
| # is fenced by its own `--run <id>`; an array, not `xargs`, because a | |
| # split invocation would run the generator twice and the second would | |
| # overwrite the first's output with a partial dataset. | |
| ARGS=() | |
| for R in "${ACCEPTED[@]}"; do | |
| ARGS+=( --run "$R" ) | |
| mapfile -t RUN_FILES < "$WORK/$R/summaries.txt" | |
| ARGS+=( "${RUN_FILES[@]}" ) | |
| done | |
| # `--merge-into` the committed dataset, because no retained run set | |
| # measures the whole workspace (see the header). A package this pass | |
| # did not measure keeps its previous weight ONLY when a turbo cache | |
| # HIT witnesses that its inputs are unchanged; anything else is left | |
| # out for the coverage check below to name. The workflow still writes | |
| # no byte the generator did not emit — the merge happens inside it. | |
| if ! node scripts/measure-test-shard-timings.mjs "${ARGS[@]}" \ | |
| --merge-into scripts/test-shard-timings.json \ | |
| --out "$WORK/refreshed.json"; then | |
| echo "::warning::The generator refused the set including run $RUN_ID; dropping that run and continuing." | |
| unset 'ACCEPTED[-1]' | |
| echo "::endgroup::"; continue | |
| fi | |
| echo "Accumulated ${#ACCEPTED[@]} run(s): ${ACCEPTED[*]}" | |
| if node scripts/ci/select-shard-timings-run.mjs --check-coverage \ | |
| --committed scripts/test-shard-timings.json \ | |
| --refreshed "$WORK/refreshed.json" \ | |
| --workspace "$RUNNER_TEMP/turbo-ls.json" \ | |
| --exclude @objectstack/dogfood; then | |
| CHOSEN="$RUN_ID" | |
| echo "::endgroup::" | |
| break | |
| fi | |
| echo "::endgroup::" | |
| done | |
| if [ -z "$CHOSEN" ]; then | |
| echo "::error::The ${#ACCEPTED[@]} eligible run(s) on main, accumulated and merged with the committed dataset, still leave a package that HAD a measured weight with neither a fresh measurement nor a turbo cache HIT to witness that it is unchanged — the shortfall above names them. Each would drop to the test-file-count ESTIMATE, which is the silent degradation this lane exists to prevent. NOTHING was regenerated and no PR was opened: this is a refusal, not a quiet success. A package that merely went unmeasured is NOT this error — that case is carried on its cache-hit witness — so a shortfall here means a suite failed, a package was renamed or removed, or its slices could not be assembled in any run." | |
| exit 1 | |
| fi | |
| # Candidates arrive newest-first, so ACCEPTED[0] is the most recent run | |
| # in the set and its commit is the one the refresh is dated from. The | |
| # full list travels alongside it: every run in it contributed | |
| # measurements, and the PR body names them all rather than implying one. | |
| NEWEST="${ACCEPTED[0]}" | |
| echo "run_id=$NEWEST" >> "$GITHUB_OUTPUT" | |
| echo "runs=${ACCEPTED[*]}" >> "$GITHUB_OUTPUT" | |
| echo "run_count=${#ACCEPTED[@]}" >> "$GITHUB_OUTPUT" | |
| echo "head_sha=$(node -e 'const rs=JSON.parse(require("fs").readFileSync(process.env.RUNNER_TEMP+"/candidates.json","utf8"));console.log(rs.find(r=>String(r.run_id)===process.argv[1]).head_sha)' "$NEWEST")" >> "$GITHUB_OUTPUT" | |
| # Byte comparison, and it decides everything downstream. `cmp -s` rather | |
| # than a diff of parsed JSON: the committed artefact is the file, so the | |
| # file is what has to differ for a PR to be worth anyone's attention. | |
| - name: Compare against the committed dataset | |
| id: compare | |
| run: | | |
| if cmp -s scripts/test-shard-timings.json "$RUNNER_TEMP/refresh/refreshed.json"; then | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| echo "The regenerated dataset is BYTE-IDENTICAL to the committed one; no PR." | tee -a "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| # The before/after halves of ruling 3, taken from the partitioner's OWN | |
| # verdict line rather than recomputed here — a second implementation of the | |
| # binning would be a second answer to grade against. | |
| # | |
| # ⛔ The AFTER leg's non-zero exit is CAPTURED, not propagated, and that is | |
| # not leniency: a red there is the partitioner refusing an honest | |
| # measurement, which is information the PR must carry rather than a reason | |
| # to withhold the PR. `set +e` around that one command, with `$?` read | |
| # immediately and before any pipe, is what keeps the verdict readable | |
| # without letting the step's own status swallow it; the text goes into the | |
| # body either way and the PR's own lint job grades it again. | |
| - name: Predicted bins, before and after | |
| id: bins | |
| if: steps.compare.outputs.changed == 'true' | |
| run: | | |
| node scripts/partition-test-shards.mjs --self-test > "$RUNNER_TEMP/bins-before.txt" 2>&1 || true | |
| cp "$RUNNER_TEMP/refresh/refreshed.json" scripts/test-shard-timings.json | |
| set +e | |
| node scripts/partition-test-shards.mjs --self-test > "$RUNNER_TEMP/bins-after.txt" 2>&1 | |
| echo "partitioner_exit=$?" >> "$GITHUB_OUTPUT" | |
| set -e | |
| echo "BEFORE: $(cat "$RUNNER_TEMP/bins-before.txt")" | |
| echo "AFTER: $(cat "$RUNNER_TEMP/bins-after.txt")" | |
| # Composition is separated from the WRITE on purpose: a `pull_request` run | |
| # of this lane must exercise the body-building — the shard durations, the | |
| # bins, the conditional blocks — without pushing anything. Both the write | |
| # step and the dry-run step below consume this file. | |
| - name: Compose the pull request body | |
| if: steps.compare.outputs.changed == 'true' | |
| env: | |
| RUN_ID: ${{ steps.generate.outputs.run_id }} | |
| RUNS: ${{ steps.generate.outputs.runs }} | |
| RUN_COUNT: ${{ steps.generate.outputs.run_count }} | |
| HEAD_SHA: ${{ steps.generate.outputs.head_sha }} | |
| PARTITIONER_EXIT: ${{ steps.bins.outputs.partitioner_exit }} | |
| USED_PAT: ${{ secrets.RELEASE_PUSH_TOKEN != '' }} | |
| run: | | |
| set -euo pipefail | |
| # Read out of the generated dataset itself rather than recomputed, so the | |
| # sentence in the PR cannot drift from the file it describes. | |
| CARRY_LINE=$(node -e ' | |
| const d = JSON.parse(require("fs").readFileSync(process.env.RUNNER_TEMP + "/refresh/refreshed.json", "utf8")); | |
| const carried = d.carriedOver ?? []; | |
| const total = Object.keys(d.packages).length; | |
| const fresh = total - carried.length; | |
| if (carried.length === 0) { | |
| console.log(`All ${total} package weights were measured in these runs; nothing was carried.`); | |
| } else { | |
| console.log( | |
| `${total} package weights: ${fresh} measured in these runs, and ${carried.length} carried ` + | |
| `forward at their previous values because a turbo cache HIT witnessed that their inputs are ` + | |
| `unchanged (so the old number still describes them). Carried: ${carried.join(", ")}.` | |
| ); | |
| } | |
| ') | |
| SHARDS=$(node -e ' | |
| const rs = JSON.parse(require("fs").readFileSync(process.env.RUNNER_TEMP + "/candidates.json", "utf8")); | |
| const r = rs.find((x) => String(x.run_id) === process.env.RUN_ID); | |
| const s = r.shard_seconds ?? {}; | |
| console.log([1,2,3,4,5,6].map((n) => (s[n] == null ? "?" : `${n}: ${s[n]}s`)).join(" | ")); | |
| ') | |
| { | |
| echo "Refreshes \`scripts/test-shard-timings.json\`, the balancing input for the Test Core" | |
| echo "shard split. Opened automatically by \`.github/workflows/shard-timings-refresh.yml\`." | |
| echo "Every byte came out of \`scripts/measure-test-shard-timings.mjs\`; nothing here was" | |
| echo "hand-edited, and no bound, timeout or matrix entry was touched." | |
| echo | |
| echo "## Source" | |
| echo | |
| echo "Measured across $RUN_COUNT accumulated run(s). No single green run measures the whole" | |
| echo "workspace — turbo's cache is namespaced per shard and only main pushes write it, so a" | |
| echo "package whose inputs have not changed is a HIT and the generator refuses hits rather" | |
| echo "than recording a replay as a duration. Runs are therefore accumulated, each fenced by" | |
| echo "its own \`--run\` group, until every package the committed dataset holds is measured" | |
| echo "again; a package seen in several of them gets the median of those observations." | |
| echo | |
| for R in $RUNS; do | |
| echo "- https://github.com/$GITHUB_REPOSITORY/actions/runs/$R" | |
| done | |
| echo | |
| echo "- Newest run in the set: \`$RUN_ID\`, commit \`$HEAD_SHA\` — the date this refresh carries." | |
| echo "- Every run above had all six \`Test Core (N/6)\` jobs conclude \`success\` with its six" | |
| echo " run-summary artifacts still retained; runs that were cancelled, failed or had lost" | |
| echo " their artifacts were rejected by name in the log before any of these were used." | |
| echo | |
| echo "$CARRY_LINE" | |
| echo | |
| echo "⚠️ This PR references #16173 and #16222 but does NOT carry a closing keyword for them," | |
| echo "because a weekly lane cannot know which cards a given run ought to retire. If this is" | |
| echo "the first refresh to land, retire those two by hand as part of merging it." | |
| echo | |
| echo "## Measured per-shard suite time on the newest run in the set" | |
| echo | |
| echo "\`\`\`" | |
| echo "$SHARDS" | |
| echo "\`\`\`" | |
| echo | |
| echo "## Predicted bins, before and after" | |
| echo | |
| echo "\`\`\`" | |
| echo "BEFORE $(cat "$RUNNER_TEMP/bins-before.txt")" | |
| echo "AFTER $(cat "$RUNNER_TEMP/bins-after.txt")" | |
| echo "\`\`\`" | |
| echo | |
| if [ "${PARTITIONER_EXIT:-0}" != "0" ]; then | |
| echo "## The partitioner's own pins RED on this refresh — read this before merging" | |
| echo | |
| echo "This is the designed behaviour, not a defect in the refresh: the acceptance bound is" | |
| echo "a ratio, and a package that has grown past what any six-way split can bin makes the" | |
| echo "pins fail with the arithmetic in the message. The remedy the partitioner names is to" | |
| echo "raise the file-level slice count for that package — ⛔ never to raise the bound, and" | |
| echo "⛔ never to hand-edit this dataset. This workflow deliberately does neither: it" | |
| echo "reports and stops, because both are decisions." | |
| echo | |
| fi | |
| if [ "$USED_PAT" != "true" ]; then | |
| echo "## No checks will start on this PR by themselves" | |
| echo | |
| echo "It was opened with the Actions \`GITHUB_TOKEN\`, and GitHub's recursion guard means a" | |
| echo "PR opened that way triggers no workflow runs. Push any commit to the branch, or close" | |
| echo "and reopen the PR, to start CI." | |
| echo | |
| fi | |
| # ⛔ No closing keyword is emitted here, in any form. GitHub's | |
| # parser matches `fixes`/`closes`/`resolves` plus a number and | |
| # ignores every negation around them, so even a sentence saying a | |
| # card is NOT closed would close it. This PR is the workflow's | |
| # OUTPUT; the cards about the workflow are referenced only. | |
| echo "Refs #16464, #16173, #16222." | |
| } > "$RUNNER_TEMP/pr-body.md" | |
| echo "Composed a $(wc -l < "$RUNNER_TEMP/pr-body.md")-line pull request body." | |
| # The WRITE. Skipped on a `pull_request` run of this lane: a PR that only | |
| # edits this workflow must never push a bot branch or open a second PR. | |
| - name: Push the refresh branch and open the pull request | |
| if: steps.compare.outputs.changed == 'true' && github.event_name != 'pull_request' | |
| env: | |
| GH_TOKEN: ${{ secrets.RELEASE_PUSH_TOKEN || github.token }} | |
| RUN_ID: ${{ steps.generate.outputs.run_id }} | |
| HEAD_SHA: ${{ steps.generate.outputs.head_sha }} | |
| run: | | |
| set -euo pipefail | |
| BRANCH="claude/shard-timings-refresh-$RUN_ID" | |
| git config user.name 'github-actions[bot]' | |
| git config user.email '41898282+github-actions[bot]@users.noreply.github.com' | |
| git switch -c "$BRANCH" | |
| git add scripts/test-shard-timings.json | |
| # Separate -m flags rather than one embedded newline: a YAML-indented | |
| # heredoc would carry its own leading whitespace into the message body. | |
| git commit \ | |
| -m "chore(ci): refresh the Test Core shard-timings dataset" \ | |
| -m "Regenerated by .github/workflows/shard-timings-refresh.yml from the test-core-run-summary artifacts of $RUN_COUNT accumulated run(s) ($RUNS), newest $RUN_ID at $HEAD_SHA. Generated, never hand-edited." | |
| git push origin "$BRANCH" | |
| PR_URL=$(gh pr create --base main --head "$BRANCH" \ | |
| --title "chore(ci): refresh the Test Core shard-timings dataset" \ | |
| --body-file "$RUNNER_TEMP/pr-body.md") | |
| echo "Opened $PR_URL" | tee -a "$GITHUB_STEP_SUMMARY" | |
| # ADDITIVE label write only. A whole-set PUT replaces the PR's labels | |
| # and destroys any that land in between — measured on this repo, one | |
| # second wide (see pr-automation.yml's header). POST names only what it | |
| # adds, so no interleaving can lose another writer's label. | |
| PR_NUMBER=$(gh pr view "$PR_URL" --json number --jq .number) | |
| gh api --method POST "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/labels" \ | |
| -f "labels[]=skip-changeset" > /dev/null | |
| # Read back, because an additive write is necessary and not sufficient: | |
| # a concurrent whole-set PUT from another workflow can still strip the | |
| # label after a successful POST. `skip-changeset` is this PR's exemption | |
| # from the changeset gate — it publishes nothing — so losing it turns | |
| # the gate red on a PR that legitimately has no changeset. | |
| if gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/labels" --jq '.[].name' \ | |
| | grep -qxF 'skip-changeset'; then | |
| echo "skip-changeset confirmed on PR #$PR_NUMBER." | |
| else | |
| echo "::warning::skip-changeset did not survive the write on PR #$PR_NUMBER (a concurrent whole-set label PUT strips it). Re-applying once." | |
| gh api --method POST "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/labels" \ | |
| -f "labels[]=skip-changeset" > /dev/null | |
| gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/labels" --jq '.[].name' \ | |
| | grep -qxF 'skip-changeset' \ | |
| || echo "::error::skip-changeset is still absent from PR #$PR_NUMBER; the changeset gate will demand a changeset this PR legitimately has none of. Apply the label by hand." | |
| fi | |
| # The dry-run half of the `pull_request` posture. Everything above ran for | |
| # real; this renders the body that WOULD have been posted, so a reviewer of | |
| # a change to this lane sees the actual output rather than the diff of the | |
| # code that produces it. | |
| - name: Dry run — the pull request this would have opened | |
| if: steps.compare.outputs.changed == 'true' && github.event_name == 'pull_request' | |
| run: | | |
| { | |
| echo "### Shard timings: dry run (no branch pushed, no PR opened, no label written)" | |
| echo | |
| echo "This is a \`pull_request\` run of the refresh lane itself. The run selection, the" | |
| echo "artifact download, the regeneration, the coverage check and the partitioner's verdict" | |
| echo "all executed for real; only the write was skipped. The body below is what a scheduled" | |
| echo "run would have posted." | |
| echo | |
| echo "---" | |
| echo | |
| cat "$RUNNER_TEMP/pr-body.md" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Say what happened when nothing changed | |
| if: steps.compare.outputs.changed == 'false' | |
| run: | | |
| { | |
| echo "### Shard timings: byte-identical, no PR" | |
| echo | |
| echo "Run \`${{ steps.generate.outputs.run_id }}\` (\`${{ steps.generate.outputs.head_sha }}\`)" | |
| echo "regenerated \`scripts/test-shard-timings.json\` byte-for-byte identically to the" | |
| echo "committed file, so there is nothing to open a pull request about. The dataset is" | |
| echo "current, and this is the loop working rather than the loop skipping." | |
| } >> "$GITHUB_STEP_SUMMARY" |