Guard README-only post-release publication without moving signed tags

This commit is contained in:
FrameBridge committed 2026-10-28 14:17:44 -04:00
1 parent 3a7f4283c3
commit de2a176391
1 file changed
+14 -1
+14 -1
View File
@@ -64,6 +64,12 @@ def check_bundle(args, source):
if not args.ops_update_from: if not args.ops_update_from:
require("refs/heads/" + args.ops_branch not in refs, "Operations branch already exists; do not overwrite") 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]) 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 return refs
def record(path, state): 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") 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. # 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] 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: if args.ops_branch:
refspecs.append("refs/heads/" + args.ops_branch + ":refs/heads/" + 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", run(["git", "--git-dir", source, "-c", "credential.helper=", "push", "--atomic",
@@ -186,7 +194,11 @@ def publish(args):
state["push_performed"] = True state["push_performed"] = True
state["phase"] = "PUSH_COMPLETED" state["phase"] = "PUSH_COMPLETED"
after = remote_refs() 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)) resolved_tag = after.get("refs/tags/" + args.tag + "^{}", after.get("refs/tags/" + args.tag))
require(resolved_tag == args.commit, "Release tag readback mismatch") require(resolved_tag == args.commit, "Release tag readback mismatch")
if args.ops_branch: if args.ops_branch:
@@ -242,6 +254,7 @@ def main():
parser.add_argument("--tag", required=True) 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-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("--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("--journal", type=Path)
parser.add_argument("--apply", action="store_true") parser.add_argument("--apply", action="store_true")
args = parser.parse_args() args = parser.parse_args()