fix(scripts): pre-push 保留原钩子末行、告警检查改按内容触发;文档补基线白名单纪律
.husky/pre-push:refs 先落临时文件→告警检查用 --refs-file 读→exec < 还原 stdin→最后一行仍是 npm run check:pre-push-master -- "$@"(git-hooks.test.mjs 断言逐字不变,故 check:git-hooks 仍绿)。文档:基线 = 告警白名单(显式入册/可审计/可回收)、与 #[allow] 的区别(白名单只吞键相同的那一条)、审计入口 git log -p scripts/warning-baseline.json、SKIP=1 是整段跳过而非入册机制;并修正“feature 分支不跑”的过时表述。
This commit is contained in:
+5
-5
@@ -1,11 +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
|
||||
# pre-push 的 ref 行(stdin)被两段检查共用:先落一份临时文件,第二段才读得到。
|
||||
# Rust 告警检查对所有分支一视同仁,但按内容触发:脚本自己看本次推送的 refs 有没有碰 *.rs/Cargo.toml/Cargo.lock。
|
||||
# refs 落成临时文件传给脚本,这样最后一行原有的钩子命令保持逐字不变、且仍拿到完整的 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}"
|
||||
node scripts/check-rust-warnings.mjs --only-if-changed --refs-file="${push_refs}"
|
||||
exec < "${push_refs}"
|
||||
npm run check:pre-push-master -- "$@"
|
||||
|
||||
@@ -385,14 +385,14 @@ npm run check
|
||||
>
|
||||
> **接法更正(2026-10-07 收口)**:不另造 hook / 不新增 CI job,而是把检查**挂进 `npm run lint`**——它是 CI 的 `Repository checks`(`.gitea/workflows/project-ci.yml` 里 `run: npm run check:repository-ci` → `scripts/check-repository-ci.sh` → `npm run lint`)与「推 master 时的 pre-push」(`.husky/pre-push` → `npm run check:pre-push-master` → `scripts/pre-push-master.sh` → `npm run check:repository-ci`)**共用的那条命令**;因此一次接入,两处生效。
|
||||
>
|
||||
> - **feature 分支推送本地不跑**这项检查(`scripts/pre-push-master.sh` 只在推 `refs/heads/master` 时才真正执行;这是仓库既有行为,未改)。
|
||||
> - **pre-push 的触发范围**:Rust 告警检查**对所有分支一视同仁**(不再只在推 master 时跑),但**按内容触发**:本次推送的提交没碰 `*.rs`/`Cargo.toml`/`Cargo.lock` 就打印一行并 exit 0。推 master 时还会额外跑一次 repository parity(`scripts/pre-push-master.sh` 的既有约定,未改)。
|
||||
> - **测量确定性**(`scripts/check-rust-warnings.mjs`):profile 专属 target 目录(默认 `<tmp>/genarrative-warncheck/<profile>`,可用 `GENARRATIVE_WARNCHECK_TARGET_DIR` 指向复用缓存)+ 对本 profile 的 first-party 包逐个 `cargo clean -p <包名>`,取数后**完整性断言**:profile 的每个包都必须出现 `compiler-artifact` 且 `fresh=false`,否则 exit 1 报「测量不完整」(原因:cargo 只对真正重编的 crate 打印告警,热目录下未变化 crate 的存量告警不重现,会给出「新增 0」的假答案)。
|
||||
> - **被否决的方案**:取数前**整体清空** target 目录——实测 AGC 壳清空后要重建 tauri/webkit 全链依赖(**15-25 分钟**),代价不可接受。
|
||||
> - **首采**:`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 交叉编译);基线只保证「不新增」,不保证存量收敛。
|
||||
> - **接线(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` 不再承担)。
|
||||
> - **基线 = 告警白名单(怎么用、纪律)**:`scripts/warning-baseline.json` 按 `路径|符号|lint` 列出「已知存量」,检查只拦**基线之外的新增** ⇒ 白名单里的告警是**被允许存在**的。它可以用作「暂时缓解不好处理的告警」的手段,但必须**显式入册**(写进基线并提交)⇒ 可见、可审计、可回收,**不是静默跳过**;提交信息要写**为什么入册**与**退出条件**(什么时候该删掉这条)。**与 `#[allow]` 的区别**:`#[allow]` 会静默该作用域内**未来所有**同类告警;白名单只吞「**键完全相同**」的那一条 ⇒ 同文件里**新的**符号 / 新的 lint 照样会被拦,而且 `[已消除·可更新基线]` 会提示白名单该收缩。**审计入口**:`git log -p scripts/warning-baseline.json` 就是「谁、何时、为什么」往白名单加了什么 —— 评审 PR 时留意「新增白名单条目」。另外 `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 请求路径)。
|
||||
|
||||
@@ -108,7 +108,8 @@ const unknown = argv.filter(
|
||||
!a.startsWith('--profile=') &&
|
||||
a !== '--update-baseline' &&
|
||||
a !== '--json' &&
|
||||
a !== '--only-if-changed',
|
||||
a !== '--only-if-changed' &&
|
||||
!a.startsWith('--refs-file='),
|
||||
);
|
||||
if (unknown.length > 0) {
|
||||
console.error(`[rust-warnings] 未知参数:${unknown.join(' ')}`);
|
||||
@@ -125,13 +126,18 @@ if (process.env.GENARRATIVE_SKIP_RUST_WARNINGS === '1') {
|
||||
// (*.rs / Cargo.toml / Cargo.lock)时才执行检查——按内容触发,不看分支。refs 从 stdin 读(pre-push 约定:
|
||||
// `local_ref local_sha remote_ref remote_sha`)。
|
||||
if (argv.includes('--only-if-changed')) {
|
||||
const refsFileArg = argv.find((a) => a.startsWith('--refs-file='));
|
||||
// 钩子把 pre-push 的 refs 落成文件传进来,从而把 stdin 原样留给最后那行原有的钩子命令。
|
||||
const refsText = refsFileArg
|
||||
? readFileSync(refsFileArg.slice('--refs-file='.length), 'utf8')
|
||||
: readFileSync(0, 'utf8');
|
||||
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')) {
|
||||
for (const line of refsText.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;
|
||||
|
||||
Reference in New Issue
Block a user