From a51fa02937c9d1baa3726d7681ffe47356213847 Mon Sep 17 00:00:00 2001 From: sunerpy Date: Tue, 10 Nov 2026 14:39:53 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20=E5=8F=91=E7=89=88=E5=80=99=E9=80=89?= =?UTF-8?q?=E7=9A=84=20Intel=20=E5=8C=85=E6=94=B9=E5=9C=A8=20Apple=20?= =?UTF-8?q?=E8=8A=AF=E7=89=87=E7=9A=84=20macos-15=20=E4=B8=8A=E4=BA=A4?= =?UTF-8?q?=E5=8F=89=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.3.1 的候选(42733161564)里 Intel 腿用了 26.8 分钟,其中 macos-15-intel 上冷编译 21.2 分钟; 同样的构建在 macos-15 上 12.0 分钟。macos-15-intel 是 GitHub 最后一个 x86_64 镜像,2027-08 下线。 - release-candidate.yml:x86_64-apple-darwin 腿的 runner 改为 macos-15,构建本来就带 --target "$MACOS_TARGET";release-targets.json 同步为 runner macos-15、cross true。 - check-macos-bundle.sh 不变:每个 Mach-O 只含本腿架构、minos ≤ 11.0,并运行包里的 --version(在 Apple 芯片上由 Rosetta 2 执行)。 - Intel 真机运行仍在 ci.yml 的 macos job 与 install-scripts.yml 的 macos-15-intel 上。 - 新测试 test_the_intel_package_builds_on_apple_silicon 在改动前失败(runner 为 macos-15-intel)。 --- .github/release-targets.json | 4 ++-- .github/workflows/release-candidate.yml | 10 +++++++--- docs/acceptance.md | 2 +- docs/roadmap.md | 2 +- docs/runbook.md | 1 + scripts/release/test_release_workflows.py | 18 ++++++++++++++++++ 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/.github/release-targets.json b/.github/release-targets.json index 3dcc4930..be332d22 100644 --- a/.github/release-targets.json +++ b/.github/release-targets.json @@ -39,9 +39,9 @@ }, { "bundles": ["dmg", "app"], - "cross": false, + "cross": true, "platform": "macos", - "runner": "macos-15-intel", + "runner": "macos-15", "target": "x86_64-apple-darwin", "updater_bundle": "app", "updater_platform": "darwin-x86_64" diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 180f4adc..2cb7f76a 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -854,12 +854,16 @@ jobs: if-no-files-found: error retention-days: 1 - # macOS, built natively for both processors (Tauri names the Intel dmg `x64`): + # macOS, both processors on Apple silicon (Tauri names the Intel dmg `x64`): # Voltip__.dmg to install from and, with the updater, # Voltip__.app.tar.gz (+ .sig) for the updater (Tauri writes it as # Voltip.app.tar.gz; `collect --rename` gives it the release name). tauri.macos.conf.json signs # the bundle ad hoc; the checks below prove the seal, the architecture, the macOS 11 floor and - # the dmg's contents. macos-15-intel is GitHub's last x86_64 image (available until 2027-08). + # the dmg's contents. The Intel package is cross-built with `--target x86_64-apple-darwin` + # (2026-11-10): on macos-15-intel the leg's cold build took 21.2 minutes, the same build 12.0 on + # macos-15, and that image, GitHub's last x86_64 one, goes away in 2027-08. The bundle check + # runs the app's --version, which here runs the x86_64 binary under Rosetta 2; ci.yml's macos + # job and install-scripts.yml still run the Intel build on macos-15-intel. bundle-macos: name: Bundle ${{ matrix.target }} needs: prepare @@ -871,7 +875,7 @@ jobs: runner: macos-15 arch: aarch64 - target: x86_64-apple-darwin - runner: macos-15-intel + runner: macos-15 arch: x64 runs-on: ${{ matrix.runner }} timeout-minutes: 120 diff --git a/docs/acceptance.md b/docs/acceptance.md index 70d16552..61962e67 100644 --- a/docs/acceptance.md +++ b/docs/acceptance.md @@ -153,7 +153,7 @@ | Windows 注入前预检(§15.3):完整性级别比较决策表、RID 分档、麦克风 `ConsentStore` 键优先级表;Win32 查询只在 `platform/windows.rs`(crate 级 `deny(unsafe_code)`,该模块单独 `allow`,每个 `unsafe` 带 SAFETY 注释);第 4 步显示预检结论 | `crates/voltip-platform/src/windows.rs`、`apps/desktop/src-tauri/src/platform/windows.rs`、`apps/desktop/src/pages/Onboarding.tsx` | `crates/voltip-platform/src/windows.rs@inject_decision_table`、`crates/voltip-platform/src/windows.rs@integrity_levels_bucket_rids_and_order`、`crates/voltip-platform/src/windows.rs@consent_store_key_priority_table`、`apps/desktop/src/pages/Onboarding.test.tsx@step 4 names the Windows injection preflight` | [~](真机 UIPI / 安全桌面 [!]) | | macOS 激活策略 / Cmd+V 键码 / 托盘(§15.2、§15.4):`activation_policy_plan(start_hidden, tray_available)` 仅两者皆真时在 build 与 run 之间设 `Accessory`,`RunEvent::Reopen` 显示主窗并补装缺失托盘;`cmd_v_keycode` 回退表与 `PASTE_HOLD_MS = 100`;托盘图标为运行时渲染的应用标志加状态角标(macOS 模板图随菜单栏明暗,Windows 彩色标志),右键菜单(打开 / 设置 / 检查更新 / 退出,中英文),关闭主窗口按平台隐藏或退出 | `crates/voltip-platform/src/{macos,tray}.rs`、`apps/desktop/src-tauri/src/platform/{mod,tray}.rs` | `crates/voltip-platform/src/macos.rs@activation_policy_table`、`crates/voltip-platform/src/macos.rs@cmd_v_keycode_fallback_table`、`crates/voltip-platform/src/tray.rs@the_colour_icon_is_the_logo`、`crates/voltip-platform/src/tray.rs@the_template_is_the_bars_and_the_cursor_alone_in_ink`、`crates/voltip-platform/src/tray.rs@regression_the_bars_and_the_cursor_have_sharp_sides_at_every_size`、`crates/voltip-platform/src/tray.rs@the_fit_keeps_the_design_at_1024_and_puts_every_edge_on_a_pixel_below`、`crates/voltip-platform/src/tray.rs@the_menu_opens_the_window_settings_updates_and_quits`、`crates/voltip-platform/src/tray.rs@closing_the_main_window_hides_it_where_something_brings_it_back`、`apps/desktop/src/app/tray-requests.test.tsx@regression: the tray's 设置… opens the settings dialog, 检查更新… the update dialog with a fresh check`、`apps/desktop/src-tauri/src/platform/mod.rs@tray_glyph_follows_the_dictation_phase`(托盘真机:`install-scripts.yml` 运行 36474636553 在 windows-2025、macos-15、macos-15-intel 的桌面上操作已发布的 0.0.5:`scripts/smoke-tray-windows.ps1` 找到通知区溢出面板里的按钮,像素是彩色标志(深蓝方块、浅色与橙色两笔),真实右键弹出原生菜单「Open Voltip / Settings… / Check for Updates… / Quit Voltip」;`scripts/smoke-tray-macos.sh` 用辅助功能读菜单栏项,是模板 V;两边逐项点击:打开让主窗口出现,设置打开主窗口里的 Settings 对话框,检查更新得到 `no update available version="0.0.5"`,退出让进程以 0 退出,截图与日志在运行的 `tray-*` 产物里) | [~](托盘真机 [x];Reopen 真机 [!]) | | 三平台打包配置(§15.5):`tauri.macos.conf.json` `bundle.macOS`(`minimumSystemVersion 11.0`、两只 sherpa dylib 走 `frameworks`、`Entitlements.plist` 仅 `device.audio-input`、`signingIdentity "-"`(本地与普通 CI 为 ad-hoc;发布包由候选工作流用固定证书签名)与 `hardenedRuntime false`、en / zh-Hans `InfoPlist.strings`),`Info.plist` `NSMicrophoneUsageDescription`,`build.rs` 暂存 `resources/macos/`;`tauri.windows.conf.json` `webviewInstallMode embedBootstrapper`、`allowDowngrades false`、`nsis.installMode currentUser`;各目标合并配置经 Tauri 自身 schema 解析 | `apps/desktop/src-tauri/{tauri.macos.conf.json,tauri.windows.conf.json,Info.plist,Entitlements.plist,build.rs}`、`apps/desktop/src-tauri/macos/*.lproj/InfoPlist.strings` | `apps/desktop/src-tauri/tests/bundle.rs@every_desktop_target_config_parses_with_the_tauri_schema`、`apps/desktop/src-tauri/tests/bundle.rs@macos_bundle_carries_the_dylibs_entitlements_minimum_version_and_microphone_string`、`apps/desktop/src-tauri/tests/bundle.rs@windows_bundle_installs_per_user_refuses_downgrades_and_names_the_webview_mode`、`apps/desktop/src-tauri/tests/bundle.rs@the_macos_and_windows_bundle_settings_stay_in_their_own_platform_files`、`apps/desktop/src/app/window-config.test.ts@lets each platform override touch only its own bundle block` | [~](`.app` 与 `.dmg` 在 GitHub 托管 macos-15 上实际打包、ad-hoc 签名并由 `.github/scripts/check-macos-bundle.sh` 检查,见 CI `macos` job 的运行摘要产物;发布流程的 macOS 腿出 `.dmg` 与更新用的 `.app.tar.gz`;公证不做,真机安装与升级待 `docs/acceptance/macos/manual-checklist.md`) | -| macOS 进发布流程(2026-09-28 用户建议;同日补充 Intel 芯片):发布新增 `bundle-macos` 矩阵腿(2026-09-28 起在 `release-candidate.yml` 里构建一次,见下面「发布改为构建一次再晋升」),Apple 芯片在 macos-15、Intel 在 macos-15-intel 原生构建,`Voltip_<版本>_{aarch64,x64}.dmg` 与更新用的 `Voltip_<版本>_{aarch64,x64}.app.tar.gz`(+ `.sig`,由 Tauri 的 `Voltip.app.tar.gz` 改名)进 GitHub Release,`latest.json` 必须含 `darwin-aarch64` 与 `darwin-x86_64`,SHA256SUMS 与证明覆盖新文件;包内带第三方许可文本(`tauri.package-macos.conf.json`);出包前检查封签完整(2026-09-29 起发布包要求固定证书,见下面「macOS 发布包固定签名」)、`Contents/MacOS` 与 `Contents/Frameworks` 里每个 Mach-O 只含本腿的架构且 minos ≤ 11.0、版本号、dmg 可挂载且带「应用程序」替身;CI 的 `macos` job 每次推送 `main` 在两种芯片上用同一脚本检查同样签名的 `.app` 与 `.dmg` 并跑无头识别;Tauri CLI 在 macOS 上改用钉住 SHA-256 的官方 zip | `.github/workflows/{release-candidate,ci}.yml`、`.github/scripts/{check-macos-bundle,install-cargo-tauri}.sh`、`.github/release-targets.json`、`scripts/release/updater-json.py`、`scripts/lib/artefact-checks.sh`、`apps/desktop/src-tauri/tauri.package-macos.conf.json` | `scripts/release/test_updater_json.py@test_collects_the_dmg_and_the_renamed_updater_tarball`、`scripts/release/test_updater_json.py@test_without_the_updater_only_the_dmg_ships`、`scripts/release/test_updater_json.py@test_the_updater_tarball_is_required_when_the_updater_is_on`、`scripts/release/test_updater_json.py@test_the_mac_updater_url_is_the_renamed_tarball`、`apps/desktop/src-tauri/tests/bundle.rs@the_macos_release_bundle_carries_the_notices_through_its_package_overlay` | [~](待首个带 macOS 的发布运行,以及 Mac 真机上的安装与升级:`docs/acceptance/macos/manual-checklist.md` 第 13、14 项) | +| macOS 进发布流程(2026-09-28 用户建议;同日补充 Intel 芯片):发布新增 `bundle-macos` 矩阵腿(2026-09-28 起在 `release-candidate.yml` 里构建一次,见下面「发布改为构建一次再晋升」),两种芯片都在 macos-15 上构建(Apple 芯片原生;Intel 自 2026-11-10 起交叉构建,包里的 `--version` 由 Rosetta 2 运行,Intel 真机运行在 CI 与安装脚本的 macos-15-intel 上),`Voltip_<版本>_{aarch64,x64}.dmg` 与更新用的 `Voltip_<版本>_{aarch64,x64}.app.tar.gz`(+ `.sig`,由 Tauri 的 `Voltip.app.tar.gz` 改名)进 GitHub Release,`latest.json` 必须含 `darwin-aarch64` 与 `darwin-x86_64`,SHA256SUMS 与证明覆盖新文件;包内带第三方许可文本(`tauri.package-macos.conf.json`);出包前检查封签完整(2026-09-29 起发布包要求固定证书,见下面「macOS 发布包固定签名」)、`Contents/MacOS` 与 `Contents/Frameworks` 里每个 Mach-O 只含本腿的架构且 minos ≤ 11.0、版本号、dmg 可挂载且带「应用程序」替身;CI 的 `macos` job 每次推送 `main` 在两种芯片上用同一脚本检查同样签名的 `.app` 与 `.dmg` 并跑无头识别;Tauri CLI 在 macOS 上改用钉住 SHA-256 的官方 zip | `.github/workflows/{release-candidate,ci}.yml`、`.github/scripts/{check-macos-bundle,install-cargo-tauri}.sh`、`.github/release-targets.json`、`scripts/release/updater-json.py`、`scripts/lib/artefact-checks.sh`、`apps/desktop/src-tauri/tauri.package-macos.conf.json` | `scripts/release/test_updater_json.py@test_collects_the_dmg_and_the_renamed_updater_tarball`、`scripts/release/test_updater_json.py@test_without_the_updater_only_the_dmg_ships`、`scripts/release/test_updater_json.py@test_the_updater_tarball_is_required_when_the_updater_is_on`、`scripts/release/test_updater_json.py@test_the_mac_updater_url_is_the_renamed_tarball`、`apps/desktop/src-tauri/tests/bundle.rs@the_macos_release_bundle_carries_the_notices_through_its_package_overlay`、`scripts/release/test_release_workflows.py@test_the_intel_package_builds_on_apple_silicon` | [~](待首个带 macOS 的发布运行,以及 Mac 真机上的安装与升级:`docs/acceptance/macos/manual-checklist.md` 第 13、14 项) | | macOS 发布包固定签名(2026-09-29,追加 C、D):发布候选的两条 macOS 腿把「Voltip Code Signing」(自签名,100 年)导入本 job 的临时钥匙串并信任它用于代码签名,Tauri 用 `APPLE_SIGNING_IDENTITY` 签名,`hardenedRuntime` 仍关闭;`check-macos-bundle.sh --expect-requirement` 要求 app 的 designated requirement 等于 `release-targets.json` 的 `macos_signing`(两条腿同一个值)、每个 Mach-O 同一证书,ad-hoc 包失败;`prepare` 要求三个证书 secrets 存在;CI `macos` job 确认这道检查拒绝 ad-hoc 包;设置向导给出辅助功能与麦克风两条重置命令,权限提示说明关掉再打开 | `.github/workflows/{release-candidate,ci}.yml`、`.github/scripts/{macos-signing-keychain,check-macos-bundle,check-keychain-across-updates,check-keychain-migration,check-keychain-preinstall}.sh`、`crates/voltip-identity/src/{secret_store,per_build,handoff,macos_keychain}.rs`、`apps/desktop/src-tauri/src/bin/keychain_preinstall.rs`、`apps/desktop/src-tauri/src/{keychain_handoff,restart}.rs`、`scripts/release/macos-signature.py`、`.github/release-targets.json`、`apps/desktop/src/pages/Onboarding.tsx`、`docs/runbook.md` | `scripts/release/test_macos_signature.py@test_an_ad_hoc_app_fails`、`scripts/release/test_macos_signature.py@test_the_release_certificate_passes_in_either_spelling`、`scripts/release/test_macos_signature.py@test_an_ad_hoc_or_foreign_library_fails`、`scripts/release/test_macos_signature.py@test_the_repository_names_the_release_certificate`、`scripts/release/test_release_workflows.py@test_the_macos_legs_sign_with_the_release_certificate`、`apps/desktop/src/pages/Onboarding.test.tsx@step 1 shows the live macOS permission states, the unsigned banner and greys the sidebar`、`.github/scripts/check-keychain-across-updates.sh@build b, a later build, is refused that entry`、`.github/scripts/check-keychain-across-updates.sh@build b removes that entry without reading it`、`.github/scripts/check-keychain-across-updates.sh@build b reads it there: no partition check`、`crates/voltip-identity/src/per_build.rs@regression_an_in_app_update_reads_the_keychain_without_asking`、`crates/voltip-identity/src/per_build.rs@regression_a_staged_update_persists_before_install_and_cleans_up_after_restart`、`crates/voltip-identity/src/per_build.rs@a_staged_absence_removes_every_stale_copy_without_asking`、`crates/voltip-identity/src/handoff.rs@a_pending_hand_over_acknowledges_only_when_the_receiver_commits`、`crates/voltip-identity/src/per_build.rs@a_hand_install_asks_once_per_entry_and_then_never`、`crates/voltip-identity/src/per_build.rs@an_entry_handed_over_as_absent_is_not_brought_back`、`crates/voltip-identity/src/handoff.rs@a_parent_that_fails_the_check_hands_over_nothing`、`crates/voltip-identity/src/handoff.rs@a_child_that_fails_the_check_is_sent_nothing`、`.github/scripts/check-keychain-preinstall.sh@staged B persisted before ACK`、`.github/scripts/check-keychain-preinstall.sh@installed B read its own partition`、`.github/scripts/check-keychain-preinstall.sh@wrong-signature staged app was stopped`、`crates/voltip-identity/src/secret_store.rs@regression_a_secret_from_an_older_build_moves_to_an_item_this_build_owns`、`crates/voltip-identity/src/secret_store.rs@a_failed_move_keeps_the_older_item`、`.github/scripts/check-keychain-migration.sh@the next signed build` | [~](0.0.7 起每个候选的两条 macOS 腿都通过这道检查。麦克风与辅助功能授权跨版本保留:用户从 0.0.7 更新到 0.0.10 后没有重新授权,听写正常插入;GitHub 的两台 Mac 上用已发布的安装包原地更新 0.0.7 → 0.0.8(运行 36676811823)与 0.0.10 → 0.0.11(运行 36674709470),tccd 判定都是允许。钥匙串(2026-09-30 更正):用户从 0.0.12 更新到 0.0.14 仍为 `voltip.identity.x25519` 与 `.meta` 各问一次。原因是登录钥匙串按构建的 cdhash 给条目分区,自签名证书没有 Team ID,每个构建各占一个分区;以前的检查用的是不做分区检查的临时钥匙串,所以「不弹窗」的结论不成立。0.0.15–0.0.16 的安装后交接已确认无效:updater 删除旧 bundle 后进程签名查询返回 `ENOENT`。修复改为安装前启动 staged 新版,持久化并 readback 后才 ACK/安装;2026-10-01 在 macOS 26.6.2 真机用同证书、不同 CDHash 的隔离账户验证 staged 与安装后正常启动均零提示、最终只留 B 条目,错误签名在安装前失败。0.0.16 安装首个修复版仍会最后询问一次,正式零提示验收从两个连续修复版开始,见 `docs/acceptance/macos/manual-checklist.md` 第 15 项) | | Android 进发布流程(2026-09-30 用户要求第 8 项前半;同日选定 GitHub 上的 APK 与 Google Play 用同一把密钥):发布候选新增 `bundle-android` 腿,工具链由 `.github/scripts/android-toolchain.sh` 固定(JDK 21、platform 37(compileSdk;2026-11-07 前为 36,Expo SDK 58 的模块要求 37)、build-tools 35.0.0、NDK 30.0.16248370(r30,当前的 LTS));Gradle 构建未签名的 arm64 APK 与 AAB,单独一步用 `sign-android-package.sh` 签名(APK 先 `zipalign -P 16`,再用 apksigner 做 v2 与 v3 签名;AAB 用 jarsigner),密钥只在这一步出现;`check-android-package.sh` 要求两个包都由 `release-targets.json` 的 `android_signing` 证书签名且只有一个签名者、APK 与每个 `.so` 的 LOAD 段按 16 KB 对齐、包名、versionName、versionCode(主版本 × 1000000 + 次版本 × 1000 + 补丁号)、targetSdk 36、原生库里没有服务商密钥、带本版本的第三方许可声明;`Voltip_<版本>_android_arm64.apk` 与 `.aab` 进 Release 和 `SHA256SUMS` 并有构建证明,不进 `latest.json`;`prepare` 要求三个 Android secrets(密钥别名写在 `android_signing.key_alias`:0.0.18 的候选把它当作 secret,日志里的每个 `voltip` 都被遮掉);CI 新增 `android` job(Android 目标的 clippy,以及用当次临时密钥走同样的签名与检查),`CI Success` 等它;`third-party-notices.py --app mobile` 生成手机端的许可声明(371 个 crate、38 个前端包)。本机实测:删掉 Gradle 的生成文件后从头构建,用临时密钥与正式密钥分别签名,检查全部通过;release DEX 保留三个插件类和它们的 `@Command` 方法;Android 目标的 clippy 查出并修正了 `clipboard.rs` 里只在 Android 上编译的一处多余 `return` | `.github/workflows/{release-candidate,ci}.yml`、`.github/scripts/{android-toolchain,sign-android-package,check-android-package}.sh`、`.github/release-targets.json`、`scripts/release/{updater-json,candidate,third-party-notices}.py`、`scripts/clippy-android.sh`、`apps/mobile/src-tauri/tauri.package-android.conf.json`、`apps/mobile/src-tauri/src/clipboard.rs`、`docs/runbook.md` | `scripts/release/test_updater_json.py@test_collects_the_apk_the_aab_and_r8s_map_with_no_updater`、`scripts/release/test_updater_json.py@test_the_updater_is_refused_for_android`、`scripts/release/test_updater_json.py@test_latest_json_leaves_the_android_leg_out`、`scripts/release/test_candidate.py@test_an_android_leg_that_claims_the_updater_is_refused`、`scripts/release/test_candidate.py@test_a_target_with_half_an_updater_is_refused`、`scripts/release/test_release_workflows.py@test_the_android_leg_signs_outside_the_build_with_the_pinned_certificate`、`scripts/release/test_release_workflows.py@test_every_leg_is_sealed_and_gated`、`scripts/release/test_release_workflows.py@test_the_android_job_builds_signs_and_checks_as_the_candidate_does_with_a_key_of_its_own`、`scripts/release/test_release_workflows.py@test_regression_the_key_alias_is_no_secret`、`scripts/release/test_third_party_notices.py@test_the_android_app_lists_its_crates_and_packages_and_no_desktop_library`、`scripts/release/test_third_party_notices.py@test_the_android_app_is_scanned_for_its_own_target`、`apps/mobile-rn/plugins/release.test.js@regression: compiles against the API level of Expo's own modules and still targets 36` | [~](待首个带 Android 的候选用仓库 secrets 里的正式密钥签名并通过检查,以及真机:从 GitHub 的 APK 安装并完成配对) | | Android 应用在设备上启动(2026-10-01:0.0.18 与 0.0.19 通过了全部包检查,在用户的小米 HyperOS 3 上却打开即闪退):原因是 `android-native-keyring-store` 经 `ndk-context` 取应用 context,而 Tauri 2.11 用的 tao 0.35 不初始化它,Rust 一启动打开 Android Keystore 就 panic(`android context was not initialized`,模拟器上复现);`MainActivity` 在 `super.onCreate` 启动 Rust 之前,经这个库导出的 JNI 入口(`io.crates.keyring.Keyring`)交给它应用 context,R8 保留该类的 native 方法;CI 新增 `android-device` job、发布候选在封存前新增 `device-android` job:把 APK 装进 Android 15 模拟器(x86_64 经 ARM 翻译运行 arm64 代码)启动,首屏的「按住说话」出现、20 秒内不退出、应用日志里没有致命错误,日志与截图上传为工件;模拟器覆盖不到的项目(麦克风、相机、剪贴板、分享面板、配对、定制系统的省电策略)列在真机清单 `docs/acceptance/android/manual-checklist.md` | `apps/mobile/src-tauri/gen/android/app/src/main/java/{dev/voltip/mobile/MainActivity,io/crates/keyring/Keyring}.kt`、`apps/mobile/src-tauri/gen/android/app/proguard-rules.pro`、`.github/scripts/android-device-smoke.sh`、`.github/workflows/{ci,release-candidate}.yml`、`docs/runbook.md` | `scripts/release/test_release_workflows.py@test_regression_ci_starts_the_android_app_on_a_device`、`scripts/release/test_release_workflows.py@test_regression_the_release_apk_starts_on_a_device_before_it_is_sealed`;CI `android-device`:修复前 0.0.19 与 3e923e8 的构建都在启动 1–2 秒后 SIGABRT(运行 36865203191) | [~](模拟器通过:PR #50 的 CI 运行 36869919549,55c0199 之后的构建首屏出现、20 秒内不退出,日志里 `created device identity backend="android-keystore"`、没有致命错误;真机按清单待用户验证) | diff --git a/docs/roadmap.md b/docs/roadmap.md index ae089d78..7a4e22f9 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -11,7 +11,7 @@ 5. **备份与诊断**:电脑上的历史、词典、规则、场景和自定义预设可以导出、导入(`docs/dictation.md` §4.6),每周、更新安装前和替换导入前各自动备份一份,每台电脑保留自己最近 5 份(§4.6.1,几台电脑可以共用一个同步文件夹);设置不进备份。没有一键诊断包。连接问题有「连接自检」(`docs/pairing.md`),其他问题还没有自检。 6. **悬浮窗健康检查**:已经预热并由事件驱动;没有渲染确认、置顶保活和多显示器 / DPI 自检。 7. **平台交付**: - - macOS:发布流程为 Apple 芯片与 Intel 芯片各出一个 `.dmg` 与更新用的 `.app.tar.gz`(2026-09-28 起;Intel 腿用 GitHub 最后一个 x86_64 镜像 `macos-15-intel`,可用到 2027-08),用项目固定的自签名证书签名(2026-09-29 起,更新后授权保留),没有公证:首次打开要在「隐私与安全性」里放行。桌面上的手测清单见 `docs/acceptance/macos/manual-checklist.md`。 + - macOS:发布流程为 Apple 芯片与 Intel 芯片各出一个 `.dmg` 与更新用的 `.app.tar.gz`(2026-09-28 起;2026-11-10 起两个包都在 Apple 芯片的 `macos-15` 上构建,Intel 包交叉编译,CI 与安装脚本仍在 GitHub 最后一个 x86_64 镜像 `macos-15-intel` 上真机运行,该镜像可用到 2027-08),用项目固定的自签名证书签名(2026-09-29 起,更新后授权保留),没有公证:首次打开要在「隐私与安全性」里放行。桌面上的手测清单见 `docs/acceptance/macos/manual-checklist.md`。 - Windows:安装包没有 Authenticode 签名。 - Android:2026-10-01 起每次发布都带 arm64 的 APK 与 AAB,用项目固定的密钥签名(`docs/runbook.md` 发布 · Android 签名),CI 的 `android` job 每次都构建并检查;还没上架 Google Play(个人账号要先完成 12 名测试者、连续 14 天的封闭测试),没有 32 位与 x86 包;iOS 未开始。 - macOS 的粘贴与复制键码按当前输入源查表(覆盖 Dvorak 等),没有逐键 `UCKeyTranslate` 反查。 diff --git a/docs/runbook.md b/docs/runbook.md index 8415a804..d5f1e7b3 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -120,6 +120,7 @@ Relay 无状态(内存里只有活动会话 / 频道),可水平扩展前 - `release.yml`(控制器):每次推送 `main`,release-please 维护 Release PR(`release-please-config.json`)。PR head 与 `main` 同步时,`dispatch_candidate` 用 `gh workflow run` 派发 `release-candidate.yml`(`mode=automatic`,带 PR 号、head、base);head 落后 `main` 时不派发,只在 step summary 说明怎么恢复。 - `release-candidate.yml`(候选):`prepare` 校验工作流身份(受保护的 `main`)、PR 身份,以及 `.github/scripts/check-release-delta.py` 的 delta 证明:head 相对 `main` 只改了 `CHANGELOG.md`、`.release-please-manifest.json` 和三份 `package.json` 的版本号,任何模式下证明失败都停止,因为构建腿带着生产和签名 secrets。之后做原 preflight 的三件事(`check-config`、内置引擎 secrets、主机名守卫),并把 head 的 `Release candidate` 置为 pending。 - 五条构建腿(Windows 交叉构建、Linux、Apple 芯片与 Intel macOS、Android)在根目录检出被构建的源码,在 `.release-tooling/` 检出本工作流所在提交的发布脚本(`updater-json.py`、`check-macos-bundle.sh`、`tauri-bundle-retry.sh`、`install-cargo-tauri.sh`、`artefact-checks.sh`、`appimage.sh`、`android-toolchain.sh`、`sign-android-package.sh`、`check-android-package.sh`);定义产品的脚本(Makefile、构建脚本、第三方声明、`forget-sherpa-onnx-build.sh`)来自源码。缓存 `release-` 只存 Cargo 的 registry(macOS 腿还有 `~/.cargo/bin` 里自己编译的 cargo-about),不存 target/(2026-10-05:各腿约 4 GB 的 target/ 缓存让仓库超过 GitHub 10 GB 的缓存上限,最久没用的条目先被删,被删的是 PR 不会读的 `ci.yml` macOS 缓存;`main` 的 Intel 腿因此冷编译 47 分钟,0.0.44 的候选没等到 `CI Success`)。构建腿每次冷编译只让候选包晚一些完成:`source-gate` 等的是 `main` 的 CI,不是构建腿。 + - 两条 macOS 腿都跑在 Apple 芯片的 `macos-15` 上(2026-11-10 起):Intel 包用 `--target x86_64-apple-darwin` 交叉构建。原因是 `macos-15-intel` 上这条腿冷编译要 21.2 分钟,同样的构建在 `macos-15` 上 12.0 分钟(0.3.1 的候选 42733161564),而且这个镜像 2027-08 下线。`check-macos-bundle.sh` 照常检查每个 Mach-O 只含本腿的架构、minos ≤ 11.0,并运行包里的 `--version`,在 Apple 芯片上由 Rosetta 2 执行;Intel 机器上的真实运行仍由 `ci.yml` 的 `macos` job 和 `install-scripts.yml` 在 `macos-15-intel` 上做。 - Linux 腿还构建无头服务端(`docs/dictation.md` §23.5):在签名步骤之前,源码的 `scripts/build-server-linux-x64.sh` 用同一份腿级 env(内置服务相同)构建、检查并打包 `voltip-server-<版本>-linux-x64.tar.gz`,把解开的包无头运行一遍;`.release-tooling` 的 `artefact-checks.sh` 再扫一次二进制(提供商密钥、`VOLTIP_SERVER_SONAMES`)。`collect --extra` 把它收为 `extra` 产物:与 Windows 的便携 zip 一样进入候选清单、证明、`SHA256SUMS` 和 Release,不进 `latest.json`(服务端没有自动更新)。 - Linux 腿的 AppImage:签名步骤之前,`.release-tooling` 的 `scripts/lib/appimage.sh` 在 tauri-bundler 的工具目录放好固定的 AppRun(核对 SHA-256,权限 0755;打包器自己下载的那份是 0770,firejail `--appimage` 下普通用户启动不了,见 `docs/dictation.md` §14.5);打包之后检查 AppImage 的每个条目都能被所有用户读取、能执行的都能被所有用户执行(`unsquashfs`,来自 `squashfs-tools`),不通过就不收集。`make linux-x64` 做同样的两步。 - `source-gate` 不重跑测试:要求 `ci.yml` 在 head 所在的 `main` 提交(backfill 时是标签提交)上 push 运行的 `CI Success` 通过,最多等 45 分钟。`CI Success` 等 `ci.yml` 里除 Codecov 上传之外的全部作业,包括两条 macOS 腿(`scripts/release/test_release_workflows.py` 检查这一点;v0.0.5 就是在它还不等 macOS 时,从一个 Intel 腿失败的提交发出去的)。`aggregate`(唯一持有 `id-token: write`、不执行项目代码的 job)用 `scripts/release/candidate.py seal` 核对五条腿齐全、每个文件与证据一致,写 `candidate-manifest.json`,证明全部文件和清单,上传 `release-candidate`(保留 14 天)。`gate` 按结果把 `Release candidate` 写成 success 或 failure(只在 automatic 模式写)。 diff --git a/scripts/release/test_release_workflows.py b/scripts/release/test_release_workflows.py index ab602b97..545c6902 100644 --- a/scripts/release/test_release_workflows.py +++ b/scripts/release/test_release_workflows.py @@ -234,6 +234,24 @@ def test_the_macos_legs_sign_with_the_release_certificate(self) -> None: self.assertEqual(config["bundle"]["macOS"]["signingIdentity"], "-") self.assertFalse(config["bundle"]["macOS"]["hardenedRuntime"]) + def test_the_intel_package_builds_on_apple_silicon(self) -> None: + # 2026-11-10: the Intel leg was the longest after the Linux one, 26.8 minutes in the 0.3.1 + # candidate (42733161564), 21.2 of them a cold build on macos-15-intel; the same build took + # 12.0 on macos-15. macos-15-intel, GitHub's last x86_64 image, goes away in 2027-08. The leg + # cross-builds with an explicit --target; the bundle check runs the app's --version, which on + # Apple silicon runs the x86_64 binary under Rosetta 2, and requires this release's version. + body = self.jobs["bundle-macos"] + runners = dict(re.findall(r"(?m)^\s+- target: (\S+)\n\s+runner: (\S+)$", body)) + self.assertEqual(runners, {"aarch64-apple-darwin": "macos-15", "x86_64-apple-darwin": "macos-15"}) + targets = json.loads((ROOT / ".github/release-targets.json").read_text(encoding="utf-8"))["targets"] + intel = next(entry for entry in targets if entry["target"] == "x86_64-apple-darwin") + self.assertEqual((intel["runner"], intel["cross"]), ("macos-15", True)) + self.assertIn('cargo tauri build --ci --target "$MACOS_TARGET"', body) + self.assertIn(".release-tooling/.github/scripts/check-macos-bundle.sh", body) + check = (ROOT / ".github/scripts/check-macos-bundle.sh").read_text(encoding="utf-8") + self.assertIn('reported=$("$app/Contents/MacOS/voltip-desktop" --version)', check) + self.assertIn('[ "$reported" = "voltip $version" ]', check) + def test_every_leg_is_sealed_and_gated(self) -> None: for job in ("aggregate", "gate"): match = re.search(r"(?m)^ needs: \[([^\]]*)\]$", self.jobs[job])