From 51ed54d845ffa3b4915c796d9192e861ae9aaad5 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Fri, 19 Jun 2026 11:55:44 +0800 Subject: [PATCH] ci: split Windows server tools advisory shard --- .github/workflows/windows-advisory.yml | 55 ++++++++++-- .../opencode/test/github/ci-workflow.test.ts | 85 ++++++++++++++++--- 2 files changed, 119 insertions(+), 21 deletions(-) diff --git a/.github/workflows/windows-advisory.yml b/.github/workflows/windows-advisory.yml index 0934a82c9..8081f7db0 100644 --- a/.github/workflows/windows-advisory.yml +++ b/.github/workflows/windows-advisory.yml @@ -114,7 +114,7 @@ jobs: if: needs.changes.outputs.docs_only != 'true' runs-on: windows-latest # Windows unit jobs are advisory, package-scoped or shard-scoped. Timeout - # budgets are explicit per matrix child; opencode has five parallel + # budgets are explicit per matrix child; opencode has seven parallel # shards, so a full stall can still consume multiple Windows runner slots. timeout-minutes: ${{ matrix.timeout_minutes }} strategy: @@ -169,26 +169,50 @@ jobs: test/settings test/settings.test.ts report_path: packages/opencode/.artifacts/unit/junit-windows-config-project.xml - # Server tools carries the slow HTTP/server route suite and adjacent - # server support tests, so it gets a little more wall-clock budget. + # Server tools carries the slow HTTP/server route suite, so it gets + # the largest wall-clock budget plus a process-level attempt timeout. - package: opencode-server-tools uses_turbo: false - timeout_minutes: 25 + timeout_minutes: 50 + attempt_timeout_minutes: 20 command: >- cd packages/opencode && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit-windows-server-tools.xml test/server - test/snapshot + report_path: packages/opencode/.artifacts/unit/junit-windows-server-tools.xml + # Browser is isolated from server routes because a Windows process + # hang after browser tests can otherwise consume the whole job + # timeout before the retry wrapper sees an exit code. + - package: opencode-browser-agent + uses_turbo: false + timeout_minutes: 30 + attempt_timeout_minutes: 10 + command: >- + cd packages/opencode && bun test + --timeout 30000 + --reporter=junit + --reporter-outfile=.artifacts/unit/junit-windows-browser-agent.xml test/browser - test/mcp + test/agent test/question + report_path: packages/opencode/.artifacts/unit/junit-windows-browser-agent.xml + - package: opencode-server-support + uses_turbo: false + timeout_minutes: 30 + attempt_timeout_minutes: 10 + command: >- + cd packages/opencode && bun test + --timeout 30000 + --reporter=junit + --reporter-outfile=.artifacts/unit/junit-windows-server-support.xml + test/snapshot + test/mcp test/effect - test/agent test/git/ test/storage - report_path: packages/opencode/.artifacts/unit/junit-windows-server-tools.xml + report_path: packages/opencode/.artifacts/unit/junit-windows-server-support.xml - package: opencode-tool-runtime uses_turbo: false timeout_minutes: 20 @@ -295,11 +319,24 @@ jobs: run: | set +e attempts=2 + attempt_timeout_minutes="${{ matrix.attempt_timeout_minutes }}" first_status= + if [ -n "$attempt_timeout_minutes" ] && ! command -v timeout >/dev/null 2>&1; then + echo "::error title=Missing GNU timeout::matrix package ${{ matrix.package }} needs process-level attempt timeout" + exit 127 + fi + for attempt in $(seq 1 "$attempts"); do echo "::group::Windows unit attempt $attempt of $attempts" - ( ${{ matrix.command }} ) + if [ -n "$attempt_timeout_minutes" ]; then + timeout "${attempt_timeout_minutes}m" bash -lc '${{ matrix.command }}' + else + ( ${{ matrix.command }} ) + fi status=$? + if [ -n "$attempt_timeout_minutes" ] && [ "$status" -eq 124 ]; then + echo "Windows unit attempt timed out after ${attempt_timeout_minutes}m" + fi echo "::endgroup::" if [ "$attempt" -eq 1 ]; then diff --git a/packages/opencode/test/github/ci-workflow.test.ts b/packages/opencode/test/github/ci-workflow.test.ts index a14b02fa3..df7937d2f 100644 --- a/packages/opencode/test/github/ci-workflow.test.ts +++ b/packages/opencode/test/github/ci-workflow.test.ts @@ -102,11 +102,30 @@ const windowsOpencodeShards = [ { suffix: "opencode-server-tools", usesTurbo: false, - timeoutMinutes: 25, + timeoutMinutes: 50, + attemptTimeoutMinutes: 20, command: - "cd packages/opencode && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit-windows-server-tools.xml test/server test/snapshot test/browser test/mcp test/question test/effect test/agent test/git/ test/storage", + "cd packages/opencode && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit-windows-server-tools.xml test/server", reportPath: "packages/opencode/.artifacts/unit/junit-windows-server-tools.xml", }, + { + suffix: "opencode-browser-agent", + usesTurbo: false, + timeoutMinutes: 30, + attemptTimeoutMinutes: 10, + command: + "cd packages/opencode && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit-windows-browser-agent.xml test/browser test/agent test/question", + reportPath: "packages/opencode/.artifacts/unit/junit-windows-browser-agent.xml", + }, + { + suffix: "opencode-server-support", + usesTurbo: false, + timeoutMinutes: 30, + attemptTimeoutMinutes: 10, + command: + "cd packages/opencode && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit-windows-server-support.xml test/snapshot test/mcp test/effect test/git/ test/storage", + reportPath: "packages/opencode/.artifacts/unit/junit-windows-server-support.xml", + }, { suffix: "opencode-tool-runtime", usesTurbo: false, @@ -642,9 +661,11 @@ describe("ci workflow", () => { // Retry budget: exactly one extra attempt (max_attempts=2). expect(unitRun).toContain("attempts=2") - // Each attempt runs in a subshell so `cd packages/...` in matrix.command - // does not leak working directory across attempts. - expect(unitRun).toContain("( ${{ matrix.command }} )") + // Each time-boxed attempt runs in a fresh shell so `cd packages/...` in + // matrix.command does not leak working directory across attempts. + expect(unitRun).toContain('attempt_timeout_minutes="${{ matrix.attempt_timeout_minutes }}"') + expect(unitRun).toContain('timeout "${attempt_timeout_minutes}m" bash -lc') + expect(unitRun).toContain("Windows unit attempt timed out") // First-attempt exit code must be exported so downstream steps and humans // can tell a recovered run apart from a clean-first-pass run. @@ -672,13 +693,19 @@ describe("ci workflow", () => { const matrixIncludes = job?.strategy?.matrix?.include ?? [] expect(matrixIncludes).toEqual( - windowsUnitJobs.map(({ jobName, usesTurbo, timeoutMinutes, command, reportPath }) => ({ - package: jobName.replace("unit-windows-", ""), - uses_turbo: usesTurbo, - timeout_minutes: timeoutMinutes, - command, - report_path: reportPath, - })), + windowsUnitJobs.map((pkg) => { + const matrixItem: Record = { + package: pkg.jobName.replace("unit-windows-", ""), + uses_turbo: pkg.usesTurbo, + timeout_minutes: pkg.timeoutMinutes, + command: pkg.command, + report_path: pkg.reportPath, + } + if ("attemptTimeoutMinutes" in pkg) { + matrixItem.attempt_timeout_minutes = pkg.attemptTimeoutMinutes + } + return matrixItem + }), ) for (const { jobName, artifactName } of windowsUnitJobs) { @@ -703,6 +730,8 @@ describe("ci workflow", () => { "opencode-session", "opencode-config-project", "opencode-server-tools", + "opencode-browser-agent", + "opencode-server-support", "opencode-tool-runtime", "opencode-platform", ]) @@ -751,6 +780,38 @@ describe("ci workflow", () => { }) }) + test("time-boxes server-derived Windows opencode shards below the job budget", () => { + const parsed = parseWorkflow(windowsAdvisoryWorkflowPath) + const matrixIncludes = parsed.jobs?.[windowsUnitJobName]?.strategy?.matrix?.include ?? [] + const serverDerivedShards = matrixIncludes.filter((item) => + ["opencode-server-tools", "opencode-browser-agent", "opencode-server-support"].includes(String(item.package)), + ) + + expect( + serverDerivedShards.map((item) => ({ + package: item.package, + timeout_minutes: item.timeout_minutes, + attempt_timeout_minutes: item.attempt_timeout_minutes, + })), + ).toEqual([ + { package: "opencode-server-tools", timeout_minutes: 50, attempt_timeout_minutes: 20 }, + { package: "opencode-browser-agent", timeout_minutes: 30, attempt_timeout_minutes: 10 }, + { package: "opencode-server-support", timeout_minutes: 30, attempt_timeout_minutes: 10 }, + ]) + + for (const item of serverDerivedShards) { + const timeoutMinutes = item.timeout_minutes + const attemptTimeoutMinutes = item.attempt_timeout_minutes + + expect(typeof timeoutMinutes).toBe("number") + expect(typeof attemptTimeoutMinutes).toBe("number") + if (typeof timeoutMinutes !== "number" || typeof attemptTimeoutMinutes !== "number") { + throw new Error(`Invalid timeout shape for ${String(item.package)}`) + } + expect(timeoutMinutes).toBeGreaterThanOrEqual(attemptTimeoutMinutes * 2 + 10) + } + }) + test("keeps Windows opencode shard paths from prefix-matching sibling test directories", () => { const parsed = parseWorkflow(windowsAdvisoryWorkflowPath) const matrixIncludes = parsed.jobs?.[windowsUnitJobName]?.strategy?.matrix?.include ?? []