chore: 加 Rust 告警的 pre-push + CI 基线感知检查(单一脚本 + 基线)

目的:不再用 Werror,改为「推送前 + CI」的显式检查,并且只拦**基线之外的新增**告警,
这样上游新代码的存量不会挡住任何人,本地开发构建也不会因为告警变红。

单一真源(本地与 CI 调同一份实现):
- 新增 `scripts/check-rust-warnings.mjs` + `npm run check:rust-warnings`:
  用 `cargo check --message-format=json` 收集告警,键 = `路径|符号|lint`(**不含行号**,抗上游行号漂移),
  与 `scripts/warning-baseline.json` 比对:新增 → 退出码 1 并逐条打印;存量减少 → 只提示可更新;
  更新用 `--profile=<name> --update-baseline`。
- 新增 `scripts/pre-push-rust-warnings.mjs`:读 pre-push 的 ref 行,**只在本次推送触碰
  `*.rs`/`Cargo.toml`/`Cargo.lock`** 时才跑(无关推送不受影响);Windows 跑 `agc-windows`+`server-rs`,
  Linux/macOS 只跑 `server-rs`。
- `.husky/pre-push`:把 ref 行落一份临时文件供两段检查共用,然后调用上面的脚本(原有的
  `check:pre-push-master` 保持不变)。
- `.gitea/workflows/project-ci.yml`:新增 `rust-warning-check` job(`runs-on: genarrative-ci`,Linux),
  跑 `--profile=server-rs --profile=agc-linux`,复用 Rust lane 的隔离编译缓存,只 `cargo check` 不 build。
- `scripts/warning-baseline.json`:`agc-windows` 14 条(上游 `b04aceb57` 等引入,逐条见
  `local://commit-1a86675f4-inventory.md`);`agc-linux` 433 条(从 CI lane 日志抽取去重,**刻意取超集**,
  含测试实例产物,首轮 CI 的多余项只显示为「存量减少」不判失败);`server-rs` 0 条。
- 文档:dev 运维「Rust 编译告警门禁」改写为现役口径(目的/命令/基线更新/覆盖分工与缺口/局限),
  旧 Werror 段落标注为历史;development-workflow 的门禁句同步改写。

覆盖分工:CI 覆盖 `server-rs` + AGC 壳 **Linux** 档;AGC 壳 **Windows 生产口径**(GTK/ring 无法在
Linux 交叉编译)**只由本地 pre-push 覆盖**。局限:`--no-verify` 可绕过;本机 target 目录污染要先
`cargo clean -p`;CI 不跑 clippy;基线只保证「不新增」。
This commit is contained in:
2026-10-07 19:22:29 +08:00
parent 9a844f69a0
commit b846f04dab
8 changed files with 2664 additions and 2 deletions
+50
View File
@@ -470,6 +470,56 @@ jobs:
- name: Validate production API deploy behavior
run: npm run check:production-api-deploy
rust-warning-check:
name: Rust warning check
runs-on: genarrative-ci
steps:
- name: Checkout source from Gitea
env:
GENARRATIVE_GITEA_FETCH_DEPTH: '1'
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
run: genarrative-gitea-checkout
- name: Validate preinstalled CI job image and sandbox
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
# 只为这个 job 需要的依赖预热;复用与 Rust lane 相同的隔离编译缓存,避免全量重编。
- name: Prepare Rust dependencies
shell: bash
run: |
set -euo pipefail
for manifest in server-rs/Cargo.toml apps/ai-game-creator-shell/src-tauri/Cargo.toml; do
for attempt in $(seq 1 5); do
if cargo fetch --locked \
--target x86_64-unknown-linux-gnu \
--manifest-path "${manifest}"; then
break
fi
if [[ "${attempt}" -eq 5 ]]; then
echo "dependency fetch failed after 5 attempts: ${manifest}" >&2
exit 1
fi
sleep $((attempt * 2))
done
done
- name: Prepare isolated Rust compilation cache
shell: bash
run: |
set -euo pipefail
node --test scripts/ci-rust-cache.test.mjs
bash scripts/ci-rust-cache.sh prepare
# 与本地 pre-push 同一个脚本(scripts/check-rust-warnings.mjs);基线感知,只拦「新增」告警。
# 覆盖:server-rs workspace(Linux 可跑,0 存量)+ AGC 壳 Linux 档。
# 不覆盖:AGC 壳「Windows + 编辑器 feature」生产口径——那档只能在 Windows 上编,由本地钩子覆盖。
- name: Run Rust warning check
run: npm run check:rust-warnings -- --profile=server-rs --profile=agc-linux
- name: Report isolated Rust compilation cache
if: always()
run: bash scripts/ci-rust-cache.sh report
repository-checks:
name: Repository checks
runs-on: genarrative-ci
+6 -1
View File
@@ -1,4 +1,9 @@
# 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
npm run check:pre-push-master -- "$@" < "${push_refs}"
node scripts/pre-push-rust-warnings.mjs < "${push_refs}"
@@ -57,7 +57,7 @@ AGC 生成准备只通过单目录 GET 确认当前账号绑定,不能重新
Windows 下的移动壳 smoke 通过 Node 启动从当前 workspace 包解析出的 Expo/EAS CLI,不直接 `spawnSync('npm.cmd')`;保留原配置与导出断言。具体入口和警告清理边界见本地开发运维文档。
AGC 的纯桌面/生产接入按模块排除测试编译,共享实现与受测入口保持可测试;拆分后同时检查普通目标和测试目标,并保留原有行为断言。命令注册与退出接线的源码检查跟随真实模块位置更新,不能因移动文件而漏检;不通过全局允许死代码或虚假调用消除告警。Rust 侧编译 warning 已纳入门禁:workspace 根 `[workspace.lints.rust] warnings = "deny"`,成员 crate 用 `[lints] workspace = true` 继承,被 `exclude` 或自带 manifest 的独立 crate 各自声明 `[lints.rust] warnings = "deny"`;能直接修的一律修掉、不以 `#[allow]` 掩盖,条件编译拆分不作为消警手段;**唯一登记的模块级例外**是 AGC 壳 `agent/codex_provider`(休眠的 codex provider 子树:app-server + 内置 CLI 请求路径)顶部的 `#![allow(dead_code)]`,其作用域、理由、失效边界与退出条件见开发运维文档「Rust 编译告警门禁」的登记条。具体边界见开发运维文档“编译告警的保留边界与待优化项”。
AGC 的纯桌面/生产接入按模块排除测试编译,共享实现与受测入口保持可测试;拆分后同时检查普通目标和测试目标,并保留原有行为断言。命令注册与退出接线的源码检查跟随真实模块位置更新,不能因移动文件而漏检;不通过全局允许死代码或虚假调用消除告警。Rust 侧编译 warning 改为**基线感知的检查**(2026-10-07 起不再用 `deny(warnings)`/`-D warnings`):单一实现 `scripts/check-rust-warnings.mjs`(`npm run check:rust-warnings`)由 `.husky/pre-push` 与 CI 的 `rust-warning-check` job 共用,只拦基线(`scripts/warning-baseline.json`,键为 `路径|符号|lint`)之外的新增告警;能直接修的一律修掉、不以 `#[allow]` 掩盖,条件编译拆分不作为消警手段;**唯一登记的模块级例外**是 AGC 壳 `agent/codex_provider`(休眠的 codex provider 子树:app-server + 内置 CLI 请求路径)顶部的 `#![allow(dead_code)]`,其作用域、理由、失效边界与退出条件见开发运维文档「Rust 编译告警门禁」的登记条。具体边界见开发运维文档“编译告警的保留边界与待优化项”。
提示词外置变更运行 `runtime_prompt_bundle_build` 与 `prompt_source_boundaries` 两个 Rust 集成测试,验证编译期文本、目录登记和源码边界;现有 `agc-rust-shard-1` 本地/CI 入口先执行这组检查,再运行分片单测。
@@ -368,6 +368,21 @@ npm run check
### Rust 编译告警门禁
> **现役口径(2026-10-07 起,PR #650)**:Rust 编译告警**不再用 Werror**——`deny(warnings)` / `-D warnings` 已全部撤掉(AGC 壳的 `cfg_attr(…, deny(warnings))`、workspace 的 `[workspace.lints.rust]`、各 crate 的 `[lints]`/`[lints.rust]` 一并移除)。
> 门禁改为「**本地 pre-push 检查 + CI 的 warning check job**」,且**基线感知**:只拦基线之外的**新增**告警。
>
> - **单一实现**:`scripts/check-rust-warnings.mjs`(`npm run check:rust-warnings`);本地钩子与 CI 调的就是这一份,钩子里不另写一套。
> - **本地 pre-push**:`.husky/pre-push` → `scripts/pre-push-rust-warnings.mjs`,只在本次推送**触碰 `*.rs` / `Cargo.toml` / `Cargo.lock`** 时才跑。
> Windows 跑 `--profile=agc-windows`(AGC 壳 Windows + 三编辑器 feature 生产口径)与 `--profile=server-rs`;Linux/macOS 只跑 `server-rs`。
> - **CI**:`.gitea/workflows/project-ci.yml` 的 `rust-warning-check` job(`runs-on: genarrative-ci`,Linux)跑 `npm run check:rust-warnings -- --profile=server-rs --profile=agc-linux`,并复用 Rust lane 的隔离编译缓存,只做 `cargo check`(不做 build)。
> - **覆盖分工与缺口**:CI 覆盖 `server-rs`(全平台可跑,0 存量)与 **AGC 壳 Linux 档**;**AGC 壳的 Windows 生产口径在 Linux CI 上无法编译**(GTK/webkit 的 `-sys` 与 `ring` 需要 Linux 工具链),因此**只由本地 pre-push 覆盖**。
> - **基线**:`scripts/warning-baseline.json`,键 = `路径 | 符号 | lint`(**不含行号**,行号只在输出里作附注,避免上游改一行导致整份基线漂移)。
> 新增 → 退出码 1 并逐条打印;存量减少 → 只提示「可更新基线」;更新:`node scripts/check-rust-warnings.mjs --profile=<name> --update-baseline`(人工确认后随提交入库)。
> 当前存量:`agc-windows` 14 条(`b04aceb57` 等 master 侧提交引入的上游告警,逐条见 `local://commit-1a86675f4-inventory.md`)、`agc-linux` 433 条(从 CI lane 日志抽取并去重,**刻意取超集**:含测试实例产物的假死告警,首轮 CI 跑出来的多余项只显示为「存量减少」而不判失败)、`server-rs` 0 条。
> - **局限**:① `git push --no-verify` 可以绕过(CI 仍会跑,但只覆盖 Linux 档 + server-rs);② 本机 target 目录被污染时会读出假数字,取数前先 `cargo clean -p <crate>`;③ CI 不跑 clippy;④ 基线只保证「不新增」,不保证存量收敛;⑤ AGC 壳 Windows 档只有在 Windows 机器上推代码时才被检查。
>
> 以下小节是**历史记录**(`deny(warnings)` 时代的落法、AGC 门禁 predicate、harness 假告警边界、2026-10-06 基线表与检查命令),保留供追溯;其中的 Werror 接线与判据已随本 PR 撤销,`deny(warnings)` 在 tip 上**已 0 命中**。
- 口径:全仓 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 请求路径)。
- **作用域**:只作用于该模块子树。其它 crate、其它目录、其它 lint(例如 `unused_imports`)与 CI 门禁都不受影响;`unused_imports` / `unused_variables` 等仍必须真修,`allow(dead_code)` 压不住它们(已实测:模块级 allow 下未使用的 `use` 依旧报错)。
+1
View File
@@ -71,6 +71,7 @@
"check:npm-workspaces": "node --test scripts/check-npm-workspaces.test.mjs && node scripts/check-npm-workspaces.mjs",
"check:repository-ci": "bash scripts/check-repository-ci.sh",
"check:rustfmt": "cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check && cargo fmt --all --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check && cargo fmt --all --manifest-path plugins/agc-unity-editor/native/unity-editor-bridge/Cargo.toml -- --check && cargo fmt --all --manifest-path plugins/agc-godot-editor/native/godot-editor-bridge/Cargo.toml -- --check",
"check:rust-warnings": "node scripts/check-rust-warnings.mjs",
"check:spacetime-schema": "node scripts/check-spacetime-schema-guard.mjs",
"check:generated-bindings": "node scripts/check-generated-bindings.mjs",
"check:game-distribution-dto-parity": "node scripts/check-game-distribution-dto-parity.mjs",
+257
View File
@@ -0,0 +1,257 @@
#!/usr/bin/env node
// Rust 编译告警的「基线感知」检查。
//
// 为什么不是 Werror:`deny(warnings)` 把 warning 变成编译错误,等于让上游新代码的存量告警
// 直接挡住所有人、也会让本地开发构建变红(PR #650 的决策记录里有完整经过)。改成
// 「本地 pre-push + CI 的显式检查」,并按基线只拦**新增**告警。
//
// 单一真源:本地 `.husky/pre-push` 与 CI 的 `warning check` job 调用的是同一份实现(本文件)。
//
// 用法:
// node scripts/check-rust-warnings.mjs # 按当前平台选默认 profile
// node scripts/check-rust-warnings.mjs --profile=server-rs --profile=agc-windows
// node scripts/check-rust-warnings.mjs --profile=agc-linux # CI(Linux)用
// node scripts/check-rust-warnings.mjs --update-baseline # 把当前结果写回基线(人工确认后)
// node scripts/check-rust-warnings.mjs --json # 机器可读
//
// 基线键 = `路径 | 符号 | lint`(**不含行号**,行号只作输出附注),存于
// `scripts/warning-baseline.json`。这样上游改一行导致的整体漂移不会让基线全部失效。
//
// 退出码:0 = 无新增;1 = 有新增(或调用失败)。存量减少只在输出里提示「可更新基线」。
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const baselinePath = join(repoRoot, 'scripts', 'warning-baseline.json');
const AGC_MANIFEST = 'apps/ai-game-creator-shell/src-tauri/Cargo.toml';
const AGC_FEATURES =
'cocos-editor-execute,unity-editor-execute,godot-editor-execute';
// profile = 一档「口径」:同一份代码在不同平台/feature 下的告警集合互不相同,因此基线按 profile 分组。
const PROFILES = {
'server-rs': {
description:
'server-rs workspace(Linux/Windows 都可跑;CI 与 pre-push 共用)',
command: [
'cargo',
'check',
'--locked',
'--workspace',
'--all-targets',
'--manifest-path',
'server-rs/Cargo.toml',
],
platforms: ['linux', 'win32', 'darwin'],
},
'agc-windows': {
description:
'AGC 壳:Windows + 三编辑器 feature 的生产口径(只有 Windows 本机可跑 → 仅 pre-push 覆盖)',
command: [
'cargo',
'check',
'--locked',
'--features',
AGC_FEATURES,
'--manifest-path',
AGC_MANIFEST,
],
platforms: ['win32'],
},
'agc-linux': {
description:
'AGC 壳:Linux target + 三编辑器 feature(CI 覆盖;本机 Windows 无法交叉编译 GTK/ring,跑不了)',
command: [
'cargo',
'check',
'--locked',
'--features',
AGC_FEATURES,
'--manifest-path',
AGC_MANIFEST,
],
platforms: ['linux'],
},
};
const argv = process.argv.slice(2);
const requested = argv
.filter((a) => a.startsWith('--profile='))
.map((a) => a.slice('--profile='.length));
const updateBaseline = argv.includes('--update-baseline');
const asJson = argv.includes('--json');
const unknown = argv.filter(
(a) =>
a.startsWith('--') &&
!a.startsWith('--profile=') &&
a !== '--update-baseline' &&
a !== '--json',
);
if (unknown.length > 0) {
console.error(`[rust-warnings] 未知参数:${unknown.join(' ')}`);
process.exit(2);
}
function defaultProfiles() {
const names = requested.length > 0 ? requested : Object.keys(PROFILES);
return names.filter((name) => {
if (requested.length > 0) return true;
return PROFILES[name].platforms.includes(process.platform);
});
}
function loadBaseline() {
if (!existsSync(baselinePath)) return { schema: 1, profiles: {} };
return JSON.parse(readFileSync(baselinePath, 'utf8'));
}
// 从 rustc 诊断里取「符号」:消息里第一个反引号片段(`fn x` / `unused import: \`a::b\`` / `field \`f\``)。
function symbolOf(message) {
const match = /`([^`]+)`/u.exec(message);
return match ? match[1] : message.slice(0, 80);
}
function lintOf(code) {
return code?.code ?? 'unknown';
}
function normalizePath(file) {
return relative(repoRoot, resolve(repoRoot, file)).replaceAll('\\', '/');
}
function collectWarnings(profile) {
const [bin, ...args] = profile.command;
const result = spawnSync(bin, [...args, '--message-format=json'], {
cwd: repoRoot,
encoding: 'utf8',
maxBuffer: 256 * 1024 * 1024,
});
if (result.error) throw result.error;
const seen = new Map();
for (const line of (result.stdout ?? '').split('\n')) {
const trimmed = line.trim();
if (!trimmed.startsWith('{')) continue;
let payload;
try {
payload = JSON.parse(trimmed);
} catch {
continue;
}
if (payload.reason !== 'compiler-message') continue;
const message = payload.message ?? {};
if (message.level !== 'warning') continue;
const text = message.message ?? '';
// ts-rs 的宏展开提示不是 lint,长期存在且与代码质量无关,不计入门禁。
if (text.includes('failed to parse serde attribute')) continue;
if (message.spans?.length && /generated \d+ warnings?/u.test(text))
continue;
const primary =
(message.spans ?? []).find((span) => span.is_primary) ??
message.spans?.[0];
if (!primary) continue;
const key = {
path: normalizePath(primary.file_name),
symbol: symbolOf(text),
lint: lintOf(message.code),
};
const id = `${key.path}|${key.symbol}|${key.lint}`;
if (!seen.has(id))
seen.set(id, { ...key, line: primary.line_start, message: text });
}
// 非 JSON 输出里的硬错误(编译不过)也算失败。
const stderr = result.stderr ?? '';
return { warnings: [...seen.values()], exitCode: result.status, stderr };
}
function keyOf(entry) {
return `${entry.path}|${entry.symbol}|${entry.lint}`;
}
const baseline = loadBaseline();
const report = {};
let hadNew = false;
let hadFailure = false;
for (const name of defaultProfiles()) {
const profile = PROFILES[name];
if (!profile) {
console.error(
`[rust-warnings] 未知 profile:${name}(可用:${Object.keys(PROFILES).join(', ')})`,
);
process.exit(2);
}
const { warnings, exitCode, stderr } = collectWarnings(profile);
const known = new Map(
(baseline.profiles[name]?.warnings ?? []).map((entry) => [
keyOf(entry),
entry,
]),
);
const fresh = warnings.filter((entry) => !known.has(keyOf(entry)));
const gone = [...known.values()].filter(
(entry) => !warnings.some((w) => keyOf(w) === keyOf(entry)),
);
report[name] = {
description: profile.description,
total: warnings.length,
new: fresh,
gone,
};
if (fresh.length > 0) hadNew = true;
if (exitCode !== 0 && warnings.length === 0) hadFailure = true;
if (updateBaseline) {
baseline.profiles[name] = {
description: profile.description,
command: profile.command.join(' '),
warnings: warnings
.map(({ path, symbol, lint }) => ({ path, symbol, lint }))
.sort((a, b) => keyOf(a).localeCompare(keyOf(b))),
};
}
if (!asJson) {
console.log(`=== ${name}:${profile.description}`);
console.log(` 命令:${profile.command.join(' ')}`);
console.log(
` 当前告警 ${warnings.length} 条 / 基线 ${known.size} 条 → 新增 ${fresh.length},存量减少 ${gone.length}`,
);
for (const entry of fresh) {
console.log(
` [新增] ${entry.path}:${entry.line} | ${entry.lint} | ${entry.symbol}`,
);
console.log(` ${entry.message}`);
}
for (const entry of gone)
console.log(
` [已消除·可更新基线] ${entry.path} | ${entry.lint} | ${entry.symbol}`,
);
if (exitCode !== 0 && warnings.length === 0) {
console.error(
` [失败] cargo 退出码 ${exitCode}(编译不过,先修编译)`,
);
console.error(stderr.split('\n').slice(-20).join('\n'));
}
}
}
if (updateBaseline) {
writeFileSync(baselinePath, `${JSON.stringify(baseline, null, 2)}\n`, 'utf8');
if (!asJson)
console.log(`\n[rust-warnings] 基线已更新:scripts/warning-baseline.json`);
}
if (asJson) console.log(JSON.stringify(report, null, 2));
if (hadFailure) process.exit(1);
if (hadNew && !updateBaseline) {
if (!asJson) {
console.log(
'\n[rust-warnings] 有基线之外的新告警:请先修掉;确实是「存量的一部分」时,用',
);
console.log(
' node scripts/check-rust-warnings.mjs --profile=<name> --update-baseline 入册并提交基线。',
);
}
process.exit(1);
}
if (!asJson) console.log('\n[rust-warnings] OK(无新增告警)');
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env node
// pre-push 的 Rust 告警检查入口:只在「本次推送的提交确实碰了 Rust 代码」时才跑,
// 避免无关推送(纯文档/前端)被编译检查拖慢。
//
// 与 CI 共用同一份检查实现:scripts/check-rust-warnings.mjs(单一真源)。
// 平台差异:Windows 跑 `agc-windows` + `server-rs`;Linux/macOS 只跑 `server-rs`
// (AGC 壳的 Windows 生产口径只能在 Windows 上编;Linux 档由 CI 的 Rust warning check job 覆盖)。
//
// 用法(由 .husky/pre-push 调用,stdin 为 `local_ref local_sha remote_ref remote_sha` 行):
// node scripts/pre-push-rust-warnings.mjs < changes.txt
import { spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const ZERO = /^0+$/u;
const RUST_RELEVANT = /(\.rs$|Cargo\.toml$|Cargo\.lock$)/u;
const stdin = readFileSync(0, 'utf8');
const pushed = [];
for (const line of stdin.split('\n')) {
const [localRef, localSha, remoteRef, remoteSha] = line.trim().split(/\s+/u);
if (!localRef || !localSha || ZERO.test(localSha)) continue;
pushed.push({ localRef, localSha, remoteRef, remoteSha });
}
function git(...args) {
const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8' });
return result.status === 0 ? (result.stdout ?? '').trim() : '';
}
const changed = new Set();
for (const { localSha, remoteSha } of pushed) {
let base = ZERO.test(remoteSha ?? '') ? '' : remoteSha;
if (!base) {
// 新分支/首次推送:用与 origin/master 的合并基点作为比较基线;取不到就用该提交的父提交。
base =
git('merge-base', 'origin/master', localSha) ||
git('rev-parse', `${localSha}^`);
}
if (!base) continue;
for (const path of git('diff', '--name-only', `${base}..${localSha}`).split(
'\n',
)) {
if (path) changed.add(path);
}
}
const relevant = [...changed].filter((path) => RUST_RELEVANT.test(path));
if (relevant.length === 0) {
console.log(
`[rust-warnings] 本次推送未触碰 Rust 代码(${changed.size} 个文件),跳过告警检查。`,
);
process.exit(0);
}
const profiles =
process.platform === 'win32' ? ['agc-windows', 'server-rs'] : ['server-rs'];
console.log(
`[rust-warnings] 本次推送触碰 Rust 文件 ${relevant.length} 个,检查 profile: ${profiles.join(', ')}`,
);
const args = [
'scripts/check-rust-warnings.mjs',
...profiles.map((name) => `--profile=${name}`),
];
const result = spawnSync(process.execPath, args, {
cwd: repoRoot,
stdio: 'inherit',
});
if (result.status !== 0) {
console.error(
'[rust-warnings] 推送被拦下:请先处理上面的新增告警(详见 dev 运维文档「Rust 编译告警门禁」)。',
);
process.exit(result.status ?? 1);
}
File diff suppressed because it is too large Load Diff