feat(scripts): pre-push 钩子对所有分支按内容触发告警检查;CI 挂到已有缓存的 Rust job
本地:.husky/pre-push 无条件调用 node scripts/check-rust-warnings.mjs --only-if-changed(脚本按 refs 判断本次推送是否触碰 *.rs/Cargo.toml/Cargo.lock,不碰则打印一行并 exit 0);推 master 才做 repository parity 的既有约定保持原样。package.json:把 check:rust-warnings 从 lint 链里摘掉(避免 master 推送跑两次、也不拖慢 repository-checks),脚本保留供手工与 CI 使用。CI:ai-game-creator-shell-rust-lane-2 加一步 --profile=agc-linux,ai-game-creator-shell-rust-crates 加一步 --profile=server-rs(两 job 已有 cargo fetch + sccache 缓存);repository-checks 不动。文档同步接法、失败模式与应急放行(GENARRATIVE_SKIP_RUST_WARNINGS=1)。
This commit is contained in:
@@ -152,6 +152,9 @@ jobs:
|
||||
- name: Run AI game creator shell Rust shard 4/4
|
||||
run: npm run check:native-shells:agc-rust-shard-4
|
||||
|
||||
- name: Run AGC shell Rust warning check (Linux)
|
||||
run: npm run check:rust-warnings -- --profile=agc-linux
|
||||
|
||||
- name: Report isolated Rust compilation cache
|
||||
if: always()
|
||||
run: bash scripts/ci-rust-cache.sh report
|
||||
@@ -266,6 +269,9 @@ jobs:
|
||||
- name: Run AI game creator shell shared crate gates
|
||||
run: npm run check:native-shells:agc-rust-crates
|
||||
|
||||
- name: Run server-rs Rust warning check
|
||||
run: npm run check:rust-warnings -- --profile=server-rs
|
||||
|
||||
- name: Report isolated Rust compilation cache
|
||||
if: always()
|
||||
run: bash scripts/ci-rust-cache.sh report
|
||||
|
||||
+8
-1
@@ -1,4 +1,11 @@
|
||||
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
|
||||
# 钩子链(npm → check:repository-ci → 测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
|
||||
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
|
||||
npm run check:pre-push-master -- "$@"
|
||||
# pre-push 的 ref 行(stdin)被两段检查共用:先落一份临时文件,第二段才读得到。
|
||||
push_refs="$(mktemp)"
|
||||
cat > "${push_refs}"
|
||||
trap 'rm -f "${push_refs}"' EXIT
|
||||
# 「推 master 才做 repository parity」的既有约定保持原样(由 scripts/pre-push-master.sh 自己判断分支)。
|
||||
npm run check:pre-push-master -- "$@" < "${push_refs}"
|
||||
# Rust 告警检查对所有分支一视同仁,但按内容触发:脚本自己看 refs 里的提交有没有碰 *.rs/Cargo.toml/Cargo.lock。
|
||||
node scripts/check-rust-warnings.mjs --only-if-changed < "${push_refs}"
|
||||
|
||||
@@ -391,7 +391,8 @@ npm run check
|
||||
> - **首采**:`agc-linux` 档本机(Windows)编不了,基线**留空**;首次 CI 运行以「采集」模式打印清单且**不判失败**,跑完需要一次 `node scripts/check-rust-warnings.mjs --profile=agc-linux --update-baseline` 入册。
|
||||
> - **开关**:`GENARRATIVE_SKIP_RUST_WARNINGS=1` 可本地跳过(日常 `npm run lint` 不想多花几分钟时用);`GENARRATIVE_WARNCHECK_DEBUG=1` 打印包集 / clean 命令 / artifact 的 fresh 表 / message 总数。
|
||||
> - **局限**:`git push --no-verify` 可绕过;CI 侧**不覆盖** AGC 壳的 Windows 生产口径(GTK/ring 无法在 Linux 交叉编译);基线只保证「不新增」,不保证存量收敛。
|
||||
> - **CI 侧现状与风险(接线方案待定)**:`repository-checks` 的 runner 镜像里**有** Rust 工具链与 AGC 所需的 Linux 依赖(该 job 的日志会打印 `rustc 1.98.1 / cargo 1.98.1 / rustfmt 1.9.0-stable`),所以检查**能跑、不会红**(`agc-linux` 首次只采集、`server-rs` 基线为 0);但该 job **没有任何 cargo 预热缓存步骤**,默认会在它自己的 target 目录里从零编译(AGC Linux 档的 tauri/webkit 全链依赖 + server-rs 全 workspace),**估 25-40 分钟**。优化方向(待定):给该 job 挂 `ci-rust-cache.sh` 并把 `GENARRATIVE_WARNCHECK_TARGET_DIR` 指过去,或把检查挪到已有缓存的 lane job 里作为一步。
|
||||
> - **接线(2026-10-07 定稿)**:**本地 = `.husky/pre-push` 对所有分支一视同仁**(`node scripts/check-rust-warnings.mjs --only-if-changed`,按**内容**触发:本次推送的提交没碰 `*.rs`/`Cargo.toml`/`Cargo.lock` 就打印一行并 exit 0;`GENARRATIVE_SKIP_RUST_WARNINGS=1` 可跳过);**“推 master 才做 repository parity”的既有约定保持原样**(`scripts/pre-push-master.sh` 自己判断分支,`scripts/git-hooks.test.mjs` 有断言守着)。**CI = 挂在两个既有 Rust job 上**:`ai-game-creator-shell-rust-lane-2` 加一步 `npm run check:rust-warnings -- --profile=agc-linux`,`ai-game-creator-shell-rust-crates` 加一步 `--profile=server-rs`——这两个 job 本来就有 `cargo fetch` + `ci-rust-cache.sh prepare/report`(sccache 编译缓存,按 rustc 版本 + workspace 路径 + source-commit key,与 job 无关),因此不新增 job、不给非 Rust job 塞缓存。该检查**不再挂在 `npm run lint` 链**里(避免 master 推送跑两次、也不拖慢 `repository-checks`);`check:rust-warnings` 脚本保留供手工跑与 CI 调用。应急放行:在那两步所在 job 里 `GENARRATIVE_SKIP_RUST_WARNINGS=1`。
|
||||
> - **CI 侧失败模式与处置**:① 前置缺失——已核不适用(镜像有 rustc/cargo/rustfmt,AGC 的 Linux 依赖也在);② 误报——`agc-linux` 首采不判失败(无基线条目时只采集)、`server-rs` 基线 0 条,剩余风险是 fresh 完整性断言在该 job 误触发:看它打印的「未重编的成员」,重跑一次;仍失败先用 `GENARRATIVE_SKIP_RUST_WARNINGS=1` 应急放行再修;③ 漏报——fresh 断言 + 集合差已实测(播种 → exit 1、减少 → 存量减少 1 + exit 0、漏清一个成员 → 26/27 + exit 1);④ 成本——挂在已有缓存的 job 上,预计额外 1-3 分钟(`repository-checks` 不再承担)。
|
||||
|
||||
- 口径:全仓 Rust **first-party** 编译 warning 清零,能直接修的一律修掉,不用 `#[allow]` 掩盖;与既有文档口径冲突的优先修订口径(同批同步改文档);确实不能修的逐条登记原因与关闭条件。仍然禁止:新增**未登记**的 `#[allow(dead_code)]`、假引用、仅为消警删除测试;生成代码不手改(要改就改生成器)。下面一条是当前**唯一**登记的例外。
|
||||
- **已登记的 `#[allow(dead_code)]` 例外(2026-10-07,PR #650「Rust 编译告警」)**:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_provider/mod.rs` 顶部的 `#![allow(dead_code)]`,覆盖它用 `#[path]` 挂载的两棵子树——`app_server`(= `agent/codex_app_server/**`,含凭据、模型目录与 OAuth 交接)与 `cli`(= `agent/codex_cli.rs`,含 CLI 请求路径)。
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@
|
||||
"check:server-rs-ddd": "npm run check:spacetime-schema && npm run check:spacetime-runtime-access && npm run check:module-runtime-artifact && node scripts/check-server-rs-ddd-boundaries.mjs",
|
||||
"lint:eslint": "eslint . --ext .ts,.tsx,.js,.mjs,.cjs --max-warnings 0",
|
||||
"typecheck": "tsc -p tsconfig.typecheck-guardrails.json --noEmit",
|
||||
"lint": "npm run check:encoding && npm run check:doc-index && npm run check:npm-workspaces && npm run check:git-hooks && npm run check:rustfmt && npm run check:rust-warnings && npm run check:spacetime-schema && npm run check:generated-bindings && npm run check:game-distribution-dto-parity && npm run check:game-distribution-price-limit-parity && npm run check:production-ops && npm run check:preview-deployer && npm run check:maintenance-page && npm run check:nginx-spa-routes && npm run check:pingora-route-parity && npm run lint:eslint && npm run typecheck",
|
||||
"lint": "npm run check:encoding && npm run check:doc-index && npm run check:npm-workspaces && npm run check:git-hooks && npm run check:rustfmt && npm run check:spacetime-schema && npm run check:generated-bindings && npm run check:game-distribution-dto-parity && npm run check:game-distribution-price-limit-parity && npm run check:production-ops && npm run check:preview-deployer && npm run check:maintenance-page && npm run check:nginx-spa-routes && npm run check:pingora-route-parity && npm run lint:eslint && npm run typecheck",
|
||||
"lint:fix": "eslint . --ext .ts,.tsx,.js,.mjs,.cjs --fix && prettier --write .",
|
||||
"format:rust": "cargo fmt --all --manifest-path server-rs/Cargo.toml && cargo fmt --all --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml && cargo fmt --all --manifest-path plugins/agc-unity-editor/native/unity-editor-bridge/Cargo.toml && cargo fmt --all --manifest-path plugins/agc-godot-editor/native/godot-editor-bridge/Cargo.toml",
|
||||
"format": "prettier --write . && npm run format:rust",
|
||||
|
||||
@@ -107,7 +107,8 @@ const unknown = argv.filter(
|
||||
a.startsWith('--') &&
|
||||
!a.startsWith('--profile=') &&
|
||||
a !== '--update-baseline' &&
|
||||
a !== '--json',
|
||||
a !== '--json' &&
|
||||
a !== '--only-if-changed',
|
||||
);
|
||||
if (unknown.length > 0) {
|
||||
console.error(`[rust-warnings] 未知参数:${unknown.join(' ')}`);
|
||||
@@ -120,6 +121,48 @@ if (process.env.GENARRATIVE_SKIP_RUST_WARNINGS === '1') {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// `--only-if-changed`:pre-push 钩子对所有分支一视同仁地调用本脚本,但只在本次推送**真的触碰 Rust 代码**
|
||||
// (*.rs / Cargo.toml / Cargo.lock)时才执行检查——按内容触发,不看分支。refs 从 stdin 读(pre-push 约定:
|
||||
// `local_ref local_sha remote_ref remote_sha`)。
|
||||
if (argv.includes('--only-if-changed')) {
|
||||
const gitOut = (...args) => {
|
||||
const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8' });
|
||||
return result.status === 0 ? (result.stdout ?? '').trim() : '';
|
||||
};
|
||||
const ZERO_SHA = /^0+$/u;
|
||||
const touched = new Set();
|
||||
for (const line of readFileSync(0, 'utf8').split('\n')) {
|
||||
const [localRef, localSha, , remoteSha] = line.trim().split(/\s+/u);
|
||||
if (!localRef || !localSha || ZERO_SHA.test(localSha)) continue;
|
||||
let base = ZERO_SHA.test(remoteSha ?? '') ? '' : remoteSha;
|
||||
if (!base) {
|
||||
base =
|
||||
gitOut('merge-base', 'origin/master', localSha) ||
|
||||
gitOut('rev-parse', `${localSha}^`);
|
||||
}
|
||||
if (!base) continue;
|
||||
for (const path of gitOut(
|
||||
'diff',
|
||||
'--name-only',
|
||||
`${base}..${localSha}`,
|
||||
).split('\n')) {
|
||||
if (path) touched.add(path);
|
||||
}
|
||||
}
|
||||
const rustPaths = [...touched].filter((path) =>
|
||||
/(\.rs$|Cargo\.toml$|Cargo\.lock$)/u.test(path),
|
||||
);
|
||||
if (rustPaths.length === 0) {
|
||||
console.log(
|
||||
`[rust-warnings] 本次推送未触碰 Rust 代码(共 ${touched.size} 个文件),跳过检查。`,
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
console.log(
|
||||
`[rust-warnings] 本次推送触碰 Rust 文件 ${rustPaths.length} 个,开始检查。`,
|
||||
);
|
||||
}
|
||||
|
||||
let warmTargetDir;
|
||||
function workspaceTargetDir() {
|
||||
if (warmTargetDir) return warmTargetDir;
|
||||
|
||||
Reference in New Issue
Block a user