fix(m169): 复审 r2 —— 守门归属改成「列表项结构判定 + 默认拒绝」,堵掉无名步骤/无名 job/flow 步骤的继承放行
r2 复审(reviewer-updatee2e-r2,p2-4items;被抓出一个真洞,mission 翻回 active)逐条落地。 P2-1(本轮核心,真洞)allowlist 的归属原来按「最近一行 `- name:`」切块,于是任何不写成 `- name: <x>` 的步骤都继承上一块的名字;被放行的 `update-e2e` 恰好是 ci.yml 里最后一个 `- name:`,所以追加到文件末尾的一切都落在它的块里。复审三条真实注入(无名 `- run:`、无名步骤的 整个 job、flow 风格步骤)当时全绿。现在: - `split_items()` 按 YAML **列表项**切块(每个 `- ` 起一项;比当前项深的行归当前项;同级或更浅的 非列表行**结束**当前项)。项名 = 该项内**同级**的 `name:` 键(键序无关;`run: |` 块里的 `name: dummy` 不算;`run: echo "name: x"` 也不算)。 - **默认拒绝**:落不到列表项上的行(`jobs:` 之前的孤儿行、job 级 `run:` 这类项外键)带 feature 即红; 没有 `name:` 的项带 feature 即红。不再有「继承上一块」这回事。 - 三条复审注入 + 本轮新增三条(`jobs:` 之前的孤儿行、`run: |` 里夹带并伪造 name、shell 变量拼接) 都进了自测向量;在**真仓库文件**上重跑也是全红。 P2-2(同族误杀): - 键序无关:允许的那一步把 `run:` 写在 `name:` 之前仍绿(旧实现会报到上一块的名字上)。 - `cargo hack check --all-targets` 不再被误判成 `--all`(正则收紧到 `--all(?![-\w])`)。 - Cargo 的 `update-test-source = [ … ]` **跨行声明**不再被误判成「写进了别的 feature 列表」 (`scan_manifest` 增加 `in_declaration` 状态)。 P2-3 `Fixture::run` 的第二条断言比错对象:原来比夹具字段 `self.dest`,手写 `fx.run(&["apply","--dest","<测试二进制>","--json"])` 两条都过。现在从 `args` 里解析真实 `--dest` 值(含 `--dest=` 形式)再 canonicalize 比较;夹具字段那份作为额外一道保留。 P2-4 `release-guard.yml` 的步骤级注释还停在 r1 的扫描面:改成「含复合 action / packaging 递归 / Makefile 等 + 归属按列表项判 + 唯一允许 ci.yml 的 update-e2e 一步」。 自测数据搬到 `packaging/tests/guard_vectors.json`(基线 8 个文件 + 26 条向量:21 红 / 6 绿), 脚本本身从 379 行降到 373 行(守门仍 ≤400);README §14 补上归属规则与「抓不住」清单。 验证(本机,全部 exit 0): - 守门正式扫描绿;`--self-test` 21 条注入全红 + 6 条基线/放行绿;向量文件缺失时 exit 1(不静默)。 - **真仓注入**(在真 ci.yml / release-build.yml / cli/Cargo.toml 上改完即还原):复审三条(无名步骤 / 无名 job / flow 步骤)全红;本轮新增三条(孤儿行 / `run: |` 夹带 / shell 拼接)全红; 三条合法写法(键序、`--all-targets`、跨行声明)全绿;还原后基线绿。 - E2E:debug 6 绿、release 6 绿(`running 3 tests` ×2,0.26s / 0.23s,非 0 measured)。 - `cargo clippy -p unlucky-cli --all-targets --features update-test-source -- -D warnings` 干净; `cargo fmt --all -- --check` 干净;全仓 400 行扫描无超限(守门 373 / 向量 194 / ci.yml 317)。 约束遵守:未 push;`PUBLISH_SLOTS` 只列 `windows-x64`(未动);release 侧仍无该 feature;跨卷 bug 未碰。 ⚠ 仍未验:CI 里 E2E 真跑起来本机验不了(只证明 workflow 写对 + 守门放行,首跑要等推送后看 Actions)。
This commit is contained in:
1 parent
28bf104041
commit
68a1f90828
5 files changed
+387
-176
No files matched your search
@@ -59,8 +59,10 @@ jobs:
|
||||
run: python3 packaging/check_publish_slots.py
|
||||
# M169:`update-test-source` 这条 feature 能让更新源指向任意本地目录,而更新链路的终点是
|
||||
# 替换用户盘上的 exe。它**默认关**、官方发布产物里不许有它,这条由脚本自动断言(不再靠注释):
|
||||
# 扫 `.github/workflows/**`、`packaging/*.py|*.sh` 与各 `Cargo.toml` 的 `default`。
|
||||
- name: 测试通道守门自测(注入变异,每条都要变红)
|
||||
# 扫 `.github/workflows/**`(含复合 action)、`packaging/**` 递归脚本、`Makefile`/`justfile`/
|
||||
# `Taskfile` 与各 `Cargo.toml`;归属按 `jobs.*.steps[]` 的**列表项**判,落不到具名步骤的行一律红。
|
||||
# 唯一允许:本仓 `ci.yml` 里名为 `update-e2e` 的那一步(且必须带 `--test update_e2e_test`)。
|
||||
- name: 测试通道守门自测(注入变异,该红的红、该绿的绿)
|
||||
run: python3 packaging/check_test_source_off.py --self-test
|
||||
- name: 发布产物不含测试通道(workflow / 打包脚本 / Cargo default 都不许开)
|
||||
run: python3 packaging/check_test_source_off.py
|
||||
@@ -255,25 +255,40 @@ impl Fixture {
|
||||
|
||||
/// 跑一条 `update` 子命令:`--data-dir` 指向临时数据根(与 GUI 一样由数据根派生暂存目录)。
|
||||
///
|
||||
/// **结构性保险(r1 复审 P2-5)**:`apply` 必须显式带 `--dest`,且 `--dest` 不许是本进程的 exe。
|
||||
/// 不带 `--dest` 就是 `Job::for_process` 的「本进程 exe」——在这套用例里那是**测试二进制**,
|
||||
/// 而 Windows 允许改名正在运行的映像,于是整轮测试会把它换成一个假启动器(症状极难查)。
|
||||
/// **结构性保险(r1 复审 P2-5 / r2 复审 P2-3)**:`apply` 必须显式带 `--dest`,且**这一次调用实际
|
||||
/// 传下去的那个 `--dest` 值**不许是本进程的 exe。不带 `--dest` 就是 `Job::for_process` 的「本进程
|
||||
/// exe」——在这套用例里那是**测试二进制**,而 Windows 允许改名正在运行的映像,于是整轮测试会把它
|
||||
/// 换成一个假启动器(症状极难查)。
|
||||
///
|
||||
/// 抓得住:以后有人手写 `fx.run(&["apply", "--json"])`(漏 `--dest`),或把 dest 指到
|
||||
/// `current_exe()`——**当场 panic**,现场不会被破坏。
|
||||
/// 比的是 **args 里那个 `--dest` 的值**(含 `--dest=<path>` 形式),不是夹具字段:r2 复审指出
|
||||
/// 「比 `self.dest`」等于只守住了夹具自己那一份,手写
|
||||
/// `fx.run(&["apply", "--dest", "<测试二进制>", "--json"])` 照样能过。夹具字段那一份作为额外一道
|
||||
/// 保留(`Fixture::apply()` 走的就是它)。
|
||||
///
|
||||
/// 抓得住:漏 `--dest`、把 `--dest` 指到本进程 exe、夹具 dest 被改成 exe——都**当场 panic**。
|
||||
/// 抓不住:绕开 `Fixture` 直接调 `unlucky_cli::run`(那等于另写一套夹具,`--data-dir` 也没人拼了)。
|
||||
pub fn run(&self, args: &[&str]) -> (i32, String, String) {
|
||||
let requested = args
|
||||
.iter()
|
||||
.position(|arg| *arg == "--dest")
|
||||
.and_then(|index| args.get(index + 1).copied())
|
||||
.or_else(|| args.iter().find_map(|arg| arg.strip_prefix("--dest=")));
|
||||
if args.first() == Some(&"apply") {
|
||||
assert!(
|
||||
args.contains(&"--dest"),
|
||||
requested.is_some(),
|
||||
"apply 必须带 --dest:不带会替换本进程 exe(这套用例里就是测试二进制)。\
|
||||
唯一的 apply 入口是 `Fixture::apply()`"
|
||||
);
|
||||
}
|
||||
let dest = std::fs::canonicalize(&self.dest)
|
||||
.expect("dest 必须存在(Fixture::new 写入了 INSTALLED)");
|
||||
if let Ok(exe) = std::env::current_exe().and_then(std::fs::canonicalize) {
|
||||
assert_ne!(dest, exe, "替换目标不许是本进程的 exe");
|
||||
let asked = requested.and_then(|path| std::fs::canonicalize(path).ok());
|
||||
let fixture = std::fs::canonicalize(&self.dest).ok();
|
||||
for dest in [asked, fixture].into_iter().flatten() {
|
||||
assert_ne!(
|
||||
dest, exe,
|
||||
"替换目标不许是本进程的 exe(`--dest` 点的就是它)"
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut full = vec!["update"];
|
||||
full.extend_from_slice(args);
|
||||
|
||||
+7
-1
@@ -65,7 +65,8 @@ Git Bash 下会静默落进 Docker VM(脚本内部用 `cygpath -m` 归一)
|
||||
| `packaging/release-notes.md` | GitHub Release 说明模板(`{{VERSION}}` / `{{SUMS}}` 占位) | ≤400 |
|
||||
| `packaging/check_screenshot.py` | 截图非空白判定(CI 门禁 + 本地手动跑,零第三方依赖) | ≤400 |
|
||||
| `packaging/check_publish_slots.py` | 发布名单守卫(M164):名单 ⇄ 产物两个方向;`--self-test` 注入 8 条回归 | ≤400 |
|
||||
| `packaging/check_test_source_off.py` | 测试通道守门(M169):workflow / 打包脚本 / `Cargo.toml default` 都不许打开 `update-test-source`;`--self-test` 注入 5 条回归 | ≤400 |
|
||||
| `packaging/check_test_source_off.py` | 测试通道守门(M169):workflow / 打包脚本 / `Cargo.toml default` 都不许打开 `update-test-source`;归属按 `jobs.*.steps[]` 列表项判、默认拒绝;`--self-test` 按 `tests/guard_vectors.json` 注入 | ≤400 |
|
||||
| `packaging/tests/guard_vectors.json` | 守门自测向量(基线 + 26 条:21 红 / 6 绿),数据与代码分开 | ≤400 |
|
||||
| `packaging/ci-runtime-smoke.md` | §11 的细节:断言口径/阈值依据/失效模式/复跑 | ≤400 |
|
||||
| `packaging/linux-smoke-docker.sh` | 本地复跑 `runtime-smoke`(Docker + xvfb) | ≤400 |
|
||||
| `packaging/linux-check-docker.sh` | 本地复跑 `ci.yml` 的两条 ubuntu 门禁(clippy / test,Docker) | ≤400 |
|
||||
@@ -126,6 +127,11 @@ Git Bash 下会静默落进 Docker VM(脚本内部用 `cygpath -m` 归一)
|
||||
`Makefile`/`justfile`/`Taskfile` 与各 `Cargo.toml` 的 `default`。**唯一允许**出现该 feature 的地方是
|
||||
`.github/workflows/ci.yml` 里名为 `update-e2e` 的那一步,且该步必须带 `--test update_e2e_test`
|
||||
(文件名 / 步骤名 / 参数任一不符即红,见脚本的 `ALLOWED_STEP`)。
|
||||
归属按 **`jobs.*.steps[]` 的列表项**判(不是「最近一行 `- name:`」):落不到具名步骤上的行——`jobs:`
|
||||
之前的孤儿行、项外键、没有 `name:` 的步骤、flow 风格步骤——**一律判红**(默认拒绝)。自测向量在
|
||||
`packaging/tests/guard_vectors.json`(26 条:21 条注入必须红 + 6 条基线/合法写法必须绿)。
|
||||
抓不住(如实记):变量间接拼接(`FEAT="update-""test-source"`)、feature 改名后接回发布、`build.rs`
|
||||
里 spawn cargo、以及「把别的步骤**改名**成 `update-e2e` 并带 `--test update_e2e_test` 却干别的事」。
|
||||
- 验收本体:`cargo test --release -p unlucky-cli --features update-test-source --test update_e2e_test
|
||||
--test update_e2e_tamper_test`(正路 + 幂等 / 篡改摘要 + 篡改包体 + 截断)。CI 里同一个命令挂在
|
||||
`ci.yml` 的 `update-e2e` job(**只在 Windows**:夹具的「真形状」是 Windows 裸 `.exe` +
|
||||
|
||||
+158
-164
@@ -1,46 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""测试通道守门(M169,r2 复审后重写):**官方发布产物里不许有 `update-test-source` feature**。
|
||||
"""测试通道守门(M169,r3:allowlist 改成**结构判定 + 默认拒绝**)。
|
||||
|
||||
那条 feature 让 `core::update::job::default_source` 可以被两个环境变量指向任意本地目录,而更新链路
|
||||
的终点是**替换用户盘上的 exe**。默认关 + 编译期不存在是唯一可靠的边界,所以「谁也没在流水线里把它
|
||||
打开」必须是自动断言,不能靠注释和记性。
|
||||
`update-test-source` 让 `core::update::job::default_source` 可被两个环境变量指向任意本地目录,而更新
|
||||
链路终点是**替换用户盘上的 exe**。「默认关 + 编译期不存在」是唯一可靠边界,所以「谁也没在流水线里
|
||||
把它打开」必须是自动断言。
|
||||
|
||||
四条断言,任何一条不成立即 exit 1(CI 里红在 `.github/workflows/release-guard.yml`):
|
||||
四条断言(任一不成立即 exit 1,CI 里红在 `.github/workflows/release-guard.yml`):
|
||||
|
||||
1. **workflow 与复合 action 里不许打开它**——唯一例外是 `.github/workflows/ci.yml` 里名为
|
||||
`update-e2e` 的那一步,且该步必须同时带 `--test update_e2e_test`(见 `ALLOWED_STEP`)。
|
||||
文件名 / 步骤名 / 命令行**三者任一不符即红**(r1 复审口径;job 名不参与判据——改 job 名不该
|
||||
解锁新能力,能力只取决于那一行命令)。为什么要开这个口子(r1 复审 P2-3):E2E 不带 feature 时
|
||||
两个目标是 0 例,而「任何 workflow 都不许出现这个字面量」会把唯一合法的 CI 调用点也堵死——
|
||||
守卫强度与回归可见性会互相吃掉。口子开得极窄:一个文件、一个步骤名、一个参数。
|
||||
2. **不许「打开所有 feature」**:`--all-features` 与 cargo hack 的 `--feature-powerset` /
|
||||
`--each-feature` / `--all` 都会把测试通道一起打开。
|
||||
3. **不许设成默认**:任何 `Cargo.toml` 里它只许作为**自己的声明**出现;进 `default` 或别的
|
||||
feature 的依赖数组即红(这条专治「扫 workflow 看不见」的开法)。
|
||||
1. **workflow / 复合 action 里不许打开它**。唯一例外:`.github/workflows/ci.yml` 里**名为
|
||||
`update-e2e` 的那个列表项**,且该项自己的文本必须带 `--test update_e2e_test`。归属按
|
||||
`jobs.*.steps[]` 的**列表项**判(见 `split_items`),不是「最近一行 `- name:`」——后者会把追加到
|
||||
文件末尾的无名步骤 / 无名 job 的步骤 / flow 风格步骤算进上一块(r2 复审 P2-1 实测三条全绿)。
|
||||
**任何落不到具名步骤上的行(`jobs:` 之前的孤儿行、项外键、无名项)一律判红**:默认拒绝。
|
||||
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` 这类第二
|
||||
构建入口(r1 复审 P2-2 的漏检来源)。扩扫描面 = 往那三个元组加一行 + 补一条 self-test 向量。
|
||||
`.github/actions/**`、`packaging/**` 递归子目录、`Makefile`/`justfile`/`Taskfile`。
|
||||
|
||||
注释剥离是**引号感知**的:`- run: echo "#"; cargo build --features update-test-source` 里第一个
|
||||
`#` 在引号内,不是注释起点(粗暴 `#.*$` 会把真命令一起吃掉,也是 P2-2 漏检之一)。
|
||||
注释剥离是**引号感知**的(`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 那一行)。
|
||||
|
||||
# 抓得住什么 / 抓不住什么
|
||||
|
||||
| 抓得住 | 抓不住 |
|
||||
|---|---|
|
||||
| workflow / 复合 action / 打包脚本 / Makefile 里打开它(含 `--features=`、引号、多行续行写法) | 变量间接引用(`--features "$FEAT"`),或 feature 名写在守门不扫的文件里 |
|
||||
| `--all-features` / `cargo hack --feature-powerset|--each-feature|--all` 一把梭 | 有人把 feature 改名后再接回发布:名字一变就脱离扫描表(改名要过 review,但守门看不见) |
|
||||
| 把 feature 塞进 `default` 或别的 feature 的依赖数组 | `build.rs` 里 spawn `cargo`、或第三方工具链自带的构建入口 |
|
||||
| allowlist 被放宽:文件名 / 步骤名 / `--test` 参数任一不符 | allowlist 被滥用:别的步骤改名成 `update-e2e` 且带 `--test update_e2e_test` 却干别的事(扫描只看字符串;merge review 会看到 ci.yml 那一行) |
|
||||
| 大小写/下划线变体不算漏检:cargo feature 名敏感,写错直接编不过(r1 复审同判) | |
|
||||
|
||||
`--self-test` 是这条守门的变异自测:每条断言的**真实回归**注入临时副本,断言该红的红、该绿的绿
|
||||
(尤其 allowlist 六个向量与 P2-2 的七条绕过)。守门自己抓不住回归时它会先红。
|
||||
`--self-test` 把每条断言的**真实回归**注入临时副本,断言该红的红、该绿的绿(含 r2 的三条绕过与四条
|
||||
误杀回归)。守门自己抓不住回归时它会先红。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -48,11 +38,9 @@ from pathlib import Path
|
||||
|
||||
#: 被守的 feature 名(改名就得同步改本文件与各 `Cargo.toml`)。
|
||||
FEATURE = "update-test-source"
|
||||
|
||||
#: 仓库根(本文件在 `packaging/` 下)与本文件自身(扫描时跳过:它必须写出 feature 名)。
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
GUARD = Path(__file__).resolve()
|
||||
|
||||
#: 扫描面(逐个列举,不用 `**` 全仓:那会把 `target/` 拖进来)。
|
||||
WORKFLOW_GLOBS = (
|
||||
".github/workflows/**/*.yml",
|
||||
@@ -71,34 +59,31 @@ BUILD_ENTRY_GLOBS = (
|
||||
"packaging/Makefile",
|
||||
)
|
||||
MANIFEST_GLOBS = ("Cargo.toml", "crates/*/Cargo.toml")
|
||||
|
||||
#: 唯一允许出现该 feature 的地方:**一个文件 + 一个步骤名 + 一个必须同时在场的参数**。
|
||||
#: 唯一允许出现该 feature 的地方:**文件 + 列表项名 + 必须同在的字符串**,三者不符即红。
|
||||
ALLOWED_STEP = {
|
||||
"workflow": ".github/workflows/ci.yml",
|
||||
"name": "update-e2e",
|
||||
"must_contain": "--test update_e2e_test",
|
||||
}
|
||||
|
||||
#: 「打开全部 feature」的开关(后三条是 cargo hack 的走法)。
|
||||
#: 「打开全部 feature」的开关。`--all` 后面不许跟 `-`/字母(否则 `--all-targets` 会被误杀)。
|
||||
ALL_FEATURES = (
|
||||
re.compile(r"--all-features\b"),
|
||||
re.compile(r"--feature-powerset\b"),
|
||||
re.compile(r"--each-feature\b"),
|
||||
re.compile(r"\bcargo\s+hack\b[^\n]*\s--all\b"),
|
||||
re.compile(r"\bcargo\s+hack\b[^\n]*\s--all(?![-\w])"),
|
||||
)
|
||||
|
||||
#: Cargo.toml 里「自己的声明」长这样:行首 `update-test-source = [`。
|
||||
#: YAML 列表项(任意缩进);名字只认键位置上的 `name:`(块风格 / flow 风格)。
|
||||
LIST_ITEM = re.compile(r"^(\s*)-\s+")
|
||||
NAME_BLOCK = re.compile(r"^['\"]?name['\"]?\s*:\s*(.+)$")
|
||||
NAME_FLOW = re.compile(r"^\{[^{}]*?\bname\s*:\s*([^,}]+)")
|
||||
#: Cargo.toml 里「自己的声明」:行首 `update-test-source = [`。
|
||||
DECLARATION = re.compile(rf"^\s*{re.escape(FEATURE)}\s*=\s*\[")
|
||||
#: workflow 的一个步骤从 `- name: <x>` 开始。
|
||||
STEP_NAME = re.compile(r"^\s*-\s*name:\s*(.+?)\s*$")
|
||||
#: 块标量开头(`run: |` / `run: >-`):其后的更深缩进行是**内容**,不是键。
|
||||
BLOCK_SCALAR = ("|", ">", "|-", ">-", "|+", ">+")
|
||||
|
||||
|
||||
def strip_comment(line: str) -> str:
|
||||
"""引号感知地剥注释:`#` 只有「行首或空白之后」且不在引号内时才开始注释。
|
||||
|
||||
YAML 的单引号 `''`、双引号 `\\` 转义这里不模拟——本守门只判断「引号有没有闭合」,够用,
|
||||
且不会把 `echo "#"; cargo build --features …` 当成注释。
|
||||
"""
|
||||
"""引号感知地剥注释:`#` 只有「行首或空白之后」且不在引号内时才开始注释。"""
|
||||
quote = None
|
||||
for index, char in enumerate(line):
|
||||
if quote is not None:
|
||||
@@ -124,64 +109,124 @@ def _lines(path: Path) -> list[tuple[int, str]]:
|
||||
return list(enumerate(path.read_text(encoding="utf-8").splitlines(), start=1))
|
||||
|
||||
|
||||
def split_steps(lines: list[tuple[int, str]]) -> list[tuple[str | None, list[tuple[int, str]]]]:
|
||||
"""按 `- name: <step>` 把 workflow 切成「步骤名 + 该步骤的行」。
|
||||
def _name_of(key_indent: int, body: list[tuple[int, str]]) -> str | None:
|
||||
"""列表项的名字:项内**同级**的 `name:` 键(键序无关;块标量内容不当键)。
|
||||
|
||||
没有 YAML 解析器(stdlib 不带、也不许引第三方),用「最近的 `- name:` 行」当步骤边界:GitHub
|
||||
workflow 就是这个形状,而本守门只关心「某个 feature 字面量落在哪一步里」。第一个 step 之前的
|
||||
行归到 `None` 块(含 `on:` / `env:` 这类全局段)。
|
||||
找不到 → `None` ⇒ 这一项落不到具名步骤 ⇒ 带 feature 就红(默认拒绝)。只认键位置上的
|
||||
`name:`:`run: echo "name: x"` 这种不算,`run: |` 块里的 `name: dummy` 也不算。
|
||||
"""
|
||||
blocks: list[tuple[str | None, list[tuple[int, str]]]] = []
|
||||
name: str | None = None
|
||||
body: list[tuple[int, str]] = []
|
||||
for number, line in lines:
|
||||
match = STEP_NAME.match(line)
|
||||
in_block = False
|
||||
for _number, line in body:
|
||||
text = line.strip()
|
||||
if not text or text.startswith("#"):
|
||||
continue
|
||||
indent = len(line) - len(line.lstrip(" "))
|
||||
item = LIST_ITEM.match(line)
|
||||
if in_block and indent > key_indent:
|
||||
continue
|
||||
in_block = False
|
||||
if item:
|
||||
code = strip_comment(line[item.end() :]) # 项首行:` - name: x` → `name: x`
|
||||
elif 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("'\"")
|
||||
if code.rstrip().endswith(BLOCK_SCALAR):
|
||||
in_block = True
|
||||
return None
|
||||
|
||||
|
||||
def split_items(
|
||||
lines: list[tuple[int, str]],
|
||||
) -> tuple[list[tuple[str | None, list[tuple[int, str]]]], list[tuple[int, str]]]:
|
||||
"""按 YAML **列表项**切块,返回((项名, 该项的行), 孤儿行)。
|
||||
|
||||
每个 `- ` 起一项;比当前项更深的行归当前项,**同级或更浅的非列表行结束当前项**(那是 job 键 /
|
||||
`steps:` / 顶层键)。落不到列表项上的行是孤儿——不属于任何步骤,带 feature 就红。
|
||||
"""
|
||||
items: list[tuple[str | None, list[tuple[int, str]]]] = []
|
||||
orphans: list[tuple[int, str]] = []
|
||||
body: list[tuple[int, str]] = []
|
||||
base, key_indent = 0, 0
|
||||
for number, line in lines:
|
||||
text = line.strip()
|
||||
if not text or text.startswith("#"):
|
||||
(body if body else orphans).append((number, line))
|
||||
continue
|
||||
indent = len(line) - len(line.lstrip(" "))
|
||||
item = LIST_ITEM.match(line)
|
||||
if item and (not body or indent <= base):
|
||||
if body:
|
||||
blocks.append((name, body))
|
||||
name = match.group(1).strip("'\"")
|
||||
body = []
|
||||
items.append((_name_of(key_indent, body), body))
|
||||
body, base, key_indent = [(number, line)], indent, item.end()
|
||||
continue
|
||||
if body and indent > base:
|
||||
body.append((number, line))
|
||||
continue
|
||||
if body:
|
||||
blocks.append((name, body))
|
||||
return blocks
|
||||
items.append((_name_of(key_indent, body), body))
|
||||
body = []
|
||||
orphans.append((number, line))
|
||||
if body:
|
||||
items.append((_name_of(key_indent, body), body))
|
||||
return items, orphans
|
||||
|
||||
|
||||
def _allowlist_reason(where: str, name: str | None, body: list[tuple[int, str]]) -> str | None:
|
||||
"""`None` = 这一步被放行;否则返回「为什么红」的一句话。"""
|
||||
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}`)"
|
||||
)
|
||||
if name is None:
|
||||
return (
|
||||
f"这一项没有 `name:`,落不到具名步骤(默认拒绝);`{where}` 里只有名为 "
|
||||
f"`{ALLOWED_STEP['name']}` 的步骤可以用它"
|
||||
)
|
||||
if name != ALLOWED_STEP["name"]:
|
||||
return (
|
||||
f"`{where}` 里只有名为 `{ALLOWED_STEP['name']}` 的步骤可以用 `{FEATURE}`"
|
||||
f"(这一步叫 `{name}`)"
|
||||
f"(这一项叫 `{name}`)"
|
||||
)
|
||||
joined = "\n".join(strip_comment(line) for _number, line in body)
|
||||
if ALLOWED_STEP["must_contain"] not in joined:
|
||||
return f"`{where}` 的 `{ALLOWED_STEP['name']}` 步骤少了 `{ALLOWED_STEP['must_contain']}`"
|
||||
if ALLOWED_STEP["must_contain"] not in text:
|
||||
return f"`{where}` 的 `{name}` 步骤少了 `{ALLOWED_STEP['must_contain']}`"
|
||||
return None
|
||||
|
||||
|
||||
def scan_workflow(path: Path, root: Path) -> list[str]:
|
||||
"""扫 workflow / 复合 action:feature 只许出现在 allowlist 点名的那一步里。"""
|
||||
def _flag_problems(where: str, body: list[tuple[int, str]]) -> list[str]:
|
||||
problems: list[str] = []
|
||||
where = path.relative_to(root).as_posix()
|
||||
for name, body in split_steps(_lines(path)):
|
||||
code_lines = [(number, strip_comment(line)) for number, line in body]
|
||||
for number, code in code_lines:
|
||||
flag = dangerous_flag(code)
|
||||
for number, line in body:
|
||||
flag = dangerous_flag(strip_comment(line))
|
||||
if flag:
|
||||
problems.append(
|
||||
f"::error file={where}:{number}::{where} 用了 `{flag}`:"
|
||||
f"它会把 `{FEATURE}` 一起打开"
|
||||
f"::error file={where}:{number}::{where} 用了 `{flag}`:它会把 `{FEATURE}` 一起打开"
|
||||
)
|
||||
hits = [(number, code) for number, code in code_lines if FEATURE in code]
|
||||
return problems
|
||||
|
||||
|
||||
def scan_workflow(path: Path, root: Path) -> list[str]:
|
||||
"""扫 workflow / 复合 action:feature 只许出现在 allowlist 点名的那一项里。"""
|
||||
where = path.relative_to(root).as_posix()
|
||||
items, orphans = split_items(_lines(path))
|
||||
problems = _flag_problems(where, orphans)
|
||||
for number, line in orphans:
|
||||
if FEATURE in strip_comment(line):
|
||||
problems.append(
|
||||
f"::error file={where}:{number}::{where} 这一行落不到任何具名步骤(默认拒绝);"
|
||||
"官方发布产物里不许有测试通道"
|
||||
)
|
||||
for name, body in items:
|
||||
problems += _flag_problems(where, body)
|
||||
codes = [(number, strip_comment(line)) for number, line in body]
|
||||
hits = [(number, code) for number, code in codes if FEATURE in code]
|
||||
if not hits:
|
||||
continue
|
||||
reason = _allowlist_reason(where, name, body)
|
||||
reason = _reason(where, name, "\n".join(code for _number, code in codes))
|
||||
if reason is None:
|
||||
continue
|
||||
for number, _code in hits:
|
||||
@@ -193,9 +238,9 @@ def scan_workflow(path: Path, root: Path) -> list[str]:
|
||||
|
||||
|
||||
def scan_code_file(path: Path, root: Path) -> list[str]:
|
||||
"""扫脚本 / 第二构建入口:没有任何 allowlist,字面量或开关出现即红。"""
|
||||
problems: list[str] = []
|
||||
"""扫脚本 / 第二构建入口:没有 allowlist,字面量或开关出现即红。"""
|
||||
where = path.relative_to(root).as_posix()
|
||||
problems: list[str] = []
|
||||
for number, line in _lines(path):
|
||||
code = strip_comment(line)
|
||||
if not code:
|
||||
@@ -208,23 +253,29 @@ def scan_code_file(path: Path, root: Path) -> list[str]:
|
||||
flag = dangerous_flag(code)
|
||||
if flag:
|
||||
problems.append(
|
||||
f"::error file={where}:{number}::{where} 用了 `{flag}`:"
|
||||
f"它会把 `{FEATURE}` 一起打开"
|
||||
f"::error file={where}:{number}::{where} 用了 `{flag}`:它会把 `{FEATURE}` 一起打开"
|
||||
)
|
||||
return problems
|
||||
|
||||
|
||||
def scan_manifest(path: Path, root: Path) -> list[str]:
|
||||
"""扫 Cargo.toml:feature 只许出现在自己的声明行上(不许进 default / 别的 feature)。"""
|
||||
problems: list[str] = []
|
||||
"""扫 Cargo.toml:feature 只许出现在自己的声明里(**含跨行数组**),不许进别的列表。"""
|
||||
where = path.relative_to(root).as_posix()
|
||||
problems: list[str] = []
|
||||
in_declaration = False
|
||||
for number, line in _lines(path):
|
||||
code = strip_comment(line).strip()
|
||||
if FEATURE not in code or DECLARATION.match(code):
|
||||
if in_declaration:
|
||||
in_declaration = "]" not in code
|
||||
continue
|
||||
if FEATURE not in code:
|
||||
continue
|
||||
if DECLARATION.match(code):
|
||||
in_declaration = "]" not in code
|
||||
continue
|
||||
problems.append(
|
||||
f"::error file={where}:{number}::{where} 把 `{FEATURE}` 写进了别的 feature 列表:"
|
||||
"只许它是自己的声明(`update-test-source = [...]`),不许进 `default` 或别的 feature"
|
||||
"只许它是自己的声明(可跨行),不许进 `default` 或别的 feature"
|
||||
)
|
||||
return problems
|
||||
|
||||
@@ -232,13 +283,13 @@ def scan_manifest(path: Path, root: Path) -> list[str]:
|
||||
def scan(root: Path) -> list[str]:
|
||||
"""扫整个仓库,返回所有问题(空 = 绿)。"""
|
||||
problems: list[str] = []
|
||||
for pattern, scanner in (
|
||||
for patterns, scanner in (
|
||||
(WORKFLOW_GLOBS, scan_workflow),
|
||||
(SCRIPT_GLOBS, scan_code_file),
|
||||
(BUILD_ENTRY_GLOBS, scan_code_file),
|
||||
(MANIFEST_GLOBS, scan_manifest),
|
||||
):
|
||||
for path in sorted({path for glob in pattern for path in root.glob(glob)}):
|
||||
for path in sorted({p for pattern in patterns for p in root.glob(pattern)}):
|
||||
if scanner is scan_code_file and path.resolve() == GUARD:
|
||||
continue
|
||||
try:
|
||||
@@ -250,78 +301,16 @@ def scan(root: Path) -> list[str]:
|
||||
|
||||
# ────────────────────────── 变异自测 ──────────────────────────
|
||||
|
||||
def _wf(*steps: str) -> str:
|
||||
"""拼一个最小 workflow(job 名与真实 `ci.yml` 一致;**job 名不参与判据**——判据锚在步骤名上,
|
||||
改 job 名不该解锁新能力。这里用真名只是让自测基线长成真文件的形状)。"""
|
||||
return "jobs:\n update-e2e:\n steps:\n" + "".join(steps)
|
||||
#: 自测数据(基线 + 向量)单独放一份 JSON:`packaging/tests/manifest.golden.json` 是同一手法。
|
||||
#: 每个向量 = 一份「覆盖/新增的文件」;期望红的是真实回归,期望绿的是防误杀(合法写法不许红)。
|
||||
#: 条目标签自带出处(`allowlist:` / `P2-1` / `P2-2` / `误杀` / `Cargo`),红的时候一眼看得出守哪条。
|
||||
VECTORS_FILE = Path(__file__).resolve().parent / "tests" / "guard_vectors.json"
|
||||
|
||||
|
||||
#: 允许的那一步(基线与若干向量共用它)。
|
||||
ALLOWED_STEP_YAML = (
|
||||
" - uses: actions/checkout@v4\n"
|
||||
" - name: update-e2e\n"
|
||||
" if: runner.os == 'Windows'\n"
|
||||
" run: |\n"
|
||||
f" cargo test --release -p unlucky-cli --features {FEATURE} \\\n"
|
||||
" --test update_e2e_test --test update_e2e_tamper_test\n"
|
||||
)
|
||||
|
||||
BASELINE: dict[str, str] = {
|
||||
".github/workflows/ci.yml": _wf(*[ALLOWED_STEP_YAML]),
|
||||
".github/workflows/release-build.yml": _wf(" - run: cargo build --release\n"),
|
||||
".github/workflows/release-publish.yml": _wf(" - run: echo publish\n"),
|
||||
"packaging/make_artifacts.py": "def build():\n return 'cargo build --release'\n",
|
||||
"packaging/tests/build.sh": "#!/bin/sh\ncargo build --release\n",
|
||||
"Makefile": "build:\n\tcargo build --release\n",
|
||||
"Cargo.toml": "[workspace]\nmembers = []\n",
|
||||
"crates/core/Cargo.toml": (
|
||||
f"[features]\n{FEATURE} = []\n# 注释里提到 update-test-source 不算违规\n"
|
||||
),
|
||||
}
|
||||
|
||||
_LEAK = f" - run: cargo build --release --features {FEATURE}\n"
|
||||
_HACK = " - run: cargo hack {mode} build\n"
|
||||
|
||||
#: 向量 =(标签、覆盖/新增文件、期望)。期望绿的两条是**防误杀**。
|
||||
VECTORS: list[tuple[str, dict[str, str], str]] = [
|
||||
("allowlist ①:ci.yml 的 update-e2e 一步", {}, "green"),
|
||||
("allowlist ②:同样的步骤放进 release-build.yml",
|
||||
{".github/workflows/release-build.yml": _wf(*[ALLOWED_STEP_YAML])}, "red"),
|
||||
("allowlist ③:步骤名改成 update-e2e-r2",
|
||||
{".github/workflows/ci.yml": _wf(ALLOWED_STEP_YAML.replace("name: update-e2e", "name: update-e2e-r2"))},
|
||||
"red"),
|
||||
("allowlist ④:少了 --test update_e2e_test",
|
||||
{".github/workflows/ci.yml": _wf(ALLOWED_STEP_YAML.replace("--test update_e2e_test ", ""))}, "red"),
|
||||
("allowlist ⑤:发布工作流里另起一步带该 feature",
|
||||
{".github/workflows/release-publish.yml": _wf(" - name: leak\n", _LEAK)}, "red"),
|
||||
("allowlist ⑥:ci.yml 里另一步(不叫 update-e2e)带该 feature",
|
||||
{".github/workflows/ci.yml": _wf(*[ALLOWED_STEP_YAML, " - name: another\n", _LEAK])}, "red"),
|
||||
("P2-2 ①:`#` 在引号内、真命令在后面",
|
||||
{".github/workflows/release-build.yml": _wf(f' - run: echo "#"; cargo build --features {FEATURE}\n')},
|
||||
"red"),
|
||||
("P2-2 ②:cargo hack --feature-powerset",
|
||||
{".github/workflows/release-build.yml": _wf(_HACK.format(mode="--feature-powerset"))}, "red"),
|
||||
("P2-2 ③:cargo hack --each-feature",
|
||||
{".github/workflows/release-build.yml": _wf(_HACK.format(mode="--each-feature"))}, "red"),
|
||||
("P2-2 ④:packaging 子目录脚本",
|
||||
{"packaging/tests/build.sh": f"#!/bin/sh\ncargo build --features {FEATURE}\n"}, "red"),
|
||||
("P2-2 ⑤:.github/actions/** 复合 action",
|
||||
{".github/actions/rebuild/action.yml":
|
||||
"runs:\n using: composite\n steps:\n"
|
||||
f" - run: cargo build --features {FEATURE}\n shell: bash\n"}, "red"),
|
||||
("P2-2 ⑥:Makefile 里的第二构建入口",
|
||||
{"Makefile": f"build:\n\tcargo build --release --features {FEATURE}\n"}, "red"),
|
||||
("P2-2 ⑦:workflow 子目录",
|
||||
{".github/workflows/nested/extra.yml": _wf(_LEAK)}, "red"),
|
||||
("P2-2 ⑧(防误杀):注释里提到该 feature",
|
||||
{".github/workflows/release-build.yml":
|
||||
_wf(f" # 这里不许加 {FEATURE}\n", " - run: cargo build --release\n")}, "green"),
|
||||
("Cargo ①:default 把 feature 带上",
|
||||
{"crates/core/Cargo.toml": BASELINE["crates/core/Cargo.toml"] + 'default = ["update-test-source"]\n'},
|
||||
"red"),
|
||||
("Cargo ②:别的 feature 依赖它",
|
||||
{"crates/core/Cargo.toml": BASELINE["crates/core/Cargo.toml"] + 'release = ["update-test-source"]\n'},
|
||||
"red"),
|
||||
def _load_vectors() -> tuple[dict[str, str], list[tuple[str, dict[str, str], str]]]:
|
||||
data = json.loads(VECTORS_FILE.read_text(encoding="utf-8"))
|
||||
return data["baseline"], [
|
||||
(item["label"], item["files"], item["expect"]) for item in data["vectors"]
|
||||
]
|
||||
|
||||
|
||||
@@ -336,12 +325,17 @@ def _scan_case(files: dict[str, str]) -> list[str]:
|
||||
|
||||
|
||||
def self_test() -> int:
|
||||
try:
|
||||
base_files, vectors = _load_vectors()
|
||||
except (OSError, ValueError, KeyError) as error:
|
||||
print(f"::error::读不到自测向量 {VECTORS_FILE}:{error}", file=sys.stderr)
|
||||
return 1
|
||||
failures: list[str] = []
|
||||
baseline = _scan_case(BASELINE)
|
||||
baseline = _scan_case(base_files)
|
||||
if baseline:
|
||||
failures.append(f"基线应当绿,实际红了:{baseline}")
|
||||
for label, changes, expect in VECTORS:
|
||||
files = dict(BASELINE)
|
||||
for label, changes, expect in vectors:
|
||||
files = dict(base_files)
|
||||
files.update(changes)
|
||||
hits = _scan_case(files)
|
||||
if expect == "red" and not hits:
|
||||
@@ -352,8 +346,8 @@ def self_test() -> int:
|
||||
for failure in failures:
|
||||
print(f"::error::{failure}", file=sys.stderr)
|
||||
return 1
|
||||
reds = sum(1 for _label, _changes, expect in VECTORS if expect == "red")
|
||||
print(f"OK: 守门自测通过({reds} 条注入全红,{len(VECTORS) - reds + 1} 条基线/放行保持绿)")
|
||||
reds = sum(1 for _label, _changes, expect in vectors if expect == "red")
|
||||
print(f"OK: 守门自测通过({reds} 条注入全红,{len(vectors) - reds + 1} 条基线/放行保持绿)")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"baseline": {
|
||||
".github/workflows/ci.yml": "jobs:\n update-e2e:\n steps:\n - name: update-e2e\n if: runner.os == 'Windows'\n run: |\n cargo test --release -p unlucky-cli --features update-test-source \\\n --test update_e2e_test --test update_e2e_tamper_test\n",
|
||||
".github/workflows/release-build.yml": "jobs:\n update-e2e:\n steps:\n - run: cargo build --release\n",
|
||||
".github/workflows/release-publish.yml": "jobs:\n update-e2e:\n steps:\n - run: echo publish\n",
|
||||
"packaging/make_artifacts.py": "def build():\n return 'cargo build --release'\n",
|
||||
"packaging/tests/build.sh": "#!/bin/sh\ncargo build --release\n",
|
||||
"Makefile": "build:\n\tcargo build --release\n",
|
||||
"Cargo.toml": "[workspace]\nmembers = []\n",
|
||||
"crates/core/Cargo.toml": "[features]\nupdate-test-source = []\n# 注释里提到它不算违规\n"
|
||||
},
|
||||
"vectors": [
|
||||
{
|
||||
"label": "allowlist:ci.yml 的 update-e2e 一步",
|
||||
"expect": "green",
|
||||
"files": {}
|
||||
},
|
||||
{
|
||||
"label": "allowlist:同一步放进 release-build.yml",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/release-build.yml": "jobs:\n update-e2e:\n steps:\n - name: update-e2e\n if: runner.os == 'Windows'\n run: |\n cargo test --release -p unlucky-cli --features update-test-source \\\n --test update_e2e_test --test update_e2e_tamper_test\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "allowlist:步骤名改成 update-e2e-r2",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/ci.yml": "jobs:\n update-e2e:\n steps:\n - name: update-e2e-r2\n if: runner.os == 'Windows'\n run: |\n cargo test --release -p unlucky-cli --features update-test-source \\\n --test update_e2e_test --test update_e2e_tamper_test\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "allowlist:少了 --test update_e2e_test",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/ci.yml": "jobs:\n update-e2e:\n steps:\n - name: update-e2e\n if: runner.os == 'Windows'\n run: |\n cargo test --release -p unlucky-cli --features update-test-source \\\n --test update_e2e_tamper_test\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "allowlist:发布工作流里另起一步",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/release-publish.yml": "jobs:\n update-e2e:\n steps:\n - name: leak\n - run: cargo build --release --features update-test-source\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "allowlist:ci.yml 里另一个具名步骤",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/ci.yml": "jobs:\n update-e2e:\n steps:\n - name: update-e2e\n if: runner.os == 'Windows'\n run: |\n cargo test --release -p unlucky-cli --features update-test-source \\\n --test update_e2e_test --test update_e2e_tamper_test\n - name: another\n - run: cargo build --release --features update-test-source\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "P2-1 ①:末尾追加无名步骤",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/ci.yml": "jobs:\n update-e2e:\n steps:\n - name: update-e2e\n if: runner.os == 'Windows'\n run: |\n cargo test --release -p unlucky-cli --features update-test-source \\\n --test update_e2e_test --test update_e2e_tamper_test\n - run: cargo build --release --features update-test-source\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "P2-1 ②:末尾追加无名步骤的整个 job",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/ci.yml": "jobs:\n update-e2e:\n steps:\n - name: update-e2e\n if: runner.os == 'Windows'\n run: |\n cargo test --release -p unlucky-cli --features update-test-source \\\n --test update_e2e_test --test update_e2e_tamper_test\n release-from-ci:\n steps:\n - run: cargo build --release --features update-test-source\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "P2-1 ③:flow 风格步骤",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/ci.yml": "jobs:\n update-e2e:\n steps:\n - name: update-e2e\n if: runner.os == 'Windows'\n run: |\n cargo test --release -p unlucky-cli --features update-test-source \\\n --test update_e2e_test --test update_e2e_tamper_test\n - {name: release-build, run: \"cargo build --features update-test-source\"}\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "P2-1 ④:步骤写在 jobs: 之前(孤儿行)",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/ci.yml": " - run: cargo build --release --features update-test-source\njobs:\n update-e2e:\n steps:\n - run: echo hi\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "P2-1 ⑤:`run: |` 块里伪造 `name: update-e2e`",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/ci.yml": "jobs:\n update-e2e:\n steps:\n - run: |\n echo 'name: update-e2e'\n cargo build --release --features update-test-source\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "P2-2 ①:`#` 在引号内、真命令在后面",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/release-build.yml": "jobs:\n update-e2e:\n steps:\n - run: echo \"#\"; cargo build --features update-test-source\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "P2-2 ②:shell 变量拼接(字面量仍在)",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/release-build.yml": "jobs:\n update-e2e:\n steps:\n - run: FEAT=\"update-test-source\"; cargo build --features \"$FEAT\"\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Cargo ①:default 把 feature 带上",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
"crates/core/Cargo.toml": "[features]\nupdate-test-source = []\n# 注释里提到它不算违规\ndefault = [\"update-test-source\"]\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Cargo ②:别的 feature 依赖它",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
"crates/core/Cargo.toml": "[features]\nupdate-test-source = []\n# 注释里提到它不算违规\nrelease = [\"update-test-source\"]\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "误杀 ①:注释里提到该 feature",
|
||||
"expect": "green",
|
||||
"files": {
|
||||
".github/workflows/release-build.yml": "jobs:\n update-e2e:\n steps:\n # 这里不许加 update-test-source\n - run: cargo build --release\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "误杀 ②:允许的那一步把 `run:` 写在 `name:` 之前(键序无关)",
|
||||
"expect": "green",
|
||||
"files": {
|
||||
".github/workflows/ci.yml": "jobs:\n update-e2e:\n steps:\n - run: cargo test --release --features update-test-source --test update_e2e_test\n name: update-e2e\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "误杀 ③:`cargo hack check --all-targets`(不打开任何 feature)",
|
||||
"expect": "green",
|
||||
"files": {
|
||||
".github/workflows/release-build.yml": "jobs:\n update-e2e:\n steps:\n - run: cargo hack check --all-targets\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "误杀 ④:Cargo 多行 feature 声明",
|
||||
"expect": "green",
|
||||
"files": {
|
||||
"crates/core/Cargo.toml": "[features]\nupdate-test-source = [\n \"unlucky-core/update-test-source\",\n \"unlucky-cli/update-test-source\",\n]\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "P2-2:cargo hack --feature-powerset",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/release-build.yml": "jobs:\n update-e2e:\n steps:\n - run: cargo hack --feature-powerset build\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "P2-2:cargo hack --each-feature",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/release-build.yml": "jobs:\n update-e2e:\n steps:\n - run: cargo hack --each-feature build\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "P2-2:cargo hack --all",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/release-build.yml": "jobs:\n update-e2e:\n steps:\n - run: cargo hack --all build\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "P2-2 扫描面:packaging/tests/build.sh",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
"packaging/tests/build.sh": "#!/bin/sh\ncargo build --features update-test-source\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "P2-2 扫描面:.github/actions/rebuild/action.yml",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/actions/rebuild/action.yml": "runs:\n using: composite\n steps:\n - run: cargo build --features update-test-source\n shell: bash\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "P2-2 扫描面:.github/workflows/nested/extra.yml",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
".github/workflows/nested/extra.yml": "jobs:\n update-e2e:\n steps:\n - run: cargo build --release --features update-test-source\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "P2-2 扫描面:Makefile",
|
||||
"expect": "red",
|
||||
"files": {
|
||||
"Makefile": "build:\n\tcargo build --release --features update-test-source\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user