From 3a7f4283c3693bda6228064421a4ea74b3c5001b Mon Sep 17 00:00:00 2001 From: FrameBridge Date: Wed, 28 Oct 2026 14:11:59 -0400 Subject: [PATCH] Guard normal operations-only fast-forward publication --- tools/publish_gitea.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tools/publish_gitea.py b/tools/publish_gitea.py index 6f9ab9a..9e4fdb3 100755 --- a/tools/publish_gitea.py +++ b/tools/publish_gitea.py @@ -50,11 +50,19 @@ def check_bundle(args, source): resolved = run(["git", "--git-dir", source, "rev-parse", args.tag + "^{}"]).stdout.decode().strip() require(resolved == args.commit, "Release tag does not point to reviewed commit") refs = remote_refs() - require(refs.get("refs/heads/main") == args.old_commit, "Canonical main changed; review before publishing") - require("refs/tags/" + args.tag not in refs, "Release tag already exists; never overwrite") + if args.ops_update_from: + require(args.ops_branch and re.fullmatch(r"[a-f0-9]{40}", args.ops_update_from), "Explicit operations update guard required") + require(refs.get("refs/heads/main") == args.commit, "Published release main changed") + require(refs.get("refs/tags/" + args.tag + "^{}") == args.commit, "Published signed tag changed") + require(refs.get("refs/heads/" + args.ops_branch) == args.ops_update_from, "Operations branch changed") + run(["git", "--git-dir", source, "merge-base", "--is-ancestor", args.ops_update_from, "refs/heads/" + args.ops_branch]) + else: + require(refs.get("refs/heads/main") == args.old_commit, "Canonical main changed; review before publishing") + require("refs/tags/" + args.tag not in refs, "Release tag already exists; never overwrite") if args.ops_branch: require(re.fullmatch(r"ops/[a-z0-9][a-z0-9/-]*", args.ops_branch), "Invalid operations branch") - require("refs/heads/" + args.ops_branch not in refs, "Operations branch already exists; do not overwrite") + 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]) return refs @@ -132,7 +140,8 @@ def publish(args): before = check_bundle(args, source) state = {"schema": 1, "repository": "gerardo/framebridge", "old_commit": args.old_commit, "commit": args.commit, "tag": args.tag, "bundle_sha256": args.bundle_sha256, - "phase": "CHECKED", "push_performed": False, "token_revoked": False} + "phase": "CHECKED", "push_performed": False, "token_revoked": False, + "ops_update_from": args.ops_update_from} if not args.apply: print(json.dumps(state, sort_keys=True)) return @@ -169,7 +178,7 @@ def publish(args): server, stop, thread, client = askpass_server(directory, token) 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 = [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.ops_branch: refspecs.append("refs/heads/" + args.ops_branch + ":refs/heads/" + args.ops_branch) run(["git", "--git-dir", source, "-c", "credential.helper=", "push", "--atomic", @@ -232,6 +241,7 @@ def main(): parser.add_argument("--old-commit", 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-update-from", help="Fast-forward only operations from this exact old commit; preserve published main and tag") parser.add_argument("--journal", type=Path) parser.add_argument("--apply", action="store_true") args = parser.parse_args()