diff --git a/.github/workflows/windows-advisory.yml b/.github/workflows/windows-advisory.yml index 58cedb176..960414e10 100644 --- a/.github/workflows/windows-advisory.yml +++ b/.github/workflows/windows-advisory.yml @@ -258,19 +258,70 @@ jobs: - name: unit id: unit + # Windows-advisory has shown transient flake (sleep-based timing in + # opencode session tests and occasional Bun native crashes on + # `windows-latest`). Linux `ci` is the load-bearing required gate; + # this workflow is advisory and non-blocking. Retry once at the + # process level so a single transient failure does not turn the + # advisory red, but keep the first-attempt failure visible in the + # step summary so persistent regressions are not hidden by retry. + # Each attempt runs in a subshell so `cd packages/...` in + # matrix.command does not leak between attempts. run: | set +e - ${{ matrix.command }} - status=$? + attempts=2 + first_status= + for attempt in $(seq 1 "$attempts"); do + echo "::group::Windows unit attempt $attempt of $attempts" + ( ${{ matrix.command }} ) + status=$? + echo "::endgroup::" + + if [ "$attempt" -eq 1 ]; then + first_status=$status + fi + + if [ "$status" -eq 0 ]; then + break + fi + + if [ "$attempt" -lt "$attempts" ]; then + { + echo "### Windows unit attempt $attempt failed (retrying)" + echo "" + echo "- package: ${{ matrix.package }}" + echo "- exit code: $status" + echo "- next attempt: $((attempt + 1)) of $attempts" + } >> "$GITHUB_STEP_SUMMARY" + fi + done + echo "exit_code=$status" >> "$GITHUB_OUTPUT" + echo "first_exit_code=$first_status" >> "$GITHUB_OUTPUT" - if [ "$status" -ne 0 ]; then + if [ "$status" -eq 0 ] && [ "$first_status" -ne 0 ]; then + # ::notice surfaces the recovery in the run-level annotations panel + # and in each check's annotation list, not just buried in the + # collapsed step summary, so flake rate stays observable. + echo "::notice title=Windows unit recovered on retry::package=${{ matrix.package }}; attempt 1 exit code=$first_status; attempt 2 succeeded" + { + echo "### Windows unit recovered on retry" + echo "" + echo "- package: ${{ matrix.package }}" + echo "- attempt 1 exit code: $first_status" + echo "- attempt 2: success" + } >> "$GITHUB_STEP_SUMMARY" + elif [ "$status" -ne 0 ]; then + # ::warning gives the run-level annotation a yellow icon on a red + # job — the step is still failing (advisory signal red), but the + # annotation makes the post-retry failure scannable at a glance. + echo "::warning title=Windows unit failed advisory signal after retry::package=${{ matrix.package }}; final exit code=$status; attempts=$attempts" { echo "### Windows unit diagnostic" echo "" echo "- package: ${{ matrix.package }}" echo "- exit code: $status" - echo "- status: failed advisory signal" + echo "- status: failed advisory signal after $attempts attempts" } >> "$GITHUB_STEP_SUMMARY" fi diff --git a/packages/opencode/test/github/ci-workflow.test.ts b/packages/opencode/test/github/ci-workflow.test.ts index efe3aa84e..d86d379d3 100644 --- a/packages/opencode/test/github/ci-workflow.test.ts +++ b/packages/opencode/test/github/ci-workflow.test.ts @@ -609,6 +609,36 @@ describe("ci workflow", () => { ) }) + test("retries the Windows unit step once on transient failure", () => { + const unitRun = stepByName(windowsUnitJobName, "unit", windowsAdvisoryWorkflowPath)?.run ?? "" + + // 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 }} )") + + // First-attempt exit code must be exported so downstream steps and humans + // can tell a recovered run apart from a clean-first-pass run. + expect(unitRun).toContain('echo "first_exit_code=$first_status" >> "$GITHUB_OUTPUT"') + + // First-attempt failure is surfaced in the step summary even when the + // retry recovers — the advisory must not silently swallow transient flake. + expect(unitRun).toContain("### Windows unit attempt $attempt failed (retrying)") + expect(unitRun).toContain("### Windows unit recovered on retry") + + // The recovered and final-failed outcomes also emit run-level annotations + // (::notice / ::warning) so the signal is visible in the Actions UI + // annotations panel, not only inside the collapsed step summary. + expect(unitRun).toContain("::notice title=Windows unit recovered on retry") + expect(unitRun).toContain("::warning title=Windows unit failed advisory signal after retry") + + // The final exit code is the last attempt's status, not the first. + // Otherwise a recovered run would still turn the advisory red. + expect(unitRun).toMatch(/exit "\$status"\s*$/) + }) + test("defines Windows unit packages and opencode shards", () => { const parsed = parseWorkflow(windowsAdvisoryWorkflowPath) const job = parsed.jobs?.[windowsUnitJobName]