From de2a17639189d496ec3701503f304d9ea024c968 Mon Sep 17 00:00:00 2001 From: FrameBridge Date: Wed, 28 Oct 2026 14:17:44 -0400 Subject: [PATCH] Guard README-only post-release publication without moving signed tags --- tools/publish_gitea.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tools/publish_gitea.py b/tools/publish_gitea.py index 9e4fdb3..6d1aefd 100755 --- a/tools/publish_gitea.py +++ b/tools/publish_gitea.py @@ -64,6 +64,12 @@ def check_bundle(args, source): if not args.ops_update_from: require("refs/heads/" + args.ops_branch not in refs, "Operations branch already exists; do not overwrite") run(["git", "--git-dir", source, "merge-base", "--is-ancestor", args.commit, "refs/heads/" + args.ops_branch]) + if args.docs_branch: + require(args.ops_update_from and re.fullmatch(r"docs/[a-z0-9][a-z0-9/-]*", args.docs_branch), "Explicit guarded documentation update required") + docs_ref = "refs/heads/" + args.docs_branch + run(["git", "--git-dir", source, "merge-base", "--is-ancestor", args.commit, docs_ref]) + changed = run(["git", "--git-dir", source, "diff", "--name-only", args.commit, docs_ref]).stdout.decode().splitlines() + require(changed == ["README.md"], "Documentation update must change README.md only") return refs def record(path, state): @@ -179,6 +185,8 @@ def publish(args): env = dict(os.environ, GIT_ASKPASS=str(client), GIT_TERMINAL_PROMPT="0", LC_ALL="C") # No force flag. The normal receive-pack checks and Gitea hooks run. refspecs = [] if args.ops_update_from else [args.commit + ":refs/heads/main", "refs/tags/" + args.tag] + if args.docs_branch: + refspecs.append("refs/heads/" + args.docs_branch + ":refs/heads/main") if args.ops_branch: refspecs.append("refs/heads/" + args.ops_branch + ":refs/heads/" + args.ops_branch) run(["git", "--git-dir", source, "-c", "credential.helper=", "push", "--atomic", @@ -186,7 +194,11 @@ def publish(args): state["push_performed"] = True state["phase"] = "PUSH_COMPLETED" after = remote_refs() - require(after.get("refs/heads/main") == args.commit, "Main ref readback mismatch") + expected_main = (run(["git", "--git-dir", source, "rev-parse", "refs/heads/" + args.docs_branch]).stdout.decode().strip() + if args.docs_branch else args.commit) + require(after.get("refs/heads/main") == expected_main, "Main ref readback mismatch") + state["main_commit"] = expected_main + state["documentation_only_main_update"] = bool(args.docs_branch) resolved_tag = after.get("refs/tags/" + args.tag + "^{}", after.get("refs/tags/" + args.tag)) require(resolved_tag == args.commit, "Release tag readback mismatch") if args.ops_branch: @@ -242,6 +254,7 @@ def main(): parser.add_argument("--tag", required=True) parser.add_argument("--ops-branch", help="Publish one new operations branch atomically with the release") parser.add_argument("--ops-update-from", help="Fast-forward only operations from this exact old commit; preserve published main and tag") + parser.add_argument("--docs-branch", help="Include one README-only fast-forward while preserving the signed release tag") parser.add_argument("--journal", type=Path) parser.add_argument("--apply", action="store_true") args = parser.parse_args()