Repository navigation
ci(fleet-patrols): prose and comments stop naming the four retired patrol workflow files #64
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 | |
| # | |
| # The house nightly shape: `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. | |
| # | |
| # The second tier-owning package is `@objectstack/example-showcase`, and it is | |
| # not that heavy: one `*.live.test.ts` file (#26445), the showcase browser | |
| # smoke, which used to be its own scheduled workflow (Showcase Smoke) and | |
| # filed nothing when red. The partitioner keeps it whole ("not the run's | |
| # floor") and places it on one shard; that shard also builds the Console SPA | |
| # and installs Chromium for it (≈ 5 min, the "Prepare the showcase browser | |
| # smoke" step), which no other shard pays. | |
| # | |
| # ## What lands where — the house idiom of `fleet-patrols.yml`'s `platform-checklist-watchdog` job | |
| # | |
| # 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. | |
| # | |
| # ⚠️ Two readers, one fact. This value is restated as `PATROL_SCHEDULE` in | |
| # the `env:` block below, where the report step computes the deadline the | |
| # card's own `Swept` line is judged against (#17720). Change the cron and | |
| # change that env in the same edit, or the card publishes a deadline nobody | |
| # schedules. | |
| - 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' | |
| # This workflow's own `cron:`, restated here so the report step can state the | |
| # cadence that makes the card's `Swept` line decidable (#17720). The sibling | |
| # patrol anchors carry the same heartbeat reading and one of them fires every | |
| # six hours, so a reader who carries a cadence over from a neighbour is wrong | |
| # by 4× — which is why each card states its own rather than leaving the | |
| # reader to go and read a workflow file. | |
| # | |
| # ⛔ A literal, not a parse of this file: the report body is inline | |
| # `github-script` that no test reaches, so the cheapest honest route is the | |
| # one with the least inline logic. The cost is the restatement above, and the | |
| # comment at the `cron:` is what pays it. | |
| PATROL_SCHEDULE: '29 5 * * *' | |
| 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 | |
| # Restore-only: scheduled runs read main's store cache; the per-push | |
| # workflows own saving it. | |
| - name: Setup workspace | |
| uses: ./.github/actions/setup-workspace | |
| with: | |
| save-store: 'false' | |
| # 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- | |
| # 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 showcase browser smoke (`examples/app-showcase/test/showcase-smoke.live.test.ts`) | |
| # drives the Console against a real backend through Playwright, and needs | |
| # two things `turbo run test` does not produce: the Console SPA the backend | |
| # serves at /_console (`packages/console/dist`, built by the `build:dist` | |
| # task — `pnpm console:build`), and a Chromium this `@playwright/test` can | |
| # launch. Only the shard the partitioner gave that package pays for them. | |
| # The test's own `^build` closure is the `test` task's, as for every | |
| # package here. | |
| # | |
| # `continue-on-error`, on purpose: the same shard carries a slice of the | |
| # CLI's e2e tier, and a Console build that fails one night must not take | |
| # that slice down with it. The night is still red, and still names the | |
| # file — the smoke then fails every test at once, and its failure text | |
| # says to read a whole-run red as the environment first; this step's red | |
| # in the log is the environment. | |
| - name: Prepare the showcase browser smoke | |
| continue-on-error: true | |
| run: | | |
| if ! grep -qE '^@objectstack/example-showcase( |$)' "$RUNNER_TEMP/shard-packages.txt"; then | |
| echo "@objectstack/example-showcase is not on this shard — nothing to prepare." | |
| exit 0 | |
| fi | |
| pnpm console:build | |
| pnpm --filter @objectstack/example-showcase exec playwright install --with-deps chromium | |
| # 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. | |
| # | |
| # One deliberate difference from ci.yml: the file-level SLICES run first | |
| # and the whole-package leg last. The whole packages here are the cheap | |
| # ones (the partitioner keeps a package whole when it is not the run's | |
| # floor) — today the showcase browser smoke — and a red in one of them | |
| # must not stop the shard's slice of the CLI's e2e tier from running. | |
| # The other way round, a red slice skips a few minutes of smoke. | |
| - 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 $SLICES __whole__; 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 showcase smoke's screenshots, traces and error context, which | |
| # Playwright writes under its default `outputDir` beside its config. | |
| # `always()` and `ignore`: a red smoke leaves them, and so does a green | |
| # one that passed only on its retry (`retries: 1` under CI) — the flaky | |
| # first attempt is the evidence a time-dependent red needs. A clean green, | |
| # and every shard without the showcase, leave nothing and upload nothing. | |
| - name: Publish the showcase smoke's Playwright diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: showcase-smoke-diagnostics-${{ matrix.shard }} | |
| path: examples/app-showcase/test-results/ | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| # 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-file: .nvmrc | |
| # 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: | | |
| // The body lives in scripts/ci/nightly-tiers-card.mjs (#26086), driven | |
| // offline by its --self-test; this step hands it the same github/context/core. | |
| const { fileNightlyCard } = await import(`${process.env.GITHUB_WORKSPACE}/scripts/ci/nightly-tiers-card.mjs`); | |
| await fileNightlyCard({ github, context, core }); | |
| - 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 |