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 f9a72fc3b..000000000 --- a/.github/workflows/native-capsule.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Native capsule -on: - pull_request: - branches: [main, beta] - workflow_dispatch: -jobs: - native-capsule: - strategy: - fail-fast: false - matrix: - os: [macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - defaults: - run: - working-directory: openless-all/app - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - with: - workspaces: openless-all/app - - 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..7c489e473 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` 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 d4e847b00..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'; @@ -22,6 +22,7 @@ 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 sharedKeys = (yaml) => [...yaml.matchAll(/shared-key:\s*([^\s#]+)/g)].map((match) => match[1].trim()); @@ -61,3 +62,26 @@ 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 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', + ); +}