Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 46 additions & 9 deletions .github/workflows/windows-advisory.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
85 changes: 73 additions & 12 deletions packages/opencode/test/github/ci-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<string, string | number | boolean> = {
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) {
Expand All @@ -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",
])
Expand Down Expand Up @@ -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 ?? []
Expand Down
Loading