R-426: decoys for the release-complete gate
COVERS "release-complete": the working-tree gate runs in a scratch clone whose origin is a scratch bare repo, against the fake Gitea. Convicted: the newest `## vX.Y.Z` with no tag anywhere, a tag parked on an unrelated commit, a tag with no package, and no-tag wins over a registry 500. Inconclusive: a registry 500. Passed: the genuine release, an `## Unreleased` heading above it, a newer version named only in prose or under `###` (the withdrawn sweep decoy, now asserted the right way round), a tag only origin has (the shallow-CI shape), and a LOCAL-only tag BY DESIGN (CI's fresh clone and the published gate's converse probe see it). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -52,6 +52,11 @@ COVERS = {
|
||||
"lexical sorting would drop out of the retention window (0.9.x vs 0.10.x); a tags api answering 500 "
|
||||
"or a non-JSON 200 is INCONCLUSIVE, never a pass - vs a clean registry, a non-semver tag and a version "
|
||||
"older than the retention window (not asserted, BY DESIGN) (R-426)",
|
||||
"release-complete": "the newest `## vX.Y.Z` with no tag anywhere, a tag parked on an unrelated commit, a tag "
|
||||
"with no package; a registry 500 is INCONCLUSIVE - vs the genuine release, an `## Unreleased` "
|
||||
"heading above it, a newer version named only in prose or under `###`, a tag that only "
|
||||
"origin has (the shallow-CI shape); a LOCAL-only tag passes BY DESIGN (CI's fresh clone "
|
||||
"and the published gate's converse probe are what see it) (R-426)",
|
||||
}
|
||||
|
||||
fails = []
|
||||
@@ -211,6 +216,68 @@ def published_cases(base):
|
||||
report("published: INCONCLUSIVE: Gitea unreachable", rc, out, 2, ("INCONCLUSIVE", "URLs tried"))
|
||||
|
||||
|
||||
# ── release-complete ─────────────────────────────────────────────────────────────────────────────
|
||||
def release_cases(base, ws):
|
||||
bare = os.path.join(ws, "origin.git")
|
||||
work = os.path.join(ws, "rc-work")
|
||||
sh(["git", "clone", "-q", "--bare", "--no-tags", "file://" + ROOT, bare], ws)
|
||||
sh(["git", "clone", "-q", "--no-tags", "file://" + bare, work], ws)
|
||||
sh(["git", "config", "user.email", "decoy@gate.invalid"], work)
|
||||
sh(["git", "config", "user.name", "decoy"], work)
|
||||
# the WORKING-TREE gate, so the file under test is the one being edited, not HEAD's
|
||||
shutil.copy(os.path.join(ROOT, "scripts", "check-release-complete.py"),
|
||||
os.path.join(work, "scripts", "check-release-complete.py"))
|
||||
sh(["git", "add", "scripts/check-release-complete.py"], work)
|
||||
sh(["git", "commit", "-q", "--allow-empty", "-m", "the gate under test"], work)
|
||||
base_sha = sh(["git", "rev-parse", "HEAD"], work)
|
||||
ch = os.path.join(work, "CHANGELOG.md")
|
||||
original = io.open(ch, encoding="utf-8").read()
|
||||
V = "9.9.9"
|
||||
|
||||
def case(name, top, expect_rc, must=(), tag=None, origin_tag=False, packaged=True, pkg_status=None):
|
||||
FAKE.reset()
|
||||
if packaged:
|
||||
FAKE.packages = {V}
|
||||
FAKE.pkg_status = pkg_status
|
||||
try:
|
||||
io.open(ch, "w", encoding="utf-8").write(top + original)
|
||||
sh(["git", "commit", "-q", "-am", name], work)
|
||||
if tag == "head":
|
||||
sh(["git", "tag", "-a", "v" + V, "-m", "decoy", "HEAD"], work)
|
||||
elif tag == "unrelated":
|
||||
empty = sh(["git", "mktree"], work) # stdin is "" — the empty tree, written to this repo
|
||||
orphan = sh(["git", "commit-tree", "-m", "unrelated", empty], work)
|
||||
sh(["git", "tag", "-a", "v" + V, "-m", "decoy", orphan], work)
|
||||
if origin_tag:
|
||||
sh(["git", "push", "-q", "origin", "HEAD:refs/tags/v" + V], work)
|
||||
rc, out = run([sys.executable, os.path.join(work, "scripts", "check-release-complete.py")],
|
||||
work, child_env(base))
|
||||
report("release-complete: " + name, rc, out, expect_rc, must)
|
||||
finally:
|
||||
run(["git", "tag", "-d", "v" + V], work)
|
||||
run(["git", "push", "-q", "origin", ":refs/tags/v" + V], work)
|
||||
sh(["git", "reset", "-q", "--hard", base_sha], work)
|
||||
|
||||
HEAD = "## v%s — 2026-10-06\n\n- decoy release\n\n" % V
|
||||
case("GENUINE: tagged at HEAD and published", HEAD, 0,
|
||||
("newest CHANGELOG version: v9.9.9", "is tagged, placed and published"), tag="head")
|
||||
case("GENUINE: an `## Unreleased` heading above the release", "## Unreleased\n\n- wip\n\n" + HEAD, 0,
|
||||
("newest CHANGELOG version: v9.9.9",), tag="head")
|
||||
case("GENUINE: a newer version named only in prose and under ###",
|
||||
"The `## v10.0.0` heading is not written yet.\n### v10.0.0 notes\n\n" + HEAD, 0,
|
||||
("newest CHANGELOG version: v9.9.9",), tag="head")
|
||||
case("GENUINE: the tag only on origin (the shallow-CI shape)", HEAD, 0,
|
||||
("exists on origin",), origin_tag=True)
|
||||
case("BY DESIGN: a LOCAL-only tag passes (CI's fresh clone sees only origin)", HEAD, 0,
|
||||
("an ancestor of HEAD",), tag="head")
|
||||
case("FACT: the newest heading has no tag anywhere", HEAD, 1, ("DOES NOT EXIST",))
|
||||
case("FACT: a tag parked on an unrelated commit", HEAD, 1, ("NOT an ancestor",), tag="unrelated")
|
||||
case("FACT: tagged, never published", HEAD, 1, ("IS NOT PUBLISHED",), tag="head", packaged=False)
|
||||
case("INCONCLUSIVE: the registry answers 500", HEAD, 2, ("INCONCLUSIVE",), tag="head", pkg_status=500)
|
||||
case("FACT beats INCONCLUSIVE: no tag AND the registry answers 500", HEAD, 1, ("DOES NOT EXIST",),
|
||||
pkg_status=500)
|
||||
|
||||
|
||||
def main():
|
||||
srv = Server(("127.0.0.1", 0), Handler)
|
||||
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
||||
@@ -219,6 +286,7 @@ def main():
|
||||
print("agent gate decoys — fake Gitea at %s, scratch %s" % (base, ws))
|
||||
try:
|
||||
published_cases(base)
|
||||
release_cases(base, ws)
|
||||
finally:
|
||||
srv.shutdown()
|
||||
srv.server_close()
|
||||
|
||||
Reference in New Issue
Block a user