From 5c8385de12061214fbc95e3ff829a38d752be5c3 Mon Sep 17 00:00:00 2001 From: mubeda <1380447+mubeda@users.noreply.github.com> Date: Fri, 13 Nov 2026 10:45:35 -0300 Subject: [PATCH 1/5] ci: run the macOS server tests in their own job Both macOS Native desktop rows hit their 240-minute limit on main after the background Git index lock step was added: the server's test build took 32 to 56 minutes there, and the run had no Rust cache. Move the three macOS server test steps (Git system configuration, index lock, host metrics) to a new job, server_tests_macos, which restores the Native desktop cache and never saves one. Raise the macOS x64 row to 300 minutes: its remaining steps needed about 260 minutes cold. Co-Authored-By: Claude Fable 5.2 --- .github/workflows/ci.yml | 96 +++++++++++++++++++--------- docs/operations/ci.md | 15 ++++- scripts/ci-platform-contract.test.ts | 25 ++++++++ 3 files changed, 102 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a0f2eb8f9..8b21f35b99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -424,14 +424,6 @@ jobs: if: matrix.platform == 'win' run: node scripts/run-msvc.mjs cargo test -p bibcode-server --test windows_terminal_shims -j 2 -- review_prompt_shim_ - - name: Run Git system configuration tests (macOS) - if: matrix.platform == 'mac' - run: cargo test -p bibcode-server --test git_system_config -j 2 - - - name: Run background Git index lock tests (macOS) - if: matrix.platform == 'mac' - run: cargo test -p bibcode-server --lib -j 2 -- background_status_ git_stop_ index_lock_cleanup_ - # #292: the host collector, its exit with a disk read held, and its runtime start and # quiesce. Each command fails the step when its filter matches no test. - name: Run host metrics tests (Windows) @@ -455,27 +447,6 @@ jobs: counted exactly-one node scripts/run-msvc.mjs cargo test -p bibcode-server --test host_metrics_exit -j 2 -- --exact a_held_disk_read_does_not_block_exit counted exactly-one node scripts/run-msvc.mjs cargo test -p bibcode-server --lib -j 2 -- --exact production::runtime::tests::host_metrics_start_with_the_runtime_and_stop_in_quiesce - - name: Run host metrics tests (macOS) - if: matrix.platform == 'mac' - shell: bash - run: | - counted() { - local expected="$1"; shift - local log; log="$(mktemp)" - "$@" 2>&1 | tee "$log" - local passed - passed="$(sed -n 's/^test result: ok\. \([0-9]*\) passed.*/\1/p' "$log" | awk '{ total += $1 } END { print total + 0 }')" - if [ "$expected" = exactly-one ] && [ "$passed" -ne 1 ]; then - echo "::error::expected exactly 1 passed test, got $passed"; exit 1 - fi - if [ "$passed" -lt 1 ]; then - echo "::error::the filter matched no test"; exit 1 - fi - } - counted at-least-one cargo test -p bibcode-server --lib -j 2 -- host_metrics:: - counted exactly-one cargo test -p bibcode-server --test host_metrics_exit -j 2 -- --exact a_held_disk_read_does_not_block_exit - counted exactly-one cargo test -p bibcode-server --lib -j 2 -- --exact production::runtime::tests::host_metrics_start_with_the_runtime_and_stop_in_quiesce - # build-desktop-artifact.ts --symbols runs dump_syms on the unstripped binary. Rust Cache # keeps ~/.cargo/bin, so a restored runner skips the compile. - name: Install dump_syms @@ -493,7 +464,7 @@ jobs: --symbols --verbose - # The macOS legs take 1-4 hours on a 5-job macOS concurrency cap, so pull + # The macOS legs take 1-5 hours on a 5-job macOS concurrency cap, so pull # requests skip them unless labelled ci:full. Main pushes and manual runs # always build them. Same steps as native_desktop. native_desktop_macos: @@ -519,9 +490,72 @@ jobs: platform: mac target: dmg arch: x64 - jobTimeoutMinutes: 240 + # A cold run (no Rust cache) needed about 260 minutes for these + # steps on 2026-11-13; arm64 needed about 200. + jobTimeoutMinutes: 300 steps: *native_desktop_steps + # Linux runs these server tests in Test and Windows in its native desktop rows; macOS Git + # (LFS, hooks), the index lock owner check (#341) and the host collector (#292) differ. + # They are their own job because the server's test build took 32 to 56 minutes inside the + # macOS native desktop rows and pushed both past their budget. Each host metrics command + # fails the step when its filter matches no test. + # The cache is the native desktop one, restored and never saved: a second macOS entry of + # this size does not fit the 10 GB Actions budget. + server_tests_macos: + name: Server tests - ${{ matrix.label }} + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'ci:full') + runs-on: ${{ matrix.runner }} + timeout-minutes: 150 + strategy: + fail-fast: false + matrix: + include: + - label: macOS arm64 + runner: macos-26 + - label: macOS x64 + runner: macos-15-intel + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@18f0487477426dce244608110aa2660c2893deb7 # 1.98.1 + + - name: Restore Rust build output + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + shared-key: native-desktop + save-if: "false" + workspaces: | + . -> target + + - name: Run Git system configuration tests (macOS) + run: cargo test -p bibcode-server --test git_system_config -j 2 + + - name: Run background Git index lock tests (macOS) + run: cargo test -p bibcode-server --lib -j 2 -- background_status_ git_stop_ index_lock_cleanup_ + + - name: Run host metrics tests (macOS) + shell: bash + run: | + counted() { + local expected="$1"; shift + local log; log="$(mktemp)" + "$@" 2>&1 | tee "$log" + local passed + passed="$(sed -n 's/^test result: ok\. \([0-9]*\) passed.*/\1/p' "$log" | awk '{ total += $1 } END { print total + 0 }')" + if [ "$expected" = exactly-one ] && [ "$passed" -ne 1 ]; then + echo "::error::expected exactly 1 passed test, got $passed"; exit 1 + fi + if [ "$passed" -lt 1 ]; then + echo "::error::the filter matched no test"; exit 1 + fi + } + counted at-least-one cargo test -p bibcode-server --lib -j 2 -- host_metrics:: + counted exactly-one cargo test -p bibcode-server --test host_metrics_exit -j 2 -- --exact a_held_disk_read_does_not_block_exit + counted exactly-one cargo test -p bibcode-server --lib -j 2 -- --exact production::runtime::tests::host_metrics_start_with_the_runtime_and_stop_in_quiesce + # A real native crash under the crash monitor on every OS, symbolicated with symbols dumped # the way the release pipeline dumps them. # macos-26 runs on main, manual runs, and ci:full pull requests only. diff --git a/docs/operations/ci.md b/docs/operations/ci.md index 3f1848613a..f7070b1b98 100644 --- a/docs/operations/ci.md +++ b/docs/operations/ci.md @@ -20,7 +20,7 @@ workflow as idle. ## Full CI and the `ci:full` label -The macOS legs take one to four hours each and share a five-job macOS +The macOS legs take one to five hours each and share a five-job macOS concurrency cap with every other workflow, so a pull request runs them only when it carries the `ci:full` label: @@ -29,6 +29,7 @@ when it carries the `ci:full` label: | Check, Test, Release Smoke | yes | yes | | Native desktop: Linux and Windows rows | yes | yes | | Native desktop: macOS arm64 and x64 | no | yes | +| Server tests: macOS arm64 and x64 | no | yes | | Crash smoke: `ubuntu-24.04`, `windows-2025` | yes | yes | | Crash smoke: `macos-26` | no | yes | | Seeded Packaged Desktop Upgrade (all jobs) | no | yes | @@ -140,7 +141,7 @@ the merge. Rust Cache keeps the binary in `~/.cargo/bin`. The macOS exception probe uses the same symbol profile so the dependencies both builds share compile once. Each matrix row declares its complete-job budget: - 240 minutes for both macOS architectures, and 120 minutes for the other four targets. + 240 minutes for macOS arm64, 300 minutes for macOS x64, and 120 minutes for the other four targets. That budget includes setup, host tests, the macOS optimized exception-recovery probe, and native packaging without shortening any check or test-owned deadline. Debug host tests cannot warm the optimized artifacts; the optimized example @@ -172,6 +173,14 @@ the merge. spaces, dollar signs, backticks, and typographic quotes. Every Git/home configuration input is fixture-owned. Other platforms skip that native case; their string assertions remain compatibility evidence. +- **Server tests (macOS)** is the job `server_tests_macos`, on `macos-26` and + `macos-15-intel` with the `ci:full` gate above. It runs the server tests whose + behaviour differs on macOS: `git_system_config`, the background Git index lock + tests, and the host metrics tests. Linux runs them in Test and Windows in its + Native desktop rows. They are a separate job because the server's test build + took 32 to 56 minutes inside the macOS Native desktop rows and pushed both past + their budget. The job restores the Native desktop Rust cache and never saves + one. Its budget is 150 minutes. - **Crash smoke** runs on `ubuntu-24.04` and `windows-2025` on every pull request, and also on `macos-26` with `ci:full`, on `main`, and in manual runs. It builds with the release symbol profile from @@ -246,7 +255,7 @@ recently used entries above that. Which workflow saves which entry: A job cancelled by `timeout-minutes` or by a newer run skips the cache post step, so the Check and Test budgets above include a cold compile. Native -desktop keeps its 120-minute and 240-minute row budgets. +desktop keeps its 120-minute, 240-minute and 300-minute row budgets. Six native-desktop entries of about 1.6 GB each are about 10 GB on their own, before Check, Test, Crash smoke, Linux Git, and the setup-vp caches. Eviction diff --git a/scripts/ci-platform-contract.test.ts b/scripts/ci-platform-contract.test.ts index c5ccd68b56..dc61a61850 100644 --- a/scripts/ci-platform-contract.test.ts +++ b/scripts/ci-platform-contract.test.ts @@ -336,6 +336,31 @@ describe("cross-platform CI contract", () => { } }); + it("runs the macOS server tests off the native desktop job, without saving a cache", () => { + const { workflow } = readWorkflow(CI_WORKFLOW_PATH); + const job = requireJob(workflow, "server_tests_macos"); + expect(job.if).toBe(FULL_CI); + const steps = job.steps ?? []; + const runs = steps.map((step) => String(step.run ?? "")).join("\n"); + for (const command of [ + "cargo test -p bibcode-server --test git_system_config", + "-- background_status_ git_stop_ index_lock_cleanup_", + "-- host_metrics::", + "--test host_metrics_exit", + "host_metrics_start_with_the_runtime_and_stop_in_quiesce", + ]) { + expect(runs, command).toContain(command); + } + const cache = steps.find((step) => String(step.uses ?? "").startsWith("Swatinem/rust-cache@")); + expect(cache?.with?.["save-if"]).toBe("false"); + // The server's test build is what pushed the macOS rows past their budget. + const native = (requireJob(workflow, "native_desktop_macos").steps ?? []) + .filter((step) => step.if === "matrix.platform == 'mac'") + .map((step) => String(step.run ?? "")) + .join("\n"); + expect(native).not.toContain("cargo test -p bibcode-server"); + }); + it("builds native desktop bundles on every supported runner and architecture", () => { const { workflow } = readWorkflow(CI_WORKFLOW_PATH); const matrix = nativeDesktopRows(workflow); From 4c696a9f07247fd5839ca93112d8da52db1e4c6c Mon Sep 17 00:00:00 2001 From: mubeda <1380447+mubeda@users.noreply.github.com> Date: Fri, 13 Nov 2026 10:52:55 -0300 Subject: [PATCH 2/5] ci: give macOS x64 330 minutes and pin the split in the contract test The Intel figure is an estimate from a cancelled run, so leave more room under the 360-minute hosted limit. The contract test now looks at every step that runs on the macOS rows, and requires 330 minutes for x64. Co-Authored-By: Claude Fable 5.2 --- .github/workflows/ci.yml | 7 ++++--- docs/operations/ci.md | 7 ++++--- scripts/ci-platform-contract.test.ts | 5 +++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b21f35b99..66cec1b812 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -490,9 +490,10 @@ jobs: platform: mac target: dmg arch: x64 - # A cold run (no Rust cache) needed about 260 minutes for these - # steps on 2026-11-13; arm64 needed about 200. - jobTimeoutMinutes: 300 + # Estimated from the 2026-11-13 run without a Rust cache, which was + # cancelled in the bundle step: about 260 minutes for these steps + # here and about 200 on arm64. Neither is a completed cold run. + jobTimeoutMinutes: 330 steps: *native_desktop_steps # Linux runs these server tests in Test and Windows in its native desktop rows; macOS Git diff --git a/docs/operations/ci.md b/docs/operations/ci.md index f7070b1b98..5416578f19 100644 --- a/docs/operations/ci.md +++ b/docs/operations/ci.md @@ -141,7 +141,7 @@ the merge. Rust Cache keeps the binary in `~/.cargo/bin`. The macOS exception probe uses the same symbol profile so the dependencies both builds share compile once. Each matrix row declares its complete-job budget: - 240 minutes for macOS arm64, 300 minutes for macOS x64, and 120 minutes for the other four targets. + 240 minutes for macOS arm64, 330 minutes for macOS x64, and 120 minutes for the other four targets. That budget includes setup, host tests, the macOS optimized exception-recovery probe, and native packaging without shortening any check or test-owned deadline. Debug host tests cannot warm the optimized artifacts; the optimized example @@ -180,7 +180,8 @@ the merge. Native desktop rows. They are a separate job because the server's test build took 32 to 56 minutes inside the macOS Native desktop rows and pushed both past their budget. The job restores the Native desktop Rust cache and never saves - one. Its budget is 150 minutes. + one. Its budget is 150 minutes. A full run now starts seven macOS jobs against + the five-job cap, so two of them wait for a free runner. - **Crash smoke** runs on `ubuntu-24.04` and `windows-2025` on every pull request, and also on `macos-26` with `ci:full`, on `main`, and in manual runs. It builds with the release symbol profile from @@ -255,7 +256,7 @@ recently used entries above that. Which workflow saves which entry: A job cancelled by `timeout-minutes` or by a newer run skips the cache post step, so the Check and Test budgets above include a cold compile. Native -desktop keeps its 120-minute, 240-minute and 300-minute row budgets. +desktop keeps its 120-minute, 240-minute and 330-minute row budgets. Six native-desktop entries of about 1.6 GB each are about 10 GB on their own, before Check, Test, Crash smoke, Linux Git, and the setup-vp caches. Eviction diff --git a/scripts/ci-platform-contract.test.ts b/scripts/ci-platform-contract.test.ts index dc61a61850..25c72fef3d 100644 --- a/scripts/ci-platform-contract.test.ts +++ b/scripts/ci-platform-contract.test.ts @@ -331,7 +331,8 @@ describe("cross-platform CI contract", () => { for (const row of matrix) { // macOS's cold optimized probe and native package use distinct dependency // feature sets, in addition to the unoptimized host test graph. - const minimum = row.platform === "mac" ? 240 : 120; + // Intel's steps were estimated at about 260 minutes without a Rust cache. + const minimum = row.platform !== "mac" ? 120 : row.arch === "x64" ? 330 : 240; expect(row.jobTimeoutMinutes, String(row.label)).toBeGreaterThanOrEqual(minimum); } }); @@ -355,7 +356,7 @@ describe("cross-platform CI contract", () => { expect(cache?.with?.["save-if"]).toBe("false"); // The server's test build is what pushed the macOS rows past their budget. const native = (requireJob(workflow, "native_desktop_macos").steps ?? []) - .filter((step) => step.if === "matrix.platform == 'mac'") + .filter((step) => !/matrix\.platform == '(win|linux)'/.test(String(step.if ?? ""))) .map((step) => String(step.run ?? "")) .join("\n"); expect(native).not.toContain("cargo test -p bibcode-server"); From dbe646ab170f86ef2defe1c3ad2e6e1ed9556335 Mon Sep 17 00:00:00 2001 From: mubeda <1380447+mubeda@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:45:29 -0300 Subject: [PATCH 3/5] test(ci): count the macOS server test job among the Rust CI jobs Co-Authored-By: Claude Fable 5.2 --- scripts/rust-workspace.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/rust-workspace.test.ts b/scripts/rust-workspace.test.ts index 06f7de18d2..97c9542dd1 100644 --- a/scripts/rust-workspace.test.ts +++ b/scripts/rust-workspace.test.ts @@ -95,7 +95,7 @@ it.layer(NodeServices.layer)("canonical Rust workspace", (it) => { ); assert.equal( ciWorkflow.match(/uses: dtolnay\/rust-toolchain@[0-9a-f]{40} # 1\.98\.1/g)?.length ?? 0, - 4, + 5, "Every Rust CI job must exercise the declared Rust 1.98.1 toolchain", ); From 09ff4ce69878c2052915b4dc17424a5e8889208f Mon Sep 17 00:00:00 2001 From: mubeda <1380447+mubeda@users.noreply.github.com> Date: Fri, 13 Nov 2026 16:48:50 -0300 Subject: [PATCH 4/5] ci: probe macOS exception recovery from a crate of its own The probe was an example of bibcode-desktop, so Cargo compiled the whole desktop and server graph a second time with the dev-dependency features before the bundle build compiled it again: 84 of the 215 minutes of the macOS arm64 row on 2026-11-13, and 81 of 228 on x64. The probe uses only objc2 and objc2-foundation, so it now lives in tools/objc-exception-probe with the objc2 features the desktop graph enables and runs under the shipped release profile. Locally it builds and passes in 16 seconds and aborts with CARGO_PROFILE_RELEASE_PANIC=abort. Both macOS rows get 330 minutes: arm64 used 215 of its 240 with a Rust cache, and the bundle build alone has taken 75 to 111 minutes. Co-Authored-By: Claude Fable 5.2 --- .github/workflows/ci.yml | 24 +++++++------- Cargo.lock | 8 +++++ Cargo.toml | 7 +++- docs/operations/ci.md | 24 +++++++------- docs/operations/release.md | 2 +- docs/reference/workspace-layout.md | 2 ++ docs/testing/macos-desktop.md | 2 +- scripts/ci-platform-contract.test.ts | 32 ++++++++++++++++--- scripts/hermetic-guard-contract.test.ts | 1 + scripts/rust-workspace.test.ts | 4 +++ tools/objc-exception-probe/Cargo.toml | 17 ++++++++++ .../objc-exception-probe/src/main.rs | 0 12 files changed, 93 insertions(+), 30 deletions(-) create mode 100644 tools/objc-exception-probe/Cargo.toml rename apps/desktop/src-tauri/examples/objc_exception_probe.rs => tools/objc-exception-probe/src/main.rs (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66cec1b812..d37ce962db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -388,18 +388,15 @@ jobs: - name: Test desktop Rust host run: vp run --filter @bibcode/desktop test - # The release symbol profile, as the bundle build below uses, so the - # dependencies both builds share compile once instead of once per - # profile. Strip and debug info do not change the unwinding it probes. + # A crate of its own with the desktop graph's objc2 features, under the + # shipped release profile. As a bibcode-desktop example it compiled the + # whole desktop and server graph a second time with test-only features: + # 84 of the 215 minutes of the macOS arm64 row on 2026-11-13. - name: Verify macOS release exception recovery if: matrix.platform == 'mac' - env: - CARGO_PROFILE_RELEASE_STRIP: none - CARGO_PROFILE_RELEASE_DEBUG: line-tables-only - CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO: packed run: | rust_target="$(rustc -Vv | sed -n 's/^host: //p')" - cargo run --release --target "$rust_target" -p bibcode-desktop --example objc_exception_probe + cargo run --release --target "$rust_target" -p bibcode-objc-exception-probe - name: Test Windows desktop E2E support contracts if: matrix.platform == 'win' @@ -481,7 +478,10 @@ jobs: platform: mac target: dmg arch: arm64 - jobTimeoutMinutes: 240 + # With a Rust cache this row took 215 minutes on 2026-11-13, 84 of + # them in the exception probe's own full build, which is gone. The + # bundle build alone has taken 75 to 111 minutes. + jobTimeoutMinutes: 330 # The release producer's runner (release.yml), so the Intel build # this tests and the cache it saves are the ones Release uses. # Seeded upgrade keeps macos-26-intel for newest-OS runtime coverage. @@ -490,9 +490,9 @@ jobs: platform: mac target: dmg arch: x64 - # Estimated from the 2026-11-13 run without a Rust cache, which was - # cancelled in the bundle step: about 260 minutes for these steps - # here and about 200 on arm64. Neither is a completed cold run. + # With a Rust cache this row took 228 minutes on 2026-11-13, 81 of + # them in the exception probe's own full build, which is gone. No + # run without a Rust cache has completed since. jobTimeoutMinutes: 330 steps: *native_desktop_steps diff --git a/Cargo.lock b/Cargo.lock index c8bdeabcb6..e002bc4cce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -501,6 +501,14 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "bibcode-objc-exception-probe" +version = "0.1.0" +dependencies = [ + "objc2", + "objc2-foundation", +] + [[package]] name = "bibcode-server" version = "0.15.3" diff --git a/Cargo.toml b/Cargo.toml index 530d4329e7..d50d06dbb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,10 @@ [workspace] -members = ["apps/desktop/src-tauri", "apps/server", "tools/updater-verifier"] +members = [ + "apps/desktop/src-tauri", + "apps/server", + "tools/objc-exception-probe", + "tools/updater-verifier", +] resolver = "3" [workspace.package] diff --git a/docs/operations/ci.md b/docs/operations/ci.md index 5416578f19..a6cbcc4c84 100644 --- a/docs/operations/ci.md +++ b/docs/operations/ci.md @@ -138,19 +138,21 @@ the merge. release symbol build (`STRIP=none`, line tables, a packed dSYM on macOS, a GNU build-id on Linux) that Release uses, so it also proves the `dump_syms` path on every pull request; the job installs `dump_syms` 2.3.9 for it, and - Rust Cache keeps the binary in `~/.cargo/bin`. The macOS exception probe uses - the same symbol profile so the dependencies both builds share compile once. + Rust Cache keeps the binary in `~/.cargo/bin`. Each matrix row declares its complete-job budget: - 240 minutes for macOS arm64, 330 minutes for macOS x64, and 120 minutes for the other four targets. + 330 minutes for each macOS row and 120 minutes for the other four targets. That budget includes setup, host tests, the macOS optimized exception-recovery probe, and native packaging without shortening any check or test-owned deadline. - Debug host tests cannot warm the optimized artifacts; the optimized example - also enables dev-dependency features (`tauri/test` and `tokio/test-util`) that - the ordinary production bundle does not use. Both optimized commands select - the same native target triple, but their feature differences require separate - compilation. Keep the real exception probe: a larger complete-job allowance - accommodates these deliberate checks without treating a cached build as the - cold-build baseline. The shared `scripts/run-msvc.mjs` launcher selects + Debug host tests cannot warm the optimized artifacts. The exception probe is + the `bibcode-objc-exception-probe` crate: it depends only on `objc2` and + `objc2-foundation`, with the `objc2` features the desktop graph enables, and + runs under the shipped release profile. Do not move it back into + `bibcode-desktop` as an example: an example enables the dev-dependency + features (`bibcode-server/hermetic-test-guard`, `tauri/test`, + `tokio/test-util`), so it compiled the whole desktop and server graph a + second time, 84 of the 215 minutes of the macOS arm64 row on 2026-11-13. + Check a change to the probe by running it once with + `CARGO_PROFILE_RELEASE_PANIC=abort`, where it must fail. The shared `scripts/run-msvc.mjs` launcher selects the requested MSVC architecture. After the Rust host tests, the Windows row alone runs `vp test run apps/desktop/e2e/support/test-project.test.ts apps/web/src/components/gitManager/gitManagerRepositoryAvailability.test.ts`. That step is the @@ -256,7 +258,7 @@ recently used entries above that. Which workflow saves which entry: A job cancelled by `timeout-minutes` or by a newer run skips the cache post step, so the Check and Test budgets above include a cold compile. Native -desktop keeps its 120-minute, 240-minute and 330-minute row budgets. +desktop keeps its 120-minute and 330-minute row budgets. Six native-desktop entries of about 1.6 GB each are about 10 GB on their own, before Check, Test, Crash smoke, Linux Git, and the setup-vp caches. Eviction diff --git a/docs/operations/release.md b/docs/operations/release.md index afe0184e2d..87aba4839d 100644 --- a/docs/operations/release.md +++ b/docs/operations/release.md @@ -71,7 +71,7 @@ Objective-C exception recovery when navigation cancels an in-flight request; `panic=abort` turns that recoverable condition into process termination. Thin LTO, one codegen unit, and symbol stripping remain enabled; the release jobs strip their binaries after dumping symbols (see [Symbol assets](#symbol-assets)). -Native macOS CI executes the release-profile `objc_exception_probe`, and the +Native macOS CI runs the `bibcode-objc-exception-probe` crate in the release profile, and the desktop crate rejects an abort profile on macOS. Each matrix job installs the frontend build toolchain, Rust, and the pinned diff --git a/docs/reference/workspace-layout.md b/docs/reference/workspace-layout.md index 0afa3b8181..341a10ec8e 100644 --- a/docs/reference/workspace-layout.md +++ b/docs/reference/workspace-layout.md @@ -23,6 +23,8 @@ explicit package subpaths. - `/infra/relay`: BiBCode Connect relay deployment package. - `/oxlint-plugin-bibcode`: repository-specific Oxlint rules. +- `/tools/objc-exception-probe`: Rust probe that checks Objective-C exception + recovery under the release profile on macOS. - `/tools/updater-verifier`: Rust tool used to verify signed updater artifacts. - `/third_party/portable-pty`: vendored Rust PTY crate included in the Cargo workspace. diff --git a/docs/testing/macos-desktop.md b/docs/testing/macos-desktop.md index 00ace5d0ae..b874f276bb 100644 --- a/docs/testing/macos-desktop.md +++ b/docs/testing/macos-desktop.md @@ -118,7 +118,7 @@ Validate Objective-C recovery with the actual optimized profile: ```sh rust_target="$(rustc -Vv | sed -n 's/^host: //p')" -cargo run --release --target "$rust_target" -p bibcode-desktop --example objc_exception_probe +cargo run --release --target "$rust_target" -p bibcode-objc-exception-probe ``` The probe must catch its native exception and exit successfully. The workspace diff --git a/scripts/ci-platform-contract.test.ts b/scripts/ci-platform-contract.test.ts index 25c72fef3d..b172f04ce1 100644 --- a/scripts/ci-platform-contract.test.ts +++ b/scripts/ci-platform-contract.test.ts @@ -329,14 +329,38 @@ describe("cross-platform CI contract", () => { const matrix = nativeDesktopRows(workflow); expect(matrix).toHaveLength(6); for (const row of matrix) { - // macOS's cold optimized probe and native package use distinct dependency - // feature sets, in addition to the unoptimized host test graph. - // Intel's steps were estimated at about 260 minutes without a Rust cache. - const minimum = row.platform !== "mac" ? 120 : row.arch === "x64" ? 330 : 240; + // macOS builds the unoptimized host test graph and the optimized native + // package with its symbols. + // With a Rust cache the macOS rows took 215 and 228 minutes on 2026-11-13. + const minimum = row.platform === "mac" ? 330 : 120; expect(row.jobTimeoutMinutes, String(row.label)).toBeGreaterThanOrEqual(minimum); } }); + it("probes macOS exception recovery without a second desktop build", () => { + const { workflow } = readWorkflow(CI_WORKFLOW_PATH); + const probe = requireJob(workflow, "native_desktop").steps?.find( + (step) => step.name === "Verify macOS release exception recovery", + ); + expect(probe?.if).toBe("matrix.platform == 'mac'"); + expect(probe?.run).toContain("cargo run --release"); + expect(probe?.run).toContain("-p bibcode-objc-exception-probe"); + // The shipped profile: an example of bibcode-desktop, or the symbol + // profile, would compile the desktop and server graph a second time. + expect(probe?.run).not.toContain("bibcode-desktop"); + expect(probe?.env).toBeUndefined(); + const manifest = NodeFS.readFileSync( + NodePath.join(REPOSITORY_ROOT, "tools/objc-exception-probe/Cargo.toml"), + "utf8", + ); + expect(manifest).not.toMatch(/bibcode-(desktop|server)|tauri/); + expect( + NodeFS.existsSync( + NodePath.join(REPOSITORY_ROOT, "apps/desktop/src-tauri/examples/objc_exception_probe.rs"), + ), + ).toBe(false); + }); + it("runs the macOS server tests off the native desktop job, without saving a cache", () => { const { workflow } = readWorkflow(CI_WORKFLOW_PATH); const job = requireJob(workflow, "server_tests_macos"); diff --git a/scripts/hermetic-guard-contract.test.ts b/scripts/hermetic-guard-contract.test.ts index 0bfc11c586..7b5ecbc7c4 100644 --- a/scripts/hermetic-guard-contract.test.ts +++ b/scripts/hermetic-guard-contract.test.ts @@ -11,6 +11,7 @@ const manifests = [ "Cargo.toml", "apps/server/Cargo.toml", "apps/desktop/src-tauri/Cargo.toml", + "tools/objc-exception-probe/Cargo.toml", "tools/updater-verifier/Cargo.toml", ]; diff --git a/scripts/rust-workspace.test.ts b/scripts/rust-workspace.test.ts index 97c9542dd1..08c6171ce3 100644 --- a/scripts/rust-workspace.test.ts +++ b/scripts/rust-workspace.test.ts @@ -27,6 +27,10 @@ const rustPackages = [ packageJsonPath: "apps/server/package.json", cargoSelectedScriptNames: ["build", "start", "test", "typecheck"], }, + { + cargoPackage: "bibcode-objc-exception-probe", + memberPath: "tools/objc-exception-probe", + }, { cargoPackage: "bibcode-updater-verifier", memberPath: "tools/updater-verifier", diff --git a/tools/objc-exception-probe/Cargo.toml b/tools/objc-exception-probe/Cargo.toml new file mode 100644 index 0000000000..76c312cfa0 --- /dev/null +++ b/tools/objc-exception-probe/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "bibcode-objc-exception-probe" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +publish = false + +# The objc2 features the macOS desktop app's dependency graph enables (`cargo +# tree -e features -i objc2` on the desktop package), so this probe unwinds +# through the same objc2 build as the app, under the same release profile. +# No dependency on the app's own crates: that is what keeps this build small. +[target.'cfg(target_os = "macos")'.dependencies] +objc2 = { workspace = true, features = ["exception", "disable-encoding-assertions"] } +objc2-foundation.workspace = true + +[lints] +workspace = true diff --git a/apps/desktop/src-tauri/examples/objc_exception_probe.rs b/tools/objc-exception-probe/src/main.rs similarity index 100% rename from apps/desktop/src-tauri/examples/objc_exception_probe.rs rename to tools/objc-exception-probe/src/main.rs From 988f0335c23196d5987d785f968761dd037c0d98 Mon Sep 17 00:00:00 2001 From: mubeda <1380447+mubeda@users.noreply.github.com> Date: Fri, 13 Nov 2026 16:51:36 -0300 Subject: [PATCH 5/5] ci: correct two comments about the exception probe's build Co-Authored-By: Claude Fable 5.2 --- .github/workflows/ci.yml | 6 +++--- scripts/ci-platform-contract.test.ts | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d37ce962db..f5dc54bee0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -290,9 +290,9 @@ jobs: native_desktop: name: Native desktop - ${{ matrix.label }} runs-on: ${{ matrix.runner }} - # Host tests, the optimized example (with dev features), and the native - # bundle have distinct Cargo builds. Budget the complete cold sequence; - # Intel needs additional headroom without shortening any validation phase. + # Host tests and the native bundle are distinct Cargo builds of the desktop + # and server graph. Budget the complete cold sequence without shortening + # any validation phase. timeout-minutes: ${{ matrix.jobTimeoutMinutes }} strategy: fail-fast: false diff --git a/scripts/ci-platform-contract.test.ts b/scripts/ci-platform-contract.test.ts index b172f04ce1..0a1269a669 100644 --- a/scripts/ci-platform-contract.test.ts +++ b/scripts/ci-platform-contract.test.ts @@ -345,8 +345,9 @@ describe("cross-platform CI contract", () => { expect(probe?.if).toBe("matrix.platform == 'mac'"); expect(probe?.run).toContain("cargo run --release"); expect(probe?.run).toContain("-p bibcode-objc-exception-probe"); - // The shipped profile: an example of bibcode-desktop, or the symbol - // profile, would compile the desktop and server graph a second time. + // An example of bibcode-desktop would compile the desktop and server + // graph a second time. No profile overrides: the probe tests the shipped + // release profile. expect(probe?.run).not.toContain("bibcode-desktop"); expect(probe?.env).toBeUndefined(); const manifest = NodeFS.readFileSync(