Skip to content

ci(harmony): HAP 收尾 + 缓存配额修复(PR 不再写 1.77 GB 大缓存) - #1239

Open
aeoform wants to merge 17 commits into
Open-Less:betafrom
aeoform:feat/harmony-ci-caches
Open

aeoform wants to merge 17 commits into
Open-Less:betafrom
aeoform:feat/harmony-ci-caches

Conversation

@aeoform

@aeoform aeoform commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

鸿蒙

  • job 名改回「HAP」,因为公开镜像已经让每个 PR 都真的编 HAP(不再依赖 vars.HARMONY_SDK_URL)
  • 删掉 Build the ArkWeb bundle 重复步骤:package-harmony-hap.sh → build-harmony-frontend.sh:12 已经 export VITE_OPENLESS_PLATFORM=harmony 并跑同一个 npm run build -- --base=./
  • 删掉工具链缓存(ci.yml / release-harmony.yml):缓存条目 1.77 GB,而 2.1 GB 下载只要 ~18 s、解压 ~70 s

缓存(真正的问题在这里)

actions/cache@v4 会每次运行自动保存,包括 refs/pull/<n>/merge —— 这种作用域此后再也不会被恢复,只是占配额、按 LRU 挤掉 beta 的依赖树。

泄漏项 每 PR 体积
harmony-tools-6.0.1.251-Linux 1.77 GB
mobile-rime-ndk26-x86_64-… 0.40 GB

→ Android Rime 改为 cache/restore@v4 + beta 门控 cache/save@v4;鸿蒙工具链直接不缓存。

契约测试 ci-cache-sharing.test.mjs 现在拒绝任何 uses: actions/cache@v4 与任何 harmony-tools- 键。

数据

  • 缓存占用 9.42 GB → 预计 ~7.6 GB(预算 8 GB)
  • 现有条目分布:25 条在 refs/heads/beta,5 条在 refs/pull/1234/merge(PR 关闭后由 weekly prune 回收)
  • 本地通过:ci-cache-sharing / ci-changed-areas / ci-cache-usage / linux-egui-release-contract / workflow-concurrency-contract / harmony-release-contract

鸿蒙一侧(合并后遗留的三件事):

- job 名字还写着 “HAP when the SDK is set”,而公开镜像已经内置 → 改为
  「Harmony (ArkTS contracts, Rust host, HAP)」;
- `Build the ArkWeb bundle` 这一步是重复劳动:`package-harmony-hap.sh` →
  `build-harmony-frontend.sh` 里的 `npm run build -- --base=./` 同时设了
  `VITE_OPENLESS_PLATFORM=harmony`,删掉这一步只省一次 vite;
- 工具链缓存删掉(ci + release 各一处):解压后 7.1 GB、缓存条目 1.77 GB,
  而下载 2.1 GB 只要 ~18 s、解压 ~70 s —— 恢复缓存比重新下载更慢,还占掉
  仓库 10 GB 配额的 18%。

缓存一侧(真正的泄漏):

- `actions/cache@v4` 会在**每次运行**自动保存,包括 `refs/pull/<n>/merge`
  这种此后再不会被恢复的作用域。鸿蒙工具链 1.77 GB 每 PR 一份、Rime NDK
  0.40 GB 每 PR 一份——两次 PR 推送就能把 beta 的依赖树挤掉;
- Android Rime 改成 `cache/restore@v4` + beta 门控的 `cache/save@v4`;
- 契约测试新增:任何 workflow 里出现 `uses: actions/cache@v4` 直接失败,
  出现 `harmony-tools-` 缓存键也失败;
- `docs/ci-trigger-and-cache-policy.md` 的配额表按实测体积重写,补上鸿蒙、
  Rime、Playwright 三行,并写清「工具链不缓存」的性价比计算。

预期:缓存占用 9.42 GB → ~7.6 GB(预算 8 GB 之内),且 PR 不再写大缓存。
用上一轮真实数据定位(run 39337856392,Windows job 35.7 min):

  7.6 min  Compile Rust backend unit tests (Windows)
  6.5 min  Check Tauri backend with Rust 1.99 MSRV      ← MSRV
  6.4 min  Check Tauri backend (cargo check)
  5.9 min  Build frontend and run frontend/contract tests
  3.6 min  Run Rust-only backend unit tests (Windows)
  3.1 min  Compile backend tests with Rust 1.99 MSRV    ← MSRV

两处浪费:

1. Windows 也在跑 MSRV,而 MSRV(1.99) 与 stable(1.99.x) 同号——和 macOS 那次
   一样的冗余,只是当初只条件化了 macOS。现在 Windows 的 1.99 工具链与两条
   MSRV 步骤都跟随 `changes.outputs.msrv_needed`,省 ~9.6 min;
2. `cargo check` 之后紧跟 `cargo test --lib --no-run`:check 只产出 metadata,
   后面的代码生成会把整张依赖图重编一遍(实测就是那 6.4 min)。去掉 check,
   把 bins 覆盖并进 `--lib --bins --no-run`,省 ~6.4 min。

契约测试补了三条断言防止回退(Windows MSRV 跟随判断、不再有独立的 cargo check、
no-run 同时覆盖 lib 与 bins)。

预期:Windows 35.7 → ~20 min,整条 CI 的墙钟从 ~36 min 降到 ~20 min
(Linux 19.8 min 成为新的关键路径)。
去掉重复 `cargo check` 时把 bins 覆盖并进了 Windows 的 no-run 编译,但
`pin-persistence-security-contract` 里的正则写死了 `--lib --no-run`,
macOS 的契约步骤因此失败。正则放宽成 `(?: --bins)?`——要求本身(Windows 至少
要编过 owning module 的测试)没有变化。

本地把 `grep -l ci.yml scripts/*.test.mjs` 的 7 个文件与 `frontend-test-runner`
全量(83 个脚本测试 + src 下所有 .test.ts)都跑过,全绿。
实测(run 39446467236,各 job 启动间隔 <15 s,没有排队,墙钟 = 最长 job):

  Windows   21.7 min   ← 原关键路径
  Linux     16.1 min
  Harmony    6.4 min
  UI motion  6.3 min
  macOS      3.9 min

- Windows 的 144 个契约测试要 spawn 144 个 node 进程,在这台 runner 上是 macOS
  的 3 倍(6.3 vs 1.9 min),而且它们排在 15 分钟的 Rust 编译后面串行执行。
  新增 `windows-frontend` job 并行跑,且只跑平台相关子集
  (`--platform-only windows` → 14/144,其余由 macOS 全量覆盖);
- Linux 的 8 分钟测试排在 7 分钟打包前面。`check-linux-egui.yml` 拆出
  `test-linux-egui` 并行 job,两个 job 显式声明同一个 `shared-key`(值等于原
  job 名,缓存键不变),否则 rust-cache 会用 job 名做键,测试 job 每次全冷。

预期墙钟 21.7 → ~15 min(Windows Rust 或 Linux 打包之一成为最长)。
上一轮 Android 最后结束(+22.9 min 起跑,10.4 min),不是因为它慢,而是
`sdkmanager --install emulator` 在 GitHub 的镜像上拿到截断的包:

    Warning: An error occurred while preparing SDK package Android Emulator:
    Error on ZipFile unknown archive

job 因此失败,重跑一次才绿——重跑排在 22 分钟之后,于是它「最后完成」。

- 固定 `emulator-build: 16555280`(repository2-3.xml 里 emulator 37.2.12 的
  linux archive 就是这个 build),不再解析 latest;
- emulator + system-images + AVD 走 restore/save 缓存(~400 MB),命中后下载
  与建 AVD 都不在关键路径上,保存只在 beta 且仅未命中时发生;
- 自装步骤带 3 次重试,失败先删半截目录再重下。

Android 本体只要 10.4 min(3.1 emulator 测试 + 2.3 Rime 编译 + 1.1 gradle),
本来就比 Windows 21.7 min 短得多。
`scripts/check-hotkey-injection.mjs` 是 40 行的 `spawnSync('cargo', ['test','-p',
'openless-core', <一个测试名>])`。它在每个跑 `npm test` 的平台上都触发一次
**独立的 cargo 编译**:

- 它跑在 workspace `app` 下 → 写 `app/target`,而 macOS/Windows 的 Tauri 测试
  写 `src-tauri/target`,两棵树互不共享,所以每一个平台都要冷编 openless-core;
- Linux job 本来就 `cargo test -p openless-core`(整 crate),这条用例早已被跑。

实测(本机,全 83 个脚本测试逐个计时):

    53.58 s  check-hotkey-injection.mjs     ← 81% 的套件时间
     2.29 s  locale-lifecycle.test.mjs
     其余 81 个都 < 1 s

删掉它,把那句用例名改成 Linux job 里的 `--exact` 重跑:二进制已经由上一行
`cargo test -p openless-core` 编好,成本≈0,但名字被改掉时会在这里直接报错,
不会静默丢失这道门。同时清掉 `npm run check:hotkey-injection` 与 README 里的
引用。

结果:脚本契约套件 65.9 s → **13.2 s**(−80%),且不再有第二条 cargo 构建树。
`release-harmony.yml` 的 job `env:` 里有

    OPENLESS_HARMONY_SIGNING_DIR: ""

注释说「等签名步骤解析出 $RUNNER_TEMP 再填」,可是 GitHub 的环境变量优先级是
step env > job env > workflow env > $GITHUB_ENV,所以签名步骤写进 $GITHUB_ENV
的那个值会被这个空串盖掉。`package-harmony-hap.sh` 于是走 `${VAR:-默认}`
回落到 `~/.local/share/harmony/signing/release` —— 那里没有密钥材料,通道不是
none 时直接报 "missing signing directory"。

删掉这个占位(脚本各处都用 `:-默认值`,不定义是安全的),并在构建步骤加一条
显式守卫:通道不是 none 而 SIGNING_DIR 为空时立刻以明确信息失败。

顺带核对所有 release 的变量需求:
- 鸿蒙:现在零配置——工具链走 checksum 固定的公开镜像,HARMONY_KEY_ALIAS 其实
  可选(sign-harmony-hap.py 默认 harmony-<channel>),文档改成
  「Variable (optional)」;HARMONY_TOOLCHAIN 只在自托管 runner 需要;
- 安卓:ANDROID_KEYSTORE_BASE64/PASSWORD、ANDROID_KEY_ALIAS/KEY_PASSWORD、
  TAURI_SIGNING_* 都在用,无需新增;
- Tauri 桌面:Apple 证书/密码 + TAURI_SIGNING_*,无需新增;
- Linux:只用 GITHUB_TOKEN,无 secrets;
- Homebrew:HOMEBREW_CASK_ADMIN_TOKEN。

契约测试新增一条:job 级 env 里不允许再出现 OPENLESS_HARMONY_SIGNING_DIR。
用户问的是「release 到应用市场还缺什么、怎么拿」。核下来缺的不是代码,是
**凭据的组织方式和获取路径**,另外本地调试签名没有入口。

1. \`package-harmony-hap.sh\` 新增 \`debug\` 通道:\`release|test|none\` 之外,
   \`debug\` 直接把签名目录指向 \`~/.local/share/harmony/signing/local-test\`,
   用 \`debug.cer/.p7b/.p12\` 签,产物只能装到该 Profile 授权的设备上。
   此前 \`sign-harmony-hap.py\` 支持 \`--prefix debug\` 但脚本没有入口,本地
   材料(bundle-name 正是 \`openless.hm.com\`,有效期到 2027-10-12)用不上。
2. \`release-harmony.yml\` 支持 **通道化 secrets**:优先读
   \`HARMONY_RELEASE_*\` / \`HARMONY_TEST_*\`,没有再回落到
   \`HARMONY_CERT_BASE64\` 等旧名。这样正式版与测试渠道的材料可以同时存在,
   不必每次 dispatch 前手动替换 secrets。
3. 密码不再经 \`$GITHUB_ENV\` 回传(YAML \`env\` 优先级高于 \`$GITHUB_ENV\`,
   会被 job 级的同名 secret 盖掉),改为写进签名目录的 \`keystore-password\` /
   \`key-password\`(600 权限),\`sign-harmony-hap.py\` 增加对后者的读取。
4. 缺 secret 时错误信息会列出**该通道**缺的具体名字。
5. \`RELEASING.md\` 补一节「Where each piece comes from」:六步从注册实名认证到
   API 密钥,附不带 DevEco 的 \`keytool\` 命令、base64 存 secret 的写法,并写清
   自动到「替换候选包」为止、上架资料与提交审核必须人工。
要上架应用市场,真正容易翻车的不是构建,是拿到华为材料之后才知道对不上:

1. `release.cer` 是 **PEM 证书链**(Huawei root CA + 叶子证书),拿第一张去比
   公钥永远不相等——这正是本机 debug 材料一开始被判失败的原因;
2. 证书与私钥库里的别名不匹配(CSR 传错账号 / 重新申请过证书);
3. Profile 是签给**另一张证书**的,或 bundleName 不是 `openless.hm.com`;
4. Profile 过期,或调试 Profile 没有授权设备 UDID。

新增 `scripts/check-harmony-signing.sh`(+ 两个 python 助手),按叶子证书比对
公钥、比对 Profile 内嵌证书、比对 `harmony/AppScope/app.json5` 的 bundleName、
检查 Profile 与证书有效期、统计调试 UDID。只读,不打印任何密钥。

`release-harmony.yml` 在写材料之后、**构建之前**跑它(6 分钟构建前失败,而不是
构建完才发现装不上)。

本机验证(debug 材料,bundleName 正是 openless.hm.com):

    ok  certificate matches the keystore key (…,Development)
    ok  chain carries 2 issuing certificate(s)
    ok  certificate valid until Oct 12 14:46:43 2027 GMT
    ok  profile was issued for this certificate
    ok  profile bundleName matches app.json5 (openless.hm.com)
    ok  profile valid 2026-10-12 → 2027-10-12
    ok  1 device UDID(s) authorised for this debug profile
    PASS
之前 `Release HarmonyOS` 的 channel 只有 release 与 test,默认 release——意味着
**任何一次 release dispatch 都必须有签名材料**,哪怕只想把未签名 HAP 附到 GitHub
Release 上也跑不通,会卡在 "missing signing secrets"。

分发策略改成三条路,默认走仓库那条:

- `channel: none`(**新默认**):构建 → 附未签名包到 Release → 结束。
  写材料、校验材料、签名的三步全部跳过,不需要任何 secret;
- `channel: release`:AppGallery 正式版,用 `release.*` 材料签名并上传;
- `channel: test`:AppGallery 测试渠道,用 `test.*` 材料。

细节:
- 「Write the signing material」「Validate the signing material」都加了
  `if: inputs.channel != 'none'`;
- 校验步骤在 none 时只跑 `--allow-unsigned` 的未签名校验,并打印原因;
- AppGallery 上传步骤要求 `channel != none`(未签名包没有可上传的东西);
- Release attach 一直只传未签名的 `OpenLess_harmony_arm64.hap` / `.hap`,
  签名包只留在 workflow artifact 里,仓库永不发布;
- CI 侧本来就一直是 `OPENLESS_HARMONY_CHANNEL: none`(`ci.yml:417`,且不引用
  任何签名脚本),这次在文档里写明。

契约测试同步:默认值断言改为 `default: none`,并新增「签名步骤在 none 下必须跳过」。
上一轮 `Package deb and rpm` 失败,日志里**一行输出都没有**,原因是我把
「Build and test fcitx5 plugin」整步挪进并行的 `test-linux-egui` 之后,
打包 job 里就没有 `scripts/linux-fcitx5-plugin/build/libopenless.so` 了。
而 `package-linux-egui.sh` 开头是一串裸的

    test -s "$PLUGIN_ROOT/libopenless.so"

`test` 不打印任何东西 → exit 1 → CI 只显示 "Process completed with exit code 1"。

两处修:

1. `build-linux-egui` 增加「Build the fcitx5 plugin for packaging」:只 cmake
   编译,`ctest` 与那几条断言仍留在并行 job,重复成本约 20 秒;
2. `package-linux-egui.sh` 的裸 `test` 全部换成 `require_file` / `require_command`,
   缺什么就说什么,并在缺失 fcitx5 产物或 release 二进制时直接给出构建命令。

契约测试同步:`build-linux-egui` 里必须出现 `cmake --build build --parallel`,
`package-linux-egui.sh` 里必须用 `require_file` 检查插件产物。

本地复现验证:`OPENLESS_LINUX_VERSION=2.0.2-Beta.1-141 bash scripts/package-linux-egui.sh`
→ deb / rpm / SHA256SUMS 三件产出正常。
`Windows checks` 挂在 `Compile Rust backend unit tests`:

    error: proc macro panicked
      --> src\lib.rs:1028:16
       | .build(tauri::generate_context!())
       = help: message: The `frontendDist` configuration is set to "../dist" but this path doesn't exist

我把 `npm test`(`pretest` 先 `npm run build`)限制到 macOS 之后,Windows 的
Rust 编译就失去了 `dist/`——而 `tauri::generate_context!()` 是在**编译期**把
`../dist` 嵌进去的。

补一步 `if: runner.os == 'Windows'` 的 `npm run build`。这样 Windows 侧前端
会被构建两次(这里一次、并行的 `windows-frontend` 一次),但两次并行发生,
墙钟仍比把契约套件重新塞回这个 job 短。

契约测试新增:Windows 下必须先出现 `run: npm run build` 再编 Tauri lib。
`resolve()` 把后缀拼在末尾(`HARMONY_CERT_BASE64_RELEASE`),而 secret 实际命名
是通道在前(`HARMONY_RELEASE_CERT_BASE64`)。于是 dispatch channel=release 时永远
回落到通道无关的名字,报:

    missing signing secrets for channel 'release': HARMONY_CERT_BASE64 …

修正为 `HARMONY_${upper}_${base#HARMONY_}`,并加两条契约断言(解析形式 + job env
里 RELEASE 组 secret 的接线)。
用 AGC 的 Connect API 客户端传签名 HAP 时被拒:

    204144641 [AppGalleryConnectPublishService]The suffix name of the software
    package is incorrect

华为应用市场只收 **App Pack(`.app`)**。原流程只产 `.hap`,所以「上传到应用市场」
这一步从来不可能成功,手动在网页上传也一样会被拒。

改动:

- `build-harmony-frontend.sh` 在商店通道额外跑 `assembleApp`(**必须是
  `--mode project`**,module 模式下会报 "Task [ 'assembleApp' ] was not found");
- hvigor 的 `signingConfigs` 用不了:它要求 `storePassword`/`keyPassword` 是
  IDE 加密后的密文(≥32 字符),CI 无法提供。改为**先产无签名 `.app`,再用
  `hap-sign-tool sign-app` 签**(实测它同时支持 `.hap` 与 `.app`);
- `sign-harmony-hap.py` 抽出 `sign_package()`,新增 `--skip-hap`,一次构建可以
  同时签 HAP 与 App Pack;
- `verify-harmony-hap.sh` 支持 `.app`:解包校验内层 HAP 的内容与版本号,签名则
  校验外层 pack(HAP 本身不再单独签名);
- workflow 的校验与 AppGallery 上传都改成 `.app`。

本地实测(release 材料,2.0.2 / 141):

    OpenLess_harmony_arm64-signed.app   24,653,341 B
    verify-app success
    App Pack: …signed.app (inner entry-default.hap)
    signature verified

并且真的传到了 AGC(app 6917618883555881703):
`uploaded OpenLess_harmony_arm64-signed.app … submit it for review`
我在把 App Pack 逻辑挪到签名之后时删掉了 `APP_PACK=""`,而 `none` 通道不进入
签名分支,后面的 `[ -n "$APP_PACK" ]` 在 `set -u` 下报 unbound variable:

    scripts/package-harmony-hap.sh: line 144: APP_PACK: unbound variable

CI 的 Harmony job 就是挂在这里。补回初始化。
而不是再加一个 workflow:`native-capsule` 之前在独立的 `native-capsule.yml` 里,
只在 `pull_request` 上触发,`rust-cache` 既没有 `save-if` 也没有 `shared-key`。

- 每次 PR 都往 `refs/pull/<n>/merge` 写一份 macOS + Windows 依赖树,这个作用域
  此后再也不会被恢复,只占配额、按 LRU 挤掉 beta 的缓存;
- beta 上没有任何 job 产出同 key 的树(workflow 不跑 push),所以 PR 侧也永远
  命中不了,每次都是冷编译。

并入 `ci.yml` 后:同一个 `Changed areas` 门禁(`needs.changes.outputs.tauri`),
一次 PR 一条状态;缓存用 `shared-key: native-capsule` + 只在 beta 写;顺带补
`components: clippy`、`permissions`、`timeout-minutes`、并发组。删除独立 workflow。

契约测试:任何 workflow 不得出现 `uses: actions/cache@v4`(自动保存),
`ci.yml` 里必须有带 shared-key/save-if 的 `native-capsule` job,且独立文件不得复活。
@aeoform
aeoform requested a review from H-Chris233 October 21, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant