-
Notifications
You must be signed in to change notification settings - Fork 853
fix(ci): give the Windows leg the budgets and the crash retry it never had #2178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -516,7 +516,7 @@ jobs: | |||||||||||||||||||||
| if [ "$suite_status" -eq 0 ]; then | ||||||||||||||||||||||
| exit 0 | ||||||||||||||||||||||
| fi | ||||||||||||||||||||||
| if ! grep -Eqi 'oh no: Bun has crashed|Segmentation fault at address|Illegal instruction|Bus error|Aborted \(core dumped\)' "$suite_log"; then | ||||||||||||||||||||||
| 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::macOS suite failed on attempt ${attempt} (exit ${suite_status}); assertion failures are not retried." | ||||||||||||||||||||||
| exit "$suite_status" | ||||||||||||||||||||||
| fi | ||||||||||||||||||||||
|
|
@@ -611,7 +611,31 @@ jobs: | |||||||||||||||||||||
| # the only one left on Bun's 5s default, and it is the slowest hardware on the board. | ||||||||||||||||||||||
| # Three of its failures were the default firing on tests that had not hung — the | ||||||||||||||||||||||
| # composed-acceptance cases spawn a real `ocx start` and were still working at 41s. | ||||||||||||||||||||||
| run: bun test --isolate --timeout 60000 tests --shard=${{ matrix.shard }}/4 | ||||||||||||||||||||||
| # | ||||||||||||||||||||||
| # The retry is the same one the macOS leg already carries, for the same reason: a Bun | ||||||||||||||||||||||
| # runtime panic is a crash in the interpreter, not a test result, and failing the shard | ||||||||||||||||||||||
| # on it reports a defect this repository does not have (#2152). An ordinary assertion | ||||||||||||||||||||||
| # failure returns its status immediately — only the crash signatures below are retried, | ||||||||||||||||||||||
| # and only once, so a genuinely broken build cannot be retried into green. | ||||||||||||||||||||||
| shell: bash | ||||||||||||||||||||||
| run: | | ||||||||||||||||||||||
| set +e | ||||||||||||||||||||||
| 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" | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- workflow context ---'
sed -n '470,500p' .github/workflows/ci.yml
sed -n '600,642p' .github/workflows/ci.yml
printf '%s\n' '--- Bun configuration ---'
if [ -f bunfig.toml ]; then
cat -n bunfig.toml
else
printf '%s\n' 'bunfig.toml not found'
fi
printf '%s\n' '--- related references ---'
rg -n --glob '.github/workflows/ci.yml' --glob 'bunfig.toml' -- '--isolate|test\.root|PIPESTATUS|suite_log|Bun 1\.3\.14|file-boundary|shard' .Repository: lidge-jun/opencodex Length of output: 4910 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- all retry command shapes ---'
rg -n -C 4 -- '--isolate|is_bun_runtime_crash|suite_log|PIPESTATUS' .github/workflows/ci.yml scripts/ci
printf '%s\n' '--- read-only invariant check ---'
python3 - <<'PY'
from pathlib import Path
workflow = Path(".github/workflows/ci.yml").read_text()
bunfig = Path("bunfig.toml").read_text()
needle = "bun test --isolate --timeout 60000 tests --shard=${{ matrix.shard }}/4"
assert needle in workflow, "Windows shard command was not found"
assert 'root = "tests"' in bunfig, "Bun test root is not configured as tests"
assert "--isolate" in workflow[workflow.index(needle)-500:workflow.index(needle)]
context = workflow[workflow.index(needle)-1200:workflow.index(needle)+len(needle)+200]
assert "Bun 1.3.14" in context, "The nearby explanation does not identify Bun 1.3.14"
assert "file" in context.lower() and "boundary" in context.lower(), \
"The nearby explanation does not identify the file-boundary crash"
print("Windows command retains tests and shard filter.")
print("Nearby workflow documentation attributes the runtime crash to the --isolate file boundary.")
print("Removing only --isolate preserves the configured suite filter and shard selection.")
PYRepository: lidge-jun/opencodex Length of output: 8946 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
workflow = Path(".github/workflows/ci.yml").read_text()
bunfig = Path("bunfig.toml").read_text()
command = "bun test --isolate --timeout 60000 tests --shard=${{ matrix.shard }}/4"
start = workflow.index(command)
context = workflow[max(0, start - 2500):start + len(command)]
assert command in workflow
assert 'root = "tests"' in bunfig
assert "Bun 1.3.14 segfaults" in context
assert "an `--isolate` file" in context
assert "workers_terminated" in context
assert "tests --shard=${{ matrix.shard }}/4" in command
replacement = command.replace("bun test --isolate", "bun test")
assert replacement == "bun test --timeout 60000 tests --shard=${{ matrix.shard }}/4"
print("The Windows command contains the documented crash-triggering option.")
print("The proposed replacement preserves timeout, tests filter, and shard selection.")
PYRepository: lidge-jun/opencodex Length of output: 245 🌐 Web query:
💡 Result: The default behavior for the --isolate option in bun test is off. By default, bun test runs all test files sharing a single global object and module registry within the same process [1]. When you explicitly enable --isolate, Bun runs each test file in a fresh JavaScript global object, ensuring that module-level state, patched built-ins, and properties on globalThis do not persist between files [1]. Conversely, when --isolate is not used, all files share the same environment, which is faster as it avoids re-evaluating imports and --preload scripts for every file [1]. Note that when using the --parallel flag, --isolate is enabled by default to ensure test safety, though you can opt out of this by passing --no-isolate [1][2]. Citations: 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
lines = Path(".github/workflows/ci.yml").read_text().splitlines()
assert lines[482].strip().startswith("# Bun 1.3.14 segfaults")
assert "`--isolate` file" in lines[482]
assert "workers_terminated" in "\n".join(lines[482:489])
command = lines[625].strip()
expected = "bun test --isolate --timeout 60000 tests --shard=${{ matrix.shard }}/4"
assert command == expected, repr(command)
assert command.replace("bun test --isolate", "bun test") == \
"bun test --timeout 60000 tests --shard=${{ matrix.shard }}/4"
assert Path("bunfig.toml").read_text().count('root = "tests"') == 1
print("Lines 483-489 document the Bun 1.3.14 --isolate file-boundary crash.")
print("Line 626 retains --timeout, tests, and the four-way shard filter.")
print("Removing only --isolate produces the intended Windows command.")
PYRepository: lidge-jun/opencodex Length of output: 343 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
lines = Path(".github/workflows/ci.yml").read_text().splitlines()
line = lines[625].strip()
command = line.split(" 2>&1", 1)[0]
assert command == "bun test --isolate --timeout 60000 tests --shard=${{ matrix.shard }}/4"
replacement = command.replace("bun test --isolate", "bun test", 1)
assert replacement == "bun test --timeout 60000 tests --shard=${{ matrix.shard }}/4"
assert lines[482].lstrip().startswith("# Bun 1.3.14 segfaults")
assert "`--isolate` file" in lines[482]
assert 'root = "tests"' in Path("bunfig.toml").read_text()
print("The Windows test command uses --isolate.")
print("The documented replacement removes only --isolate.")
print("The tests filter and shard selection remain unchanged.")Repository: lidge-jun/opencodex Length of output: 391 Remove Bun 1.3.14 can segfault at an 🧰 Tools🪛 zizmor (1.29.0)[warning] 626-626: code injection via template expansion (template-injection): may expand into attacker-controllable code (template-injection) 🤖 Prompt for AI AgentsSource: Learnings |
||||||||||||||||||||||
| suite_status="${PIPESTATUS[0]}" | ||||||||||||||||||||||
|
Comment on lines
+626
to
+627
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Fail the step when Line 627 captures only If Proposed fix bun test --isolate --timeout 60000 tests --shard=${{ matrix.shard }}/4 2>&1 | tee "$suite_log"
- suite_status="${PIPESTATUS[0]}"
+ pipeline_statuses=("${PIPESTATUS[@]}")
+ suite_status="${pipeline_statuses[0]}"
+ tee_status="${pipeline_statuses[1]}"
+ if [ "$tee_status" -ne 0 ]; then
+ echo "::error::Windows shard log capture failed (exit ${tee_status})."
+ exit "$tee_status"
+ fi📝 Committable suggestion
Suggested change
🧰 Tools🪛 zizmor (1.29.0)[warning] 626-626: code injection via template expansion (template-injection): may expand into attacker-controllable code (template-injection) 🤖 Prompt for AI Agents |
||||||||||||||||||||||
| 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." | ||||||||||||||||||||||
| exit "$suite_status" | ||||||||||||||||||||||
| fi | ||||||||||||||||||||||
| echo "::warning::Bun runtime crash in Windows shard ${{ matrix.shard }}/4 (exit ${suite_status}, attempt ${attempt})." | ||||||||||||||||||||||
| done | ||||||||||||||||||||||
| echo "::error::Bun runtime crash repeated on Windows shard ${{ matrix.shard }}/4; failing after one retry." | ||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| - name: CLI help smoke | ||||||||||||||||||||||
| run: bun run src/cli/index.ts help | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,21 @@ function hasExactShellCommand(run: string | undefined, expected: string): boolea | |
| .includes(expected); | ||
| } | ||
|
|
||
| /** | ||
| * Same intent as {@link hasExactShellCommand}, but for a command that is the HEAD of a | ||
| * pipeline. The retry loops capture the suite with `… 2>&1 | tee "$suite_log"`, so an exact | ||
| * whole-line match would reject the very shape the retry requires. Anchoring at the start of | ||
| * the line still rejects an `echo` of the command or a commented-out copy, which is what the | ||
| * exact match was protecting against. | ||
| */ | ||
| function hasShellCommandHead(run: string | undefined, expected: string): boolean { | ||
| return (run ?? "") | ||
| .split(/\r?\n/) | ||
| .map(line => line.trim()) | ||
| .filter(line => line.length > 0 && !line.startsWith("#")) | ||
| .some(line => line === expected || line.startsWith(`${expected} `)); | ||
| } | ||
|
Comment on lines
+58
to
+64
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject shell operators after Line 63 accepts Require the expected pipeline syntax after the command, such as 🤖 Prompt for AI Agents |
||
|
|
||
| function expectSecureLinuxKeyringBootstrap(workflow: string): void { | ||
| const smokeStep = workflow | ||
| .split("- name: OS keyring create/read/delete smoke")[1] | ||
|
|
@@ -224,17 +239,52 @@ describe("GitHub Actions hardening", () => { | |
| // Three composed-acceptance failures were that default firing on tests still working | ||
| // at 41s. Pin the flag so the leg cannot silently drift back to the default. | ||
| const windowsTestCommand = `bun test --isolate --timeout 60000 tests --shard=\${{ matrix.shard }}/${windowsShards.length}`; | ||
| expect(hasExactShellCommand(`echo ${windowsTestCommand}`, windowsTestCommand)).toBe(false); | ||
| expect(hasShellCommandHead(`echo ${windowsTestCommand}`, windowsTestCommand)).toBe(false); | ||
| // Binding the assertion to an executable line is only half the guarantee: a | ||
| // step carrying the exact command still runs nothing under `if: false`, and | ||
| // the suite would stay green against a Windows leg that never tests. Require | ||
| // the matching step to be unconditional. | ||
| const windowsTestSteps = winSteps.filter(step => hasExactShellCommand(step.run, windowsTestCommand)); | ||
| const windowsTestSteps = winSteps.filter(step => hasShellCommandHead(step.run, windowsTestCommand)); | ||
| expect(windowsTestSteps.length).toBeGreaterThan(0); | ||
| expect(windowsTestSteps.every(step => step.if === undefined)).toBe(true); | ||
| expect(winSteps.some(step => step.if === "runner.environment == 'self-hosted'" | ||
| && step.run?.includes("git clean -xffd"))).toBe(true); | ||
|
|
||
| // The three crash-signature lists must stay identical, and they must not key on | ||
| // `panic(thread`. | ||
| // | ||
| // Bun emits BOTH `panic(thread 2852)` and `panic(main thread)` for the same class of | ||
| // failure, so a grep anchored on the numbered form silently misses half of them and the | ||
| // shard fails on a crash it was supposed to retry. This repository already learned that | ||
| // once — `devlog/_fin/260731_pr_issue_triage_round/050_windows_ci_flake_rca.md` names | ||
| // `Internal assertion failure` as the stable fingerprint — and #2152 reintroduced it. | ||
| // Three copies of one list is the real hazard, so pin the sync rather than the text. | ||
| const crashSignatures = [ | ||
| "oh no: Bun has crashed", | ||
| "Internal assertion failure", | ||
| "Segmentation fault at address", | ||
| "Illegal instruction", | ||
| "Bus error", | ||
| ]; | ||
| const windowsTestRun = windowsTestSteps[0]?.run ?? ""; | ||
| const batchScript = await readText("scripts/ci/run-bun-test-batches.sh"); | ||
| for (const signature of crashSignatures) { | ||
| expect(`macos:${signature}:${macosTestRun.includes(signature)}`).toBe(`macos:${signature}:true`); | ||
| expect(`windows:${signature}:${windowsTestRun.includes(signature)}`).toBe(`windows:${signature}:true`); | ||
| expect(`script:${signature}:${batchScript.includes(signature)}`).toBe(`script:${signature}:true`); | ||
| } | ||
| // The thread-numbered form must not be the anchor anywhere. | ||
| expect(macosTestRun).not.toContain("panic\\(thread"); | ||
| expect(windowsTestRun).not.toContain("panic\\(thread"); | ||
| expect(batchScript).not.toContain("panic\\(thread"); | ||
|
Comment on lines
+253
to
+279
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Compare the complete crash-signature expressions. Lines 262-275 verify only five required substrings. They do not verify Extract the quoted 🤖 Prompt for AI Agents |
||
|
|
||
| // Windows carries the same bounded retry as macOS: one attempt, crash-only. | ||
| expect(hasExactShellCommand(windowsTestRun, "set +e")).toBe(true); | ||
| expect(windowsTestRun).toContain("for attempt in 1 2"); | ||
| expect(windowsTestRun).not.toContain("while true"); | ||
| expect(windowsTestRun).toContain("assertion failures are not retried"); | ||
| expect(windowsTestRun).toContain("failing after one retry"); | ||
|
|
||
| // Every job that runs the root suite must build the GUI first, unconditionally. | ||
| // Tests that fetch the served dashboard read their session bootstrap out of | ||
| // `gui/dist/index.html`; with no build the server has no index to serve and the | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a Bun crash occurs late in a Windows shard, this loop reruns the entire suite shard, but
platform-windowsstill has the original 15-minute job timeout at line 556, which also includes checkout, dependency installation, and the GUI build. Consequently, the second attempt can be cancelled by the job-level ceiling even when it would pass, so the new crash retry does not reliably preserve Windows coverage; increase the outer timeout to budget for two attempts or retry a smaller test unit.AGENTS.md reference: .github/AGENTS.md:L18-L18
Useful? React with 👍 / 👎.