From ec9c1f4fe78fff5eebe6f043ad28f93e4031bd38 Mon Sep 17 00:00:00 2001 From: t Date: Sat, 5 Sep 2026 10:36:50 +0900 Subject: [PATCH] ci(windows): six shards instead of four, ceiling unchanged Run 33934756997 cancelled a GREEN windows 3/4 at 25m12s, mid-file with every prior case passing - the same truncation the ceiling's comment records for its 15-minute predecessor. Across five runs of this branch, completed Windows shards took 17-25 minutes; 3/4 never finished under 21. The bound itself is right (a wedged shard must still die); the work per shard grew into it. Cut the work instead of raising the bound: six shards put each leg at roughly two-thirds of the four-shard wall time, back inside the margin 25 was chosen to give. The matrix, the --shard divisor, and the job name move together. ci-workflows.test.ts pinned the Windows matrix to Linux's; that invariant was about tiling the suite exactly, not about the two platforms sharing a count. It now pins Windows to its own divisor and to a contiguous 1..N matrix, and the Linux divisor check reads the TEST_SHARD env line it was always meant to observe (it had been matching the Windows --shard literal by coincidence). --- .github/workflows/ci.yml | 18 ++++++++++++------ tests/ci-workflows/ci-workflows.test.ts | 20 +++++++++++++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d15e696000..568a3d29d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -656,7 +656,7 @@ jobs: # now means Linux + macOS + the gates; Windows re-enters the gate when the # tracked failures are fixed, not before. platform-windows: - name: windows ${{ matrix.shard }}/4 + name: windows ${{ matrix.shard }}/6 needs: select-windows-runner if: >- github.event_name == 'workflow_dispatch' && (github.event.inputs.lane == '' || github.event.inputs.lane == 'all') @@ -673,11 +673,17 @@ jobs: # margin. 25 leaves the outer bound in place — a wedged shard still dies — # while making a completed shard the normal outcome. The crash-retry below can # double a shard's work, and this ceiling has to cover that second attempt too. + # + # Four shards then grew into the ceiling: across five runs of one branch, completed + # shards took 17-25 minutes and run 33934756997 cancelled a green 3/4 at 25m12s — + # the same truncation as above. The bound is kept; the work per shard is cut instead. + # Six shards put each leg at roughly two-thirds of the four-shard wall time, back + # inside the margin 25 was chosen to provide. timeout-minutes: 25 strategy: fail-fast: false matrix: - shard: [1, 2, 3, 4] + shard: [1, 2, 3, 4, 5, 6] steps: - name: Show selected runner shell: bash @@ -745,18 +751,18 @@ jobs: set -uo pipefail suite_log="$(mktemp -t ocx-windows-suite.XXXXXX)" for attempt in 1 2; do - bun test --isolate --timeout 60000 tests --shard=${{ matrix.shard }}/4 2>&1 | tee "$suite_log" + bun test --isolate --timeout 60000 tests --shard=${{ matrix.shard }}/6 2>&1 | tee "$suite_log" suite_status="${PIPESTATUS[0]}" if [ "$suite_status" -eq 0 ]; then exit 0 fi if ! grep -Eqi 'oh no: Bun has crashed|Internal assertion failure|Segmentation fault at address|Illegal instruction|Bus error|Aborted \(core dumped\)' "$suite_log"; then - echo "::error::Windows shard ${{ matrix.shard }}/4 failed on attempt ${attempt} (exit ${suite_status}); assertion failures are not retried." + echo "::error::Windows shard ${{ matrix.shard }}/6 failed on attempt ${attempt} (exit ${suite_status}); assertion failures are not retried." exit "$suite_status" fi - echo "::warning::Bun runtime crash in Windows shard ${{ matrix.shard }}/4 (exit ${suite_status}, attempt ${attempt})." + echo "::warning::Bun runtime crash in Windows shard ${{ matrix.shard }}/6 (exit ${suite_status}, attempt ${attempt})." done - echo "::error::Bun runtime crash repeated on Windows shard ${{ matrix.shard }}/4; failing after one retry." + echo "::error::Bun runtime crash repeated on Windows shard ${{ matrix.shard }}/6; failing after one retry." exit 1 - name: CLI help smoke diff --git a/tests/ci-workflows/ci-workflows.test.ts b/tests/ci-workflows/ci-workflows.test.ts index 7418e89a27..436b4d1308 100644 --- a/tests/ci-workflows/ci-workflows.test.ts +++ b/tests/ci-workflows/ci-workflows.test.ts @@ -171,7 +171,10 @@ describe("GitHub Actions hardening", () => { const linuxShards = (ci.jobs?.test as { strategy?: { matrix?: { shard?: number[] } } }) ?.strategy?.matrix?.shard ?? []; expect(linuxShards).toEqual([1, 2, 3, 4]); - expect(workflow).toContain(`--shard=\${{ matrix.shard }}/${linuxShards.length}`); + // The Linux shards pass their divisor through TEST_SHARD to the batch runner. Before + // Windows sharded differently, this assertion matched the Windows step's --shard + // literal by coincidence; pin the Linux env line so it observes the Linux job. + expect(workflow).toContain(`TEST_SHARD: \${{ matrix.shard }}/${linuxShards.length}`); // Every job that runs tests/ must fetch tags, because one of those tests reads // them. tests/ci-workflows/release-version-line.test.ts compares package.json against the @@ -187,13 +190,20 @@ describe("GitHub Actions hardening", () => { expect(`${jobName}:${String(checkout?.with?.["fetch-tags"])}`).toBe(`${jobName}:true`); } - // Windows uses the same shard matrix after the single-leg isolate budget was - // replaced. Keep the two matrices equal so a future edit cannot reintroduce - // a partial Windows suite while Linux stays fully tiled. + // Windows shards more finely than Linux: the same suite takes 17-25 minutes per + // quarter on windows-latest, which is the leg's own 25-minute ceiling (run + // 33934756997 cancelled a green 3/4 at 25m12s). The invariant that matters is the + // one above — the matrix and the divisor tile the suite exactly — so pin the + // Windows matrix to its own divisor rather than to Linux's, and pin it to be + // contiguous from 1 so a dropped entry cannot leave a slice of the suite unrun. const windowsShards = (ci.jobs?.["platform-windows"] as { strategy?: { matrix?: { shard?: number[] } }; })?.strategy?.matrix?.shard ?? []; - expect(windowsShards).toEqual(linuxShards); + expect(windowsShards).toEqual([1, 2, 3, 4, 5, 6]); + expect(windowsShards).toEqual(windowsShards.map((_, i) => i + 1)); + const windowsSteps = (ci.jobs?.["platform-windows"] as { steps?: Array<{ run?: string }> })?.steps ?? []; + expect(windowsSteps.some(step => step.run?.includes(`--shard=\${{ matrix.shard }}/${windowsShards.length}`))).toBe(true); + expect(ci.jobs?.["platform-windows"]?.name).toBe(`windows \${{ matrix.shard }}/${windowsShards.length}`); // The aggregate gate is the check a human trusts. Three ways to break it // silently: drop `if: always()` so it skips (and a skipped job reports