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:
1 parent
bff48e27fa
commit
bc1b09b776
6 files changed
+397
-86
No files matched your search
@@ -19,10 +19,15 @@
|
||||
「别再造假绿的尺子」的血泪账直接冲突。公钥是公开物,回显它是安全的;**种子(`seed_hex`)在本脚本里
|
||||
一次都不打印**。
|
||||
|
||||
退出码是对外接口(改动即行为变化,`packaging/tests/signing_ready_selftest.py` 钉着它):
|
||||
`0` 就绪 / `1` 未就绪 / `2` 用法错(含 trust.rs 读不到、标记丢了 —— 那时脚本形不成判断,不是「未就绪」)。
|
||||
②还有第三种读数:**未判定**。CI 的播报 workflow 刻意只在 `main` ref 上注入 Secret(生产签名种子等于
|
||||
「给全量已装客户端签可信清单」的能力,既有设计把它锁在 tag ref 后面),非 main ref 的运行里这一行会写
|
||||
「未判定 —— 非 main ref(按设计不注入)」,**不写「没配」**:把「策略不注入」说成「没配」就是一次假读数。
|
||||
|
||||
**它不是门禁**:CI 里的 `.github/workflows/signing-ready.yml` 只把这三行搬进 job summary,
|
||||
退出码是对外接口(改动即行为变化,`packaging/tests/signing_ready_selftest.py` 钉着它):
|
||||
`0` 就绪 / `1` 未就绪(含 ② 未判定) / `2` 用法错(trust.rs 或 `--key` 读不到、标记丢了 ——
|
||||
那时脚本形不成判断,不是「未就绪」)。
|
||||
|
||||
**它不是门禁**:CI 里的 `.github/workflows/signing-status.yml` 只把这三行搬进 job summary,
|
||||
不因「未就绪」变红。理由见 `packaging/docs/signing-runbook.md` §6。
|
||||
|
||||
python3 packaging/check_signing_ready.py
|
||||
@@ -41,6 +46,10 @@ SIGN = ROOT / "packaging/sign_manifest.py"
|
||||
RUNBOOK = "packaging/docs/signing-runbook.md"
|
||||
#: 未就绪时的固定措辞:判据与「人类一眼看懂后果」绑在一起(用例按字面断言这一句)。
|
||||
FAIL_CLOSED = "这会让已安装用户永远验不过(fail-closed)"
|
||||
#: 「这一趟按设计没有钥」的原因(人话)。CI 的 `signing-status.yml` 在非 main ref 上把它设成一句话,
|
||||
#: 于是 ② 读成「未判定」而不是「没配」—— 把**策略不注入**说成**没配置**是一次假读数(M255 r1 P2-1)。
|
||||
WITHHELD_ENV = "ULL_SIGNING_KEY_WITHHELD"
|
||||
|
||||
|
||||
sys.path.insert(0, str(ROOT / "packaging"))
|
||||
|
||||
@@ -82,15 +91,29 @@ def table_state(path: Path) -> tuple[bool, str, list[tuple[str, str]]]:
|
||||
return False, f"未就绪 —— 标记之间 0 条 PinnedKey{hint}({path}:{line_no} 起)", []
|
||||
|
||||
|
||||
def key_state(key_file: str | None, pinned: list[tuple[str, str]]) -> tuple[bool, str, str | None]:
|
||||
def read_key_text(key_file: str | None) -> str:
|
||||
"""② 的输入文本:`--key <文件>` 或环境变量 `ULL_MANIFEST_SIGNING_KEY`。
|
||||
|
||||
读不了(不存在 / 是目录 / 无权限 / 非 UTF-8)⇒ `OSError` / `UnicodeDecodeError`,由 `main` 翻成
|
||||
**退出码 2**(用法错)。⚠ 这里必须抛而不是吞:吞掉就会变成 rc=1,与「未就绪」同码,
|
||||
而事实是「你给的路径就是错的」——两者混了,退出码就不再可信(M255 r1 P2-2)。
|
||||
"""
|
||||
if key_file:
|
||||
return Path(key_file).read_text(encoding="utf-8")
|
||||
return os.environ.get(KEY_ENV, "")
|
||||
|
||||
|
||||
def key_state(raw: str, source: str, pinned: list[tuple[str, str]]) -> tuple[bool, str, str | None]:
|
||||
"""② 判据:环境里有钥、解析得出、且它的公钥在 ① 的表里。
|
||||
|
||||
返回 `(就绪?, 该行文本, 可直接贴进 trust.rs 的那一行 PinnedKey)`。
|
||||
**任何分支都不打印种子**;回显的只有 `key_id` 与公钥十六进制。
|
||||
"""
|
||||
raw = Path(key_file).read_text(encoding="utf-8") if key_file else os.environ.get(KEY_ENV, "")
|
||||
source = f"--key {key_file}" if key_file else f"环境变量 {KEY_ENV}"
|
||||
if not raw.strip():
|
||||
reason = os.environ.get(WITHHELD_ENV, "").strip()
|
||||
if reason:
|
||||
# 「策略上不注入」≠「没配置」。这一行必须能分辨,否则非 main ref 的运行会报假的「没配」。
|
||||
return False, f"未判定(未注入)—— 这一趟的钥按设计没有注入:{reason};这不是「没配」", None
|
||||
return False, f"未就绪 —— {source} 未设置/为空(没有私钥就签不出任何清单)", None
|
||||
try:
|
||||
key_id, seed = load_key(raw)
|
||||
@@ -122,8 +145,10 @@ def release_state(dist: Path | None, assets: Path | None) -> tuple[bool | None,
|
||||
return False, f"无 —— 本地 {dist} 里有 manifest.json 但**没有** .sig(那份清单客户端会拒收)"
|
||||
if assets is not None and assets.is_file():
|
||||
text = assets.read_text(encoding="utf-8", errors="replace")
|
||||
has_manifest = "manifest.json" in text
|
||||
has_sig = f"manifest.json.sig" in text
|
||||
# 先剥掉 `.sig` 再判清单:`manifest.json.sig` 里含 `manifest.json`,只做子串匹配时
|
||||
# 「资产里只有签名」会被读成「清单和签名都在」(r1 复审实测抓到的假读数)。
|
||||
has_sig = "manifest.json.sig" in text
|
||||
has_manifest = "manifest.json" in text.replace("manifest.json.sig", "")
|
||||
if has_manifest and has_sig:
|
||||
return True, f"有 —— {assets} 的资产清单里同时有 manifest.json 与 manifest.json.sig"
|
||||
if has_manifest:
|
||||
@@ -135,17 +160,22 @@ def release_state(dist: Path | None, assets: Path | None) -> tuple[bool | None,
|
||||
|
||||
|
||||
def fix_hint(table_ok: bool, key_ok: bool, paste: str | None) -> str:
|
||||
"""未就绪时的**一行**修复命令(不带任何密钥值;钥的事只指向 Secret 名与手册)。"""
|
||||
"""未就绪时的**一行**修复命令(不带任何密钥值;钥的事只指向 Secret 名与手册)。
|
||||
|
||||
指针必须落在手册**真实**的章节上:`packaging/tests/signing_ready_wiring_selftest.py` 会拿这里的
|
||||
`§N` 去对 `signing-runbook.md` 的标题集合,漂了当场红(M255 r1 P2-3:`signing-ready.yml` 那个
|
||||
文件名根本不存在,而指针正是本文件的交付物)。
|
||||
"""
|
||||
if not table_ok and paste:
|
||||
return f"修复:把这一行贴进 {TRUST_RS.name} 的 PINNED-KEYS 标记之间 —— {paste}(手册 {RUNBOOK} §3)"
|
||||
return f"修复:把这一行贴进 {TRUST_RS.name} 的 PINNED-KEYS 标记之间 —— {paste}(手册 {RUNBOOK} §2 步③)"
|
||||
if not table_ok:
|
||||
return (
|
||||
"修复:python3 packaging/sign_manifest.py --generate-key --key-id <key-id> "
|
||||
f"→ 私钥贴进 Secret {KEY_ENV},打印的 PinnedKey 那行贴进 trust.rs 标记之间(手册 {RUNBOOK} §1–§3)"
|
||||
f"→ 私钥贴进 Secret {KEY_ENV},打印的 PinnedKey 那行贴进 trust.rs 标记之间(手册 {RUNBOOK} §2 步①–③)"
|
||||
)
|
||||
if not key_ok and paste:
|
||||
return f"修复:把这一行贴进 {TRUST_RS.name} 的 PINNED-KEYS 标记之间 —— {paste}(手册 {RUNBOOK} §3)"
|
||||
return f"修复:把密钥贴进仓库 Secret {KEY_ENV}(内容 = --generate-key 打印的那行 JSON;手册 {RUNBOOK} §2)"
|
||||
return f"修复:把这一行贴进 {TRUST_RS.name} 的 PINNED-KEYS 标记之间 —— {paste}(手册 {RUNBOOK} §2 步③)"
|
||||
return f"修复:把密钥贴进仓库 Secret {KEY_ENV}(内容 = --generate-key 打印的那行 JSON;手册 {RUNBOOK} §2 步②)"
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
@@ -162,8 +192,17 @@ def main(argv: list[str] | None = None) -> int:
|
||||
except (OSError, UnicodeDecodeError, RuntimeError) as error:
|
||||
print(f"error: 读不了信任表:{error}(脚本无法形成判断,按用法错退出)", file=sys.stderr)
|
||||
return 2
|
||||
key_ok, key_line, paste = key_state(args.key, pinned)
|
||||
try:
|
||||
key_text = read_key_text(args.key)
|
||||
except (OSError, UnicodeDecodeError) as error:
|
||||
# 与 `--trust` 同款:输入读不到 = 用法错(2)。**不能**退化成 1 —— 1 是「签名未就绪」的读数,
|
||||
# 混了就会把「我路径写错了」读成「签名没配好」(M255 r1 P2-2)。
|
||||
print(f"error: 读不了钥文件 {args.key}:{error}(这是用法错,不是「未就绪」)", file=sys.stderr)
|
||||
return 2
|
||||
source = f"--key {args.key}" if args.key else f"环境变量 {KEY_ENV}"
|
||||
key_ok, key_line, paste = key_state(key_text, source, pinned)
|
||||
release_ok, release_line = release_state(args.dist, args.release_assets)
|
||||
withheld = not key_ok and not key_text.strip() and bool(os.environ.get(WITHHELD_ENV, "").strip())
|
||||
|
||||
lines: list[Line] = [
|
||||
("① 信任表 PRODUCTION", table_ok, table_line),
|
||||
@@ -175,7 +214,11 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
ready = table_ok and key_ok
|
||||
if ready:
|
||||
print(f"结论:就绪({args.trust} 里钉了钥,环境里的钥与它同源)—— 可以按 {RUNBOOK} §4 发一次带 .sig 的 Release 并用 --verify-pinned 回验")
|
||||
print(f"结论:就绪({args.trust} 里钉了钥,环境里的钥与它同源)—— 可以按 {RUNBOOK} §2 步⑤ 发一次带 .sig 的 Release,再按 §5 回验")
|
||||
elif withheld:
|
||||
# 「未判定」不许伪装成「未就绪」:这一趟根本没拿到钥,不是仓库缺钥。
|
||||
tail = f";① 仍是空的 ⇒ {FAIL_CLOSED}" if not table_ok else ""
|
||||
print(f"结论:未判定(② 这一趟没注入钥,见上面那行的原因){tail}")
|
||||
else:
|
||||
blockers = [label for label, ok, _ in lines[:2] if not ok]
|
||||
print(f"结论:未就绪(差 {'、'.join(blockers)})—— {FAIL_CLOSED}")
|
||||
@@ -184,11 +227,12 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if args.line:
|
||||
summary = os.environ.get("GITHUB_STEP_SUMMARY")
|
||||
if summary:
|
||||
verdict = "就绪" if ready else ("未判定" if withheld else "未就绪")
|
||||
body = ["## 签名就绪度(M255,播报不是门禁)", ""]
|
||||
body += [f"- **{label}**:{text}" for label, _ok, text in lines]
|
||||
body += [
|
||||
"",
|
||||
f"- 结论:**{'就绪' if ready else '未就绪'}**",
|
||||
f"- 结论:**{verdict}**",
|
||||
f"- 本 job 的绿色**不代表签名就绪**:判据只看上面三行字,退出码恒为 0(见 `{RUNBOOK}` §6)。",
|
||||
]
|
||||
Path(summary).write_text("\n".join(body) + "\n", encoding="utf-8")
|
||||
|
||||
Reference in new issue
Block a user