chore(ci): optimize CI caching, concurrency, and Tauri security (#937)

* ci: optimize CI caching, concurrency, and Tauri security

Address multiple CI/CD efficiency and security gaps identified during
project optimization analysis. The changes reduce PR build times,
prevent wasteful concurrent runs, tighten the Tauri security boundary,
and eliminate unnecessary submodule fetches on non-macOS jobs.

CI workflow improvements:
- Add swatinem/rust-cache@v2 to both ci.yml jobs (android-check and
  cross-platform), matching the caching already present in
  release-tauri.yml. This converts PR Rust compilation from full
  rebuilds to incremental builds, saving an estimated 60-80% of
  compile time per run.
- Add concurrency groups to all three workflows. PR pushes cancel
  superseded runs to free concurrency slots; tag pushes and manual
  dispatches are isolated by run_id to avoid mutually canceling
  release builds. Previously, rapid PR pushes triggered redundant
  parallel CI runs with no cancellation.
- Unify Node version to 22 across release-tauri.yml and
  android-apk.yml, which were previously pinned to 20. This ensures
  consistent build environments across all workflows.

Submodule and security hardening:
- Conditionally fetch the vendor/qwen-asr submodule only on macOS
  jobs across all three workflows. The submodule is compiled
  exclusively by build.rs on macOS (build_qwen_asr_macos); Linux,
  Windows, and Android builds never reference it. Removing the
  unconditional recursive checkout eliminates a network fetch and
  a submodule-availability failure point on every non-macOS job.
- Set withGlobalTauri to false in tauri.conf.json. The frontend
  exclusively uses @tauri-apps/api imports and never references the
  window.__TAURI__ global, so disabling it removes an unnecessary
  XSS attack surface without functional impact.

Dependency hygiene:
- Move @types/dompurify from dependencies to devDependencies in
  package.json. Type declaration packages are build-time only and
  should not ship in the production bundle.

* fix(ci): address review findings

* fix(ci): isolate manual CI workflow runs

---------

Co-authored-by: Chris233 <h-chris233@outlook.com>
This commit is contained in:
KingandChris233 authored and GitHub committed 2026-08-10 23:06:12 +08:00
1 parent 4496eba30b
commit c84bfb98f3
11 files changed
+161 -18

No files matched your search

+8 -3
View File
@@ -13,6 +13,11 @@ on:
- 'v*-tauri'
workflow_dispatch:
# 同一 tag 重复推送只跑最新一次;workflow_dispatch 用 run_id 隔离避免互相取消。
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
cancel-in-progress: ${{ github.event_name == 'push' }}
jobs:
build-android-apk:
permissions:
@@ -25,8 +30,8 @@ jobs:
OPENLESS_RELEASE_CHANNEL: ${{ (endsWith(github.ref_name, '-beta-tauri') || contains(github.ref_name, '-Beta.')) && 'beta' || 'stable' }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
# vendor/qwen-asr 仅 macOS 上 build.rs 会编译;Android APK 构建完全不需要
# 子模块,去掉递归拉取省一次网络 fetch + 失败点。
- name: Detect build mode
id: mode
@@ -136,7 +141,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: "22"
cache: npm
cache-dependency-path: openless-all/app/package-lock.json
+19 -6
View File
@@ -13,6 +13,11 @@ on:
branches: [main, beta]
workflow_dispatch:
# 同一 PR 快速重复推送时取消旧运行;workflow_dispatch 用 run_id 隔离。
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
android-check:
name: Android cargo check
@@ -22,9 +27,8 @@ jobs:
working-directory: openless-all/app
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
# vendor/qwen-asr 仅 macOS 上 build.rs 会编译(build.rs:49 build_qwen_asr_macos);
# Android/Linux 的 cargo check 不需要,去掉递归拉取省一次网络 fetch + 失败点。
- name: Setup Android SDK + NDK
uses: android-actions/setup-android@v3
with:
@@ -71,6 +75,10 @@ jobs:
with:
targets: aarch64-linux-android,x86_64-linux-android
- uses: swatinem/rust-cache@v2
with:
workspaces: 'openless-all/app/src-tauri -> target'
- uses: gradle/actions/setup-gradle@v4
- name: Install Linux check dependencies
@@ -170,9 +178,10 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
# vendor/qwen-asr 是 macOS 上 build.rs 必须的 git submodule。Windows
# checkout 也拉一份保持与 release-tauri.yml 一致,开销几秒可以忽略。
submodules: recursive
# vendor/qwen-asr 仅 macOS 上 build.rs 会编译(build.rs:49 build_qwen_asr_macos)。
# Windows/Linux 的 cargo check 不需要子模块;用矩阵条件避免非 macOS job
# 多拉一次 git submodule(省网络 fetch + 去掉一个失败点)。
submodules: ${{ matrix.os == 'macos-latest' && 'recursive' || 'false' }}
- uses: actions/setup-node@v4
with:
@@ -185,6 +194,10 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- uses: swatinem/rust-cache@v2
with:
workspaces: 'openless-all/app/src-tauri -> target'
- name: Install Linux check dependencies
if: runner.os == 'Linux'
run: |
+9 -4
View File
@@ -21,6 +21,11 @@ on:
- 'v*-tauri'
workflow_dispatch:
# 同一 tag 重复推送只跑最新一次;workflow_dispatch 用 run_id 隔离避免互相取消。
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
cancel-in-progress: ${{ github.event_name == 'push' }}
jobs:
build:
permissions:
@@ -60,13 +65,13 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
# vendor/qwen-asr 是 macOS 上 build.rs 必须的 git submodule(cc-rs 编译
# Open-Less/qwen-asr fork 的 C 源),不拉就会在 mac 端 cargo build 阶段挂掉。
submodules: recursive
# vendor/qwen-asr 仅 macOS 上 build.rs 会编译(build.rs:49 build_qwen_asr_macos);
# Windows/Linux 的 cargo build 不需要子模块,去掉非 macOS job 的递归拉取。
submodules: ${{ startsWith(matrix.platform, 'macos') && 'recursive' || 'false' }}
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: "22"
cache: npm
cache-dependency-path: 'openless-all/app/package-lock.json'
+3 -1
View File
@@ -17,7 +17,6 @@
"@tauri-apps/plugin-dialog": "^2.7.2",
"@tauri-apps/plugin-shell": "^2.3.5",
"@tauri-apps/plugin-updater": "^2.10.1",
"@types/dompurify": "^3.0.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dompurify": "^3.4.8",
@@ -33,6 +32,7 @@
},
"devDependencies": {
"@tauri-apps/cli": "^2.1.0",
"@types/dompurify": "^3.0.5",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
@@ -2320,6 +2320,7 @@
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/trusted-types": "*"
@@ -2363,6 +2364,7 @@
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/validate-npm-package-name": {
+1 -1
View File
@@ -41,7 +41,6 @@
"@tauri-apps/plugin-dialog": "^2.7.2",
"@tauri-apps/plugin-shell": "^2.3.5",
"@tauri-apps/plugin-updater": "^2.10.1",
"@types/dompurify": "^3.0.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dompurify": "^3.4.8",
@@ -57,6 +56,7 @@
},
"devDependencies": {
"@tauri-apps/cli": "^2.1.0",
"@types/dompurify": "^3.0.5",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
@@ -0,0 +1,34 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
const packageJson = JSON.parse(
await readFile(new URL('../package.json', import.meta.url), 'utf8'),
);
const packageLock = JSON.parse(
await readFile(new URL('../package-lock.json', import.meta.url), 'utf8'),
);
const lockRoot = packageLock.packages?.[''];
assert.ok(lockRoot, 'package-lock.json must contain the root package entry');
assert.deepEqual(
lockRoot.dependencies,
packageJson.dependencies,
'package-lock root dependencies must match package.json',
);
assert.deepEqual(
lockRoot.devDependencies,
packageJson.devDependencies,
'package-lock root devDependencies must match package.json',
);
const dompurifyTypes = packageLock.packages['node_modules/@types/dompurify'];
const trustedTypes = packageLock.packages['node_modules/@types/trusted-types'];
assert.equal(dompurifyTypes?.dev, true, '@types/dompurify must remain development-only');
assert.equal(
Boolean(trustedTypes?.dev || trustedTypes?.devOptional),
true,
'@types/trusted-types must not be classified as a production-only dependency',
);
console.log('package-lock-root-contract.test.mjs passed');
@@ -0,0 +1,42 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
const config = JSON.parse(
await readFile(new URL('../src-tauri/tauri.conf.json', import.meta.url), 'utf8'),
);
const lifecycleE2e = await readFile(
new URL('./windows-openless-lifecycle-e2e.py', import.meta.url),
'utf8',
);
const miscCommands = await readFile(
new URL('../src-tauri/src/commands/misc.rs', import.meta.url),
'utf8',
);
assert.equal(
config.app.withGlobalTauri,
false,
'the application must not expose the global Tauri API bundle',
);
assert.match(
lifecycleE2e,
/window\.__TAURI_INTERNALS__\.invoke\(/,
'the Windows lifecycle E2E must invoke through the Tauri IPC bridge',
);
assert.doesNotMatch(
lifecycleE2e,
/window\.__TAURI__\./,
'the Windows lifecycle E2E must not depend on the disabled global Tauri API',
);
assert.match(
miscCommands,
/window\.__TAURI_INTERNALS__\.invoke\(/,
'the cursor-context debug example must use the available Tauri IPC bridge',
);
assert.doesNotMatch(
miscCommands,
/\b__TAURI__\./,
'debug documentation must not recommend the disabled global Tauri API',
);
console.log('tauri-api-surface-contract.test.mjs passed');
@@ -74,7 +74,7 @@ class CdpClient:
args_json = json.dumps(args or {}, ensure_ascii=False)
expression = f"""
(async () => {{
const value = await window.__TAURI__.core.invoke({json.dumps(command)}, {args_json});
const value = await window.__TAURI_INTERNALS__.invoke({json.dumps(command)}, {args_json});
return JSON.stringify(value ?? null);
}})()
"""
@@ -0,0 +1,42 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
const workflows = {
android: await readFile(new URL('../../../.github/workflows/android-apk.yml', import.meta.url), 'utf8'),
ci: await readFile(new URL('../../../.github/workflows/ci.yml', import.meta.url), 'utf8'),
tauriRelease: await readFile(
new URL('../../../.github/workflows/release-tauri.yml', import.meta.url),
'utf8',
),
};
const isolatedManualGroup =
"group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}";
const cancelSupersededTagPush = "cancel-in-progress: ${{ github.event_name == 'push' }}";
for (const [name, source] of [
['Android release', workflows.android],
['Tauri release', workflows.tauriRelease],
]) {
assert.ok(
source.includes(isolatedManualGroup),
`${name} workflow must isolate manual runs while grouping repeated pushes of the same tag`,
);
assert.ok(
source.includes(cancelSupersededTagPush),
`${name} workflow must cancel an older run when the same tag is pushed again`,
);
}
assert.ok(
workflows.ci.includes(isolatedManualGroup),
'PR CI must isolate manual runs while grouping pull-request and branch runs by ref',
);
assert.ok(
workflows.ci.includes(
"cancel-in-progress: ${{ github.event_name == 'pull_request' }}",
),
'PR CI must cancel superseded pull-request runs without cancelling branch pushes',
);
console.log('workflow-concurrency-contract.test.mjs passed');
@@ -222,7 +222,7 @@ fn resolve_openless_log_path() -> Result<std::path::PathBuf, String> {
/// 微信里点进输入框,探针在那时才真正开始读。
///
/// ```js
/// await __TAURI__.core.invoke('debug_read_cursor_context', { delayMs: 3000 })
/// await window.__TAURI_INTERNALS__.invoke('debug_read_cursor_context', { delayMs: 3000 })
/// ```
#[tauri::command]
pub async fn debug_read_cursor_context(
+1 -1
View File
@@ -10,7 +10,7 @@
"frontendDist": "../dist"
},
"app": {
"withGlobalTauri": true,
"withGlobalTauri": false,
"macOSPrivateApi": true,
"windows": [
{