Skip to content

Commit 6e7f7ca

Browse files
authored
Group CI test logs and surface failures on the default test path
Add per-test-file ::group:: markers, per-test ::error annotations, and GITHUB_STEP_SUMMARY failure tables to the default (non-bucketed) CI test path. Failure information is coordinated across Deno's per-file module instances via a sidecar counter file and size-budgeted against the GitHub 1 MiB silent-drop limit. Gated on QUARTO_TESTS_GHA_ORCHESTRATED and GITHUB_ACTIONS so local output remains unchanged; #13807 bucket-log format is preserved unchanged. - Per-test ::error annotations capped at 9 per-test errors + 1 aggregate annotation per step, coordinated across module instances - Step summary failure tables with complete failure record, self-truncating near 512 KiB to stay below the silent-drop limit - Log invariant checker (check-gha-log.ts) to verify grouping and annotation placement across Deno version bumps - Design doc and verification plan at dev-docs/ci-test-log-grouping-design.md
1 parent 4eed4a3 commit 6e7f7ca

19 files changed

Lines changed: 3137 additions & 201 deletions

‎.claude/rules/testing/built-version-ci.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ Invariants when editing:
2424
- `test-ff-matrix.yml` owns the ff-matrix bucket glob.
2525
- Scheduler jobs in `test-smokes-built.yml` set per-leg OS scope through their `runners:` inputs.
2626
- Keep the per-call suffix in `test-ff-matrix.yml`'s concurrency group so sibling calls cannot cancel one another.
27+
- Every `test-smokes.yml` and `test-ff-matrix.yml` call site passes a
28+
`label-tag` that distinguishes same-OS jobs in the combined run summary.
29+
Per-OS jobs may share a tag because labels also include the OS.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
paths:
3+
- .github/workflows/test-smokes.yml
4+
- .github/workflows/test-smokes-built.yml
5+
- .github/workflows/test-smokes-parallel.yml
6+
- .github/workflows/test-ff-matrix.yml
7+
- .github/workflows/update-test-timing.yml
8+
- src/tools/github.ts
9+
- tests/test.ts
10+
- tests/gha-grouping.ts
11+
- tests/integration/playwright-tests.test.ts
12+
- tests/tools/check-gha-log.ts
13+
- tests/unit/check-gha-log.test.ts
14+
- tests/unit/gha-grouping.test.ts
15+
- tests/unit/github-actions-reporting.test.ts
16+
- tests/unit/harness-failure-reporting.test.ts
17+
- tests/unit/harness-reporting-fixture.ts
18+
---
19+
20+
# GitHub Actions test reporting
21+
22+
`llm-docs/github-actions-test-reporting.md` is the deep dive. Read only the
23+
sections relevant to the edit:
24+
25+
- Workflow ownership or environment wiring: "Ownership" and "Workflow contract".
26+
- Group opening, closing, or log validation: "Log grouping" and "Verification".
27+
- Annotations, labels, excerpts, or step summaries: "Failure reporting" and
28+
"Annotation budget and labels".
29+
30+
For a trivial edit, the invariants below are sufficient.
31+
32+
Keep these invariants:
33+
34+
- A step has one group and annotation owner. Bucket loops set
35+
`QUARTO_TESTS_GHA_ORCHESTRATED=1`; otherwise the harness owns them.
36+
- Step-summary entries are emitted in both paths.
37+
- Failure lines and Deno's final failure sections remain outside groups.
38+
- The annotation count is step-wide; Deno module state is only per test file.
39+
- Every reusable-workflow call passes a `label-tag` that distinguishes same-OS
40+
jobs in the run summary.
41+
- Do not enable parallel test-file execution without redesigning grouping.

‎.github/workflows/test-ff-matrix.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ on:
3838
required: false
3939
type: string
4040
default: '["ubuntu-latest", "windows-latest"]'
41+
label-tag:
42+
description: "Short alphanumeric tag for distinguishing same-OS jobs in failure summaries"
43+
required: false
44+
type: string
45+
default: ""
4146
workflow_dispatch:
4247
inputs:
4348
extra-r-packages:
@@ -100,3 +105,4 @@ jobs:
100105
quarto-artifact-run-id: ${{ inputs.quarto-artifact-run-id }}
101106
ref: ${{ inputs.ref }}
102107
runners: ${{ inputs.runners || '["ubuntu-latest", "windows-latest"]' }}
108+
label-tag: ${{ inputs.label-tag || 'ffdev' }}

‎.github/workflows/test-smokes-built.yml‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ jobs:
120120
quarto-artifact-name: built-quarto-linux-amd64
121121
runners: '["ubuntu-latest"]'
122122
buckets: ${{ github.event.inputs.buckets }} # empty = full run
123+
label-tag: bldsmk
123124

124125
run-playwright-artifact:
125126
name: Playwright tests against built artifact
@@ -134,6 +135,7 @@ jobs:
134135
quarto-artifact-name: built-quarto-linux-amd64
135136
runners: '["ubuntu-latest"]'
136137
buckets: '["integration/playwright-tests.test.ts"]'
138+
label-tag: bldpw
137139

138140
run-ff-matrix-artifact:
139141
name: Feature-format matrix against built artifact
@@ -147,6 +149,7 @@ jobs:
147149
quarto-install: artifact
148150
quarto-artifact-name: built-quarto-linux-amd64
149151
runners: '["ubuntu-latest"]'
152+
label-tag: bldff
150153

151154
# Test an existing published release.
152155
resolve-release:
@@ -210,6 +213,7 @@ jobs:
210213
quarto-version: ${{ needs.resolve-release.outputs.version }}
211214
runners: '["ubuntu-latest", "windows-latest"]'
212215
buckets: ${{ github.event.inputs.buckets }} # empty = full run
216+
label-tag: relsmk
213217

214218
run-playwright-release:
215219
name: Playwright tests against published release
@@ -224,6 +228,7 @@ jobs:
224228
quarto-version: ${{ needs.resolve-release.outputs.version }}
225229
runners: '["ubuntu-latest"]'
226230
buckets: '["integration/playwright-tests.test.ts"]'
231+
label-tag: relpw
227232

228233
run-ff-matrix-release:
229234
name: Feature-format matrix against published release
@@ -237,6 +242,7 @@ jobs:
237242
quarto-install: release
238243
quarto-version: ${{ needs.resolve-release.outputs.version }}
239244
runners: '["ubuntu-latest", "windows-latest"]'
245+
label-tag: relff
240246

241247
# Reuse artifacts from a completed create-release run.
242248
resolve-nightly:
@@ -342,6 +348,7 @@ jobs:
342348
quarto-artifact-run-id: ${{ needs.resolve-nightly.outputs.run-id }}
343349
runners: '["ubuntu-latest"]'
344350
buckets: ${{ github.event.inputs.buckets }} # empty = full run
351+
label-tag: ngtsmk
345352

346353
run-smokes-nightly-windows:
347354
name: Smoke tests against nightly build (windows)
@@ -358,6 +365,7 @@ jobs:
358365
quarto-artifact-run-id: ${{ needs.resolve-nightly.outputs.run-id }}
359366
runners: '["windows-latest"]'
360367
buckets: ${{ github.event.inputs.buckets }} # empty = full run
368+
label-tag: ngtsmk
361369

362370
run-smokes-nightly-mac:
363371
name: Smoke tests against nightly build (macOS)
@@ -374,6 +382,7 @@ jobs:
374382
quarto-artifact-run-id: ${{ needs.resolve-nightly.outputs.run-id }}
375383
runners: '["macos-latest"]'
376384
buckets: ${{ github.event.inputs.buckets }} # empty = full run
385+
label-tag: ngtsmk
377386

378387
# Each caller job accepts one OS-specific artifact.
379388
run-playwright-nightly-linux:
@@ -391,6 +400,7 @@ jobs:
391400
quarto-artifact-run-id: ${{ needs.resolve-nightly.outputs.run-id }}
392401
runners: '["ubuntu-latest"]'
393402
buckets: '["integration/playwright-tests.test.ts"]'
403+
label-tag: ngtpw
394404

395405
run-playwright-nightly-mac:
396406
name: Playwright tests against nightly build (macOS)
@@ -407,6 +417,7 @@ jobs:
407417
quarto-artifact-run-id: ${{ needs.resolve-nightly.outputs.run-id }}
408418
runners: '["macos-latest"]'
409419
buckets: '["integration/playwright-tests.test.ts"]'
420+
label-tag: ngtpw
410421

411422
run-ff-matrix-nightly-linux:
412423
name: Feature-format matrix against nightly build (linux)
@@ -422,6 +433,7 @@ jobs:
422433
quarto-artifact-name: Deb Zip
423434
quarto-artifact-run-id: ${{ needs.resolve-nightly.outputs.run-id }}
424435
runners: '["ubuntu-latest"]'
436+
label-tag: ngtff
425437

426438
run-ff-matrix-nightly-windows:
427439
name: Feature-format matrix against nightly build (windows)
@@ -437,3 +449,4 @@ jobs:
437449
quarto-artifact-name: Windows Zip
438450
quarto-artifact-run-id: ${{ needs.resolve-nightly.outputs.run-id }}
439451
runners: '["windows-latest"]'
452+
label-tag: ngtff

‎.github/workflows/test-smokes-parallel.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,5 @@ jobs:
104104
with:
105105
buckets: ${{ matrix.buckets.files }}
106106
extra-r-packages: ${{ inputs.extra-r-packages }}
107+
# The bucket index distinguishes same-OS matrix jobs.
108+
label-tag: b${{ matrix.buckets.num }}

‎.github/workflows/test-smokes.yml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ on:
4949
required: false
5050
type: string
5151
default: '["ubuntu-latest", "windows-latest"]'
52+
label-tag:
53+
description: "Short alphanumeric tag for distinguishing same-OS jobs in failure summaries"
54+
required: false
55+
type: string
56+
default: ""
5257
workflow_dispatch:
5358
inputs:
5459
buckets:
@@ -91,6 +96,8 @@ jobs:
9196
env:
9297
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/ms-playwright-browsers
9398
RUST_BACKTRACE: ${{ startsWith(matrix.os, 'macos') && 'full' || '' }}
99+
# Distinguishes this call from other jobs on the same OS.
100+
QUARTO_TESTS_GHA_LABEL_TAG: ${{ inputs.label-tag }}
94101
steps:
95102
- name: Checkout Repo
96103
uses: actions/checkout@v6
@@ -500,6 +507,8 @@ jobs:
500507
# Useful as TinyTeX latest release is checked in run-test.sh
501508
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
502509
BUCKETS_JSON: ${{ inputs.buckets }}
510+
# Prevent the harness from duplicating this loop's workflow commands.
511+
QUARTO_TESTS_GHA_ORCHESTRATED: "1"
503512
run: |
504513
haserror=0
505514
failed_tests=()
@@ -549,6 +558,8 @@ jobs:
549558
# Useful as TinyTeX latest release is checked in run-test.sh
550559
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
551560
BUCKETS_JSON: ${{ inputs.buckets }}
561+
# Prevent the harness from duplicating this loop's workflow commands.
562+
QUARTO_TESTS_GHA_ORCHESTRATED: "1"
552563
run: |
553564
$haserror=$false
554565
$failed_tests=@()

‎.github/workflows/update-test-timing.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ jobs:
1313
with:
1414
buckets: ""
1515
time-test: true
16+
label-tag: timing
1617

1718
create-pr:
1819
name: Create PR with updated timing file

‎llm-docs/built-version-testing-architecture.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ Key points:
189189

190190
The feature-format bucket glob (`../dev-docs/feature-format-matrix/qmd-files/**/*.qmd`) is defined only in `test-ff-matrix.yml`.
191191
Built-mode callers use its `workflow_call` trigger, while its existing dev triggers remain.
192-
The workflow forwards install, artifact, ref, runner, and R-package inputs to `test-smokes.yml`, with dev defaults for non-call triggers.
192+
The workflow forwards install, artifact, ref, runner, R-package, and `label-tag` inputs to `test-smokes.yml`, with dev defaults for non-call triggers.
193+
The tag defaults to `ffdev` for standalone dev triggers and distinguishes same-OS jobs in the shared failure summary.
193194

194195
Reusable-workflow concurrency is evaluated in the caller's context. The group therefore includes a suffix based on `inputs.runners` and `github.run_id`, preventing sibling feature-format legs from canceling one another.
195196
Dev triggers use a constant `-dev` suffix.
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
---
2+
main_commit: e5850df75
3+
analyzed_date: 2026-09-15
4+
key_files:
5+
- .github/workflows/test-smokes.yml
6+
- .github/workflows/test-smokes-parallel.yml
7+
- .github/workflows/test-ff-matrix.yml
8+
- src/tools/github.ts
9+
- tests/test.ts
10+
- tests/gha-grouping.ts
11+
- tests/integration/playwright-tests.test.ts
12+
- tests/tools/check-gha-log.ts
13+
---
14+
15+
# GitHub Actions test reporting
16+
17+
Quarto's test harness groups full-run logs by test file and reports failures
18+
through annotations and the GitHub Actions step summary. Bucketed workflows
19+
already group each bucket and emit bucket-level annotations in their shell
20+
loops, so the harness must not emit competing workflow commands.
21+
22+
## Ownership
23+
24+
There are two execution paths:
25+
26+
- **Harness-owned:** `QUARTO_TESTS_GHA_ORCHESTRATED` is unset. The harness
27+
emits groups, annotations, and step-summary entries. This does not imply a
28+
single `deno test` process: `run-tests.sh`'s timing mode (`QUARTO_TEST_TIMING`,
29+
used by `test-smokes.yml`) invokes separate `deno test` processes for test
30+
files, and invokes `smoke-all.test.ts` separately for each smoke-all document.
31+
It leaves the flag unset, so each invocation is independently harness-owned.
32+
- **Orchestrated:** A workflow loop invokes `run-tests` once per file (or
33+
bucket of files) and sets `QUARTO_TESTS_GHA_ORCHESTRATED=1`. The loop owns
34+
groups and bucket-level annotations; the harness emits only step-summary
35+
entries. A direct test runner can still emit its own file-specific
36+
annotations.
37+
38+
`harnessOwnsStep()` in `src/tools/github.ts` implements this distinction.
39+
Workflow commands are otherwise gated by `GITHUB_ACTIONS`.
40+
41+
Step-summary entries are emitted in both paths. A bucketed step can contain many
42+
harness processes, all appending to the same summary file.
43+
44+
## Log grouping
45+
46+
GitHub Actions groups cannot nest. Starting a group while another is open
47+
implicitly closes the first, and the later `::endgroup::` leaves subsequent
48+
output ungrouped. Each execution path must therefore have one group owner.
49+
50+
For harness-owned runs:
51+
52+
1. The first `test()` registration in a file opens the group before Deno prints
53+
that file's reporter header. The file URL is recovered from the V8 call
54+
stack.
55+
2. The test body opens or corrects the group from `context.origin`, which is
56+
authoritative. This is also the fallback when stack parsing fails.
57+
3. A failure closes the group before teardown, annotation output, and Deno's
58+
`FAILED` line.
59+
4. The file's `unload` handler closes a passing file's group before Deno prints
60+
the next file or the final `ERRORS` and `FAILURES` sections.
61+
62+
Deno creates a separate module graph for each test file and fires `unload` once
63+
per file. Module-level grouping and summary state is therefore per file, not per
64+
`deno test` process.
65+
66+
Workflow command markers must start at column 0 and end with a newline. ANSI
67+
color is valid in group bodies but must be removed from annotations and step
68+
summaries.
69+
70+
## Failure reporting
71+
72+
`tests/test.ts` reports each harness failure once, after cleanup and teardown.
73+
When a primary test failure and a teardown or cleanup failure both occur, the
74+
report includes both while preserving the primary exception.
75+
76+
Each failure contributes:
77+
78+
- A summary table row with a short navigation label, file, test name, and
79+
duration.
80+
- A clustered detail block containing the reproduction command and a bounded
81+
output excerpt.
82+
- A `::error` annotation when the harness owns the step and the annotation
83+
budget allows it.
84+
85+
Rows are written immediately. Detail blocks are buffered until the file's
86+
`unload` event because GitHub-flavored Markdown ends a table at the first
87+
non-row line. Failures with the same first three non-empty excerpt lines share
88+
one detail block. The block renders only the first member's excerpt and says
89+
so, because later lines (for `noErrorsOrWarnings`, the stack and any further
90+
messages) can differ; the step log stays the complete record. `pendingClusters` is a module-level map, and Deno gives each
91+
test-file execution its own module graph, so this clustering only merges
92+
failures within one module instance, not across the step.
93+
94+
Summary writes are best-effort and limited to 512 KiB, leaving margin below
95+
GitHub's 1 MiB per-step limit. Full rows degrade to name-only rows when
96+
necessary. If no candidate fits, the harness emits one truncation notice and
97+
leaves the complete failure record in the step log. Individual excerpts are also
98+
byte-limited so one long line cannot consume the summary budget.
99+
100+
## Annotation budget and labels
101+
102+
GitHub displays at most 10 error annotations per step. The harness emits nine
103+
per-test annotations, then one aggregate annotation when the tenth failure is
104+
recorded.
105+
106+
The count must span test-file module instances. `AnnotationBudget` stores it in
107+
a sidecar file derived from `GITHUB_STEP_SUMMARY`, which is unique to the step
108+
and writable by the runner. Tests run serially, so no locking is needed. If the
109+
sidecar cannot be read or written, reporting remains best-effort and falls back
110+
to local counting.
111+
112+
Failure labels combine:
113+
114+
- `RUNNER_OS`, reduced to `L`, `W`, `M`, or `X`;
115+
- `QUARTO_TESTS_GHA_LABEL_TAG`, supplied by the reusable-workflow caller;
116+
- the step-wide failure ordinal.
117+
118+
The run summary concatenates summaries from multiple jobs. Every reusable
119+
workflow call that can share an OS with another call in the same run must
120+
therefore pass a distinct `label-tag`. Per-OS jobs may share a tag because the
121+
OS prefix already distinguishes them.
122+
123+
## Workflow contract
124+
125+
When editing the smoke workflows:
126+
127+
- Set `QUARTO_TESTS_GHA_ORCHESTRATED=1` on loops that emit their own `::group::`
128+
or `::error` commands.
129+
- Pass `label-tag` at every `test-smokes.yml` and `test-ff-matrix.yml` call
130+
site.
131+
- Keep tags short and alphanumeric. `failureLabel()` discards other characters
132+
and keeps at most eight.
133+
- Do not add `deno test --parallel`; interleaved workflow commands would corrupt
134+
grouping. Parallel support would require buffered, atomic output.
135+
136+
## Verification
137+
138+
Unit coverage is in:
139+
140+
- `tests/unit/gha-grouping.test.ts`
141+
- `tests/unit/github-actions-reporting.test.ts`
142+
- `tests/unit/harness-failure-reporting.test.ts`
143+
- `tests/unit/check-gha-log.test.ts`
144+
145+
`tests/tools/check-gha-log.ts` validates captured logs for balanced, non-nested
146+
groups, column-zero markers, and failure sections outside groups. Run it after
147+
changing the grouping logic or upgrading Deno:
148+
149+
```bash
150+
GITHUB_ACTIONS=true ./run-tests.sh <subset> | tee log.txt
151+
deno run --allow-read tests/tools/check-gha-log.ts log.txt
152+
```
153+
154+
Direct `Deno.test` files that bypass the Quarto harness (e.g.
155+
`playwright-tests.test.ts`) receive none of the harness's own grouping,
156+
annotation, or summary logic. They can still use file-specific reporting or be
157+
wrapped by workflow-level reporting. On GitHub Actions,
158+
`tests/integration/playwright-tests.test.ts` emits its own annotations; when a
159+
bucket loop owns the step, it calls `gha.error` directly instead of using the
160+
step-wide annotation budget. Bucket loops in `test-smokes.yml` also wrap each
161+
bucket in their own `::group::`/`::endgroup::` pair and emit a bucket-level
162+
failure annotation regardless of which files the bucket runs.

0 commit comments

Comments
 (0)