Skip to content

Group CI logs and surface failures on the default (non-bucketed) test run#14715

Open
cderv wants to merge 12 commits into
test/smoke-tests-built-versionfrom
test/gha-log-grouping
Open

Group CI logs and surface failures on the default (non-bucketed) test run#14715
cderv wants to merge 12 commits into
test/smoke-tests-built-versionfrom
test/gha-log-grouping

Conversation

@cderv

@cderv cderv commented Jul 21, 2026

Copy link
Copy Markdown
Member

Problem

The default (non-bucketed) test path runs one giant deno test over every discovered file and emits no workflow commands, so the CI log is thousands of flat lines — finding a failure means scrolling or text search. This is the path the smoke leg of test-smokes-built.yml runs by default (empty buckets); the playwright and ff-matrix legs pass a non-empty buckets, so they already go through the per-file ::group:: bucket loop added by #13787 and #13807. Those PRs grouped only the bucket loops (one file per run-tests.sh invocation) — the default path was left flat, and no per-test ::error annotation or step-summary table existed on either path.

What it does

Per-test-file ::group:: markers plus per-test ::error annotations on the default path, and GITHUB_STEP_SUMMARY failure tables in every mode:

  • Annotations are capped at 9 per-test errors + 1 inline aggregate per step, coordinated across Deno's per-file module instances via a sidecar counter file (a module-level counter is not a per-step budget, since Deno instantiates each test file's module graph separately).
  • Step summary tables are the complete failure record, size-budgeted against the ~1 MiB silent-drop bug (self-truncate near 512 KiB, coordinated cross-process via the summary file size).
  • All group/annotation emission is gated on the harness owning the step: the QUARTO_TESTS_GHA_ORCHESTRATED env contract (set by the bucket loops) keeps the GHA TESTS - Improve logging to group in the right place  #13807 bucket-log format byte-identical and doubles as a no-revert kill switch. Everything else gates on GITHUB_ACTIONS, so local output is byte-identical.
  • Includes tests/tools/check-gha-log.ts, a mechanical invariant checker to re-run on Deno version bumps (reporter framing is version-sensitive).

Design of record (constraints, spike results, invariants, verification plan) lives in the branch at dev-docs/ci-test-log-grouping-design.md.

Evidence

Fork trial matrix with seeded failures, Linux + Windows:

  • Default path — run 29826031431: grouped log; exactly 9 per-test + 1 aggregate annotations from 12 failing files; checker green on both OSes' raw logs.
  • Orchestrated bucket — run 29767185290: bucket format unchanged, zero harness annotations.
  • 12-failing-file bucket — run 29767190539: GitHub silently drops annotations past 10/step (observed); the step summary stays complete. This is the motivating case.

Two real bugs the trial caught and fixed before this PR: Deno's per-file module instances invalidating a per-process budget, and explicit undefined args triggering default parameters so unit tests touched the real CI counter/summary/env (null-sentinel fix).

Notes

Note

Stacked on #14706; GitHub retargets this to main when it merges.

Related but independent finding from the trial: #14711 (cold-cache renv deadlock).

A built-mode (source=build) confirmation run on this branch is linked in a follow-up comment.

AI-assisted PR
  • AI tool used: Claude Code
  • Codebase grounding: local clone, DeepWiki MCP
  • Human review: I have reviewed, tested, and verified the AI-generated content before submitting.

claude and others added 11 commits July 20, 2026 19:09
Plans GitHub Actions log grouping for the default (non-bucketed) full test
run, plus ::error annotations and GITHUB_STEP_SUMMARY failure tables.
Documents runner constraints (no nested groups, annotation caps, summary
size limits), spike-verified marker placement in Deno's test output
framing, phased implementation, invariants, and a verification plan.
No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFvZcK71eJ3gmmDT6RcFDd
…ping design

The gate itself is applied on the built-version testing branch; this only
retains the design doc's pointer to the follow-up plan that documents how
the gate is eventually removed.
Review findings addressed:
- Annotation cap was defined per process but GitHub's limit is per step;
  in bucket mode many run-tests.sh processes share one step. The harness
  now emits annotations only when it owns the step (default path), gated
  by QUARTO_TESTS_GHA_ORCHESTRATED (replacing QUARTO_TESTS_NO_LOG_GROUP,
  which also covers grouping); bucket mode keeps the YAML per-file errors,
  and the step summary is documented as the complete record in all modes.
- Step-summary size budget is now stat-based on the shared summary file so
  it holds across processes within a step.
- The lazy-vs-eager group closure choice is now explicit (lazy chosen),
  invariant 3 is scoped to harness-registered tests, and the eager
  fallback is defined next to the policy instead of as an aside.
- The julia-engine env-strip-list sync gains a mechanical drift check: a
  quarto-cli unit test comparing kStripEnvVars with the subtree helper
  after the pull, added to the follow-up procedure and acceptance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFvZcK71eJ3gmmDT6RcFDd
The workflow-command toolkit in github.ts (error/escapeData/escapeProperty,
group markers) was dead code for the test harness. Phase 1 of the CI
test-log design (dev-docs/ci-test-log-grouping-design.md) needs a few more
primitives so the harness can surface test failures as annotations and a
step-summary table:

- stripAnsi: annotation and step-summary text do not render ANSI, so color
  codes from captured output must be removed first.
- harnessOwnsStep: the harness emits per-test annotations only when no outer
  orchestrator (the bucket-loop YAML) has claimed the step; parameterized so
  unit tests need not mutate process-global env.
- AnnotationBudget: GitHub silently drops past 10 errors per step, so cap at
  9 per-test annotations plus one aggregate.
- stepSummary/stepSummarySize + a 512 KiB budget: content near the 1 MiB
  per-step limit can be silently dropped, and bucket mode has many processes
  appending to one file, so the file size is the cross-process coordinator.
- summary table/detail formatters: <pre> with HTML-escaped bodies so hostile
  test output cannot break out of the markdown table or a code fence.

All gate on GITHUB_ACTIONS or take an injectable path, so nothing changes off
CI. Unit tests cover the cap/aggregate counter, the ownership gate, ANSI
stripping, the stat-based budget including the degrade path, and escaping of
hostile test names.
Phase 1 of dev-docs/ci-test-log-grouping-design.md. On the default
(non-bucketed) full run, one deno test process produces thousands of flat log
lines with no ::error annotations and no step summary, so a failure is only
findable by scrolling. The harness now, in its existing failure path and only
when GITHUB_ACTIONS=true:

- emits one ::error annotation per failed test (file + title + repro command
  and a 20-line ANSI-stripped excerpt), pointing at the smoke-all document
  when the failure is a doc test (the file a developer opens) rather than the
  harness .test.ts file;
- appends a step-summary table row per failure — the complete failure record,
  emitted in every mode — with an expandable output block flushed after the
  rows (GFM ends a table at the first non-row line, so details cannot sit
  between rows) and degrading to name-only once the shared summary file
  crosses the size budget.

Since one process is the whole step on this path, the module-level annotation
budget is exactly a per-step budget. Annotations are gated on harness step
ownership: the two bucket-loop steps in test-smokes.yml already emit their own
per-file ::group and ::error, so they set QUARTO_TESTS_GHA_ORCHESTRATED and
the harness suppresses annotations there (summary rows still emit). No
::group/::endgroup is emitted by the harness — that is Phase 2. Local output
is byte-identical.
GFM ends a table at the first non-row line, so per-failure detail blocks
cannot interleave with table rows; the implementation streams rows into
one contiguous table and flushes buffered detail blocks plus the
aggregate ::error at an unload listener. Update Phase 1 to describe the
implemented shape and its accepted hard-crash caveat (rows are written
at failure time, so the table stays the complete record).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFvZcK71eJ3gmmDT6RcFDd
The bucketed CI runs got navigable logs by wrapping each file in a
::group:: at the workflow level (#13787/#13807), but the default
non-bucketed run is one giant deno test whose thousands of flat lines
have no structure. Have the harness itself open a group per test file so
that path collapses the same per-test noise the bucket loop does.

Grouping lives in a small state machine (gha-grouping.ts) so the
single-open-group invariant — the GitHub runner cannot nest groups — is
enforced and unit-testable in isolation. The harness opens/transitions
the group at the start of each test's fn, closes it on the failure path
before fail() throws (so the FAILED line and ::error land outside the
collapsed group), and closes any open group at unload before the Phase 1
aggregate error and Deno's terminal ERRORS/FAILURES sections.

Every emission is gated on harnessOwnsStep() (on CI, no outer
orchestrator), the same ownership switch Phase 1 uses: local runs and
orchestrated bucket runs stay byte-identical, so the #13807 bucket log
format is untouched. Closure is lazy per file; direct Deno.test files
are a documented exemption. Design and invariants:
dev-docs/ci-test-log-grouping-design.md.
The per-file grouping depends on how Deno's test reporter frames output
(markers replayed at column 0, unload flushing before the terminal
sections) — behavior that can shift across Deno upgrades. This dev-only
script reads a captured run log and mechanically asserts the invariants:
no nested groups, markers at column 0, and no harness FAILED line or
ERRORS/FAILURES header trapped inside a collapsed group. Run it after
any Deno bump to catch reporter drift before it ships a broken log.
Status header now reflects the implemented state (Phase 3 optional, fork
trial matrix still pending). The checker section documents that
run-tests.ps1 must be invoked via pwsh -File with shell-level redirection:
pwsh -Command with *> feeds the redirect into the script's arg parsing,
blanking the file arguments and silently running the full suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFvZcK71eJ3gmmDT6RcFDd
… annotation budget

Review #1991: initDenoDom/prereq/chdir/setup ran after the per-file group
opened but outside the guarded failure path, and teardown could throw
after it — any of those failures left the FAILED result line inside a
collapsed group. The whole test body is now wrapped in an outer catch
that closes the group (idempotently) before rethrowing.

Review #1986 (confirmed empirically on the pinned Deno 2.7.14): Deno
instantiates each test file's module graph separately — module state
resets per file and unload fires once per file — so the module-level
annotation counter was a per-FILE budget, not per-step, and failures
spanning >10 files would silently lose annotations. The budget now
coordinates through a sidecar counter file derived from
GITHUB_STEP_SUMMARY (unique per step; no locking needed as files run
sequentially without --parallel), and the aggregate ::error is emitted
inline by the failure that crosses the cap (a per-file unload cannot
know it is last). The same finding corrects the spike interpretation:
the inter-file ::endgroup:: was the previous file's own unload, not a
cross-file transition — grouping output is unchanged, and foreign
(non-harness) test files now provably run with no group open. Design
doc updated throughout; unit tests cover the new decision API and
cross-instance coordination through a shared counter file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFvZcK71eJ3gmmDT6RcFDd
…tinel)

The trial run (cderv/quarto-cli run 29767179626) failed 3 reporting unit
tests that had passed locally: passing an explicit undefined for the
injectable counter/summary/orchestrated parameters triggers the JS
default parameter, which on CI resolves to the REAL per-step sidecar
counter (already exhausted by earlier failures), the REAL
GITHUB_STEP_SUMMARY file (the test appended to the run's summary), and
the REAL QUARTO_TESTS_GHA_ORCHESTRATED variable (set in bucket steps, so
the gate tests would flip once these files run inside a CI bucket).

AnnotationBudget, stepSummary, stepSummarySize, and harnessOwnsStep now
take an explicit null sentinel meaning 'no file / treat as unset',
resolved with === undefined checks so null survives; unit tests pass
null. Everything else the trial exercised behaved as designed: 9+1
annotations from the step-wide budget across 12 failing files, zero
harness annotations in orchestrated bucket runs, grouping invariants
checker green on real CI logs from both OSes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFvZcK71eJ3gmmDT6RcFDd
@posit-snyk-bot

posit-snyk-bot commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

The doc claimed every test-smokes-built.yml leg reaches the flat default path; only the smoke leg does (empty buckets). The playwright and ff-matrix legs pass non-empty buckets and take the already-grouped orchestrated loop, where the harness stays silent by design. Also flip the stale trial-has-not-run status, correct the file-origin mechanism to context.origin (not a captured stack), and fix two line/step-name citations (run-tests.sh deno test invocation, the Restore R packages grouping step).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants