Skip to content

ci(tests): stage the browser font set once (no apt in a shard), re-measure weights, 6 → 8 shards - #7102

Merged
rbuergi merged 6 commits into
mainfrom
fix/ci-shard-budget-remeasure
Oct 24, 2026
Merged

rbuergi merged 6 commits into
mainfrom
fix/ci-shard-budget-remeasure

Conversation

@rbuergi

@rbuergi rbuergi commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

What was cut, and why

On 2026-10-23, attempt 1 of three runs turned Consolidate test results red because shards hit the 20-minute job cap:

  • PR run 40011143666, shard 4.
  • PR run 40011313475, shards 2 and 4.
  • main run 40027189653, shard 4.

Nothing was hung. In every cut shard, all projects were still making progress when the cap hit.

  • Memex.Portal.Shared.Test part 3/3 entered a new class every few seconds right up to the cancel.
  • On green runs that part measures 315–343 s.
  • The PluginBundleClient.ReadIndex stacks come from ModuleReloadFaultedTest, which injects that fault on purpose.

Two causes stacked:

  1. The browser step spent its whole 480 s budget on a stalled Ubuntu mirror. It ran playwright-core install --with-deps for its apt half, and on ubuntu-24.04 that half adds only nine font packages. Each cut shard spent exactly 8:00 there; healthy shards spend 20–70 s. The step then gave up on the fonts and carried on anyway. Triage node: rbuergi/Feedback/20261024-browser-step-spends-480s-on-optional-apt-fonts.
  2. Stale weights. Shard 4 printed ~454 s of load but ran 875–894 s.

The fix

No apt in a shard

  • New job browser-fonts ("Stage the browser font set"). It runs in parallel with the build and needs nothing.
    • On a warm cache it does a lookup and nothing else.
    • On a miss it first checks .github/chromium-fonts.list against the pinned Playwright's own install-deps --dry-run. A font package that is neither listed nor already installed fails the job, so the set stays the one deploy/base-images/portal-ai/Dockerfile ships.
    • It then downloads the .debs with a private apt state (own lists and cache, locking off, as the runner user, so no system apt lock is involved). It extracts their font files with dpkg-deb -x and saves them as the chromium-fonts cache. It caches nothing if any part fails.
    • The logic is in .github/scripts/stage-chromium-fonts.sh; key mode gives both jobs one cache-key definition.
  • The shards only restore and unpack.
    • "Restore the browser font set" has timeout-minutes: 2. That is deliberately longer than its 1 min download abort, so a stalled download ends as a reported cache miss and never as the step cap failing the shard (review thread).
    • "Provision the headless browser" has timeout-minutes: 5. It downloads the browser from the CDN (150 s budget, 90 s per attempt; the 115 MiB download takes about 2 s), installs the font set (40 s bound) and runs the smoke print.
    • Gone from the shards: --with-deps, the apt lock config, wait-for-apt.sh and the orphaned-apt reap.
    • A missing cache is a ::warning:: and the smoke print still decides. That is the same contract a failed --with-deps had.

Guards, re-modelled

  • ShardWeightHeadroomGuard.TheIdealShardLoadFitsTheShardJobCap.
    • It takes the browser's cost as the two steps' step-level timeout-minutes, 420 s, which GitHub enforces end to end.
    • It sums the load over every project the scheduler discovers, with unlisted ones at DEFAULT_WEIGHT.
    • Eight shards pass (513 s ≤ 567 s). Seven fail (586 s).
  • New: TheProvisionStepsInnerBoundsFitItsCap. The download budget, kill grace, font install and smoke print add up to 285 s, inside the step's 300 s. A stall therefore fails with its own message before the step cap fires.
  • New: TheFontRestoreMissesBeforeItsCap. It requires the restore step's cap to exceed the download abort by at least 30 s.
  • New: BrowserProvisioningGateGuard.TheTestShardsNeverReachAPackageMirror. It forbids --with-deps, install-deps and apt-get in the test job. A repo with no --with-deps provisioner is no longer a vacuous failure.

Weights and splits

  • Each weight is the max over three green full main runs (40014951907, 40021939021, 40027189653).
  • I chose part counts by replaying the trx per-class durations through the workflow's sorted every-Nth class split. That replay reproduces the measured parts exactly.
Project Parts Predicted part seconds
Memex.Portal.Shared.Test 3→4 240/179/243/254
MeshWeaver.Graph.Test 3→4 210/221/252/172
MeshWeaver.Messaging.Hub.Test 2→3 136/223/154 (part 2/2 was 409–416 s, 87% of the per-project cap)
MeshWeaver.Hosting.Test 3→2 223/226

MeshWeaver.Compiler.Pipeline.Test keeps #7087's split.

CI_SHARD_TOTAL 6→8

Eight is what the guard allows. The matrix has 8 shards and 8 upload steps, and both SHARD_TOTAL settings read ${{ env.CI_SHARD_TOTAL }}. The job cap is unchanged.

Evidence

  • The staging and install, in ubuntu:24.04 as a non-root user with sudo.
    • stage fetched 9 packages (21 MB, 192 font files).
    • install took fontconfig from 13 to 47 faces (WenQuanYi Zen Hei, IPAGothic, FreeSerif, Loma, Unifont).
    • Playwright 1.62.1's dry run asks for exactly the nine listed packages.
    • Drift negative control: dropping fonts-wqy-zenhei from the list makes stage fail with "installs font package(s) fonts-wqy-zenhei on this release, which drifted.list does not stage".
  • Guard negative controls, run locally against a mutated workflow and then restored:
    • CI_SHARD_TOTAL: 7 → red: "Expected 586.43 to be less than or equal to 567 … 4105 s … at CI_SHARD_TOTAL=7".
    • (previous round) Provision step capped at 5 min with the earlier 345 s of inner bounds → red: "bounds … add up to 345 s, more than its 300 s".
    • Restore step timeout-minutes: 1 → red: "Expected 0 to be greater than or equal to 30".
    • install --with-deps put back into the shard → red, naming the line.
  • Local gates.
    • MeshWeaver.Documentation.Test builds with -c Release -warnaserror (0 warnings, 0 errors), and its full run passed 917 of 917.
    • actionlint (with shellcheck) is clean, shellcheck on the script is clean, check-workflow-shell.py reports 0 live findings, and check-workflow-timeouts.py reports 0 violations.

What this does NOT establish

  • The new job has not run on a GitHub runner yet. This PR's own run is the first: a cache miss, then the stage. I tested the same script only on arm64 ubuntu:24.04 against ports.ubuntu.com, not on the amd64 runner image.
  • Font fidelity is "same files", not "same install". No package postinst runs: no update-alternatives for the generic Japanese-gothic name and no mkfontdir. Chromium resolves faces through fontconfig, which indexes the files directly, but I did not compare a printed PDF against one from a real --with-deps install.
  • Shard times are estimates. The heaviest of the eight shards should run about 545 s, from today's class list. Adding a class shifts the every-Nth parity.
  • Cache-key edge case. The browser-fonts job checks out the event's ref. A shard tests the fresh merge with main. If main changes the font list mid-flight, the keys differ, and the shards warn and print without the staged fonts for that run.

Refs #7087.

Pairs-with: none — CI configuration, CI scripts, test-tree guards and a doc page only; no public surface changes.

🤖 Generated with Claude Code

…lled browser step no longer cuts a shard

Shards were cancelled at the 20-minute job cap on PR runs 40011143666 and
40011313475 and on main run 40027189653 (attempt 1 each), with every
project still progressing. Memex.Portal.Shared.Test was not hung: part 3/3
entered a new class every few seconds until the cut and measures 315-343 s
on green runs.

Two causes stacked:
- the browser step spent its whole 480 s budget on a stalled apt mirror
  (normally 20-70 s);
- shard 4 owed ~880 s of tests against a printed ~454 s (stale weights),
  and the re-measured suite is ~3,900 s, i.e. ~650 s per shard at six
  even when balanced, while the cap leaves ~570 s after a stalled step.

Changes:
- weights re-measured over three green full main runs (2026-10-23);
- splits chosen from trx per-class durations through the workflow's
  sorted every-Nth split (which reproduces the measured parts exactly):
  Portal.Shared 3->4, Graph 3->4, Messaging.Hub 2->3 (part 2/2 was
  409-416 s, 87% of the per-project cap, invisible to the total/N
  guard), Hosting 3->2 (now 223/226);
- CI_SHARD_TOTAL 6->8, matrix and upload steps; SHARD_TOTAL now reads
  CI_SHARD_TOTAL;
- new guard ShardWeightHeadroomGuard.TheIdealShardLoadFitsTheShardJobCap
  reads the cap, the browser budget and the shard count from the workflow.

Compiler.Pipeline's line is left to #7087.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 24, 2026 06:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new guard omits scheduled fallback-weighted projects and understates browser overhead, so it can pass without the intended headroom.

2 open issues
What changed in this PR

This PR rebalances MeshWeaver’s CI test workload to reduce shard cancellations without increasing job timeouts.

Changes:

  • Refreshes project weights and split counts, and increases parallel shards from six to eight.
  • Adds a shard-budget guard and parser self-test.
  • Documents the timing evidence and remaining estimation limits; Compiler.Pipeline splitting remains assigned to #7087.
File Description
test/​MeshWeaver.Documentation.Test/​ShardWeightHeadroomGuard.cs Adds ideal-load budget validation and parser checks.
src/​MeshWeaver.Documentation/​Data/​Architecture/​ReadingCiSignals.md Documents shard sizing and timing limitations.
.github/​workflows/​dotnet-test.yml Expands the matrix and artifact uploads to eight shards.
.github/​scripts/​shard-assign.sh Updates measured weights, project splits, and supporting comments.

Review effort: Balanced

Comment thread test/MeshWeaver.Documentation.Test/ShardWeightHeadroomGuard.cs Outdated
Comment thread test/MeshWeaver.Documentation.Test/ShardWeightHeadroomGuard.cs Outdated
…wser step really do; 8 -> 10 shards

Review on #7102:
- the guard summed the weight table, so unlisted suites (scheduled at
  DEFAULT_WEIGHT) counted nothing and stale rows counted; it now sums every
  test/**/*.csproj shard-assign.sh discovers, at its table weight or the
  default;
- it reserved BROWSER_STEP_BUDGET only, while the last attempt's
  --kill-after grace, the orphaned-apt reap and the smoke print's timeout run
  after that deadline; it now reads all of them (588 s end to end).

Measured that way the suite is 3,832 s against ~462 s of test room per
shard, so eight shards fail the guard (479 > 416). Ten pass (383; ~410 once
#7087 splits Compiler.Pipeline).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 24, 2026 06:50
@rbuergi rbuergi changed the title ci(tests): re-measure shard weights, re-split, 6 → 8 shards — a stalled browser step no longer cuts a shard ci(tests): re-measure shard weights, re-split, 6 → 10 shards — a stalled browser step no longer cuts a shard Oct 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The new shard timings remain estimates, and documented peak loads can still exceed the available test budget, requiring human acceptance of the remaining timeout risk.

1 open issue
2 resolved since last review

Review effort: Balanced

Comment thread test/MeshWeaver.Documentation.Test/ShardWeightHeadroomGuard.cs Outdated
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

    31 files  +2      31 suites  +2   1h 7m 36s ⏱️ - 1m 25s
14 384 tests +5  14 187 ✅ +5  197 💤 ±0  0 ❌ ±0 
14 414 runs  +5  14 217 ✅ +5  197 💤 ±0  0 ❌ ±0 

Results for commit dc3e3d0. ± Comparison against base commit b85e51b.

This pull request removes 44 and adds 23 tests. Note that renamed tests count towards both.

   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---)
   --- End of inner exception stack trace ---, expected: True)
   --- End of inner exception stack trace ---, isDenial: True)
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
 ---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
 ---> (Inner Exception #1) System.InvalidOperationException: boom<---
 ---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
…
Memex.Portal.Shared.Test.InstanceIdRulesMatchTheRegistryTest ‑ TheSetupHostAgreesWithTheRegistry(candidate: "521d9fa5-5198-4e57-a1ef-cbab7ca2aff6")
Memex.Portal.Shared.Test.PublishStorageFaultIsNotAConflictTest ‑ AStorageFault_IsA500_NamingTheError(fault: System.AggregateException: One or more errors occurred. (Access to the path '/data/modules/activation.d/MeshWeaver.Markdown.Export.json' is denied.)
 ---> System.UnauthorizedAccessException: Access to the path '/data/modules/activation.d/MeshWeaver.Markdown.Export.json' is denied.
   --- End of inner exception stack trace ---)
Memex.Portal.Shared.Test.PublishStorageFaultIsNotAConflictTest ‑ AStorageFault_IsA500_NamingTheError(fault: System.InvalidOperationException: landing faulted
 ---> System.UnauthorizedAccessException: Access to the path '/data/modules/activation.d/MeshWeaver.Markdown.Export.json' is denied.
   --- End of inner exception stack trace ---)
Memex.Portal.Shared.Test.SessionDenialIsAnAnswerTest ‑ OnlyAVerdictReadsAsADenial(shape: "the same verdict nested, as a late denial dispatch"···, failure: System.InvalidOperationException: write failed
 ---> System.UnauthorizedAccessException: Access denied
   --- End of inner exception stack trace ---, isDenial: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall as an aggregate's FIRST member", fault: System.AggregateException: One or more errors occurred. (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall as an aggregate's SECOND member", fault: System.AggregateException: One or more errors occurred. (The operation has timed out.) (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> System.TimeoutException: The operation has timed out.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall nested in an aggregate inside a wrapper", fault: System.InvalidOperationException: outer
 ---> System.AggregateException: One or more errors occurred. (Value does not fall within the expected range.) (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> System.ArgumentException: Value does not fall within the expected range.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---

   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall wrapped as InnerException", fault: System.InvalidOperationException: outer
 ---> MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "an aggregate of unrelated faults", fault: System.AggregateException: One or more errors occurred. (The operation has timed out.) (Value does not fall within the expected range.)
 ---> System.TimeoutException: The operation has timed out.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
, expected: False)
MeshWeaver.Documentation.Test.BrowserProvisioningGateGuard ‑ TheTestShardsNeverReachAPackageMirror
…

♻️ This comment has been updated with latest results.

rbuergi and others added 2 commits October 24, 2026 10:24
…e; no apt in a shard; 10 -> 8 shards

The shards' browser step ran `playwright-core install --with-deps` for its
apt half, which on ubuntu-24.04 adds only nine font packages. A stalled
Ubuntu mirror spent the step's whole 480 s on them, and that cut shards on
2026-10-23 (PR runs 40011143666 and 40011313475, main run 40027189653).

- New `browser-fonts` job ("Stage the browser font set"), parallel with the
  build. On a cache miss it checks .github/chromium-fonts.list against the
  pinned Playwright's `install-deps --dry-run` and downloads the .debs with a
  private apt state (no system lock, no root). It extracts their font files
  without installing them and saves the `chromium-fonts` cache. On a hit it is
  a lookup.
- Shards restore that cache (step cap 1 min) and unpack it. The provision
  step (step cap 6 min) now only downloads the browser (180 s budget, 90 s per
  attempt), installs the fonts and runs the smoke print. No apt, no lock, no
  reap. A missing cache is a warning; the smoke print still decides.
- ShardWeightHeadroomGuard reads the browser's cost as the two steps'
  `timeout-minutes` (420 s) and checks that the provision step's inner bounds
  fit its cap. Eight shards pass it (513 <= 567 s); seven fail (586).
- BrowserProvisioningGateGuard: TheTestShardsNeverReachAPackageMirror forbids
  `--with-deps` / `install-deps` / `apt-get` in the test job. A repo with no
  `--with-deps` provisioner is no longer a vacuous failure.
- ToImmutableDictionary for the weight lookup (review on #7102).

Verified locally in ubuntu:24.04 as a non-root user: stage fetched 9 packages
(21 MB, 192 font files), install took fontconfig from 13 to 47 faces, and a
list missing fonts-wqy-zenhei made stage fail naming it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 24, 2026 09:16
@rbuergi rbuergi changed the title ci(tests): re-measure shard weights, re-split, 6 → 10 shards — a stalled browser step no longer cuts a shard ci(tests): stage the browser font set once (no apt in a shard), re-measure weights, 6 → 8 shards Oct 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The font-cache deadline can skip shard tests, and the staging script violates the required shell gate.

2 open issues
1 resolved since last review

Review effort: Balanced

Comment thread .github/scripts/stage-chromium-fonts.sh Outdated
Comment thread .github/workflows/dotnet-test.yml Outdated
…ad of swallowing it

check-workflow-shell flagged `asked=$(… 2>/dev/null | … || :)`: a failed or
stalled `install-deps --dry-run` became an empty list with no trace. The dry
run's output now goes to a file with stderr kept. Exit 0/1 is read (1 is how
playwright-core reports missing deps). 124/137 is a named stall, and anything
else is a named failure, each printing the dry run's output. An empty font
list still refuses the stage, again with that output.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 24, 2026 09:49
… restore step's cap

Review on #7102: the restore step's 1 min cap equalled its 1 min
SEGMENT_DOWNLOAD_TIMEOUT_MINS, whose clock starts after the cache lookup. So
the cap could fire first and fail the shard over an optional input, before
the browser step's warning and the tests. The restore cap is now 2 min, and
the new TheFontRestoreMissesBeforeItsCap guard requires at least 30 s of room
past the abort.

The provision step goes 6 -> 5 min (download budget 180 -> 150 s, font
install 70 -> 40 s; inner bounds 285 of 300 s), so the browser's end-to-end
bound stays 420 s and eight shards still pass the shard budget (513 <= 567 s).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Competing cache-download and step deadlines can fail shards before the intended warning-and-smoke fallback runs.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Leave timeout headroom for cache-miss fallback

.github/​workflows/​dotnet-test.yml:1071

The cache segment timeout and the entire restore step cap are both 60 seconds, but the segment timer starts after cache lookup and download setup. A stalled segment can therefore hit the step cap before the action returns a cache miss, failing the shard and skipping browser provisioning and tests instead of reaching the warning-and-smoke fallback. Give the internal timeout enough headroom inside the step cap for that fallback to run, and re-check the shard-budget accounting if the outer cap changes.

2 resolved since last review

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 24, 2026 09:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The supplied evidence does not yet verify cold-cache font staging on the amd64 GitHub runner used by this workflow.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Low severity Use an immutable collection instead of mutable ToList()

test/​MeshWeaver.Documentation.Test/​BrowserProvisioningGateGuard.cs:131

This new ToList() introduces a mutable collection despite the immutable-collections requirement in AGENTS.md (“Collections Policy”). The guard only enumerates and counts these lines, so use an immutable list without changing its behavior.

Review effort: Balanced

@rbuergi
rbuergi merged commit 1db0b1e into main Oct 24, 2026
61 of 62 checks passed
@rbuergi
rbuergi deleted the fix/ci-shard-budget-remeasure branch October 24, 2026 14:14
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.

2 participants