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
121 changes: 78 additions & 43 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand All @@ -424,14 +421,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)
Expand All @@ -455,27 +444,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
Expand All @@ -493,7 +461,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:
Expand All @@ -510,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.
Expand All @@ -519,9 +490,73 @@ jobs:
platform: mac
target: dmg
arch: x64
jobTimeoutMinutes: 240
# 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

# 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.
Expand Down
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
36 changes: 24 additions & 12 deletions docs/operations/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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 |
Expand Down Expand Up @@ -137,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 both macOS architectures, 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
Expand All @@ -172,6 +175,15 @@ 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. 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
Expand Down Expand Up @@ -246,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 and 240-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
Expand Down
2 changes: 1 addition & 1 deletion docs/operations/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/workspace-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/testing/macos-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 54 additions & 3 deletions scripts/ci-platform-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,13 +329,64 @@ 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.
const minimum = row.platform === "mac" ? 240 : 120;
// 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");
// 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(
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");
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) => !/matrix\.platform == '(win|linux)'/.test(String(step.if ?? "")))
.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);
Expand Down
1 change: 1 addition & 0 deletions scripts/hermetic-guard-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
];

Expand Down
Loading
Loading