fix(packaging): 播报 workflow 的种子只在 main 注入 + --key 用法错回 2 + 指针修正(M255 r1 P2-1/2/3)

P2-1(安全):`signing-status.yml` 原先把生产签名种子注入到 `pull_request` 与任意 ref 的
`workflow_dispatch` 运行里,而 CI 跑的是那个 ref 自己的代码 ⇒ 任何能推分支的人都能把种子搬走
(这把种子能给全量已装客户端签可信清单)。改成:
  - 去掉 `pull_request` 触发;
  - 注入行加 ref 门:`github.ref == 'refs/heads/main' && secrets.… || ''`;
  - 非 main 用 `ULL_SIGNING_KEY_WITHHELD` 说明原因,于是 ② 读「未判定(未注入)」而不是「没配」。
runbook §2 步② 加硬顺序:先合这笔、再录 Secret。

P2-2:`--key` 读不了(不存在 / 是目录 / 非 UTF-8)曾在 main 的 try 之外抛 traceback + rc=1,
与「未就绪」同码;现在统一 rc=2 + 一行说明,并补用例 `key_unreadable`。

P2-3:4 处文案指针修正(`signing-ready.yml` → `signing-status.yml`;§3 → §2 步③;§1–§3 →
§2 步①–③;§4 → §2 步⑤ / §5)。新增两条机检:`section_refs`(输出的 `§N` 必须是 runbook 真实
章节号)与 `signing_ready_wiring_selftest.py`(文档里的 `packaging/…`/`crates/…`/`.github/…`
路径必须真存在),后者另带三处变异重放:去掉 ref 门 / 加回 pull_request / 改坏文档路径 ⇒ 必须红。

顺带修掉 r1 复审指出的两处小问题:`--release-assets` 里只有 `manifest.json.sig` 时的子串假读数
(先剥 `.sig` 再判清单)、③ 那个从不参与判定的 bool 保持原样(无行为影响)。

selftest 拆成两份(400 行红线):判据本体 + 接线/文案。guards.yml 仍 399 行零改动。
This commit is contained in:
AskaEth committed 2026-11-11 20:17:04 +08:00
1 parent bff48e27fa
commit bc1b09b776
6 files changed
+397 -86

No files matched your search

@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""`signing-status.yml` 的**接线**自测(M255 r1 从 `signing_ready_selftest.py` 分出)。
拆成两份的唯一原因是全仓 400 行红线(`guards.yml` 的 `file-size`):判据本体(三行读数 / `0/1/2` /
两个方向的退出码变异)留在 `signing_ready_selftest.py`,这里只管**接线与文案里的指针**。
钉住的判据(每条都对应一次真实会咬人的回归):
| 判据 | 抓什么 |
|---|---|
| 不是门禁 | 没有别的 workflow `uses:` 它;全文无 error 级注解;就绪度那步抓 `rc=$?` 后 `exit 0`;文件头两句口径都在 |
| **生产种子只在 main 注入** | 注入行必须是 `github.ref == 'refs/heads/main' && secrets.<KEY_ENV>` 这个条件式;`on:` 块里**不许出现 `pull_request`**;非 main 另有 `ULL_SIGNING_KEY_WITHHELD` 说明原因(于是 ② 读「未判定」而不是「没配」) |
| 文档指针真的存在 | `signing-runbook.md` / `signing.md` 里出现的 `packaging/…`、`crates/…`、`.github/…` 路径必须真在仓库里(r1 P2-3:`signing-ready.yml` 那个文件名根本不存在) |
| 变异必须红 | ①去掉 ref 门 ②加回 `pull_request` ③把文档里一个路径改坏 —— 三处各自重放,**对应用例必须报红** |
为什么要 ref 门(这是本文件存在的最重要理由):生产签名种子能给**全量已安装客户端**签出被信任的清单
(自更新链 = 用户端 RCE)。仓库内分支的 PR 与任意分支的 `workflow_dispatch` 都**拿得到**仓库 Secret
(只有 fork / Dependabot 拿不到),而 CI 会在**那个 PR 自己的代码**上执行 checker。既有设计把种子锁在
tag ref 后面(`release.yml` 的 `if: … github.ref_type == 'tag'`),播报文件不许在旁边开一条新路。
python3 packaging/tests/signing_ready_wiring_selftest.py
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
WORKFLOW_DIR = ROOT / ".github/workflows"
WORKFLOW = WORKFLOW_DIR / "signing-status.yml"
RUNBOOK = ROOT / "packaging/docs/signing-runbook.md"
SIGNING_MD = ROOT / "packaging/docs/signing.md"
sys.path.insert(0, str(ROOT / "packaging"))
# 环境变量名从 checker 取,不写第二份字面量:两处漂开时,「ref 门注入了 A、checker 读 B」会静默失效。
from check_signing_ready import KEY_ENV, WITHHELD_ENV # noqa: E402
MAIN_REF = "refs/heads/main"
GATE = f"github.ref == '{MAIN_REF}' && secrets.{KEY_ENV}"
#: 文档里允许出现的「路径」形状:仓库根的四个前缀(相对路径如 `docs/x.md` 不在此列,避免误判)。
PATH_RE = re.compile(r"(?:packaging|crates|\.github|\.cargo)/[A-Za-z0-9_.\-]+(?:/[A-Za-z0-9_.\-]+)*")
def on_block(text: str) -> str:
"""取 `on:` 那一段(到下一个顶格键为止),并**去掉注释行**——注释里会正当地提到 `pull_request`。"""
lines = text.splitlines()
start = next((i for i, line in enumerate(lines) if re.match(r"^on:\s*$", line)), None)
if start is None:
return ""
body: list[str] = []
for line in lines[start + 1 :]: # 跳过 `on:` 那一行本身(它顶格,会让下面的 break 立刻触发)
if line.startswith(" ") or not line:
if not line.strip().startswith("#"):
body.append(line)
continue
break
return "\n".join(body)
def check_workflow(text: str) -> list[str]:
"""返回该 workflow 文本没过的接线判据(空 = 通过)。变异用例把同一函数套在改坏的副本上。"""
problems: list[str] = []
for phrase in ("播报,不是门禁", "不应被设为 required"):
if phrase not in text:
problems.append(f"文件头必须写清「{phrase}」(不阻断要是可核的事实,不是靠没接线)")
if "::error::" in text:
problems.append("播报 workflow 里不许出现 error 级注解(会让人以为构建挂了,与播报定位冲突)")
if "rc=$?" not in text or "exit 0" not in text:
problems.append("就绪度那一步必须抓 `rc=$?` 后 `exit 0`(未就绪不许让 job 变红)")
# ── 安全口径:生产签名种子只在 main 注入 ──
if GATE not in text:
problems.append(f"注入行必须是 `{GATE} || ''` 这个条件式(去掉 ref 门 = 把种子交给任意分支的代码)")
if f"{WITHHELD_ENV}:" not in text:
problems.append(f"非 main 分支必须给 `{WITHHELD_ENV}` 一个原因串(否则 ② 会报假的「没配」)")
if "pull_request" in on_block(text):
problems.append("`on:` 块里不许出现 `pull_request`(仓库内分支的 PR 拿得到 Secret,而 CI 跑的是 PR 自己的代码)")
return problems
def wired_elsewhere() -> list[str]:
"""扫全部 workflow:有人 `uses:` 本文件 ⇒ 播报变成了别人的必需检查。"""
hits: list[str] = []
for path in sorted(WORKFLOW_DIR.glob("*.y*ml")):
if path.resolve() == WORKFLOW.resolve():
continue
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
if re.match(r"^\s*uses:\s*", line) and WORKFLOW.name in line:
hits.append(f"{path.relative_to(ROOT).as_posix()}:{number}: {line.strip()}")
return hits
def check_doc_paths(text: str, where: str) -> list[str]:
"""文档里出现的仓库路径必须真实存在(r1 P2-3:指针错会把人送到无关文件/章节)。"""
problems: list[str] = []
for token in sorted(set(PATH_RE.findall(text))):
if not (ROOT / token).exists():
problems.append(f"{where} 里写了不存在的路径:{token}")
return problems
def case_wiring(failures: list[str]) -> None:
"""真实文件的接线判据 + 文档路径存在性。"""
if not WORKFLOW.is_file():
failures.append(f"接线:{WORKFLOW} 不存在")
return
text = WORKFLOW.read_text(encoding="utf-8")
failures.extend(f"接线:{item}" for item in check_workflow(text))
hits = wired_elsewhere()
if hits:
failures.append("接线:有 workflow `uses:` 了 " + WORKFLOW.name + " ⇒ 播报变成了别人的必需检查:\n " + "\n ".join(hits))
for doc in (RUNBOOK, SIGNING_MD):
failures.extend(f"指针:{item}" for item in check_doc_paths(doc.read_text(encoding="utf-8"), doc.name))
def case_mutation(failures: list[str]) -> None:
"""变异重放:把接线改坏,**同一组判据原样套上去必须报红**(r1 收尾要求的第 ④ 项)。"""
real = WORKFLOW.read_text(encoding="utf-8")
if GATE not in real:
failures.append("变异:真实文件里就没有 ref 门,先修 P2-1 再谈变异")
return
vectors = [
(
"去掉 ref 门(种子对任意 ref 可见)",
real.replace(GATE, f"secrets.{KEY_ENV}"),
"ref 门",
),
(
"加回 pull_request 触发",
real.replace(" workflow_dispatch:\n", " pull_request:\n workflow_dispatch:\n", 1),
"pull_request",
),
(
"去掉非 main 的 WITHHELD 说明",
"\n".join(line for line in real.splitlines() if WITHHELD_ENV not in line) + "\n",
WITHHELD_ENV,
),
]
for label, mutated, expect in vectors:
if mutated == real:
failures.append(f"变异「{label}」没改到任何字节(变异无处下手,用例什么都没证明)")
continue
problems = check_workflow(mutated)
if not any(expect in item for item in problems):
failures.append(f"变异「{label}」没有被抓:期望命中「{expect}」,实际 {problems or '零问题'}")
# 文档指针的变异:把 runbook 里一个真实路径改成不存在的名字,同一判据必须报红。
runbook = RUNBOOK.read_text(encoding="utf-8")
broken = runbook.replace("packaging/docs/signing.md", "packaging/docs/signing-oldext.md", 1)
if broken == runbook:
failures.append("变异「文档路径」:runbook 里找不到可替换的路径,用例失效")
elif not check_doc_paths(broken, "mutated-runbook"):
failures.append("变异「文档路径」:改成不存在的路径后判据仍然是绿的 —— 路径存在性没被判")
def main() -> int:
failures: list[str] = []
case_mutation(failures)
case_wiring(failures)
for line in failures:
print(f"FAIL {line}", file=sys.stderr)
print("signing-status wiring selftest:", "ok" if not failures else f"{len(failures)} 处失败")
return 0 if not failures else 1
if __name__ == "__main__":
raise SystemExit(main())