feat(release): 抬档门禁(破坏性 ⇒ 0.x 抬次 / ≥1.0.0 抬主)+ 1.0.0 前置(稳定契约承诺)
原来预检只"打印破坏性条数",抬哪一档靠人自觉 ⇒ 改成硬门禁: - **抬档门禁**:破坏性条数取自**目标提交里候选版本那一段**(发版事实看提交,不看工作区)。 `0.x` 阶段有破坏性 ⇒ 必须抬**次版本**(或主版本),只抬修订 ⇒ 阻断并报出该抬到哪一档; `≥1.0.0` 之后有破坏性 ⇒ 必须抬**主版本**,抬次/修订都阻断。 另加"候选号必须大于上一个正式版本"(版本只增不减)。 - **1.0.0 前置**:上一版本是 `0.x` 时,候选 `1.0.0` = 对外承诺契约稳定 ⇒ 必须先有写实的 《稳定契约承诺》(覆盖 接口 / 数据模型 / 配置 / CLI 输出与退出码,不留 `待填`/`TODO`/`TBD`), **且候选发布记录里链接它**,缺任一条 ⇒ 阻断;`1.x → 2.0.0` 不重复要求。 - 预检新增"版本走向"事实行(上一版本 → 候选 / 抬哪一档 / 破坏性 N 条 ⇒ 要求哪一档 ✅⛔); `docs/RELEASE_RUNBOOK.md` 新增"抬哪一档"对照表与《稳定契约承诺》段(故意留占位)。 验证:release_preflight --self-test 32 分支全过(含新增 9 条);反向夹具新增 §24 六条真实临时仓库用例 (0.x 只抬修订 ⇒ 阻断、0.x 抬次 ⇒ 放行、≥1.0.0 抬次 ⇒ 阻断、1.0.0 留占位 ⇒ 阻断、写实+链接 ⇒ 放行、 写实但没链接 ⇒ 仍阻断)⇒ 夹具 91/0;make verify 15/15。 于是 0.1.0 之后的第一个提交(本提交)开发号 = 0.1.1。
This commit is contained in:
@@ -64,6 +64,26 @@ python3 scripts/version.py --identity # JSON:release_version / build_kind
|
||||
- 二进制里的注入字段(`internal/version`)与上表一一对应;契约测试断言:
|
||||
注入 == CHANGELOG 顶部(发布构建)、开发构建必须是 `dev` 形状(**不许声称正式版**),且**跳过不算通过**。
|
||||
|
||||
## 二·补、抬哪一档(硬门禁,预检会拦)
|
||||
|
||||
| 抬哪一位 | 什么情况 | 预检判据(不满足 ⇒ 阻断,发不出去) |
|
||||
|---|---|---|
|
||||
| **修订** `0.1.N → 0.1.N+1` | 只修 bug、内部改动:对外行为不变、无破坏性、无成规模新能力 | 候选号必须 **> 当前开发号**(开发号到 `0.1.7` 时只能发 `0.1.8` 及以上);候选 ≤ 上一个正式版本 ⇒ 阻断(号只增不减) |
|
||||
| **次版本** `0.1.x → 0.2.0` | **有破坏性变化**(0.x 阶段一律走次版本),或成规模的对外新能力/行为变化 | 候选发布记录里破坏性条数 ≥1 且当前是 `0.x` ⇒ 必须抬**次版本**;只抬修订 ⇒ 阻断 |
|
||||
| **主版本** `0.x → 1.0.0`、`1.x → 2.0.0` | 首次声明"对外契约稳定"(`1.0.0`);之后**任何**破坏性变化(`2.0.0`) | ≥1.0.0 阶段破坏性 ⇒ 必须抬**主版本**(抬次/修订都阻断);**首发 `1.0.0` 还要求本文《稳定契约承诺》写实并在发布记录里链接它** |
|
||||
|
||||
判据里用的"破坏性条数"取自**目标提交里候选版本那一段**(发版事实看提交里的记录,不看工作区)。
|
||||
|
||||
## 二·补·2、《稳定契约承诺》(发 1.0.0 的前置条件)
|
||||
|
||||
> ⛔ 现在**故意留着占位**:只要还有 `(待填)` / `TODO` / `TBD`,预检在候选为 `1.0.0`(且上一个正式版本是 `0.x`)时就会阻断。
|
||||
> 逐项写实、并在 `CHANGELOG.md` 的 `## [1.0.0]` 记录里链接本文后,门禁才放行。四项一个都不能少。
|
||||
|
||||
- **接口**(待填):对外 HTTP / CLI / SDK 接口的稳定范围与版本策略(哪些字段冻结、变更怎么通知)。
|
||||
- **数据模型**(待填):库表 / 文件 / 消息结构的兼容窗口与迁移责任。
|
||||
- **配置**(待填):配置项与默认值的冻结范围、弃用流程、回退方式。
|
||||
- **CLI 输出与退出码**(待填):脚本可依赖的输出形态与退出码约定。
|
||||
|
||||
## 三、破坏性变化 / 数据迁移怎么写
|
||||
|
||||
在 `CHANGELOG.md` 的 `### Changed` 里:
|
||||
|
||||
@@ -805,5 +805,103 @@ else
|
||||
no "预检留下了 tag:$(git tag -l 'v*' | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "§24 抬档门禁(破坏性 ⇒ 0.x 抬次 / ≥1.0.0 抬主)与 1.0.0 前置(《稳定契约承诺》)"
|
||||
cat >/tmp/kg_bump_lib.py <<'LIB'
|
||||
import os, pathlib, shutil, subprocess, tempfile
|
||||
|
||||
|
||||
def mk(prev, cand, breaking_in_changed, stab_section=None, link_in_record=False):
|
||||
"""造临时仓库:CHANGELOG 顶部 = 候选版本,下面留上一版本;docs 里有/没有承诺段。"""
|
||||
d = pathlib.Path(tempfile.mkdtemp(prefix="kg-bump-"))
|
||||
(d / "scripts").mkdir()
|
||||
for n in ("check_changelog.py", "release_preflight.py", "version.py"):
|
||||
(d / "scripts" / n).write_text(pathlib.Path("scripts", n).read_text(encoding="utf-8"),
|
||||
encoding="utf-8")
|
||||
(d / "docs").mkdir()
|
||||
doc = "# 发布运行手册\n"
|
||||
if stab_section:
|
||||
doc += "\n## 稳定契约承诺(占位)\n\n" + stab_section + "\n"
|
||||
(d / "docs" / "RELEASE_RUNBOOK.md").write_text(doc, encoding="utf-8")
|
||||
(d / ".gitea").mkdir()
|
||||
(d / ".gitea" / "required-contexts.txt").write_text("build / build (push)\n", encoding="utf-8")
|
||||
changed = ("\n### Changed\n\n- **破坏性**:接口下线,升级见 "
|
||||
"[发布文档](docs/RELEASE_RUNBOOK.md)\n") if breaking_in_changed else ""
|
||||
added = "- 新东西" + ("(稳定契约承诺见 [本文](docs/RELEASE_RUNBOOK.md))" if link_in_record else "")
|
||||
(d / "CHANGELOG.md").write_text(
|
||||
f"# Changelog\n\n## [{cand}] - 2026-10-13\n\n### Added\n\n{added}\n{changed}"
|
||||
f"\n## [{prev}] - 2026-09-01\n\n### Fixed\n\n- 旧\n", encoding="utf-8")
|
||||
(d / "no-hooks").mkdir()
|
||||
env = {**os.environ, "GIT_AUTHOR_NAME": "f", "GIT_AUTHOR_EMAIL": "f@f",
|
||||
"GIT_COMMITTER_NAME": "f", "GIT_COMMITTER_EMAIL": "f@f",
|
||||
"GIT_CONFIG_COUNT": "1", "GIT_CONFIG_KEY_0": "core.hooksPath",
|
||||
"GIT_CONFIG_VALUE_0": str(d / "no-hooks")}
|
||||
for a in (["init", "-q", "-b", "main"], ["add", "-A"], ["commit", "-q", "-m", "chore: 夹具"]):
|
||||
subprocess.run(["git", *a], cwd=d, env=env, check=True, capture_output=True)
|
||||
return d
|
||||
|
||||
|
||||
def pre(prev, cand, breaking, *args, **kw):
|
||||
d = mk(prev, cand, breaking, **kw)
|
||||
try:
|
||||
r = subprocess.run(["python3", "scripts/release_preflight.py", "--version", cand, "--no-api"],
|
||||
cwd=d, capture_output=True, text=True)
|
||||
return r.stdout + r.stderr
|
||||
finally:
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
LIB
|
||||
FULL="冻结 /api/v1;变更提前一个次版本通知。|表结构兼容窗口 2 个大版本。|配置项冻结,弃用先警告一个小版本。|0/1/2 语义固定。"
|
||||
|
||||
pycheck "破坏性 + 0.x 只抬修订(0.1.0→0.1.3)⇒ 阻断,并给出抬档结论" <<'PY'
|
||||
import kg_bump_lib as L # noqa
|
||||
out = L.pre("0.1.0", "0.1.3", True)
|
||||
good = "必须抬**次版本**" in out and "⛔ 抬档不够" in out
|
||||
print(("✅ " if good else "❌ ") + "版本走向行:" + next((l.strip() for l in out.splitlines()
|
||||
if "版本走向" in l), "(没有这行)"))
|
||||
raise SystemExit(0 if good else 1)
|
||||
PY
|
||||
pycheck "破坏性 + 0.x 抬次(0.1.0→0.2.0)⇒ 该条不拦(✅ 抬档够)" <<'PY'
|
||||
import kg_bump_lib as L # noqa
|
||||
out = L.pre("0.1.0", "0.2.0", True)
|
||||
good = "必须抬**次版本**" not in out and "✅ 抬档够" in out
|
||||
print(("✅ " if good else "❌ ") + next((l.strip() for l in out.splitlines() if "版本走向" in l), ""))
|
||||
raise SystemExit(0 if good else 1)
|
||||
PY
|
||||
pycheck "破坏性 + ≥1.0.0 只抬次(1.2.3→1.3.0)⇒ 阻断(必须抬主)" <<'PY'
|
||||
import kg_bump_lib as L # noqa
|
||||
out = L.pre("1.2.3", "1.3.0", True)
|
||||
good = "必须抬**主版本**" in out
|
||||
print(("✅ " if good else "❌ ") + next((l.strip() for l in out.splitlines() if "版本走向" in l), ""))
|
||||
raise SystemExit(0 if good else 1)
|
||||
PY
|
||||
pycheck "首发 1.0.0 但《稳定契约承诺》留占位 ⇒ 阻断(报出占位与缺项)" <<'PY'
|
||||
import kg_bump_lib as L # noqa
|
||||
out = L.pre("0.9.3", "1.0.0", False,
|
||||
stab_section="- **接口**(待填)\n- **数据模型**(待填)\n")
|
||||
good = "稳定契约承诺" in out and "占位" in out and "没覆盖「配置」" in out
|
||||
print(("✅ " if good else "❌ ") + next((l.strip() for l in out.splitlines() if "1.0.0 前置" in l), ""))
|
||||
raise SystemExit(0 if good else 1)
|
||||
PY
|
||||
pycheck "1.0.0 + 承诺段写实 + 发布记录里链接 ⇒ 该条不拦" <<'PY'
|
||||
import kg_bump_lib as L # noqa
|
||||
sec = ("- **接口**:冻结 /api/v1;变更提前一个次版本通知。\n"
|
||||
"- **数据模型**:表结构兼容窗口 2 个大版本。\n"
|
||||
"- **配置**:配置项冻结,弃用先警告一个小版本。\n"
|
||||
"- **CLI 输出与退出码**:0/1/2 语义固定。\n")
|
||||
out = L.pre("0.9.3", "1.0.0", False, stab_section=sec, link_in_record=True)
|
||||
good = "候选进入 1.0.0" not in out and "✅ 已写实" in out
|
||||
print(("✅ " if good else "❌ ") + next((l.strip() for l in out.splitlines() if "1.0.0 前置" in l), ""))
|
||||
raise SystemExit(0 if good else 1)
|
||||
PY
|
||||
pycheck "1.0.0 + 承诺段写实但发布记录里**没链接** ⇒ 仍阻断" <<'PY'
|
||||
import kg_bump_lib as L # noqa
|
||||
sec = ("- **接口**:冻结 /api/v1。\n- **数据模型**:兼容 2 个大版本。\n"
|
||||
"- **配置**:配置项冻结。\n- **CLI 输出与退出码**:0/1/2 固定。\n")
|
||||
out = L.pre("0.9.3", "1.0.0", False, stab_section=sec, link_in_record=False)
|
||||
good = "候选进入 1.0.0" in out and "没有链接" in out
|
||||
print(("✅ " if good else "❌ ") + next((l.strip() for l in out.splitlines() if "1.0.0 前置" in l), ""))
|
||||
raise SystemExit(0 if good else 1)
|
||||
PY
|
||||
|
||||
printf '\n\033[1m════ 反向验收汇总 ════\033[0m\n ✅ 通过 %d | ❌ 失败 %d\n' "$pass" "$fail"
|
||||
[ "$fail" -eq 0 ] || exit 1
|
||||
[ "$fail" -eq 0 ] || exit 1
|
||||
|
||||
@@ -213,6 +213,61 @@ def _ver_tuple(v: str) -> tuple[int, int, int]:
|
||||
return a, b, c
|
||||
|
||||
|
||||
BREAK_ITEM = re.compile(r"^\s*(?:[-*+]|\d+[.)])?\s*\*\*破坏性\*\*")
|
||||
STABILITY_HEADING = re.compile(r"(?m)^##+\s*.*稳定契约承诺")
|
||||
# 「稳定契约承诺」必须逐项覆盖的对外契约面(缺项 / 留占位 ⇒ 不许发 1.0.0)
|
||||
STABILITY_ITEMS = ("接口", "数据模型", "配置", "CLI 输出与退出码")
|
||||
STABILITY_PLACEHOLDERS = ("待填", "TODO", "TBD", "xxx", "XXX")
|
||||
STABILITY_DOCS = ("docs/RELEASE_RUNBOOK.md", "docs/STABILITY_CONTRACT.md")
|
||||
|
||||
|
||||
def released_in_commit(sha: str, *, root: Path = ROOT) -> list[str]:
|
||||
"""目标提交里 CHANGELOG 的已发布版本清单(顶部在前)。"""
|
||||
rc, text = _git_read("show", f"{sha}:CHANGELOG.md", cwd=root)
|
||||
return released_versions(text) if rc == 0 else []
|
||||
|
||||
|
||||
def breaking_in_commit(sha: str, version: str, *, root: Path = ROOT) -> int:
|
||||
"""**候选版本那一段**里的破坏性变化条数(发版事实看提交里的记录,不看工作区)。"""
|
||||
rc, text = _git_read("show", f"{sha}:CHANGELOG.md", cwd=root)
|
||||
if rc != 0:
|
||||
return 0
|
||||
return sum(1 for line in block_of(text, version).splitlines() if BREAK_ITEM.match(line))
|
||||
|
||||
|
||||
def bump_level(prev: str | None, cand: str) -> str | None:
|
||||
"""抬了哪一档:major / minor / patch;比不出来(首发 / 号没涨)⇒ None。"""
|
||||
if not (prev and SEMVER.match(prev) and SEMVER.match(cand)):
|
||||
return None
|
||||
a, b = _ver_tuple(prev), _ver_tuple(cand)
|
||||
if b[0] > a[0]:
|
||||
return "major"
|
||||
if b[0] == a[0] and b[1] > a[1]:
|
||||
return "minor"
|
||||
if b[0] == a[0] and b[1] == a[1] and b[2] > a[2]:
|
||||
return "patch"
|
||||
return None
|
||||
|
||||
|
||||
def stability_contract_problems(*, root: Path = ROOT) -> list[str]:
|
||||
"""《稳定契约承诺》是否写实(发 1.0.0 的前置条件);返回缺失项(空 = 合格)。"""
|
||||
doc = next((root / d for d in STABILITY_DOCS
|
||||
if (root / d).is_file()
|
||||
and STABILITY_HEADING.search((root / d).read_text(encoding="utf-8"))), None)
|
||||
if doc is None:
|
||||
return [f"找不到《稳定契约承诺》段(在 {' 或 '.join(STABILITY_DOCS)} 里以 `## …稳定契约承诺` 开头)"]
|
||||
text = doc.read_text(encoding="utf-8")
|
||||
sec = text[STABILITY_HEADING.search(text).start():]
|
||||
problems: list[str] = []
|
||||
for item in STABILITY_ITEMS:
|
||||
if item not in sec:
|
||||
problems.append(f"没覆盖「{item}」")
|
||||
holes = [w for w in STABILITY_PLACEHOLDERS if w in sec]
|
||||
if holes:
|
||||
problems.append(f"还留着占位没填({'、'.join(holes)})")
|
||||
return problems
|
||||
|
||||
|
||||
def readiness(f: dict) -> tuple[bool, list[str], list[str]]:
|
||||
"""把**事实**翻成(是否就绪, 阻塞项, 准备建议)——纯函数,可用假事实直接自检。
|
||||
|
||||
@@ -245,6 +300,30 @@ def readiness(f: dict) -> tuple[bool, list[str], list[str]]:
|
||||
advice.append("下一次正式发布建议用 <主>.<次+1>.0(保证正式号严格大于此前所有开发号)")
|
||||
except ValueError:
|
||||
pass
|
||||
prev, level = f.get("prev_version"), f.get("bump_level")
|
||||
breaking_n = int(f.get("breaking_in_candidate") or 0)
|
||||
if prev and level and breaking_n:
|
||||
prev_major = _ver_tuple(str(prev))[0]
|
||||
if prev_major == 0 and level == "patch":
|
||||
blocks = (f"有 {breaking_n} 条破坏性变化,但 {prev} → {cand} 只抬了**修订**:"
|
||||
f"0.x 阶段破坏性变化必须抬**次版本**(例如 "
|
||||
f"0.{_ver_tuple(str(prev))[1] + 1}.0)—— 否则使用者按'只是修 bug'升级会踩坑")
|
||||
blockers.append(blocks)
|
||||
advice.append(f"把候选改成 0.{_ver_tuple(str(prev))[1] + 1}.0(抬次版本),"
|
||||
f"并在发布文档里写清升级/迁移与回退")
|
||||
if prev_major >= 1 and level != "major":
|
||||
blockers.append(f"有 {breaking_n} 条破坏性变化,但 {prev} → {cand} 没有抬**主版本**:"
|
||||
f"≥1.0.0 之后破坏性变化必须抬**主版本**(例如 {prev_major + 1}.0.0)")
|
||||
advice.append(f"把候选改成 {prev_major + 1}.0.0(抬主版本)")
|
||||
if level is None and prev and SEMVER.match(str(prev)) and SEMVER.match(str(cand)) \
|
||||
and _ver_tuple(str(cand)) <= _ver_tuple(str(prev)):
|
||||
blockers.append(f"候选 {cand} 不大于上一个正式版本 {prev}(版本号只增不减)")
|
||||
if f.get("first_1_0_0") and not f.get("stability_ok"):
|
||||
problems = f.get("stability_problems") or ["(未取到具体缺失项)"]
|
||||
blockers.append("候选进入 1.0.0 = 对外承诺契约稳定:必须先把《稳定契约承诺》写实、"
|
||||
"并在候选发布记录里链接它 —— 缺:" + ";".join(str(x) for x in problems))
|
||||
advice.append("在 docs 的《稳定契约承诺》里逐项写实 接口 / 数据模型 / 配置 / CLI 输出与退出码,"
|
||||
"并在本次 CHANGELOG 的发布记录里链接该文档;不满足就先发 0.x")
|
||||
if f.get("candidate_tagged"):
|
||||
blockers.append(f"远端已经有 tag v{cand} ⇒ 不能覆盖已发布的 tag(版本号只增不减)")
|
||||
if f.get("dirty"):
|
||||
@@ -371,6 +450,36 @@ def main(argv: list[str]) -> int:
|
||||
tag = f"v{candidate}"
|
||||
required, policy_src = required_contexts()
|
||||
dev_number, dev_commits = current_dev_number()
|
||||
rels = released_in_commit(want_sha)
|
||||
prev_version = next((v for v in rels if v != candidate), None)
|
||||
level = bump_level(prev_version, candidate)
|
||||
breaking_n = breaking_in_commit(want_sha, candidate)
|
||||
first_1_0_0 = (SEMVER.match(candidate) is not None and _ver_tuple(candidate)[0] == 1
|
||||
and (prev_version is None or _ver_tuple(prev_version)[0] == 0))
|
||||
stab_problems: list[str] = []
|
||||
if first_1_0_0:
|
||||
stab_problems = stability_contract_problems()
|
||||
rc, ctext = _git_read("show", f"{want_sha}:CHANGELOG.md")
|
||||
blk = block_of(ctext, candidate) if rc == 0 else ""
|
||||
if not (any(d in blk for d in STABILITY_DOCS) or "稳定契约承诺" in blk):
|
||||
stab_problems.append("候选发布记录里没有链接《稳定契约承诺》"
|
||||
f"(应引 {' 或 '.join(STABILITY_DOCS)})")
|
||||
stab_ok = not stab_problems
|
||||
level_cn = {"major": "主版本", "minor": "次版本", "patch": "修订"}.get(level or "", "看不出来(首发 / 号没涨)")
|
||||
prev_major = _ver_tuple(prev_version)[0] if (prev_version and SEMVER.match(prev_version)) else None
|
||||
if not breaking_n:
|
||||
tail = "候选记录里没有破坏性变化 ⇒ 抬哪一档不限(但候选号必须 > 当前开发号)"
|
||||
elif prev_major is None:
|
||||
tail = f"候选记录里有 {breaking_n} 条破坏性变化(首发,无上一版本可比)"
|
||||
else:
|
||||
need = ("0.x 阶段:有破坏性必须抬次版本(或主版本)" if prev_major == 0
|
||||
else "≥1.0.0:有破坏性必须抬主版本")
|
||||
ok = (level in ("minor", "major")) if prev_major == 0 else (level == "major")
|
||||
tail = f"候选记录里有 {breaking_n} 条破坏性变化 ⇒ {need} {'✅ 抬档够' if ok else '⛔ 抬档不够'}"
|
||||
print(f" 版本走向 : {prev_version or '(首发)'} → {candidate}(抬{level_cn})|{tail}")
|
||||
if first_1_0_0:
|
||||
print(f" 1.0.0 前置 : 《稳定契约承诺》"
|
||||
f"{'✅ 已写实' if stab_ok else '⛔ 未写实:' + ';'.join(stab_problems)}")
|
||||
print(f" 当前开发号 : {dev_number or '(无正式基线 / 取不到 tag ⇒ dev+g<短SHA>)'}"
|
||||
f"{f'(自 v{current} 起 {dev_commits} 个提交)' if (dev_number and dev_commits is not None) else ''}")
|
||||
top_in_commit = top_version_in_commit(want_sha)
|
||||
@@ -437,6 +546,8 @@ def main(argv: list[str]) -> int:
|
||||
"candidate": candidate, "tag": tag, "first_release": first_release,
|
||||
"explicit_version": bool(args.version), "in_target_commit": in_target,
|
||||
"top_in_commit": top_in_commit, "dev_number": dev_number,
|
||||
"prev_version": prev_version, "bump_level": level, "breaking_in_candidate": breaking_n,
|
||||
"first_1_0_0": first_1_0_0, "stability_ok": stab_ok, "stability_problems": stab_problems,
|
||||
"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()),
|
||||
@@ -495,6 +606,9 @@ def self_test() -> int:
|
||||
"remote_tags_checked": True, "required_contexts": ["build / build (push)"],
|
||||
"contexts": {"build / build (push)": "success"}, "contexts_error": "",
|
||||
"release_exists": False, "release_query_error": "", "no_api": False,
|
||||
# 版本走向/破坏性/1.0.0 前置(默认:无上一版本、无破坏性 ⇒ 新判据不参与)
|
||||
"prev_version": None, "bump_level": None, "breaking_in_candidate": 0,
|
||||
"first_1_0_0": False, "stability_ok": True, "stability_problems": [],
|
||||
}
|
||||
|
||||
def rd(name: str, ready_want: bool, needle: str = "", **over) -> None:
|
||||
@@ -526,6 +640,31 @@ def self_test() -> int:
|
||||
True, "", candidate="0.2.0", dev_number="0.1.12", in_target_commit=True,
|
||||
contexts={"build / build (push)": "success"})
|
||||
rd("候选版本没进目标提交 ⇒ 不就绪", False, "还没写进目标提交", in_target_commit=False)
|
||||
# ── 抬档判据:破坏性变化 ⇒ 0.x 抬次、≥1.0.0 抬主 ──
|
||||
rd("破坏性 + 0.x 只抬修订(0.1.0→0.1.3)⇒ 不就绪",
|
||||
False, "必须抬**次版本**", candidate="0.1.3", prev_version="0.1.0", bump_level="patch",
|
||||
breaking_in_candidate=1)
|
||||
rd("破坏性 + 0.x 抬次(0.1.0→0.2.0)⇒ 该条不拦", True,
|
||||
candidate="0.2.0", prev_version="0.1.0", bump_level="minor", breaking_in_candidate=2)
|
||||
rd("破坏性 + ≥1.0.0 只抬次(1.2.3→1.3.0)⇒ 不就绪",
|
||||
False, "必须抬**主版本**", candidate="1.3.0", prev_version="1.2.3", bump_level="minor",
|
||||
breaking_in_candidate=1)
|
||||
rd("破坏性 + ≥1.0.0 抬主(1.2.3→2.0.0)⇒ 该条不拦", True,
|
||||
candidate="2.0.0", prev_version="1.2.3", bump_level="major", breaking_in_candidate=1)
|
||||
rd("无破坏性 + 只抬修订(0.1.0→0.1.3)⇒ 抬档不拦", True,
|
||||
candidate="0.1.3", prev_version="0.1.0", bump_level="patch", breaking_in_candidate=0)
|
||||
rd("候选号没涨(0.2.0→0.2.0)⇒ 不就绪", False, "只增不减",
|
||||
candidate="0.2.0", prev_version="0.2.0", bump_level=None, breaking_in_candidate=0)
|
||||
# ── 1.0.0 首见:必须有写实的《稳定契约承诺》 ──
|
||||
rd("首发 1.0.0 但《稳定契约承诺》没写实 ⇒ 不就绪", False, "稳定契约承诺",
|
||||
candidate="1.0.0", prev_version="0.9.3", bump_level="major", first_1_0_0=True,
|
||||
stability_ok=False, stability_problems=["没覆盖「配置」", "还留着占位没填(待填)"])
|
||||
rd("首发 1.0.0 且《稳定契约承诺》写实 ⇒ 该条不拦", True,
|
||||
candidate="1.0.0", prev_version="0.9.3", bump_level="major", first_1_0_0=True,
|
||||
stability_ok=True, stability_problems=[])
|
||||
rd("已经是 1.x 再抬到 2.0.0 ⇒ 不要求重写承诺段", True,
|
||||
candidate="2.0.0", prev_version="1.4.0", bump_level="major", breaking_in_candidate=1,
|
||||
first_1_0_0=False)
|
||||
rd("候选版本只在历史里(顶部是别的版本)⇒ 不就绪,且信息里给出真实顶部版本",
|
||||
False, "顶部**已发布版本是 `## [0.3.0]`",
|
||||
in_target_commit=False, top_in_commit="0.3.0", candidate="0.2.0")
|
||||
@@ -639,7 +778,7 @@ def self_test() -> int:
|
||||
print(f" · {p}", file=sys.stderr)
|
||||
return 1
|
||||
print("✅ 预检自检通过:写类 git 命令与 POST/PUT/DELETE/PATCH 在预检路径上全部被拒;版本推进正确;"
|
||||
"就绪判定 21 个分支(context 缺项/pending/failure/查询异常、无策略、Release 非 404、"
|
||||
"就绪判定 32 个分支(context 缺项/pending/failure/查询异常、无策略、Release 非 404、"
|
||||
"tag 占用、未核对远端、首发未显式指定、记录未进提交…逐条阻断;证据齐才就绪);"
|
||||
"『顶部版本才算数』与『写进提交才算数』用临时仓库验证;"
|
||||
"`--execute` 明确拒绝且零写操作、零 API 调用")
|
||||
|
||||
Reference in New Issue
Block a user