diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 00000000..6e47254d --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,11 @@ +# cargo-nextest, which `make coverage-gate` runs the Rust tests with (cargo llvm-cov nextest). +# Each test runs in a process of its own and the tests of all binaries share the workers, where +# `cargo test` ran the binaries one after another (2026-11-10: 101 binaries, 426 s of their own +# time on the runner, voltip-core's tests/long.rs alone 209 s). +[profile.default] +# Every failure of the run is reported, not only the first. +fail-fast = false +# A test still running after 60 s is reported as SLOW; after ten such periods it is stopped and +# fails as TIMEOUT, so a hung test fails the gate and is named in its log long before the CI +# step's own 25-minute limit. +slow-timeout = { period = "60s", terminate-after = 10 } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 786e32ea..014d1b5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,7 @@ env: # Toolchain pins. The Rust toolchain itself comes from rust-toolchain.toml (rustup reads it); # these are the cargo subcommands `make verify` / `make windows-x64` need. CARGO_LLVM_COV_VERSION: 0.8.7 + CARGO_NEXTEST_VERSION: 0.9.151 CARGO_DENY_VERSION: 0.19.9 CARGO_XWIN_VERSION: 0.20.2 CARGO_ABOUT_VERSION: 0.9.2 @@ -184,10 +185,10 @@ jobs: rustup target add aarch64-apple-darwin x86_64-pc-windows-msvc rustc --version && cargo --version && cargo clippy --version && cargo fmt --version - - name: cargo-llvm-cov and cargo-deny (prebuilt, pinned) + - name: cargo-llvm-cov, cargo-nextest and cargo-deny (prebuilt, pinned) uses: taiki-e/install-action@c509219cf42fba86913069efd0ade2458c59337b # v2.90.1 with: - tool: cargo-llvm-cov@${{ env.CARGO_LLVM_COV_VERSION }},cargo-deny@${{ env.CARGO_DENY_VERSION }} + tool: cargo-llvm-cov@${{ env.CARGO_LLVM_COV_VERSION }},cargo-nextest@${{ env.CARGO_NEXTEST_VERSION }},cargo-deny@${{ env.CARGO_DENY_VERSION }} - name: Cache Cargo registry and target/ uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 @@ -226,8 +227,9 @@ jobs: run: pnpm install --frozen-lockfile # 2–6 minutes on main (2026-09-30). A test that hangs fails the step here instead of holding - # the job to its hour (and a release candidate's source gate to its 45 minutes); the log - # below still uploads and names the test "running for over 60 seconds". + # the job to its hour (and a release candidate's source gate to its 45 minutes). nextest + # names it as SLOW after 60 seconds and stops it as TIMEOUT after ten minutes + # (.config/nextest.toml); the log below still uploads. - name: Rust gates (VERIFY_GATES=rust scripts/verify-all.sh) timeout-minutes: 25 env: diff --git a/AGENTS.md b/AGENTS.md index fc9264b7..1789fa2f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,9 +129,10 @@ runs landed in a row on 2026-09-30, and one of them stalled a release for an hou - The `macos` jobs do not run on pull requests. After a merge, check that `main`'s run is green on both Macs; a red `macos` job is fixed like any other, and until it is, `CI Success` on `main` is red and no release candidate passes its source gate. -- A CI job that runs twice its usual time is a hung test. The Rust gates step stops at 25 minutes - by itself; cancel the run to learn sooner. Either way the gate logs still upload, and the hung - test is the one "running for over 60 seconds". +- A CI job that runs twice its usual time is a hung test. The coverage gate runs the tests with + cargo-nextest (`.config/nextest.toml`), which names a test as `SLOW` after 60 seconds and stops + it as `TIMEOUT` after ten minutes; the Rust gates step stops at 25 minutes by itself. Cancel the + run to learn sooner. Either way the gate logs still upload and name the hung test. - Ubuntu packages in `ci.yml`, `release-candidate.yml` and `release.yml` go through `.github/scripts/apt-install.sh`, never a bare `apt-get`: each download attempt has a deadline, and a later attempt fetches from archive.ubuntu.com instead of the Azure mirror (2026-10-01: that diff --git a/Makefile b/Makefile index f58da2df..4c398641 100644 --- a/Makefile +++ b/Makefile @@ -51,9 +51,10 @@ frontend-dist-dirs: @mkdir -p apps/desktop/dist apps/mobile/dist .PHONY: coverage-gate -coverage-gate: frontend-dist-dirs ## Run the Rust tests once, instrumented, and enforce the line-coverage floor (COVERAGE_MIN) +coverage-gate: frontend-dist-dirs ## Run the Rust tests once, instrumented (cargo-nextest, .config/nextest.toml), and enforce the line-coverage floor (COVERAGE_MIN) @command -v cargo-llvm-cov >/dev/null 2>&1 || { echo "cargo-llvm-cov not found — cargo install cargo-llvm-cov"; exit 2; } - @$(CARGO) llvm-cov --workspace --all-targets --features "$(RUST_FEATURES)" --summary-only \ + @command -v cargo-nextest >/dev/null 2>&1 || { echo "cargo-nextest not found — cargo install cargo-nextest --locked"; exit 2; } + @$(CARGO) llvm-cov nextest --workspace --all-targets --features "$(RUST_FEATURES)" --summary-only \ --ignore-filename-regex '$(COVERAGE_EXCLUDE)' \ | tee -a /dev/stderr \ | awk -v min='$(COVERAGE_MIN)' '\ diff --git a/docs/runbook.md b/docs/runbook.md index d5f1e7b3..826e59d9 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -113,7 +113,7 @@ Relay 无状态(内存里只有活动会话 / 频道),可水平扩展前 工作流都跑在 GitHub 托管的 runner 上: - `ci.yml`:push `main` / PR → `changes`(`.github/scripts/changed-code.sh`:只改了 `docs/**`、`*.md`、`LICENSE*` 时 `code=false`;新分支、API 失败或一次推送 300 个以上文件都算改了代码)→ 并行的 `verify-rust`(`VERIFY_GATES=rust`:格式、features、clippy、交叉检查、覆盖率下跑一遍测试、cargo-deny、IPC e2e)与 `verify-web`(`VERIFY_GATES=web`:web 门禁、台账、主机名守卫、发布脚本测试,外加带 `VOLTIP_PRODUCTION_HOSTS` 的守卫;fork 与 Dependabot 的 PR 没有 secrets,跳过这一步)、`codecov`(上传覆盖率,只做报告,不阻塞)、`windows-cross`(`make windows-x64`,无内置服务)→ `windows-native`(Windows Server 上无头运行便携包与安装后的 exe)、`hooks-windows`(Windows 桌面上真实的单键钩子)、`smoke-desktop`(Xvfb 与纯 Wayland 冒烟、X11 真实钩子、无头识别)、`android`(对 Android 目标跑 clippy;用 `scripts/build-android-rn.sh --unsigned` 构建 React Native 版未签名的 release APK 与 AAB,用当次生成的临时密钥签名,再跑与候选相同的包检查)→ `android-device`(把这个 APK 装进 Android 15 模拟器并启动:首屏出现、20 秒内不退出、日志里没有致命错误,`.github/scripts/android-device-smoke.sh`)、`ci-success` 聚合(CI 的总结论);另有 `candidate-status` 给不是发布 PR 的 PR head 写 `Release candidate = success`(见下面的 ruleset)。只改文档时 `verify-web` 照常跑,其余构建、测试、平台 job 跳过;`ci-success` 只在 `changes` 判定为只改文档时接受跳过,其他任何跳过、失败或取消都判失败。CI 不注入任何内置引擎值。 -- CI 的速度(2026-09-28 实测后调整):Rust 测试只在覆盖率门禁里跑一遍(原先 `cargo test` 与 `cargo llvm-cov` 各编一遍、各跑一遍,各约 170 秒);`verify` 拆成可并行的两半;Cargo 缓存只从 `main` 保存(PR 的缓存只有同一个 PR 能用,而仓库 10 GB 的上限会把 `main` 的挤掉),不缓存 `~/.cargo/bin`;dev / test 构建只留行号表(`CARGO_PROFILE_DEV_DEBUG=line-tables-only`);清理预装 SDK 只在剩余空间不足 40 GB 时做(`.github/scripts/free-disk-space.sh`,托管 runner 实测开跑时有 86 GB);CI 的 Windows 包复用按全部输入(锁文件、清单、`about.toml`、生成脚本与许可证文本)缓存的第三方声明,省掉约两分钟的 cargo-about(`VOLTIP_NOTICES_CACHED=1`,发布总是重新生成);下载失败重试(`CARGO_NET_RETRY`、`RUSTUP_MAX_RETRIES`);桌面壳的 lib 只编成 `rlib`(2026-10-05):Tauri 模板为手机准备的 `staticlib` 和 `cdylib` 让 release 构建的每个依赖都同时生成机器码和 bitcode,去掉后依赖只生成 thin LTO 用的 bitcode(`-C linker-plugin-lto`),本机 32 核冷构建 224 → 156 秒,target/ 3.3 → 2.4 GB;macOS 腿的钥匙串测试程序是带 `keychain-harness` feature 的 bin 而不是 example(example 会带上 dev-dependencies,它们的 feature 让应用构建的 60 多个 crate 重编一遍,Intel Mac 冷跑时 11 分钟),应用构建之后只需再编这个包本身;voltip-server 的 release 构建单独用 16 个代码生成单元(2026-11-10,根 `Cargo.toml`):只有一个单元时整个 crate 是一个 LLVM 模块,LLVM 优化要 862 秒、ThinLTO 链接 303 秒,候选的 Linux 腿为此等 19–34 分钟;拆开后 4 个并行任务下分别是 2 秒和 26 秒,其余包仍是一个单元。`verify-*.log` 里每道门都记了耗时(`secs=`)。 +- CI 的速度(2026-09-28 实测后调整):Rust 测试只在覆盖率门禁里跑一遍(原先 `cargo test` 与 `cargo llvm-cov` 各编一遍、各跑一遍,各约 170 秒);这一遍由 cargo-nextest 运行(2026-11-10,`cargo llvm-cov nextest`,设置在 `.config/nextest.toml`):每个测试一个进程,所有测试二进制的测试一起排队,不再等一个二进制跑完才开始下一个;本机 32 核上整个覆盖率门禁 431 → 278 秒,测试数(2086 个通过、42 个跳过)和行覆盖率(92.95%)不变;跑过 60 秒的测试在日志里标为 `SLOW`,跑过 10 分钟的被终止并记为 `TIMEOUT`;`verify` 拆成可并行的两半;Cargo 缓存只从 `main` 保存(PR 的缓存只有同一个 PR 能用,而仓库 10 GB 的上限会把 `main` 的挤掉),不缓存 `~/.cargo/bin`;dev / test 构建只留行号表(`CARGO_PROFILE_DEV_DEBUG=line-tables-only`);清理预装 SDK 只在剩余空间不足 40 GB 时做(`.github/scripts/free-disk-space.sh`,托管 runner 实测开跑时有 86 GB);CI 的 Windows 包复用按全部输入(锁文件、清单、`about.toml`、生成脚本与许可证文本)缓存的第三方声明,省掉约两分钟的 cargo-about(`VOLTIP_NOTICES_CACHED=1`,发布总是重新生成);下载失败重试(`CARGO_NET_RETRY`、`RUSTUP_MAX_RETRIES`);桌面壳的 lib 只编成 `rlib`(2026-10-05):Tauri 模板为手机准备的 `staticlib` 和 `cdylib` 让 release 构建的每个依赖都同时生成机器码和 bitcode,去掉后依赖只生成 thin LTO 用的 bitcode(`-C linker-plugin-lto`),本机 32 核冷构建 224 → 156 秒,target/ 3.3 → 2.4 GB;macOS 腿的钥匙串测试程序是带 `keychain-harness` feature 的 bin 而不是 example(example 会带上 dev-dependencies,它们的 feature 让应用构建的 60 多个 crate 重编一遍,Intel Mac 冷跑时 11 分钟),应用构建之后只需再编这个包本身;voltip-server 的 release 构建单独用 16 个代码生成单元(2026-11-10,根 `Cargo.toml`):只有一个单元时整个 crate 是一个 LLVM 模块,LLVM 优化要 862 秒、ThinLTO 链接 303 秒,候选的 Linux 腿为此等 19–34 分钟;拆开后 4 个并行任务下分别是 2 秒和 26 秒,其余包仍是一个单元。`verify-*.log` 里每道门都记了耗时(`secs=`)。 - `ci.yml` 的 `macos` job(2026-09-28 从 `macos.yml` 并入):推送 `main` 或手动运行时在 `macos-15`(Apple 芯片)与 `macos-15-intel`(Intel)上构建 ad-hoc 签名的 `.app` 和 `.dmg`(发布包用固定证书,见下面「macOS 签名」),用 `.github/scripts/check-macos-bundle.sh` 检查,并确认发布用的检查会拒绝这个 ad-hoc 包,跑 macOS 单元测试、真实的事件 tap(仅 Apple 芯片,先在 TCC 里给测试程序授予辅助功能)、无头识别,以及桌面上的 `scripts/smoke-tray-macos.sh`(标题栏左端的内容离红绿灯至少 12 pt,菜单栏图标、菜单与各项操作,截图随产物保存);不跑在 pull request 上。在 push 和手动运行时 `CI Success` 等它(规则在 `.github/scripts/ci-success.sh`,只有 pull request 上允许它被跳过),所以 `main` 上任何一台 Mac 失败,下一个候选都过不了 `source-gate`;Cargo 缓存从 `main` 保存。 - release-please 的 PR(作者 `github-actions[bot]`、分支 `release-please--branches--main--*`、标签 `autorelease: pending`)和它合并到 `main` 的发布提交只改版本号和 CHANGELOG,只跑 `verify-web`:候选在同一个 head 上构建并检查每个安装包,而 head 的基点已经通过 CI(`.github/scripts/changed-code.sh`)。同样的改动换个作者或没有标签,照常跑完整 CI。 - 发布是「构建一次再晋升」(2026-09-28 用户选定):安装包在发布 PR 上构建、签名、证明并封存,合并后只校验和发布,不再编译。