r2 复审:r1 那 11 条 + M3 已闭合(复审自己重放 12/12 红),但它新造的 40 条变异仍有 11 条漏网,
其中三条是「1 行改绿」:把 `probe.outputs.available` 改指向 `reason`/字面量(门永久关、每次绿);
判据步的退出码在标记表之外被吞(`|| echo 先忽略` / `; :` / `| cat` / `if !` / `true # 原行`,
复审真 bash 实测 exit 0);自检步被指向另一个脚本(自检根本不跑)。
三条 P1:
- 新判据 9:`probe.outputs.{available,log,png}` 必须逐个等于 `${{ steps.detect.outputs.<同名> }}`;
- 新判据 5:把 `id: judge` 的 `run` 原文真跑两次 —— 正例必须 exit 0、CPU 档日志必须 exit≠0,
且 `bash -e` 与**裸 bash** 各跑一遍(`; :` 只在后者现形,实测 `bash -e`=1 / 裸 bash=0)。
这一条一次封掉整类「吞码」,不依赖标记表;
- 判据 2:`id: wiring` 的 `run` 必须含 `gpu_shot_ci_selftest.py`,且 `vectors`/`wiring` 必须是**单条命令**
(不许换行/管道/`;`/`&`/反引号/`#`/`$(`)⇒ `|| echo 先忽略` 无从下手。
两条 P2:
- 新判据 10:`on:` 必须同时有 `push`(含 `main`)/`pull_request`/`workflow_call`;
- 「载体依赖」这条边界补进四处(自检 docstring / gpu-evidence.md §8 / README / workflow 头注释):
整个 workflow 被删掉就没有东西会跑自检,`guards.yml` 399/400 行塞不进去。
止步线(写进 docstring 与 §8):封闭清单 10 类 / 33 条变异向量;明确不覆盖「改判据脚本」「删掉整个
workflow」「求值器与 GitHub 语义差」「判据 4 的合法调用替换」;本文档不宣称「不可能被改成总是绿」;
**自 2026-11-11 起停止加固,扩项须由人类新开 mission**。
新增 `packaging/tests/gpu_shot_ci_expr.py`(`if:` 小求值器,判据脚本仍守 ≤400 行:377/215/64)。
65 lines
3.2 KiB
Python
65 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
||
"""GitHub `if:` 表达式的小求值器(M253 / r2 复审返工,判据 3 用)。
|
||
|
||
为什么需要它:字符串存在性判据被两轮复审证伪过 —— `if: needs.probe.outputs.available == 'true' || always()`
|
||
用 `in` 判照样绿,`if: true` 也绿。所以这里**真求值**:给 `needs.probe.outputs.available` 分别取
|
||
`'true'` / `'false'` / `''`,门必须分别判真 / 假 / 假 —— 门到底还挂不挂,由语义决定,不由子串决定。
|
||
|
||
**只认一小撮语法,其余一律抛 `ConditionError`(调用方按红处理,宁可假红)**:
|
||
`==` / `!=` / `&&` / `||` / 括号 / 单引号字符串 / 数字 / `needs.<job>.outputs.<名>` / `true|false|null` /
|
||
`always()|success()|failure()|cancelled()`(后四个按「假定上游成功」取常量)。**不支持 `!`**:GitHub 与
|
||
Python 的 `!` 优先级不同,认不出比误判安全。不是 GitHub 语义的替身,是它的一个保守子集。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
|
||
AVAILABLE_IDENT = "needs.probe.outputs.available"
|
||
CALL_FUNCS = {"always": True, "success": True, "failure": False, "cancelled": False}
|
||
_TOKEN_RE = re.compile(r"(?P<string>'[^']*')|(?P<number>\d+)|(?P<ident>[A-Za-z_][\w.\-]*)|(?P<op>==|!=|&&|\|\||[()!])")
|
||
|
||
|
||
class ConditionError(Exception):
|
||
"""`if:` 表达式认不出 —— 认不出即判红,绝不默认放过。"""
|
||
|
||
|
||
def evaluate_condition(raw, available: str) -> bool:
|
||
"""把 GitHub 的 `if:` 表达式求值。只认一小撮语法,其余抛 `ConditionError`(= 判红)。"""
|
||
text = str(raw).strip()
|
||
if text.startswith("${{") and text.endswith("}}"):
|
||
text = text[3:-2].strip()
|
||
for name, value in CALL_FUNCS.items():
|
||
text = re.sub(rf"\b{name}\s*\(\s*\)", "true" if value else "false", text)
|
||
if re.search(r"\b(" + "|".join(CALL_FUNCS) + r")\s*\(", text):
|
||
raise ConditionError("函数只支持不带参数的形式")
|
||
parts: list[str] = []
|
||
pos = 0
|
||
while pos < len(text):
|
||
if text[pos].isspace():
|
||
pos += 1
|
||
continue
|
||
match = _TOKEN_RE.match(text, pos)
|
||
if not match:
|
||
raise ConditionError(f"认不出的字符 {text[pos]!r}")
|
||
pos = match.end()
|
||
token = match.group()
|
||
if match.lastgroup in ("string", "number"):
|
||
parts.append(token)
|
||
elif match.lastgroup == "ident":
|
||
if token == AVAILABLE_IDENT:
|
||
parts.append("__available")
|
||
elif token in ("true", "false", "null"):
|
||
parts.append({"true": "True", "false": "False", "null": "None"}[token])
|
||
else:
|
||
raise ConditionError(f"认不出的标识符 `{token}`")
|
||
else:
|
||
if token == "!":
|
||
raise ConditionError("不支持 `!`(GitHub 与 Python 的优先级不同,宁可认不出)")
|
||
parts.append({"&&": "and", "||": "or"}.get(token, token))
|
||
source = " ".join(parts)
|
||
try:
|
||
return bool(eval(source, {"__builtins__": {}}, {"__available": available})) # noqa: S307 - 词表是白名单,见上
|
||
except Exception as error: # noqa: BLE001 - 任何求值失败都按「认不出」处理
|
||
raise ConditionError(f"求值失败({error}):`{source}`") from None
|