← Commit history

release.py: --publish/--stage require --source <commit you pulled> and refuse unless the wiki head is that commit (checked before build, signing, upload and the stamp push). A source pushed between pull and publish shipped stale code in 0.2.52 and the stamp push overwrote that commit's CHANGELOG.

Colby Knox ·2e737a469d ·1d ago ·parent 1ce4e35
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2 files changed +35−7
PUBLISHING.md+1−1
@@ -18,7 +18,7 @@ only in which manifest points at which version.  ```bash python3 scripts/release.py                      # lint + cargo test + build, zip into dist/. Publishes nothing.-python3 scripts/release.py --publish "<one sentence for the release log>"+python3 scripts/release.py --publish "<one sentence for the release log>" --source <the commit you pulled> python3 scripts/release.py --promote <ver>  --attest "<the human's exact words>" --by "<name>" python3 scripts/release.py --rollback <ver> --attest "<the human's exact words>" --by "<name>" ```
scripts/release.py+34−6
@@ -80,6 +80,24 @@ def run(*a, **kw):     return subprocess.run(a, check=True, **kw)  +def wiki_head():+    out = subprocess.run(["adom-wiki", "repo", "log", PAGE, "--limit", "1", "--json"],+                         check=True, capture_output=True, text=True).stdout+    d = json.loads(out)+    return ((d.get("data") or d).get("log") or [{}])[0].get("hash", "")+++def assert_source_is_head(source, when):+    """The working copy is a pull, and a pull records no commit, so the operator names the commit+    they pulled. Another session can push a new source between that pull and this publish; building+    then ships stale code, and the stamp push overwrites their CHANGELOG with ours."""+    head = wiki_head()+    if not source or not head.startswith(source):+        raise SystemExit(f"REFUSED ({when}): the wiki head of {PAGE} is {head[:7]}, but --source says {source or '(none)'}. "+                         f"Pull again (adom-wiki repo pull {PAGE}), read what changed, and rerun with --source {head[:7]}.")+    return head++ def now_iso():     return datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z") @@ -371,15 +389,17 @@ def two_curl_split_check():     return served  -def stage_only(log_line):+def stage_only(log_line, source):     """A build waiting for a live battery: signed and uploaded as a release asset, but NO tier moves     and no skills pkg row. Its manifest is archived so a later publish or pin can point at it."""+    assert_source_is_head(source, "before build")     ver = json.loads((SRC / "bridge.json").read_text())["version"]     lint_versions(ver)     lint_agent_script()     n = lint_verbs()     print(f"verb lint ok: {n} verbs")     build_exe()+    assert_source_is_head(source, "before signing")     sign_pe_files(ver)     zname, zpath, sha = build_zip(ver)     subprocess.run(["adom-wiki", "release", "create", PAGE, ver, "--title", f"altium-bridge {ver}",@@ -390,15 +410,18 @@ def stage_only(log_line):     print(f"STAGED {ver}: signed zip sha256 {sha[:12]} uploaded; no tier moved. Archived scripts/releases/{ver}.json.")  -def ship_insiders(log_line):+def ship_insiders(log_line, source):+    assert_source_is_head(source, "before build")     ver = json.loads((SRC / "bridge.json").read_text())["version"]     lint_versions(ver)     lint_agent_script()     n = lint_verbs()     print(f"verb lint ok: {n} verbs declared and dispatchable")     build_exe()+    assert_source_is_head(source, "before signing")     sign_pe_files(ver)     zname, zpath, sha = build_zip(ver)+    assert_source_is_head(source, "before upload")      print(f"== release {ver} on {PAGE} (bytes live on the product page, both tiers) ==")     subprocess.run(["adom-wiki", "release", "create", PAGE, ver, "--title", f"altium-bridge {ver}",@@ -448,6 +471,7 @@ def ship_insiders(log_line):         "-m", f"altium-bridge {ver} (insiders): {log_line}", cwd=work)      print("== source stamps to the product page (public manifest NOT touched) ==")+    assert_source_is_head(source, "before the stamp push")     run("adom-wiki", "repo", "push", PAGE, "-m", f"v{ver} (insiders): {log_line}",         "--files", "CHANGELOG.md", "src/bridge.json", "src/BRIDGE_VERSION", "package.json", "page.json",         "bridge-rs/src/main.rs", cwd=ROOT)@@ -518,6 +542,10 @@ def promote(ver, words, by, rollback=False):     print(f"PUBLIC tier now serves {ver} ({'rollback' if rollback else 'promote'}), attested by {by}.")  +def source_arg(args):+    return args[args.index("--source") + 1] if "--source" in args and len(args) > args.index("--source") + 1 else ""++ def main():     args = sys.argv[1:]     if "--promote" in args:@@ -536,14 +564,14 @@ def main():         i = args.index("--publish")         log_line = args[i + 1] if len(args) > i + 1 and not args[i + 1].startswith("--") else ""         if not log_line:-            raise SystemExit('usage: release.py --publish "<one changelog sentence for the release log>"')-        ship_insiders(log_line)+            raise SystemExit('usage: release.py --publish "<one changelog sentence for the release log>" --source <commit you pulled>')+        ship_insiders(log_line, source_arg(args))     elif "--stage" in args:         i = args.index("--stage")         log_line = args[i + 1] if len(args) > i + 1 and not args[i + 1].startswith("--") else ""         if not log_line:-            raise SystemExit('usage: release.py --stage "<one line: what this staged build is for>"')-        stage_only(log_line)+            raise SystemExit('usage: release.py --stage "<one line: what this staged build is for>" --source <commit you pulled>')+        stage_only(log_line, source_arg(args))     else:         # lint + build only: packaging needs a signed exe, so the zip is made by --stage/--publish         ver = json.loads((SRC / "bridge.json").read_text())["version"]