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:
@@ -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
@@ -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` 依旧报错)。
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(无新增告警)');
|
||||
@@ -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
Reference in New Issue
Block a user