补两处残余(本轮不推、不打真实 tag、不部署;全部用临时仓库与 spy 验):
1) version_in_commit 必须比"目标提交 CHANGELOG 的**顶部**已发布版本",不是"历史里出现过":
- 新增 top_version_in_commit(),version_in_commit() 直接比它;预检输出把该提交的顶部版本打出来
(诊断可审计:例如"顶部是 `## [0.3.0]`,不是 0.2.0"),阻断信息里也带上真实顶部版本。
- 顺带兜住"最新在最上面 / 版本只增不减"的排序要求。
- 负例(自检 + 夹具各一遍):顶部 0.3.0、历史里 0.2.0 ⇒ 候选 0.2.0 拒绝;候选 0.3.0 通过。
2) 按只读约定**禁用 --execute 自动发布路径**:
- 删掉写路径(打 tag / 推 tag / 建 Release 的代码整段移除),`--execute` / `--approve-version` /
`--approve-sha` 只保留参数兼容(不报 unknown argument),传了就**明确拒绝**(rc=2)。
- 拒绝发生在收集事实之前 ⇒ **零写操作、零 API 调用**;自检用 spy 断言拒绝路径
"一个子进程都没跑、一个 API 都没发",并扫描生产路径不许出现直接执行 git 写命令的行。
- 输出里也不再推荐自动路径;RELEASE_RUNBOOK 第 4 步改为由人执行。
验证:release_preflight --self-test 绿(就绪 20 分支 + 顶部/历史两态 + --execute 拒绝零写 +
源码写路径扫描);反向夹具 84/0(含 --execute rc=2 且跑完仓里 tag 不变);make verify 15/15 success。
This commit is contained in:
@@ -13,12 +13,14 @@
|
||||
版本号**只增不减**(回退走部署层,不改这个文件);每个项目只有本文件这一个版本来源。
|
||||
3. **跑预检(只读)**:`python3 scripts/release_preflight.py --version X.Y.Z`
|
||||
—— 输出**两段**:`准备建议`(还差什么)与 `发布就绪判定`(是否就绪)。
|
||||
⚠️ 预检**不执行、也不推荐自动发布路径**;正式发版动作由人按本文执行。
|
||||
⚠️ 预检**不执行、也不推荐自动发布路径**:**自动发布路径已禁用** —— `--execute` / `--approve-*`
|
||||
只保留参数兼容(不报 unknown argument),传了会被**明确拒绝**(rc=2,拒绝发生在收集事实之前 ⇒
|
||||
**零写操作、零 API 调用**)。正式发版动作由人按本文执行。
|
||||
**就绪 = 证据齐(缺一条都不算)**:
|
||||
|
||||
| 判据 | 说明 |
|
||||
|---|---|
|
||||
| 候选版本**已写进目标提交** | 查 `git show <sha>:CHANGELOG.md`;只在工作区写了、没提交 ⇒ 不就绪 |
|
||||
| 候选版本**已是目标提交里的顶部版本** | 查 `git show <sha>:CHANGELOG.md`;只在工作区写了、没提交 ⇒ 不就绪;只在**历史**里出现过(顶部是别的版本)⇒ **也不就绪**;预检会把该提交的顶部版本打出来 |
|
||||
| 完整 40 位 SHA 指向准确提交 | 目标提交 == HEAD,或 `--sha` 逐字符一致 |
|
||||
| 记录检查通过 | 结构与 tag 一致性(真查远端) |
|
||||
| **必需 context 集合全部 success** | 集合来自 `.gitea/required-contexts.txt`;缺失/为空 ⇒ 没有策略 ⇒ 阻断;失败 / 缺项 / pending / 查询异常一律阻断 |
|
||||
@@ -28,9 +30,9 @@
|
||||
|
||||
自查(不依赖网络、不打真 tag):`python3 scripts/release_preflight.py --self-test`、
|
||||
`python3 scripts/version.py --self-test`。
|
||||
4. **批准后执行**:`python3 scripts/release_preflight.py --execute --version X.Y.Z
|
||||
--approve-version X.Y.Z --approve-sha <完整SHA>`(两个批准参数必须逐字符等于预检结果)。
|
||||
它创建**注释 tag** `vX.Y.Z`(指向验收过的那个提交)并建关联 Release,正文取自 `CHANGELOG.md`。
|
||||
4. **由人执行**(预检不做这件事):创建**注释 tag** `vX.Y.Z`(指向验收过的那个提交,完整 40 位 SHA)
|
||||
并建关联 Release,正文取自 `CHANGELOG.md`。⚠️ `release_preflight.py --execute` **已被禁用**
|
||||
(见第 3 步),不要试图用它自动发版。
|
||||
5. **读回**:`git ls-remote --tags origin` 里该 tag 的 SHA == 本地 `git rev-parse vX.Y.Z^{commit}`;
|
||||
Release 的 `tag_name` 正确。**已发布的 tag 不移动、不覆盖。**
|
||||
|
||||
|
||||
@@ -686,15 +686,15 @@ def seed(d):
|
||||
for args in (["init", "-q", "-b", "main"], ["add", "-A"], ["commit", "-q", "-m", "chore: 夹具"]):
|
||||
subprocess.run(["git", *args], cwd=d, check=True, capture_output=True)
|
||||
|
||||
def pre(d):
|
||||
p = subprocess.run(["python3", "scripts/release_preflight.py", "--version", "0.2.0", "--no-api"],
|
||||
def pre(d, *args):
|
||||
p = subprocess.run(["python3", "scripts/release_preflight.py", *args, "--no-api"],
|
||||
cwd=d, capture_output=True, text=True)
|
||||
return p.returncode, (p.stdout + p.stderr)
|
||||
|
||||
cases = []
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
d = pathlib.Path(tmp) / "r"; d.mkdir(); seed(d)
|
||||
rc1, t1 = pre(d)
|
||||
rc1, t1 = pre(d, "--version", "0.2.0")
|
||||
cases.append(("记录只在工作区(没提交)⇒ 明说『还没写进目标提交』且不就绪",
|
||||
rc1 == 1 and "还没写进目标提交" in t1 and "现在**不能**发" in t1))
|
||||
cases.append(("就绪判定里含『准备建议』与『发布就绪判定』两段(不是混在一起)",
|
||||
@@ -709,7 +709,7 @@ with tempfile.TemporaryDirectory() as tmp:
|
||||
subprocess.run(["git", "add", "-A"], cwd=d, check=True, capture_output=True)
|
||||
subprocess.run(["git", "commit", "-q", "-m", "chore(release): 0.2.0 记录"], cwd=d, check=True,
|
||||
capture_output=True)
|
||||
rc2, t2 = pre(d)
|
||||
rc2, t2 = pre(d, "--version", "0.2.0")
|
||||
cases.append(("记录进了提交 ⇒ 『还没写进目标提交』这条阻塞**消失**",
|
||||
"还没写进目标提交" not in t2))
|
||||
cases.append(("仍然不就绪(没核对远端 / 没有 CI 证据)⇒ 不许给可发结论",
|
||||
@@ -717,11 +717,43 @@ with tempfile.TemporaryDirectory() as tmp:
|
||||
cases.append(("候选记录存在 ≠ 已发布:不该出现『已存在』类误判",
|
||||
"已存在" not in t2.replace("不存在(可发)", "")))
|
||||
|
||||
# 候选版本必须“**是**目标提交里的顶部版本”:只在历史里出现过不算
|
||||
d2 = pathlib.Path(tmp) / "r2"; d2.mkdir()
|
||||
(d2 / "scripts").mkdir(parents=True); (d2 / "docs").mkdir(); (d2 / ".gitea").mkdir()
|
||||
for n in ("check_changelog.py", "release_preflight.py"):
|
||||
(d2 / "scripts" / n).write_text(pathlib.Path("scripts", n).read_text(encoding="utf-8"),
|
||||
encoding="utf-8")
|
||||
(d2 / "docs" / "RELEASE_RUNBOOK.md").write_text("# 发布说明\n", encoding="utf-8")
|
||||
(d2 / ".gitea" / "required-contexts.txt").write_text("build / build (push)\n", encoding="utf-8")
|
||||
(d2 / "CHANGELOG.md").write_text(
|
||||
"# Changelog\n\n## [0.3.0] - 2026-03-01\n\n### Added\n\n- 新\n\n"
|
||||
"## [0.2.0] - 2026-02-01\n\n### Fixed\n\n- 旧\n", encoding="utf-8")
|
||||
(d2 / "no-hooks").mkdir(exist_ok=True)
|
||||
for args in (["init", "-q", "-b", "main"], ["add", "-A"], ["commit", "-q", "-m", "chore: 夹具"]):
|
||||
subprocess.run(["git", *args], cwd=d2, check=True, capture_output=True)
|
||||
_, t3 = pre(d2, "--version", "0.2.0")
|
||||
cases.append(("候选版本只在**历史**里(顶部是 0.3.0)⇒ 明确『还没写进目标提交』",
|
||||
"还没写进目标提交" in t3))
|
||||
_, t4 = pre(d2, "--version", "0.3.0")
|
||||
cases.append(("候选版本就是该提交的**顶部**版本 ⇒ 该条阻塞消失", "还没写进目标提交" not in t4))
|
||||
|
||||
bad = [n for n, okv in cases if not okv]
|
||||
for n, okv in cases:
|
||||
print(("✅ " if okv else "❌ ") + n)
|
||||
raise SystemExit(1 if bad else 0)
|
||||
PY
|
||||
pycheck "预检:--execute 自动发布路径已禁用(明确拒绝、rc=2、零写操作:跑完仓里 tag 不变)" <<'PY'
|
||||
import subprocess
|
||||
before = subprocess.run(["git", "tag", "-l"], capture_output=True, text=True).stdout.split()
|
||||
p = subprocess.run(["python3", "scripts/release_preflight.py", "--execute",
|
||||
"--approve-version", "9.9.9", "--approve-sha", "0" * 40],
|
||||
capture_output=True, text=True)
|
||||
after = subprocess.run(["git", "tag", "-l"], capture_output=True, text=True).stdout.split()
|
||||
okv = (p.returncode == 2 and "已禁用" in (p.stderr or "") and not p.stdout.strip()
|
||||
and before == after)
|
||||
print(("✅ " if okv else "❌ ") + f"rc={p.returncode}|tag {before} → {after}|输出为空={not p.stdout.strip()}")
|
||||
raise SystemExit(0 if okv else 1)
|
||||
PY
|
||||
if grep -qE "git tag -a|--data-binary|POST" /tmp/kg-pf.log; then
|
||||
no "预检输出里出现了写操作迹象"
|
||||
else
|
||||
|
||||
+105
-64
@@ -19,9 +19,8 @@
|
||||
python3 scripts/release_preflight.py # 建议 patch(默认)
|
||||
python3 scripts/release_preflight.py --bump minor
|
||||
python3 scripts/release_preflight.py --version 0.2.0
|
||||
python3 scripts/release_preflight.py --self-test # 只读保证的自检
|
||||
# 批准后才执行(本轮不执行):
|
||||
python3 scripts/release_preflight.py --execute --approve-version 0.2.0 --approve-sha <完整SHA>
|
||||
python3 scripts/release_preflight.py --self-test # 只读保证 + 就绪判定的自检
|
||||
# ⚠️ 自动发布路径已禁用:--execute 会被明确拒绝(rc=2,零写操作)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -38,6 +37,7 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
CHANGELOG = ROOT / "CHANGELOG.md"
|
||||
FULL_SHA = re.compile(r"^[0-9a-f]{40}$")
|
||||
RELEASE_HEADING = re.compile(r"^##\s*\[(\d+\.\d+\.\d+)\]", re.MULTILINE)
|
||||
SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
|
||||
# 预检路径允许的只读 git 子命令(写命令在这里直接拒)
|
||||
GIT_READ_OK = {"ls-remote", "rev-parse", "log", "show", "cat-file", "for-each-ref",
|
||||
@@ -160,15 +160,23 @@ def required_contexts(root: Path = ROOT) -> tuple[list[str], str]:
|
||||
return out, (f"{f.name}({len(out)} 条)" if out else f"{f.name} 只有注释 / 是空的")
|
||||
|
||||
|
||||
def version_in_commit(sha: str, version: str, *, root: Path = ROOT) -> bool:
|
||||
"""候选版本是否**已经写进那个提交**(查该提交里的 CHANGELOG.md,不看工作区)。
|
||||
|
||||
⚠️ 这是"准备 vs 就绪"的分界线:工作区里写了、没提交 ⇒ 还不是就绪。
|
||||
"""
|
||||
def top_version_in_commit(sha: str, *, root: Path = ROOT) -> str | None:
|
||||
"""那个提交里 CHANGELOG.md 的**顶部**已发布版本;没有已发布条目 ⇒ None。"""
|
||||
rc, blob = _git_read("show", f"{sha}:CHANGELOG.md", cwd=root)
|
||||
if rc != 0:
|
||||
return False
|
||||
return bool(re.search(rf"^##\s*\[{re.escape(version)}\]", blob, re.MULTILINE))
|
||||
return None
|
||||
m = RELEASE_HEADING.search(blob)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def version_in_commit(sha: str, version: str, *, root: Path = ROOT) -> bool:
|
||||
"""候选版本是否**已是那个提交里的顶部已发布版本**(查该提交里的 CHANGELOG.md,不看工作区)。
|
||||
|
||||
⚠️ 必须比"**顶部**那条":候选版本在历史里出现过(比如旧版本条目还列在下面)**不算就绪** ——
|
||||
那说明这次发版的记录还没写进目标提交。也正因如此,比较顶部同时隐含了
|
||||
"最新在最上面 / 版本只增不减" 的排序要求。
|
||||
"""
|
||||
return top_version_in_commit(sha, root=root) == version
|
||||
|
||||
|
||||
def release_state(status: int) -> str:
|
||||
@@ -195,7 +203,9 @@ def readiness(f: dict) -> tuple[bool, list[str], list[str]]:
|
||||
blockers.append(f"还没有已发布版本:首个正式版本号必须由人显式指定(--version X.Y.Z);"
|
||||
f"{cand} 只是建议值,**不当作已发布**")
|
||||
if not f.get("in_target_commit"):
|
||||
blockers.append(f"候选版本 {cand} 还没写进目标提交(该提交的 CHANGELOG.md 里没有 `## [{cand}]`)"
|
||||
top = f.get("top_in_commit")
|
||||
blockers.append(f"候选版本 {cand} 还没写进目标提交(该提交的 CHANGELOG.md **顶部**已发布版本是 "
|
||||
f"{('`## [' + top + ']`') if top else '(没有已发布版本条目)'},不是 {cand})"
|
||||
f"—— 版本记录要先提交,再发版")
|
||||
advice.append(f"把 `## [Unreleased]` 改成 `## [{cand}] - YYYY-MM-DD`(四类小节、"
|
||||
f"破坏性变化带发布文档链接)并**提交**,然后重跑预检")
|
||||
@@ -257,6 +267,14 @@ def main(argv: list[str]) -> int:
|
||||
args = ap.parse_args(argv)
|
||||
if args.self_test:
|
||||
return self_test()
|
||||
# ⚠️ 本预检是**只读**的:按约定**禁用自动发布路径**。`--execute` / `--approve-*` 只保留参数兼容
|
||||
# (不报 unknown argument),但传了就**明确拒绝**,且拒绝发生在收集事实之前 ⇒ **零写操作、
|
||||
# 零 API 调用**。正式发版动作由人按 docs/RELEASE_RUNBOOK.md 执行。
|
||||
if args.execute:
|
||||
print("⛔ 本预检是只读的:自动发布路径(打 tag / 推 tag / 建 Release)**已禁用**,不执行。\n"
|
||||
" 正式发版动作请由人按 docs/RELEASE_RUNBOOK.md 执行;`--execute` / `--approve-version` /\n"
|
||||
" `--approve-sha` 仅保留参数兼容,传了也不会产生任何写操作。", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
text = CHANGELOG.read_text(encoding="utf-8") if CHANGELOG.is_file() else ""
|
||||
rel = released_versions(text)
|
||||
@@ -316,10 +334,12 @@ def main(argv: list[str]) -> int:
|
||||
# ── 事实收集(只读)──────────────────────────────────────────────
|
||||
tag = f"v{candidate}"
|
||||
required, policy_src = required_contexts()
|
||||
in_target = version_in_commit(want_sha, candidate)
|
||||
mark = "✅ 已有" if in_target else "❌ 没有"
|
||||
print(f"\n 目标提交内记录: {mark} `## [{candidate}]`"
|
||||
f"(版本记录要先提交,再发版;查的是 {want_sha[:12]})")
|
||||
top_in_commit = top_version_in_commit(want_sha)
|
||||
in_target = top_in_commit == candidate
|
||||
mark = "✅ 就是顶部" if in_target else "❌ 不是顶部"
|
||||
print(f"\n 目标提交内记录: {mark}(该提交的 CHANGELOG.md 顶部已发布版本 = "
|
||||
f"{('`## [' + top_in_commit + ']`') if top_in_commit else '(没有已发布版本条目)'};"
|
||||
f"候选 {candidate};查的是 {want_sha[:12]})")
|
||||
print(f" 必需 context : {len(required)} 条({policy_src})"
|
||||
+ (":" + "、".join(required) if required else ""))
|
||||
|
||||
@@ -377,6 +397,7 @@ def main(argv: list[str]) -> int:
|
||||
facts = {
|
||||
"candidate": candidate, "tag": tag, "first_release": first_release,
|
||||
"explicit_version": bool(args.version), "in_target_commit": in_target,
|
||||
"top_in_commit": top_in_commit,
|
||||
"dirty": len(dirty_lines), "head_full": bool(FULL_SHA.match(head)),
|
||||
"sha_is_head": want_sha == head, "checker_ok": checker_ok,
|
||||
"unreleased_present": bool(unreleased.strip()),
|
||||
@@ -401,53 +422,7 @@ def main(argv: list[str]) -> int:
|
||||
print(f" ⛔ {b}")
|
||||
print(f" ⇒ 现在**不能**发 {candidate}(未就绪)")
|
||||
|
||||
if not args.execute:
|
||||
return 1 if blockers else 0
|
||||
|
||||
# ── 显式批准后的执行路径(本轮不跑)────────────────────────────────
|
||||
if blockers:
|
||||
print("⛔ 预检有阻塞项,**不执行**打 tag/建 Release", file=sys.stderr)
|
||||
return 1
|
||||
if args.approve_version != candidate or args.approve_sha != head:
|
||||
print("⛔ 批准参数与预检结果不符(--approve-version/--approve-sha 必须逐字符等于预检结果)—— 不执行",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
print(f"\n── 执行(已获批准)──\n git tag -a {tag} -m 'release {candidate}' → push → 建 Release → 读回")
|
||||
p = subprocess.run(["git", "tag", "-a", tag, "-m", f"release {candidate}"], cwd=ROOT,
|
||||
capture_output=True, text=True)
|
||||
if p.returncode:
|
||||
print(f"❌ 打 tag 失败:{p.stderr[:200]}", file=sys.stderr)
|
||||
return 1
|
||||
p = subprocess.run(["git", "push", "origin", tag], cwd=ROOT, capture_output=True, text=True)
|
||||
if p.returncode:
|
||||
print(f"❌ 推送 tag 失败(**不建 Release**):{p.stderr[:200]}", file=sys.stderr)
|
||||
return 1
|
||||
rc, back = _git_read("ls-remote", "--tags", "origin", tag)
|
||||
back_sha = (back.split()[0] if back.strip() else "").strip()
|
||||
rc2, tagged = _git_read("rev-parse", f"{tag}^{{commit}}")
|
||||
ok_tag = bool(back_sha) and tagged.strip() == head
|
||||
print(f" 读回远端 {tag} = {back_sha[:12] or '(空)'}|本地 tag 指向 {tagged.strip()[:12]}"
|
||||
f" ⇒ {'✅ 一致' if ok_tag else '❌ 不一致'}")
|
||||
if not ok_tag:
|
||||
return 1
|
||||
body = json.dumps({"tag_name": tag, "name": tag, "body": block_of(text, current) or unreleased,
|
||||
"draft": False, "prerelease": False}).encode()
|
||||
req = urllib.request.Request(f"{base}/api/v1/repos/{slug}/releases", data=body, method="POST",
|
||||
headers={"Authorization": "token " + token,
|
||||
"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as x:
|
||||
created = json.loads(x.read().decode() or "{}")
|
||||
except (urllib.error.URLError, OSError) as e:
|
||||
print(f"❌ 建 Release 失败(tag 已经推上去了,别重复推):{e}", file=sys.stderr)
|
||||
return 1
|
||||
st, got = _api_read(base, f"/repos/{slug}/releases/tags/{urllib.parse.quote(tag)}", token)
|
||||
same = st == 200 and (got.get("tag_name") == tag)
|
||||
print(f" Release 读回:HTTP {st} tag_name={got.get('tag_name')!r} id={got.get('id')}"
|
||||
f" ⇒ {'✅ 一致' if same else '❌ 不一致'}")
|
||||
print(f" (说明来自 CHANGELOG,不维护第二份正文;tag 不可移动、不可覆盖)")
|
||||
return 0 if same else 1
|
||||
|
||||
return 1 if blockers else 0
|
||||
|
||||
def self_test() -> int:
|
||||
"""预检的**只读保证**要能被证明:写命令与 POST 必须在预检路径上被拒。"""
|
||||
@@ -507,6 +482,9 @@ def self_test() -> int:
|
||||
rd("首发**显式**指定且记录已进提交 ⇒ 就绪", True,
|
||||
first_release=True, explicit_version=True, contexts={"build / build (push)": "success"})
|
||||
rd("候选版本没进目标提交 ⇒ 不就绪", False, "还没写进目标提交", in_target_commit=False)
|
||||
rd("候选版本只在历史里(顶部是别的版本)⇒ 不就绪,且信息里给出真实顶部版本",
|
||||
False, "顶部**已发布版本是 `## [0.3.0]`",
|
||||
in_target_commit=False, top_in_commit="0.3.0", candidate="0.2.0")
|
||||
rd("工作区脏 ⇒ 不就绪", False, "未提交", dirty=3)
|
||||
rd("目标提交 != HEAD ⇒ 不就绪", False, "目标提交 != HEAD", sha_is_head=False)
|
||||
rd("记录检查不通过 ⇒ 不就绪", False, "检查不通过", checker_ok=False)
|
||||
@@ -549,6 +527,68 @@ def self_test() -> int:
|
||||
if not version_in_commit(sha2, "0.2.0", root=dd):
|
||||
problems.append("版本写进提交后仍判成『没有』")
|
||||
|
||||
# ── 候选版本必须"是目标提交里的**顶部**版本"(历史里出现过不算)──
|
||||
with _tf.TemporaryDirectory() as d:
|
||||
dd = Path(d)
|
||||
(dd / "no-hooks").mkdir(exist_ok=True)
|
||||
env = {**dict(**__import__("os").environ), "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t",
|
||||
"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t",
|
||||
"GIT_CONFIG_COUNT": "1", "GIT_CONFIG_KEY_0": "core.hooksPath",
|
||||
"GIT_CONFIG_VALUE_0": str(dd / "no-hooks")}
|
||||
(dd / "CHANGELOG.md").write_text(
|
||||
"# Changelog\n\n## [0.3.0] - 2026-03-01\n\n### Added\n\n- 新\n\n"
|
||||
"## [0.2.0] - 2026-02-01\n\n### Fixed\n\n- 旧\n", encoding="utf-8")
|
||||
subprocess.run(["git", "init", "-q", "-b", "main"], cwd=dd, env=env, check=True)
|
||||
subprocess.run(["git", "add", "-A"], cwd=dd, env=env, check=True)
|
||||
subprocess.run(["git", "commit", "-q", "-m", "chore: 夹具"], cwd=dd, env=env, check=True)
|
||||
sha = subprocess.run(["git", "rev-parse", "HEAD"], cwd=dd, capture_output=True,
|
||||
text=True).stdout.strip()
|
||||
if version_in_commit(sha, "0.2.0", root=dd):
|
||||
problems.append("候选版本 0.2.0 只在**历史**里出现(顶部是 0.3.0),却被判成『已写进目标提交』")
|
||||
if not version_in_commit(sha, "0.3.0", root=dd):
|
||||
problems.append("候选版本 0.3.0 就是该提交的顶部版本,却没判成『已写进目标提交』")
|
||||
|
||||
# ── `--execute` 必须明确拒绝,且**零写操作、零 API 调用** ──
|
||||
calls: list[list[str]] = []
|
||||
api_hits: list[tuple] = []
|
||||
real_run, real_api = subprocess.run, _api_read
|
||||
|
||||
def _spy_run(*a, **k):
|
||||
if a and isinstance(a[0], (list, tuple)):
|
||||
calls.append([str(x) for x in a[0]])
|
||||
return real_run(*a, **k)
|
||||
|
||||
def _spy_api(*a, **k):
|
||||
api_hits.append(a)
|
||||
return real_api(*a, **k)
|
||||
|
||||
subprocess.run = _spy_run # 模块里用的是同一个 subprocess 模块对象
|
||||
globals()["_api_read"] = _spy_api
|
||||
try:
|
||||
rc_exec = main(["--execute", "--approve-version", "9.9.9", "--approve-sha", "0" * 40])
|
||||
finally:
|
||||
subprocess.run = real_run
|
||||
globals()["_api_read"] = real_api
|
||||
if rc_exec != 2:
|
||||
problems.append(f"--execute 没有被明确拒绝(退出码 {rc_exec},期望 2)")
|
||||
if api_hits:
|
||||
problems.append(f"--execute 路径竟然发了 API 请求:{len(api_hits)} 次")
|
||||
if calls:
|
||||
problems.append(f"--execute 拒绝路径不该执行任何子进程(零写操作):{calls[:3]}")
|
||||
|
||||
# 源码里不许再有"直接执行 git 写命令"的行(防止以后被悄悄加回来)。
|
||||
# 只看**生产路径**(`def self_test()` 之前):自检自己在临时仓库里 `git commit` 造夹具是允许的。
|
||||
prod = Path(__file__).read_text(encoding="utf-8").split("def self_test()")[0]
|
||||
write_words = ('"tag"', '"push"', '"update-ref"', '"commit"')
|
||||
bad_lines = []
|
||||
for line in prod.splitlines():
|
||||
if "subprocess.run(" not in line or '"git"' not in line:
|
||||
continue
|
||||
if any(w in line for w in write_words):
|
||||
bad_lines.append(line.strip()[:120])
|
||||
if bad_lines:
|
||||
problems.append(f"预检的生产路径里有直接执行 git 写命令的行:{bad_lines[:2]}")
|
||||
|
||||
if problems:
|
||||
print("❌ 预检自检失败:", file=sys.stderr)
|
||||
for p in problems:
|
||||
@@ -557,9 +597,10 @@ def self_test() -> int:
|
||||
print("✅ 预检自检通过:写类 git 命令与 POST/PUT/DELETE/PATCH 在预检路径上全部被拒;版本推进正确;"
|
||||
"就绪判定 19 个分支(context 缺项/pending/failure/查询异常、无策略、Release 非 404、"
|
||||
"tag 占用、未核对远端、首发未显式指定、记录未进提交…逐条阻断;证据齐才就绪);"
|
||||
"『写进提交才算数』用临时仓库验证")
|
||||
"『顶部版本才算数』与『写进提交才算数』用临时仓库验证;"
|
||||
"`--execute` 明确拒绝且零写操作、零 API 调用")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
|
||||
Reference in New Issue
Block a user