fix(m169): r3 四条就地修完 —— 只认同级 name:、纳入 .cargo/config.toml、登记 env 交接边界、校准两处数字

合并门禁只认 clean,r3 的 p2-4items 就地修掉(无行为改动,判据更严,不放松任何一条)。

P2-1(一行 + 三向量)`_name_of` 现在只认**同级** `name:`(`item.end() == key_indent`):更深缩进的
`- name:`(`env:`/`with:` 子列表、`run: |` 块标量里的行)不再被当成项名;同一项里出现两个**不同**
的 `name:` 返回 `None`(无法判定 ⇒ 默认拒绝,不去猜 YAML 重复键语义)。新向量:name 藏在 env 子项 /
藏在 `run: |` 里 / 两个不同 name ⇒ 必须红。

P2-2 `.cargo/config*.toml` 进扫描面(`CONFIG_GLOBS`:根与 `crates/*` 下的 `config.toml`/`config`),
新增 `scan_cargo_config`:feature 字面量(覆盖 `--cfg feature="…"`、`[env] RUSTFLAGS`)走原有判据,
`rustc-wrapper` / `rustc-workspace-wrapper` **一律判红**(wrapper 注入什么由程序决定,文本扫描看不见;
合法用途如 sccache 需要 review 明确同意,不能顺手加)。新向量:config.toml 用 `--cfg` 开该 feature
⇒ 红;设 rustc-wrapper ⇒ 红;普通 config.toml ⇒ 绿(防误杀)。

P2-3 运行期 env 交接写进守门自己的「抓不住」表(脚本头 + README §14):第一步把 feature 写进
`$GITHUB_ENV`、第二步只用 `--features "$FEAT"`——第二步没有字面量,**任何静态扫描都看不见**,这是本
守门的固有边界(引 YAML 解析器也修不了,因为这是数据流不是结构),不是漏判。

P2-4 两处过期数字:`release-guard.yml` 的「注入 14 条」→ 26 条注入 + 6 条防误杀/放行(另加基线,
条数以 `--self-test` 输出为准);README 的「26 条:21 红 + 6 绿」→「32 条向量:26 红 + 6 绿 + 基线」。

守门脚本 399 行(≤400,仍零第三方依赖);自测数据 32 条向量在 `packaging/tests/guard_vectors.json`。

验证(本机,未跑全量 cargo —— tower 已在合并态跑过 workspace 全量 EXIT=0/2648 passed):
- `--self-test`:26 条注入全红 + 7 条基线/防误杀绿,exit 0。
- **真仓注入 8 组**(改真 ci.yml / 新建真 .cargo/config.toml,改完即还原):env 子项里的 name ⇒ 红;
  `run: |` 里的 name ⇒ 红;两个不同 name ⇒ 红;`--cfg feature=` ⇒ 红;`rustc-wrapper` ⇒ 红;
  普通 config.toml ⇒ 绿;r2 的无名步骤 / flow 步骤回归 ⇒ 仍红;还原后基线绿。
- 正式扫描(真仓)exit 0;全仓 400 行扫描无超限;改动文件全 LF。
This commit is contained in:
worker-updatee2e committed 2026-11-08 11:27:21 +08:00
1 parent 68a1f90828
commit d7adbd1172
4 files changed
+110 -37

No files matched your search

+55 -29
View File
@@ -15,16 +15,17 @@
2. **不许「打开所有 feature」**:`--all-features`、`cargo hack --feature-powerset|--each-feature|--all`。
3. **不许设成默认**:`Cargo.toml` 里它只许是**自己的声明**(含跨行数组);进 `default` 或别的
feature 的依赖数组即红。
4. **扫描面逐项列举**(`WORKFLOW_GLOBS` / `SCRIPT_GLOBS` / `BUILD_ENTRY_GLOBS`):含
`.github/actions/**`、`packaging/**` 递归子目录、`Makefile`/`justfile`/`Taskfile`。
4. **扫描面逐项列举**(`WORKFLOW_GLOBS` / `SCRIPT_GLOBS` / `BUILD_ENTRY_GLOBS` / `CONFIG_GLOBS`):
含 `.github/actions/**`、`packaging/**` 递归子目录、`Makefile`/`justfile`/`Taskfile`、以及
`.cargo/config*.toml`(`--cfg feature="…"` 与 `rustc-wrapper` 都能绕过 `--features`,见
`scan_cargo_config`)。
注释剥离是**引号感知**的(`echo "#"; cargo build --features …` 里那个 `#` 不是注释)。
抓得住/抓不住 的完整清单见 `packaging/README.md` §14。要点:变量间接拼接(`FEAT="update-""test-source"`)、
feature 改名后接回发布、`build.rs` 里 spawn cargo、以及「有人把别的步骤**改名**成 `update-e2e` 并带
`--test update_e2e_test` 却干别的事」都抓不住(扫描只看字符串;merge review 会看到 ci.yml 那一行)。
注释剥离是**引号感知**的(`echo "#"; cargo build --features …` 里那个 `#` 不是注释)。抓不住的都是
**故意手打**或**运行期数据流**(完整清单见 `packaging/README.md` §14):变量拼接、两步 `$GITHUB_ENV`
交接(第二步没有字面量——静态扫描的固有边界)、feature 改名后接回、`build.rs` 里 spawn cargo、改名复用。
`--self-test` 把每条断言的**真实回归**注入临时副本,断言该红的红、该绿的绿(含 r2 的三条绕过与四条
误杀回归)。守门自己抓不住回归时它会先红。
`--self-test` 把每条断言的**真实回归**注入临时副本(数据在 `packaging/tests/guard_vectors.json`),
断言该红的红、该绿的绿。守门自己抓不住回归时它会先红。
"""
from __future__ import annotations
@@ -59,6 +60,15 @@ BUILD_ENTRY_GLOBS = (
"packaging/Makefile",
)
MANIFEST_GLOBS = ("Cargo.toml", "crates/*/Cargo.toml")
#: cargo 配置:`--cfg feature="…"` 与 `rustc-wrapper` 都能在没有 `--features` 时打开这条通道(P2-2)。
CONFIG_GLOBS = (
".cargo/config.toml",
".cargo/config",
"crates/*/.cargo/config.toml",
"crates/*/.cargo/config",
)
#: 任意 wrapper 都能注入 `--cfg`,文本扫描看不见 ⇒ 一律判红。
WRAPPER_KEY = re.compile(r"^\s*(rustc|rustc-workspace)-wrapper\s*=")
#: 唯一允许出现该 feature 的地方:**文件 + 列表项名 + 必须同在的字符串**,三者不符即红。
ALLOWED_STEP = {
"workflow": ".github/workflows/ci.yml",
@@ -112,10 +122,12 @@ def _lines(path: Path) -> list[tuple[int, str]]:
def _name_of(key_indent: int, body: list[tuple[int, str]]) -> str | None:
"""列表项的名字:项内**同级**的 `name:` 键(键序无关;块标量内容不当键)。
找不到 → `None` ⇒ 这一项落不到具名步骤 ⇒ 带 feature 就红(默认拒绝)。只认键位置上的
`name:`:`run: echo "name: x"` 这种不算,`run: |` 块里的 `name: dummy` 也不算。
「同级」= 项首行 `- ` 之后那一列(`item.end() == key_indent`,r3 复审 P2-1):更深缩进的
`- name:`(`with:` / `env:` 子列表、`run: |` 内容)**不算**项名。找不到、或出现两个**不同**的
`name:` 都返回 `None` ⇒ 落不到具名步骤 ⇒ 带 feature 就红(默认拒绝)。
"""
in_block = False
found: str | None = None
for _number, line in body:
text = line.strip()
if not text or text.startswith("#"):
@@ -125,19 +137,23 @@ def _name_of(key_indent: int, body: list[tuple[int, str]]) -> str | None:
if in_block and indent > key_indent:
continue
in_block = False
if item:
if item and item.end() == key_indent:
code = strip_comment(line[item.end() :]) # 项首行:` - name: x` → `name: x`
elif indent == key_indent:
elif not item and indent == key_indent:
code = strip_comment(text)
else:
continue
for pattern in (NAME_BLOCK, NAME_FLOW):
match = pattern.match(code.strip())
if match:
return match.group(1).strip().rstrip("}").strip("'\"")
value = match.group(1).strip().rstrip("}").strip("'\"")
if found is not None and found != value:
return None
found = value
break
if code.rstrip().endswith(BLOCK_SCALAR):
in_block = True
return None
return found
def split_items(
@@ -145,8 +161,8 @@ def split_items(
) -> tuple[list[tuple[str | None, list[tuple[int, str]]]], list[tuple[int, str]]]:
"""按 YAML **列表项**切块,返回((项名, 该项的行), 孤儿行)。
每个 `- ` 起一项;比当前项更深的行归当前项,**同级或更浅的非列表行结束当前项**(那是 job 键 /
`steps:` / 顶层键)。落不到列表项上的行是孤儿——不属于任何步骤,带 feature 就红。
每个 `- ` 起一项;更深的行归当前项,**同级或更浅的非列表行结束当前项**(job 键 / `steps:` /
顶层键)。落不到列表项上的行是孤儿——不属于任何步骤,带 feature 就红。
"""
items: list[tuple[str | None, list[tuple[int, str]]]] = []
orphans: list[tuple[int, str]] = []
@@ -179,20 +195,11 @@ def split_items(
def _reason(where: str, name: str | None, text: str) -> str | None:
"""`None` = 这一项被放行;否则返回「为什么红」的一句话(按最具体的那条给)。"""
if where != ALLOWED_STEP["workflow"]:
return (
f"`{FEATURE}` 只许出现在 `{ALLOWED_STEP['workflow']}` 的 "
f"`{ALLOWED_STEP['name']}` 步骤里(这里是 `{where}`)"
)
return f"`{FEATURE}` 只许出现在 `{ALLOWED_STEP['workflow']}` 的 `{ALLOWED_STEP['name']}` 步骤里(这里是 `{where}`)"
if name is None:
return (
f"这一项没有 `name:`,落不到具名步骤(默认拒绝);`{where}` 里只有名为 "
f"`{ALLOWED_STEP['name']}` 的步骤可以用它"
)
return f"这一项没有同级 `name:`,落不到具名步骤(默认拒绝);`{where}` 里只有 `{ALLOWED_STEP['name']}` 可以用它"
if name != ALLOWED_STEP["name"]:
return (
f"`{where}` 里只有名为 `{ALLOWED_STEP['name']}` 的步骤可以用 `{FEATURE}`"
f"(这一项叫 `{name}`)"
)
return f"`{where}` 里只有名为 `{ALLOWED_STEP['name']}` 的步骤可以用 `{FEATURE}`(这一项叫 `{name}`)"
if ALLOWED_STEP["must_contain"] not in text:
return f"`{where}` 的 `{name}` 步骤少了 `{ALLOWED_STEP['must_contain']}`"
return None
@@ -238,7 +245,7 @@ def scan_workflow(path: Path, root: Path) -> list[str]:
def scan_code_file(path: Path, root: Path) -> list[str]:
"""扫脚本 / 第二构建入口:没有 allowlist,字面量或开关出现即红。"""
"""扫脚本 / 构建入口 / cargo config:没有 allowlist,字面量或开关出现即红。"""
where = path.relative_to(root).as_posix()
problems: list[str] = []
for number, line in _lines(path):
@@ -280,6 +287,24 @@ def scan_manifest(path: Path, root: Path) -> list[str]:
return problems
def scan_cargo_config(path: Path, root: Path) -> list[str]:
"""扫 `.cargo/config*.toml`:`--cfg feature="…"` 与 `rustc-wrapper` 都能绕过 `--features`。
r3 复审 P2-2 实测:`RUSTFLAGS='--cfg=feature="update-test-source"'` 能让 `#[cfg(feature = …)]`
段在没有 `--features` 时编进来。`[build] rustflags` / `[env]` 靠 feature 字面量抓(走
`scan_code_file`);`rustc-wrapper` 抓不住(注入什么由程序决定)⇒ 一律判红,合法用途要 review 同意。
"""
problems = scan_code_file(path, root)
where = path.relative_to(root).as_posix()
for number, line in _lines(path):
if WRAPPER_KEY.match(strip_comment(line)):
problems.append(
f"::error file={where}:{number}::{where} 设了 rustc wrapper:wrapper 能注入任意 "
f"`--cfg`,静态扫描看不见它是否打开了 `{FEATURE}`(默认拒绝)"
)
return problems
def scan(root: Path) -> list[str]:
"""扫整个仓库,返回所有问题(空 = 绿)。"""
problems: list[str] = []
@@ -287,6 +312,7 @@ def scan(root: Path) -> list[str]:
(WORKFLOW_GLOBS, scan_workflow),
(SCRIPT_GLOBS, scan_code_file),
(BUILD_ENTRY_GLOBS, scan_code_file),
(CONFIG_GLOBS, scan_cargo_config),
(MANIFEST_GLOBS, scan_manifest),
):
for path in sorted({p for pattern in patterns for p in root.glob(pattern)}):