ci(test): e2e and live filename tiers move to a nightly run on main #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
| name: Nightly Tiers | |
| # The `e2e` and `live` test tiers, run nightly on `main` (#16455). | |
| # | |
| # Maintainer direction (2026-09-07, verbatim): 「我想的是测试会不会太多,是否都是 | |
| # 必要的,是不是应该砍,每次修改都要完整的测试吗」. The two tiers leave the per-PR | |
| # and merge-queue runs (ci.yml's Test Core now runs under `OS_TEST_TIERS=queue`) | |
| # and run here, under `OS_TEST_TIERS=nightly`, selected by their EXISTING | |
| # filename tiers only — `*.e2e.test.*` and `*.live.test.*` — with no test file | |
| # renamed, deleted or edited to move it. `scripts/nightly-tiers.mjs` is the one | |
| # reader of the switch; its header carries the contract, the measured | |
| # population and the reason it is one module rather than a line per config. | |
| # | |
| # ## Shape | |
| # | |
| # `coverage-nightly.yml`'s: `schedule` + `workflow_dispatch`, node 22, pnpm, | |
| # RESTORE-ONLY caches (a scheduled run reads main's pnpm store and Build Core's | |
| # turbo seeds; the per-push workflows own saving them, and a nightly that saved | |
| # would churn the rationed 10 GB pool the way PR-side saves once did — see | |
| # ci.yml's "Restore Turbo cache"). The turbo cache restore is what keeps the | |
| # dependency closure's `build` tasks a replay: `OS_TEST_TIERS` is hashed into | |
| # the `test` tasks ONLY (turbo.json), so the builds hit main's entries while | |
| # every test task misses by construction and runs for real. | |
| # | |
| # ## Sharding — TWO shards, derived rather than picked | |
| # | |
| # The package set is not the workspace: `scripts/nightly-tiers.mjs --packages` | |
| # enumerates exactly the packages that own a tier file, in the `turbo ls` | |
| # document shape, and `scripts/partition-test-shards.mjs` splits THAT with the | |
| # same measured weights, the same LPT placement and the same | |
| # `<pkg> k/n` slice grammar Test Core uses. Measured at 6eba38f5a3: one | |
| # package, `@objectstack/cli`, owning all 60 tier files; its whole suite was | |
| # 1231s on a hosted runner (run 34009395649) and the e2e tier is ~75% of it | |
| # (packages/cli/vitest.config.ts, the priced trade), so ≈ 15 min of tests plus | |
| # ~5 min of restore/install per runner. Under 60 minutes on ONE shard already — | |
| # but `FILE_SHARDED_PACKAGES` cuts the CLI into two vitest slices, and the | |
| # partitioner REFUSES to place two slices of one package in one bin | |
| # (`assertSlicesSpread`), so one shard is not a legal count and two is the | |
| # smallest one that is. Each shard then carries one slice, ≈ 8 min of tests. | |
| # Raise the matrix (three places below: `matrix.shard`, the job `name:`, and | |
| # `--shard k/N`) only when a second tier-owning package arrives heavy enough | |
| # for the partitioner's own balance line to say so. | |
| # | |
| # ## What lands where — the house idiom of `platform-checklist-watchdog.yml` | |
| # | |
| # On green this workflow files nothing, edits nothing and closes nothing. On | |
| # red the `report` job — ONE writer, after both shards, never the shards | |
| # themselves (two matrix legs racing the same lookup would mint two cards) — | |
| # opens or refreshes ONE card: fixed title `nightly-tiers: red on main`, labels | |
| # `bug` · `domain:devx` · `priority:p1` on create only (grading is the devx | |
| # seat's and a refresh must not undo it), body carrying the run link and the | |
| # failing files, de-duplicated by scanning OPEN issues for that exact title | |
| # prefix through the repo-scoped REST API with this workflow's own token. A | |
| # CLOSED card is not reopened: red again after it was answered is a regression, | |
| # filed fresh. | |
| # | |
| # The body marker is plain text, ⛔ never an HTML comment: this platform's body | |
| # sanitizer is measured to eat short angle-bracket fragments, and a de-dup key | |
| # that can be swallowed files a duplicate a day. The failing files come from | |
| # vitest's own JSON reporter (`--reporter=json`, written per package, collected | |
| # per shard, read by `scripts/nightly-tiers.mjs --failing-files`), never from a | |
| # regex over the log — the log is presentation, the report is the contract. | |
| # | |
| # ⛔ A run on any ref but `main` NEVER writes to the board — a | |
| # `workflow_dispatch` on a branch, or the `pull_request` run below, proves the | |
| # transport, the sharding and the rendering on a real runner and publishes the | |
| # would-be card to the run summary instead. That is how the card that landed | |
| # this was smoke-tested. | |
| # | |
| # ⛔ This job is NOT a required context and must never become one: it runs on | |
| # no pull request but one editing its own two files, so a ruleset naming it | |
| # would block every other PR forever on a check that cannot report. | |
| on: | |
| schedule: | |
| # Nightly. The minute is offset off the top of the hour ON PURPOSE (scheduled | |
| # workflows queue behind everyone else's `:00` cron) and off every sibling | |
| # patrol's minute, so two of them never contend for the same runner minute; | |
| # 05:29 sits after `rerun-safety-nightly` (04:00) and `coverage-nightly` | |
| # (05:00) have started and well before `showcase-smoke` (07:00). | |
| - cron: '29 5 * * *' | |
| workflow_dispatch: {} | |
| # ⛔ PATHS-FILTERED TO THIS FILE AND THE SWITCH READER IT INVOKES, and that | |
| # filter is the whole reason this trigger is allowed to exist: a change to | |
| # the nightly itself is exercised before it merges — the posture every patrol | |
| # in this repo keeps, and the one `scripts/pm/dispatch-gates.mjs` pins | |
| # tree-wide (a family reachable only from a scheduled workflow is refused | |
| # there by name). A trigger that fires only on a pull request editing these | |
| # two files puts ≈ 15 minutes of e2e on no unrelated PR's critical path. | |
| # | |
| # ⛔ Do NOT widen this list toward the tests themselves: a `packages/**` | |
| # entry here would put the two tiers back on the per-PR path, which is the | |
| # thing the card this landed removed. A `pull_request` run never writes to | |
| # the board — its `github.ref` is the merge ref, not `main`, and the board | |
| # write is gated on `main` by name. | |
| pull_request: | |
| paths: | |
| - '.github/workflows/test-nightly-tiers.yml' | |
| - 'scripts/nightly-tiers.mjs' | |
| # Least privilege. The shards read the repo and publish artifacts of their own | |
| # run; the report job writes issues and nothing else — no label on anybody's | |
| # PR, no state, no comment stream. | |
| permissions: | |
| contents: read | |
| issues: write | |
| # One nightly at a time PER REF. A dispatch overlapping the schedule on `main` | |
| # would have two `report` jobs racing the same lookup, and the loser would mint | |
| # a duplicate; a `pull_request` run never writes, so it need not queue behind | |
| # `main`'s run — keying the group by ref keeps exactly the writers serial. | |
| concurrency: | |
| group: test-nightly-tiers-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| # The card's IDENTITY — the title prefix the de-dup scan keys on, and the | |
| # plain-text body marker that is its second way in. Single-sourced here so | |
| # the report step and a reader of this file see the same strings. | |
| CARD_TITLE: 'nightly-tiers: red on main' | |
| CARD_MARKER: os-nightly-tiers | |
| # Applied on CREATE only, additively; never rewritten on a refresh. | |
| CARD_LABELS: 'bug,domain:devx,priority:p1' | |
| jobs: | |
| tiers: | |
| name: Nightly tiers (${{ matrix.shard }}/2) | |
| runs-on: ubuntu-latest | |
| # Generous against the ≈ 13 min a shard is expected to take, and still | |
| # inside the hour the card set as the whole run's bound. A shard killed here | |
| # produces no vitest report; the report job says so rather than reading it | |
| # as "no failing files". | |
| timeout-minutes: 45 | |
| strategy: | |
| # Both shards run to completion: the card should carry every failing | |
| # file, not the first shard's. | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2] | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| # Kept as this job's own step rather than folded into the composite | |
| # below: `scripts/check-node-version.mjs` scans `.github/workflows/*.yml` | |
| # only and reports how many setup-node steps it audited, so a step moved | |
| # out of sight would silently shrink its census. | |
| - 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. | |
| - 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- | |
| # Restore-only, from Build Core's namespace: that job builds the whole | |
| # workspace on every main push, so its entries replay every `build` task | |
| # this run depends on. The `test` tasks cannot replay from it — their | |
| # hash carries `OS_TEST_TIERS`, which no push run has ever set to | |
| # `nightly` — which is the property that makes a nightly a test run and | |
| # not a cache read. | |
| - name: Restore Turbo cache | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: .turbo/cache | |
| key: ${{ runner.os }}-turbo-build-core-${{ github.ref_name }}-${{ github.sha }} | |
| restore-keys: | | |
| ${{ runner.os }}-turbo-build-core-${{ github.ref_name }}- | |
| ${{ runner.os }}-turbo-build-core- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| # Two readings before a single test runs, both loud on failure: | |
| # | |
| # --check every tier-owning package honours the switch, judged by what | |
| # vitest COLLECTS under each setting (a package that adopted a | |
| # tier without reading the switch would otherwise run its whole | |
| # suite here under the nightly's name, and its tier files in the | |
| # merge queue — both silently); | |
| # --packages the tier-owning packages, in the `turbo ls` document shape | |
| # the partitioner reads, so this run shards exactly them. | |
| # | |
| # An EMPTY shard short-circuits the test step: `turbo run test` with zero | |
| # --filter args runs the entire workspace. | |
| - name: Compute this shard's package set | |
| run: | | |
| node scripts/nightly-tiers.mjs --self-test | |
| node scripts/nightly-tiers.mjs --check | |
| node scripts/nightly-tiers.mjs --packages > "$RUNNER_TEMP/tier-packages.json" | |
| node scripts/partition-test-shards.mjs "$RUNNER_TEMP/tier-packages.json" \ | |
| --shard ${{ matrix.shard }}/2 \ | |
| > "$RUNNER_TEMP/shard-packages.txt" | |
| echo 'Items on this shard (a package name, or a package plus a k/n file-level slice):' | |
| cat "$RUNNER_TEMP/shard-packages.txt" | |
| # The same leg loop as ci.yml's "Run this shard's tests", under the | |
| # nightly setting, with vitest's JSON reporter added beside the default | |
| # one: `--outputFile.json` is RELATIVE so each package writes its own | |
| # report under its own root (an absolute path would make a second | |
| # tier-owning package overwrite the first's), and the default reporter | |
| # stays so the stall guard still sees output flush. | |
| # | |
| # `--log-order=stream` is MANDATORY under the guard and `--concurrency=4` | |
| # matches the hosted runner's cores — both reasons are in ci.yml beside | |
| # the step this mirrors; NODE_OPTIONS arms the stall guard's SIGUSR2 | |
| # diagnostic reports the same way. | |
| # | |
| # ⛔ A failing leg STOPS the remaining ones, as in ci.yml: a second full | |
| # leg on a job that is already red only risks turning an informative red | |
| # into a killed job with no report at all. | |
| - name: Run this shard's nightly tiers | |
| env: | |
| OS_TEST_TIERS: nightly | |
| NODE_OPTIONS: --report-on-signal --report-signal=SIGUSR2 --report-directory=${{ runner.temp }}/stall-reports | |
| run: | | |
| if [ ! -s "$RUNNER_TEMP/shard-packages.txt" ]; then | |
| echo "No packages on this shard — nothing to test." | |
| exit 0 | |
| fi | |
| export VITEST_MAX_WORKERS="$(node scripts/vitest-worker-cap.mjs)" | |
| mkdir -p "$RUNNER_TEMP/stall-reports" | |
| FILTERS="" | |
| SLICES="" | |
| while read -r PKG SLICE; do | |
| [ -n "$PKG" ] || continue | |
| if [ -n "$SLICE" ]; then | |
| SLICES="$SLICES $PKG=$SLICE" | |
| else | |
| FILTERS="$FILTERS --filter=$PKG" | |
| fi | |
| done < "$RUNNER_TEMP/shard-packages.txt" | |
| REPORTER="--reporter=default --reporter=json --outputFile.json=.nightly-tiers/vitest-report.json" | |
| STATUS=0 | |
| LOGS="" | |
| for LEG in __whole__ $SLICES; do | |
| if [ "$LEG" = __whole__ ]; then | |
| [ -n "$FILTERS" ] || continue | |
| LOG="$RUNNER_TEMP/nightly-tiers-packages.log" | |
| set -- pnpm turbo run test $FILTERS --concurrency=4 --summarize --log-order=stream -- $REPORTER | |
| else | |
| PKG="${LEG%%=*}" | |
| SLICE="${LEG#*=}" | |
| LOG="$RUNNER_TEMP/nightly-tiers-slice-$(printf '%s' "$PKG" | tr -c 'A-Za-z0-9' '-').log" | |
| set -- pnpm turbo run test "--filter=$PKG" --concurrency=4 --summarize --log-order=stream -- "--shard=$SLICE" $REPORTER | |
| fi | |
| LOGS="$LOGS $LOG" | |
| node scripts/run-with-stall-guard.mjs --log "$LOG" --stall-minutes 10 \ | |
| --report-dir "$RUNNER_TEMP/stall-reports" -- "$@" || { STATUS=$?; break; } | |
| done | |
| : > "$RUNNER_TEMP/nightly-tiers.log" | |
| for LOG in $LOGS; do | |
| if [ -f "$LOG" ]; then | |
| cat "$LOG" >> "$RUNNER_TEMP/nightly-tiers.log" | |
| fi | |
| done | |
| exit $STATUS | |
| # Gather what the report job needs, red or green: every package's vitest | |
| # JSON report (named by the package directory so two packages never | |
| # collide in the merged download) and the tail of this shard's log. | |
| # `if: always()` — a red shard is exactly when this earns its keep. | |
| - name: Collect this shard's reports | |
| if: always() | |
| run: | | |
| OUT="$RUNNER_TEMP/nightly-reports/shard-${{ matrix.shard }}" | |
| mkdir -p "$OUT" | |
| find . -path '*/node_modules' -prune -o -path '*/.nightly-tiers/vitest-report.json' -print \ | |
| | while read -r REPORT; do | |
| PKG_DIR="$(dirname "$(dirname "$REPORT")")" | |
| cp "$REPORT" "$OUT/$(printf '%s' "$PKG_DIR" | sed 's#^\./##' | tr -c 'A-Za-z0-9' '-').json" | |
| done | |
| if [ -f "$RUNNER_TEMP/nightly-tiers.log" ]; then | |
| tail -n 200 "$RUNNER_TEMP/nightly-tiers.log" > "$OUT/log-tail.txt" | |
| fi | |
| echo "collected:" | |
| ls -la "$OUT" | |
| - name: Publish this shard's reports | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: nightly-tiers-reports-${{ matrix.shard }}-of-2 | |
| path: ${{ runner.temp }}/nightly-reports/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| # The same two questions ci.yml's guard answers about a red suite — was | |
| # every test vitest COUNTED actually run, and did every scheduled package | |
| # report at all — asked of this shard's log against this shard's schedule. | |
| - name: Test completeness guard | |
| if: always() | |
| run: | | |
| if [ ! -f "$RUNNER_TEMP/nightly-tiers.log" ]; then | |
| echo "No test log — the test step did not get far enough to produce one." | |
| exit 0 | |
| fi | |
| node scripts/check-test-completeness.mjs "$RUNNER_TEMP/nightly-tiers.log" \ | |
| --scheduled "$RUNNER_TEMP/shard-packages.txt" \ | |
| --package-list "$RUNNER_TEMP/tier-packages.json" | |
| - name: Upload stall diagnostic reports | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: stall-reports-nightly-tiers-${{ matrix.shard }} | |
| path: ${{ runner.temp }}/stall-reports/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| report: | |
| name: File or refresh the red card | |
| needs: tiers | |
| # `always()` spelled out: this job must run precisely when a shard FAILED or | |
| # was killed, which is the case the implicit `success()` wrapper would | |
| # skip. The board write below is further gated on the result and the ref. | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22' | |
| # No `pnpm install`: `scripts/nightly-tiers.mjs --failing-files` imports | |
| # `node:` builtins and two repo-local helpers only, so installing the | |
| # workspace here would buy nothing and would give an unattended nightly a | |
| # lockfile it could fail on. | |
| - name: Download the shards' reports | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: nightly-tiers-reports-* | |
| path: ${{ runner.temp }}/nightly-reports | |
| merge-multiple: true | |
| # The verdict, read from the matrix job's RESULT — a status read, on | |
| # purpose: `failure` is a red tier, `cancelled` is a shard killed by its | |
| # timeout or by a human, and neither is a green nightly. The failing | |
| # files come from vitest's own JSON reports; a red run that produced none | |
| # (a build failure, a stall kill) says so in the body instead of reading | |
| # as "nothing failed". | |
| - name: Read the verdict | |
| id: verdict | |
| env: | |
| TIERS_RESULT: ${{ needs.tiers.result }} | |
| run: | | |
| echo "tiers result: $TIERS_RESULT" | |
| echo "result=$TIERS_RESULT" >> "$GITHUB_OUTPUT" | |
| set +e | |
| node scripts/nightly-tiers.mjs --failing-files "$RUNNER_TEMP/nightly-reports" \ | |
| > "$RUNNER_TEMP/failing-files.txt" 2> "$RUNNER_TEMP/failing-files.err" | |
| code=$? | |
| set -e | |
| # Captured with NO pipe in between: `cmd | tail` would report the | |
| # pipe's status, and this exit code decides whether the list below is | |
| # a reading or a refusal. | |
| echo "failing_files_exit=$code" >> "$GITHUB_OUTPUT" | |
| cat "$RUNNER_TEMP/failing-files.err" >&2 || true | |
| echo "failing nightly-tier files:" | |
| cat "$RUNNER_TEMP/failing-files.txt" | |
| - name: File or refresh the card | |
| # RED only, on MAIN only. On green nothing in this job writes anything; | |
| # on a branch (the smoke test a card's dev dispatches) the body goes to | |
| # the run summary and the board is untouched. | |
| if: steps.verdict.outputs.result != 'success' && github.ref == 'refs/heads/main' | |
| uses: actions/github-script@v9 | |
| env: | |
| TIERS_RESULT: ${{ steps.verdict.outputs.result }} | |
| FAILING_FILES_EXIT: ${{ steps.verdict.outputs.failing_files_exit }} | |
| with: | |
| # Delivery is retried, never assumed: this write is the entire product | |
| # of a red run, and a transient answer from the issues endpoint would | |
| # otherwise discard a completed sweep. | |
| retries: 3 | |
| script: | | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const title = process.env.CARD_TITLE; | |
| const marker = process.env.CARD_MARKER; | |
| const labels = process.env.CARD_LABELS.split(',').map((s) => s.trim()).filter(Boolean); | |
| const result = process.env.TIERS_RESULT; | |
| const runUrl = `${process.env.GITHUB_SERVER_URL}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; | |
| const read = (name) => { | |
| try { return fs.readFileSync(path.join(process.env.RUNNER_TEMP, name), 'utf8'); } | |
| catch { return ''; } | |
| }; | |
| const failing = read('failing-files.txt').split('\n').map((s) => s.trim()).filter(Boolean); | |
| const extractorExit = process.env.FAILING_FILES_EXIT; | |
| // The log tails, bounded: a GitHub issue body caps at 65536 chars, | |
| // and the run link carries the whole log anyway. | |
| const tails = []; | |
| const reportsDir = path.join(process.env.RUNNER_TEMP, 'nightly-reports'); | |
| const walk = (dir) => { | |
| let entries = []; | |
| try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } | |
| for (const e of entries) { | |
| const p = path.join(dir, e.name); | |
| if (e.isDirectory()) walk(p); | |
| else if (e.name === 'log-tail.txt') tails.push({ shard: path.basename(dir), text: fs.readFileSync(p, 'utf8') }); | |
| } | |
| }; | |
| walk(reportsDir); | |
| const BUDGET = 40000; | |
| const perTail = Math.max(2000, Math.floor(BUDGET / Math.max(1, tails.length))); | |
| const tailBlocks = tails.sort((a, b) => a.shard.localeCompare(b.shard)).flatMap(({ shard, text }) => [ | |
| `### ${shard} — last lines of the test log`, | |
| '', | |
| '```', | |
| text.length > perTail ? `… (${text.length - perTail} chars elided) …\n${text.slice(-perTail)}` : text, | |
| '```', | |
| '', | |
| ]); | |
| const filesSection = failing.length > 0 | |
| ? ['## Failing files', '', ...failing.map((f) => `- \`${f}\``), ''] | |
| : extractorExit !== '0' | |
| ? ['## Failing files', '', `⚠️ The report reader exited ${extractorExit}; no per-file verdict is available. Read the run log.`, ''] | |
| : ['## Failing files', '', '⚠️ No vitest report named a failing nightly-tier file, yet the tiers job did not succeed — the run died before or outside vitest (a build failure, a stall kill, a cancelled shard). Read the run log.', '']; | |
| const body = [ | |
| `${marker} — machine-findable marker for this generated card. ⛔ Do not delete this line: it is how the nightly finds this card instead of filing a new one every night.`, | |
| '', | |
| `# ${title}`, | |
| '', | |
| `_Swept ${new Date().toISOString()} · [run log](${runUrl}) · commit \`${process.env.GITHUB_SHA}\` · trigger \`${context.eventName}\` · tiers job result \`${result}\`._`, | |
| '', | |
| 'The `e2e` and `live` test tiers — the files named `*.e2e.test.*` and `*.live.test.*` — run here', | |
| 'nightly on `main` under `OS_TEST_TIERS=nightly` and nowhere else (the per-PR and merge-queue', | |
| 'Test Core runs under `queue`). A red here is a real suite that no pull request will red on;', | |
| 'this card is the channel that sees it. Nothing is blocked by it.', | |
| '', | |
| '⛔ The remedy is never to rename, skip or delete the failing test to make the nightly green.', | |
| 'Reproduce locally with `OS_TEST_TIERS=nightly pnpm --filter <pkg> test`, fix what it names, and', | |
| 'let the next nightly refresh this card.', | |
| '', | |
| ...filesSection, | |
| ...tailBlocks, | |
| '_Filed by `.github/workflows/test-nightly-tiers.yml`. Generated by [Claude Code](https://claude.ai/code)_', | |
| ].join('\n'); | |
| // ── De-dup: the OPEN cards carrying the title prefix ───────────── | |
| // Repo-scoped REST, this workflow's token, bounded pages; a | |
| // truncated scan cannot establish ABSENCE, and filing on an | |
| // unestablished absence is how a nightly mints a duplicate a day. | |
| const isThisCard = (i) => !i.pull_request | |
| && (String(i.title ?? '').startsWith(title) || String(i.body ?? '').includes(marker)); | |
| const MAX_PAGES = 10; | |
| const candidates = []; | |
| let scanComplete = true; | |
| for (let page = 1; page <= MAX_PAGES; page += 1) { | |
| const res = await github.rest.issues.listForRepo({ | |
| owner, repo, state: 'open', sort: 'created', direction: 'asc', per_page: 100, page, | |
| }); | |
| candidates.push(...res.data.filter(isThisCard)); | |
| if (res.data.length < 100) break; | |
| if (page === MAX_PAGES) scanComplete = false; | |
| } | |
| if (!scanComplete) { | |
| throw new Error(`the open-issue scan hit its ${MAX_PAGES}-page bound without completing — absence is NOT established, so nothing was filed. The failing files are in this run's summary.`); | |
| } | |
| // The OLDEST open card wins — that is the one any duplicates were | |
| // closed against. | |
| const existing = candidates.sort((a, b) => a.number - b.number)[0] ?? null; | |
| if (existing) { | |
| // Rewritten in place, never a comment per run. ⛔ Labels are NOT | |
| // rewritten — grading is the devx seat's and a refresh must not | |
| // undo it. | |
| await github.rest.issues.update({ owner, repo, issue_number: existing.number, body }); | |
| core.info(`refreshed nightly-tiers card #${existing.number} (${body.length} chars, tiers ${result})`); | |
| core.notice(`nightly tiers are red on main — card #${existing.number} refreshed.`); | |
| return; | |
| } | |
| const created = await github.rest.issues.create({ | |
| owner, repo, title, body, | |
| // Additive on create; ⛔ nothing here ever replaces a whole label | |
| // set (`check:whole-set-label-write` refuses that verb outright). | |
| labels, | |
| }); | |
| core.info(`filed nightly-tiers card #${created.data.number} (${body.length} chars, tiers ${result})`); | |
| core.notice(`nightly tiers are red on main — card #${created.data.number} filed.`); | |
| - name: Publish the verdict to the run summary | |
| # Always: on green this IS the whole product; on a branch it is the | |
| # smoke test's evidence; on a red main run it makes the run | |
| # self-contained for whoever opens it from the card. | |
| if: always() | |
| env: | |
| TIERS_RESULT: ${{ steps.verdict.outputs.result }} | |
| run: | | |
| { | |
| echo "### Nightly tiers — tiers job result \`${TIERS_RESULT:-unread}\`" | |
| echo | |
| if [ "${{ github.ref }}" != "refs/heads/main" ]; then | |
| echo "_Board write skipped: this run is on \`${{ github.ref }}\`, not \`main\`. A branch run proves the sharding, the switch and the rendering without filing anything._" | |
| echo | |
| fi | |
| if [ "$TIERS_RESULT" = "success" ]; then | |
| echo "Green: every nightly-tier file passed. Nothing filed, nothing edited, nothing closed." | |
| else | |
| echo "Failing nightly-tier files (from the vitest JSON reports; empty means the run died outside vitest — read the log):" | |
| echo | |
| echo '```' | |
| cat "$RUNNER_TEMP/failing-files.txt" 2>/dev/null || echo '(no list produced)' | |
| echo '```' | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Fail the run if the verdict was never read | |
| # LAST, on purpose: the card is filed BEFORE this can turn the job red. | |
| # A red TIER is not this job's failure — the shard already went red and | |
| # the card carries it. What must never look green is a report job that | |
| # finished without reading the matrix result at all: nothing downstream | |
| # could tell that apart from a green nightly. | |
| if: always() && steps.verdict.outputs.result == '' | |
| run: | | |
| echo "the verdict step recorded no result, so this run is NOT a reading about the nightly tiers -- it is neither a green one nor a red one. See this run's summary." | |
| exit 1 |