From e20cb828c5ef84acbd9a3b81e3a8ccd93343cf91 Mon Sep 17 00:00:00 2001 From: aeoform <2790848120@qq.com> Date: Wed, 21 Oct 2026 23:44:49 +0800 Subject: [PATCH 1/2] =?UTF-8?q?ci(native-capsule):=20=E7=BA=B3=E5=85=A5?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E4=BD=93=E7=B3=BB=EF=BC=8CPR=20=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E5=86=99=E4=B8=80=E6=AC=A1=E6=80=A7=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `native-capsule.yml` 是上游新加的 job,只在 `pull_request` 上跑,而它的 `rust-cache` 步没有 `save-if`、也没有 `shared-key`: - 每次 PR 都往 `refs/pull//merge` 写一份 macOS + Windows 的依赖树 —— 这个 作用域此后不会被任何运行恢复,纯粹占配额、按 LRU 挤掉 beta 的缓存; - key 里带的是 job 名 `native-capsule`,而 beta 上没有任何 job 产出同 key 的 树(这个 workflow 根本不跑 push),所以 PR 侧也永远命中不了,冷编译是必然。 改成和其余 job 一致: - 增加 `push: branches: [main, beta]`:beta 的 push 负责把树焐热; - `shared-key: native-capsule`(稳定前缀,不随 job 名变化); - `save-if` 只在 beta 的 push / dispatch 上为真,PR 只读; - `workspaces: openless-all/app -> target`(原写法没有目标目录映射,rust-cache 会按 workspace 默认猜测); - 补 `permissions: contents: read`、`timeout-minutes: 30`、并发组 (PR 内重复推送互相取消,beta push 排队); - clippy 需要 `components: clippy`,原来没装,靠 runner 预装碰运气。 契约测试 `ci-cache-sharing.test.mjs` 增加三条断言(必须跑 beta push、必须 beta 门控 save-if、必须有 shared-key),并在缓存策略文档里写明这个坑。 --- .github/workflows/native-capsule.yml | 34 ++++++++++++++++++- docs/ci-trigger-and-cache-policy.md | 1 + .../app/scripts/ci-cache-sharing.test.mjs | 22 ++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/.github/workflows/native-capsule.yml b/.github/workflows/native-capsule.yml index f9a72fc3b..67f1e9686 100644 --- a/.github/workflows/native-capsule.yml +++ b/.github/workflows/native-capsule.yml @@ -1,8 +1,28 @@ name: Native capsule + +# The capsule crates (`openless-native-capsule`, `openless-capsule`) compile into +# the desktop shells, so a break there would otherwise only surface inside the +# long Tauri matrix. They are checked on both desktop platforms in a job of their +# own. +# +# `push` on the default branches is what keeps the Rust cache warm: a +# pull-request-only workflow can never save a tree the next PR is allowed to +# restore, because GitHub scopes caches to the ref that saved them. on: pull_request: branches: [main, beta] + push: + branches: [main, beta] workflow_dispatch: + +permissions: + contents: read + +concurrency: + # Repeated pushes to one pull request cancel the older run; beta pushes queue. + group: native-capsule-${{ github.event_name == 'pull_request' && github.ref || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: native-capsule: strategy: @@ -10,15 +30,27 @@ jobs: matrix: os: [macos-latest, windows-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 30 defaults: run: working-directory: openless-all/app steps: - uses: actions/checkout@v4 + with: + submodules: false - uses: dtolnay/rust-toolchain@stable + with: + components: clippy - uses: Swatinem/rust-cache@v2 with: - workspaces: openless-all/app + # Stable prefix per platform instead of the job name, so the same tree + # is reused if the job is ever split or renamed. + shared-key: native-capsule + workspaces: openless-all/app -> target + # Pull requests restore the beta tree and never write their own copy: + # `refs/pull//merge` scopes are never restored again and only evict + # the branch caches (see docs/ci-trigger-and-cache-policy.md). + save-if: ${{ github.ref == 'refs/heads/beta' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }} - run: cargo check --locked -p openless-native-capsule - run: cargo test --locked -p openless-capsule --features ui,host - run: cargo clippy --locked -p openless-native-capsule -p openless-capsule --features openless-capsule/ui,openless-capsule/host -- -D warnings diff --git a/docs/ci-trigger-and-cache-policy.md b/docs/ci-trigger-and-cache-policy.md index 55e6dcf3a..2b2cad932 100644 --- a/docs/ci-trigger-and-cache-policy.md +++ b/docs/ci-trigger-and-cache-policy.md @@ -74,6 +74,7 @@ GitHub 每仓库保留 10 GB 缓存,超出按 LRU 静默淘汰;淘汰后 job - `build-linux-egui` 保留 PR 内自写并开 `cache-on-failure: true`:失败的运行也会把依赖缓存存下来,下一次迭代仍然热;同时关掉 dev/test debug info,缓存更小、链接更快。 - key 结构为 `v0-rust---<环境哈希>-`:环境哈希含工具链与 `CARGO_PROFILE_*`,所以 stable / MSRV / release 天然分开,不要用跨平台 `shared-key` 混合。 - `Cargo.lock` 变化时精确键失效,但 `restore-keys` 前缀回退会拿到同前缀的旧缓存,所以“锁变了”不等于“全冷”——前提是那份没被淘汰,这正是要守预算的原因。 +- `native-capsule.yml` 原本只在 `pull_request` 上跑:它写的缓存落在 `refs/pull//merge`,而那是一个**此后再也不会被恢复**的作用域,等于每个 PR 白写一份依赖树;同时 beta 上没有任何 job 产出同 key 的树,所以 PR 永远冷启动。现在它也在 `beta`/`main` 的 `push` 上跑,用 `shared-key: native-capsule` 做稳定前缀、`save-if` 只在 beta 写,PR 只读。 ## 4. 维护 diff --git a/openless-all/app/scripts/ci-cache-sharing.test.mjs b/openless-all/app/scripts/ci-cache-sharing.test.mjs index d4e847b00..3d09e31bc 100644 --- a/openless-all/app/scripts/ci-cache-sharing.test.mjs +++ b/openless-all/app/scripts/ci-cache-sharing.test.mjs @@ -22,6 +22,8 @@ const release = read('release-tauri.yml'); const androidApk = read('android-apk.yml'); const linux = read('check-linux-egui.yml'); const linuxRelease = read('release-linux-egui.yml'); +const releaseHarmony = read('release-harmony.yml'); +const nativeCapsule = read('native-capsule.yml'); const sharedKeys = (yaml) => [...yaml.matchAll(/shared-key:\s*([^\s#]+)/g)].map((match) => match[1].trim()); @@ -61,3 +63,23 @@ assert.match(linuxRelease, /release_tag: \$\{\{ inputs\.tag \}\}/); assert.match(linuxRelease, /checkout_ref: \$\{\{ inputs\.tag \}\}/); console.log('ci-cache-sharing contract test passed'); + +// `native-capsule` used to run on pull requests only, with an ungated +// `rust-cache` step: every PR saved a dependency tree under +// `refs/pull//merge`, and beta never produced one to restore. It now runs on +// beta pushes and writes only there. +assert.match( + nativeCapsule, + /push:[\s\S]*?branches: \[main, beta\]/, + 'native-capsule.yml must run on beta pushes so the PRs have a tree to restore', +); +assert.match( + nativeCapsule, + /save-if: \$\{\{ github\.ref == 'refs\/heads\/beta'/, + 'native-capsule.yml must only save its cache from beta', +); +assert.match( + nativeCapsule, + /shared-key: native-capsule/, + 'native-capsule.yml must use a stable shared-key instead of the job name', +); From 3412e22141cdded13b93cba6071e243535c77076 Mon Sep 17 00:00:00 2001 From: aeoform <2790848120@qq.com> Date: Wed, 21 Oct 2026 23:50:25 +0800 Subject: [PATCH 2/2] =?UTF-8?q?ci:=20=E6=8A=8A=E5=8E=9F=E7=94=9F=E8=83=B6?= =?UTF-8?q?=E5=9B=8A=E5=B9=B6=E8=BF=9B=20ci.yml=EF=BC=8C=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E5=8D=95=E5=BC=80=E4=B8=80=E4=B8=AA=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原来的 `native-capsule.yml` 是独立 workflow,代价是多一次运行、多一条状态、多一套 缓存策略,而且它只在 `pull_request` 上触发——这也是它缓存永远命中不了的根因。 并入 `ci.yml` 后: - 复用同一个 `Changed areas` 门禁(`needs.changes.outputs.tauri != 'false'`), 只有真正能触及 Rust 的改动才跑; - 一次 PR 只有一条 CI 状态,矩阵 `macos-latest` / `windows-latest` 失败互不影响; - 缓存策略与其余 job 同源:`shared-key: native-capsule` + `save-if` 只在 beta 写, PR 只读,不再往 `refs/pull//merge` 写一次性依赖树; - 删除 `.github/workflows/native-capsule.yml`。 契约测试改为在 `ci.yml` 里定位 `native-capsule` job,并断言那个独立 workflow 文件必须不存在。 --- .github/workflows/ci.yml | 39 +++++++++++++ .github/workflows/native-capsule.yml | 56 ------------------- docs/ci-trigger-and-cache-policy.md | 2 +- .../app/scripts/ci-cache-sharing.test.mjs | 44 ++++++++------- 4 files changed, 63 insertions(+), 78 deletions(-) delete mode 100644 .github/workflows/native-capsule.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65ab70c5a..e30f10f7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -389,6 +389,45 @@ jobs: # 散文改动跳过整条 Linux 构建与打包链;输出缺失/未知时退回 full。 scope: ${{ needs.changes.outputs.linux == 'false' && 'none' || 'full' }} + native-capsule: + # The capsule crates compile into the desktop shells, so a break there would + # otherwise only surface inside the long Tauri matrix. They are checked on + # both desktop platforms inside this workflow rather than in a second + # workflow: one run per pull request, one `Changed areas` gate, one cache + # policy. + needs: [changes] + if: needs.changes.outputs.tauri != 'false' + name: Native capsule (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 25 + defaults: + run: + working-directory: openless-all/app + steps: + - uses: actions/checkout@v4 + with: + submodules: false + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + with: + # Stable prefix per platform instead of the job name, so renaming or + # splitting the job keeps the tree. + shared-key: native-capsule + workspaces: openless-all/app -> target + # Pull requests restore the beta tree and never write their own copy: + # `refs/pull//merge` scopes are never restored again and only evict + # the branch caches (see docs/ci-trigger-and-cache-policy.md). + save-if: ${{ github.ref == 'refs/heads/beta' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }} + - run: cargo check --locked -p openless-native-capsule + - run: cargo test --locked -p openless-capsule --features ui,host + - run: cargo clippy --locked -p openless-native-capsule -p openless-capsule --features openless-capsule/ui,openless-capsule/host -- -D warnings + cross-platform: needs: [changes] # 失败即运行:只有判定脚本明确说“够不到”才跳过。 diff --git a/.github/workflows/native-capsule.yml b/.github/workflows/native-capsule.yml deleted file mode 100644 index 67f1e9686..000000000 --- a/.github/workflows/native-capsule.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Native capsule - -# The capsule crates (`openless-native-capsule`, `openless-capsule`) compile into -# the desktop shells, so a break there would otherwise only surface inside the -# long Tauri matrix. They are checked on both desktop platforms in a job of their -# own. -# -# `push` on the default branches is what keeps the Rust cache warm: a -# pull-request-only workflow can never save a tree the next PR is allowed to -# restore, because GitHub scopes caches to the ref that saved them. -on: - pull_request: - branches: [main, beta] - push: - branches: [main, beta] - workflow_dispatch: - -permissions: - contents: read - -concurrency: - # Repeated pushes to one pull request cancel the older run; beta pushes queue. - group: native-capsule-${{ github.event_name == 'pull_request' && github.ref || github.run_id }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -jobs: - native-capsule: - strategy: - fail-fast: false - matrix: - os: [macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - timeout-minutes: 30 - defaults: - run: - working-directory: openless-all/app - steps: - - uses: actions/checkout@v4 - with: - submodules: false - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - uses: Swatinem/rust-cache@v2 - with: - # Stable prefix per platform instead of the job name, so the same tree - # is reused if the job is ever split or renamed. - shared-key: native-capsule - workspaces: openless-all/app -> target - # Pull requests restore the beta tree and never write their own copy: - # `refs/pull//merge` scopes are never restored again and only evict - # the branch caches (see docs/ci-trigger-and-cache-policy.md). - save-if: ${{ github.ref == 'refs/heads/beta' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }} - - run: cargo check --locked -p openless-native-capsule - - run: cargo test --locked -p openless-capsule --features ui,host - - run: cargo clippy --locked -p openless-native-capsule -p openless-capsule --features openless-capsule/ui,openless-capsule/host -- -D warnings diff --git a/docs/ci-trigger-and-cache-policy.md b/docs/ci-trigger-and-cache-policy.md index 2b2cad932..7c489e473 100644 --- a/docs/ci-trigger-and-cache-policy.md +++ b/docs/ci-trigger-and-cache-policy.md @@ -74,7 +74,7 @@ GitHub 每仓库保留 10 GB 缓存,超出按 LRU 静默淘汰;淘汰后 job - `build-linux-egui` 保留 PR 内自写并开 `cache-on-failure: true`:失败的运行也会把依赖缓存存下来,下一次迭代仍然热;同时关掉 dev/test debug info,缓存更小、链接更快。 - key 结构为 `v0-rust---<环境哈希>-`:环境哈希含工具链与 `CARGO_PROFILE_*`,所以 stable / MSRV / release 天然分开,不要用跨平台 `shared-key` 混合。 - `Cargo.lock` 变化时精确键失效,但 `restore-keys` 前缀回退会拿到同前缀的旧缓存,所以“锁变了”不等于“全冷”——前提是那份没被淘汰,这正是要守预算的原因。 -- `native-capsule.yml` 原本只在 `pull_request` 上跑:它写的缓存落在 `refs/pull//merge`,而那是一个**此后再也不会被恢复**的作用域,等于每个 PR 白写一份依赖树;同时 beta 上没有任何 job 产出同 key 的树,所以 PR 永远冷启动。现在它也在 `beta`/`main` 的 `push` 上跑,用 `shared-key: native-capsule` 做稳定前缀、`save-if` 只在 beta 写,PR 只读。 +- 原生胶囊(`native-capsule` job)原本是一个**独立的** `pull_request`-only workflow:它写的缓存落在 `refs/pull//merge`,而那是一个**此后再也不会被恢复**的作用域,等于每个 PR 白写一份依赖树;同时 beta 上没有任何 job 产出同 key 的树,所以 PR 永远冷启动。现在它已并入 `ci.yml`(同一个 `Changed areas` 门禁、同一次运行),用 `shared-key: native-capsule` 做稳定前缀、`save-if` 只在 beta 写,PR 只读。 ## 4. 维护 diff --git a/openless-all/app/scripts/ci-cache-sharing.test.mjs b/openless-all/app/scripts/ci-cache-sharing.test.mjs index 3d09e31bc..e12617d1e 100644 --- a/openless-all/app/scripts/ci-cache-sharing.test.mjs +++ b/openless-all/app/scripts/ci-cache-sharing.test.mjs @@ -10,7 +10,7 @@ // Runs without `npm ci` (the Linux contract job has no node_modules), so the // workflows are read as text. import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -23,7 +23,6 @@ const androidApk = read('android-apk.yml'); const linux = read('check-linux-egui.yml'); const linuxRelease = read('release-linux-egui.yml'); const releaseHarmony = read('release-harmony.yml'); -const nativeCapsule = read('native-capsule.yml'); const sharedKeys = (yaml) => [...yaml.matchAll(/shared-key:\s*([^\s#]+)/g)].map((match) => match[1].trim()); @@ -64,22 +63,25 @@ assert.match(linuxRelease, /checkout_ref: \$\{\{ inputs\.tag \}\}/); console.log('ci-cache-sharing contract test passed'); -// `native-capsule` used to run on pull requests only, with an ungated -// `rust-cache` step: every PR saved a dependency tree under -// `refs/pull//merge`, and beta never produced one to restore. It now runs on -// beta pushes and writes only there. -assert.match( - nativeCapsule, - /push:[\s\S]*?branches: \[main, beta\]/, - 'native-capsule.yml must run on beta pushes so the PRs have a tree to restore', -); -assert.match( - nativeCapsule, - /save-if: \$\{\{ github\.ref == 'refs\/heads\/beta'/, - 'native-capsule.yml must only save its cache from beta', -); -assert.match( - nativeCapsule, - /shared-key: native-capsule/, - 'native-capsule.yml must use a stable shared-key instead of the job name', -); +// `native-capsule` used to be a separate pull-request-only workflow: it wrote a +// dependency tree under `refs/pull//merge` (never restored again) while beta +// produced nothing to restore. It now lives in this workflow, gated by the same +// `Changed areas` verdict, and saves only from beta. +{ + const capsuleJob = ci.match(/\n native-capsule:[\s\S]*?(?=\n [a-z-]+:\n)/)?.[0] ?? ''; + assert.ok(capsuleJob, 'ci.yml must run the capsule crates as the native-capsule job'); + assert.match( + capsuleJob, + /shared-key: native-capsule/, + 'the native-capsule job must use a stable shared-key instead of the job name', + ); + assert.match( + capsuleJob, + /save-if: \$\{\{ github\.ref == 'refs\/heads\/beta'/, + 'the native-capsule job must only save its cache from beta', + ); + assert.ok( + !existsSync(join(root, '.github/workflows/native-capsule.yml')), + 'native-capsule.yml must stay merged into ci.yml', + ); +}