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
110 changes: 15 additions & 95 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -545,8 +545,8 @@ jobs:
# Two shards. Unsharded, this job was the critical path on every green dev
# push (mean 14.9 min against a 4.7 min Linux maximum; devlog
# 260905_test_modularization_and_windows/003). Two halves finish in ~7.7 and
# cost 0.6 extra macOS minutes of setup per run. The whole-pool control that
# the single job used to provide lives in macos-control below, on dispatch.
# cost 0.6 extra macOS minutes of setup per run. The full-membership control
# runs the same bounded batches without sharding on explicit dispatch.
timeout-minutes: 20
strategy:
fail-fast: false
Expand Down Expand Up @@ -587,101 +587,21 @@ jobs:
cd gui
bun run build

# No attempt is ever repeated here. A Bun panic is the interpreter dying mid-suite,
# which is process death a user would have seen; a second execution that happens not
# to die does not un-kill the first, and a leg that reports green on it is reporting
# something that did not happen. This leg retried a crash exactly once until
# 2026-09-17, the Linux batch runner swept crashed batches into green, and the result
# was that Bun 1.4.2's preload segfault stayed invisible on every lane except Windows.
#
# `is_bun_runtime_crash` from scripts/ci/bun-crash-signatures.sh survives, and this leg,
# the Windows leg, the macOS control and the Linux batch runner all still source that one
# definition. Its job is now diagnosis only: it decides which failure message is printed,
# never whether the leg fails.
- name: Test
env:
MACOS_TEST_SHARD: ${{ matrix.shard }}
- name: Setup bounded batch utilities
run: |
# GitHub Actions starts bash `run:` blocks with `-e`. Disable
# errexit so a Bun crash reaches PIPESTATUS and the classifier below,
# instead of aborting the step before either can be read.
set +e
set -uo pipefail
# One shared classifier for every lane; see scripts/ci/bun-crash-signatures.sh.
source scripts/ci/bun-crash-signatures.sh

run_macos_suite() {
local suite_log suite_status
suite_log="$(mktemp -t ocx-macos-suite.XXXXXX)" || return $?
# The per-test ceiling applies to every invocation, including each isolated
# serial file. One attempt, whatever the outcome.
bun test --isolate --timeout 60000 "$@" 2>&1 | tee "$suite_log"
suite_status="${PIPESTATUS[0]}"
if [ "$suite_status" -eq 0 ]; then
rm -f "$suite_log"
return 0
fi
if is_bun_runtime_crash "$suite_status" "$suite_log"; then
echo "::error::Bun runtime crash in the macOS suite (exit ${suite_status}); a crash is process death, not a test result, and it fails this leg on the first occurrence."
else
echo "::error::macOS suite failed (exit ${suite_status})."
fi
rm -f "$suite_log"
return "$suite_status"
}

case "$MACOS_TEST_SHARD" in
1|2) ;;
*) echo "::error::Invalid macOS test shard"; exit 64 ;;
esac
serial_manifest="$(bun -e 'import { SERIAL_FULL_SUITE_FILES } from "./scripts/test.ts"; console.log(SERIAL_FULL_SUITE_FILES.join("\n"));')"
manifest_status=$?
if [ "$manifest_status" -ne 0 ]; then
exit "$manifest_status"
fi
serial_files=()
ignore_args=()
serial_count=0
while IFS= read -r file; do
if [[ ! "$file" =~ ^[[:alnum:]_./-]+$ || "$file" == /* || "/$file/" == *"/../"* || "/$file/" == *"/./"* ]]; then
echo "::error::Invalid serial test path"
exit 1
fi
for ((index=0; index<serial_count; index++)); do
if [ "${serial_files[$index]}" = "$file" ]; then
echo "::error::Duplicate serial test path"
exit 1
fi
done
# The established basename glob must exclude exactly the owned file.
# Refuse missing files or collisions instead of silently losing tests.
base="${file##*/}"
matches="$(find tests -type f -name "$base")" || exit $?
if [ "$matches" != "tests/$file" ]; then
echo "::error::Missing or ambiguous serial test path: $file"
exit 1
fi
serial_files[$serial_count]="$file"
ignore_args+=("--path-ignore-patterns" "**/$base")
serial_count=$((serial_count + 1))
done <<< "$serial_manifest"
if [ "$serial_count" -eq 0 ]; then
echo "::error::Empty serial test manifest"
exit 1
fi
brew list coreutils >/dev/null 2>&1 || brew install coreutils
echo "$(brew --prefix coreutils)/libexec/gnubin" >> "$GITHUB_PATH"

run_macos_suite tests "--shard=$MACOS_TEST_SHARD/2" "${ignore_args[@]}"
suite_status=$?
if [ "$suite_status" -ne 0 ]; then exit "$suite_status"; fi
for ((index=0; index<serial_count; index++)); do
if [ "$((index % 2 + 1))" -ne "$MACOS_TEST_SHARD" ]; then continue; fi
file="${serial_files[$index]}"
echo "::group::macOS isolated $file"
run_macos_suite --parallel=1 "./tests/$file"
suite_status=$?
echo "::endgroup::"
if [ "$suite_status" -ne 0 ]; then exit "$suite_status"; fi
done
- name: Test in fresh-process batches
env:
TEST_SHARD: ${{ matrix.shard }}/2
BUN_TEST_FILE_SCOPE: all
BUN_TEST_BATCH_SIZE: '12'
BUN_TEST_PARALLEL: '1'
BUN_TEST_BATCH_TIMEOUT_SECONDS: '300'
OCX_TEST_NO_QUEUE: '1'
OCX_TEST_FULL_SUITE: '1'
run: bash scripts/ci/run-bun-test-batches.sh "$TEST_SHARD"

- name: CLI help smoke
run: bun run src/cli/index.ts help
Expand Down
12 changes: 12 additions & 0 deletions devlog/_plan/260922_native_tray_release/040_release.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,15 @@ no remaining architecture gap. Independent A audit follows.
Latest owner steering: Latest owner instruction ci 걍 무시하고 머지해 executed: PR5490 admin squash merged to dev6c2f7676dcedba21bdbacf4fb84a7b2c286d1ee6 at2026-09-22T09:24:09Z. Priorcandidate3d64 hadPR/allplatform/serviceSUCCESS. D1 now precedesD0 by explicituseroverride; no fabricated B order. D0reviewfinding gatewaypartialbookkeeping remains narrowfollowup beforefreezeRC/publish. Bothpreview+stabledeployment remainsauthorized. No-local-tests andno-verify unchanged.

Independent A audit: Volta NEAR-PASS. Both text gaps are folded: D0 explicitly requires credential-boundary security review under MAINTAINERS.md in addition to ordinary source review; the original local-verification wording above now matches the latest no-local-execution restriction. New promotion drafts #5510/#5511 are provisional and will receive the corrected RC. No release has been published.

### D0b — ordinary macOS shard process boundaries
The post-merge dev run35710172686 hit its 20-minute limit in macOS shard1. Its last recorded
passing cases were in catalog-full-picker-order around09:30:51; no further test output appeared
before cancellation around09:48:13. The precise subsequent blocked import/cleanup boundary is
not visible in the log. The same membership passed under the bounded full-control batches.
Replace ordinary macOS shard monolithic processes and their separate serial loop with that
shared batch runner: sorted all-file1/2 and2/2, maximum12files, parallel1,300-second batch bound,
60-second per-test ceiling, existing20-minute job cap. Preserve singleton families, fail-red
attribution and all tests. The actual workflow harness must prove complete/disjoint membership,
exact-path collision handling and failure disposition. Sol source/security review precedes the
owner-authorized immediate dev merge; local checks remain NOT RUN.
9 changes: 5 additions & 4 deletions structure/ops/docs-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,14 @@ it is promoted, so those files follow the promotion model rather than ordinary i
`scripts/test.ts` owns `SERIAL_FULL_SUITE_FILES`, the shared process-isolation roster. Local
full-suite runs, both macOS paths, and `scripts/ci/run-bun-test-batches.sh` execute those files
alone with fresh process homes. Hosted batches preserve sorted round-robin shard membership
and split only process boundaries; every selected file still runs once. The macOS control
runs the complete 1/1 file list in sequential batches of at most 12 files with one worker.
and split only process boundaries; every selected file still runs once. Ordinary macOS shards
select 1/2 and 2/2 from the full sorted file list; macOS control selects 1/1. Both execute
sequential batches of at most 12 files with one worker.
Storage-policy and API-usage families run as singletons, as do manifest-declared files.
This preserves full test membership but does not claim cross-batch shared-process coverage.
Every primary assertion failure, timeout or crash fails the run; diagnostic singleton
attribution never turns a failed primary green. Each control batch has a 300-second process
bound plus 15 seconds for forced reap, inside the unchanged 75-minute job cap. Other batch
bound plus 15 seconds for forced reap, inside unchanged 20-minute shard and 75-minute control job caps. Other batch
lanes keep their existing defaults; optional parallelism must be a positive integer.

The Windows selector is an operational stability control, not a security boundary. A pull request
Expand Down Expand Up @@ -382,7 +383,7 @@ runs the full suite in nine shards only on manual `workflow_dispatch` with `lane
empty lane). Pushes to `dev`, `main` and `preview` do not activate that Windows matrix, and an
aggregate green `ci` check on those events legitimately includes a deliberate Windows skip.

No recovery retry can turn a failed workflow green. Linux, Windows and macOS control use
No recovery retry can turn a failed workflow green. Linux, Windows, macOS shards and macOS control use
`scripts/ci/run-bun-test-batches.sh`, but
each lane owns its measured process shape: Linux keeps the default twelve files and 120 seconds;
Windows uses six files and 480 seconds. macOS control uses twelve files, 300 seconds and one
Expand Down
2 changes: 1 addition & 1 deletion structure/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ CI enumerates that domain layout through `scripts/ci/run-bun-test-batches.sh`. I
scope and 12-file/120-second process shape leave the dedicated Linux storage-policy and api-usage
jobs out of the general shards. The manual Windows matrix selects all-file scope and overrides the
process shape to six files and 480 seconds, so batching changes process size without changing the
platform suite's file set. macOS control selects the complete 1/1 list in twelve-file, one-worker
platform suite's file set. macOS shards select 1/2 and 2/2; control selects 1/1, all in twelve-file, one-worker
batches bounded to 300 seconds, with dedicated worker-heavy families kept singleton. These
dedicated batch steps set `OCX_TEST_NO_QUEUE=1`: their sequential
processes are one logical runner, while each process still installs its own isolated home and test
Expand Down
25 changes: 6 additions & 19 deletions tests/ci-workflows/ci-bun-crash-classifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { repoPath } from "../helpers/repo-root";

const CLASSIFIER_REL = "scripts/ci/bun-crash-signatures.sh";
const SOURCE_LINE = `source ${CLASSIFIER_REL}`;

/** Every signature that means "the interpreter died", not "a test failed". */
const CRASH_SIGNATURES = [
Expand All @@ -50,24 +48,15 @@ function read(...segments: string[]): string {
}

/** The literal text of one `run:` block, located by a line only that block contains. */
function runBlockContaining(workflow: string, anchor: string): string {
const index = workflow.indexOf(anchor);
expect(`anchor present: ${anchor}`).toBe(`anchor present: ${anchor}`);
expect(index).toBeGreaterThan(-1);
const start = workflow.lastIndexOf("run: |", index);
expect(start).toBeGreaterThan(-1);
const end = workflow.indexOf("\n - name:", index);
return workflow.slice(start, end === -1 ? undefined : end);
}

describe("the Bun crash classifier is shared", () => {
const classifier = read("scripts", "ci", "bun-crash-signatures.sh");
const batchScript = read("scripts", "ci", "run-bun-test-batches.sh");
const workflow = read(".github", "workflows", "ci.yml");

const lanes = {
"macos-shard": runBlockContaining(workflow, "run_macos_suite tests"),
};
const jobs = (Bun.YAML.parse(workflow) as { jobs: Record<string, { steps: Array<{ run?: string }> }> }).jobs;
const lanes = Object.fromEntries(["test", "platform-macos", "macos-control", "platform-windows"].map(name => [
name, jobs[name]!.steps.map(step => step.run ?? "").filter(run => run.includes("scripts/ci/run-bun-test-batches.sh")).join("\n"),
]));

test("the signatures exist in the classifier", () => {
for (const signature of CRASH_SIGNATURES) {
Expand All @@ -90,13 +79,11 @@ describe("the Bun crash classifier is shared", () => {

test("every direct lane and the shared batch runner use the classifier", () => {
for (const [name, text] of Object.entries(lanes)) {
expect(`${name}:sources:${text.includes(SOURCE_LINE)}`).toBe(`${name}:sources:true`);
expect(`${name}:calls:${text.includes("is_bun_runtime_crash \"$suite_status\" \"$suite_log\"")}`)
.toBe(`${name}:calls:true`);
expect(`${name}:delegates:${text.includes("scripts/ci/run-bun-test-batches.sh")}`).toBe(`${name}:delegates:true`);
}
expect(batchScript).toContain("bun-crash-signatures.sh");
expect(batchScript).toContain('is_bun_runtime_crash "$status" "$log_file"');
expect(workflow.match(/run: bash scripts\/ci\/run-bun-test-batches\.sh/g)).toHaveLength(3);
expect(workflow.match(/run: bash scripts\/ci\/run-bun-test-batches\.sh/g)).toHaveLength(4);
});

test("the thread-numbered panic form is the anchor nowhere", () => {
Expand Down
33 changes: 10 additions & 23 deletions tests/ci-workflows/ci-workflows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,35 +258,22 @@ describe("GitHub Actions hardening", () => {
const macosSteps = (ci.jobs?.["platform-macos"] as { steps?: { name?: string; env?: Record<string, string>; run?: string }[] })?.steps ?? [];
// The 60s per-test ceiling is part of the pinned shape: dropping it silently
// restores the timing-flake class this lane kept surfacing.
const macosTestStep = macosSteps.find(step => step.name === "Test");
expect(macosTestStep?.env?.MACOS_TEST_SHARD).toBe("${{ matrix.shard }}");
expect(hasShellCommandHead(macosTestStep?.run, 'bun test --isolate --timeout 60000 "$@"')).toBe(true);
expect(hasExactShellCommand(macosTestStep?.run, 'run_macos_suite tests "--shard=$MACOS_TEST_SHARD/2" "${ignore_args[@]}"')).toBe(true);
expect(hasExactShellCommand(macosTestStep?.run, 'run_macos_suite --parallel=1 "./tests/$file"')).toBe(true);
expect(macosTestStep?.run).toContain('import { SERIAL_FULL_SUITE_FILES } from "./scripts/test.ts"');
const macosTestStep = macosSteps.find(step => step.name === "Test in fresh-process batches");
expect(macosTestStep?.env).toMatchObject({
TEST_SHARD: "${{ matrix.shard }}/2", BUN_TEST_FILE_SCOPE: "all", BUN_TEST_BATCH_SIZE: "12",
BUN_TEST_PARALLEL: "1", BUN_TEST_BATCH_TIMEOUT_SECONDS: "300", OCX_TEST_NO_QUEUE: "1", OCX_TEST_FULL_SUITE: "1",
});
expect(macosTestStep?.run).toBe('bash scripts/ci/run-bun-test-batches.sh "$TEST_SHARD"');
expect(macosSteps.some(step => step.run?.includes("coreutils") && step.run.includes("GITHUB_PATH"))).toBe(true);
const macosShards = (ci.jobs?.["platform-macos"] as {
strategy?: { "fail-fast"?: boolean; matrix?: { shard?: number[] } };
})?.strategy;
expect(macosShards?.["fail-fast"]).toBe(false);
expect(macosShards?.matrix?.shard).toEqual([1, 2]);

// The macOS leg retries NOTHING. It carried a crash-only retry until
// 2026-09-17, on the reasoning that a Bun panic is a runtime defect rather than a
// test result. Both halves of that are true and the conclusion still does not
// follow: a panic is process death a user would have seen, and a second execution
// that happens not to die does not un-kill the first. Pin the absence of the loop
// and of its vocabulary, so it cannot return in a renamed form.
const macosTestRun = macosTestStep?.run ?? "";
// Actions invokes multiline `run:` blocks with `bash -e`. The retry loop
// is gone but errexit must still be disabled before the crash-prone command:
// otherwise exit 133 aborts the step before PIPESTATUS can be inspected and the
// failure is reported without saying what kind it was.
expect(hasExactShellCommand(macosTestRun, "set +e")).toBe(true);
// The crash signatures themselves moved to scripts/ci/bun-crash-signatures.sh; that one
// definition and every lane that sources it are pinned by ci-bun-crash-classifier.test.ts.
expect(macosTestRun).toContain("it fails this leg on the first occurrence");
expect(macosTestRun).not.toContain("for attempt in");
expect(macosTestRun).not.toContain("while true");
// Failure disposition belongs to the shared runner; the workflow has no recovery loop.
expect(macosTestStep?.run).not.toContain("for attempt in");
expect(macosTestStep?.run).not.toContain("while true");
expect((ci.jobs?.["platform-macos"] as { needs?: string; if?: string })?.needs).toBe("changes");
expect((ci.jobs?.["platform-macos"] as { if?: string })?.if)
.toBe("github.event_name != 'pull_request' || needs.changes.outputs.ci == 'true'");
Expand Down
Loading
Loading