From 153a408e107fdd6a71bd4ac2e0dc47689dbf7239 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl Date: Mon, 31 Aug 2026 11:15:01 +0000 Subject: [PATCH] feat: decide when merge-door releases resume --- lib/preflight.sh | 81 ++++++++++++++++++++++++++++++++++++++++++ test/preflight.test.sh | 8 ++++- 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100755 lib/preflight.sh diff --git a/lib/preflight.sh b/lib/preflight.sh new file mode 100755 index 0000000..f34b01c --- /dev/null +++ b/lib/preflight.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# lib/preflight.sh — the merge door's resume decision, pure and exhaustively +# tested (issue #273). +# +# A merge-door run creates the tag before the artifact hook and release. A +# failed hook or publish therefore leaves a tag but no release. Re-running the +# same merge commit must resume after that irreversible step; a published +# release or a tag naming another commit must still refuse. +# +# Pure: no repository or forge reads. The workflow establishes four facts: +# +# VER the version being released +# MERGE_SHA the commit this door would tag +# TAG_SHAS object names returned for the direct and peeled tag refs, +# newline-separated; empty means the tag is absent +# RELEASED yes|no — whether a published release for VER exists +# +# Output: resume=yes or resume=no on stdout, notices to stdout, refusals to +# stderr, return 1 on refusal. +# +# The decision table (this IS the spec — issue #273): +# +# | # | RELEASED | TAG_SHAS contains MERGE_SHA | result | +# |---|----------|------------------------------|---------------------------| +# | 1 | yes | either | REFUSE: already released | +# | 2 | no | empty | resume=no: ordinary run | +# | 3 | no | yes | resume=yes + resume NOTICE | +# | 4 | no | non-empty, no | REFUSE: tag is elsewhere | + +release_preflight() { + local tag_sha sha + + if [ -z "${VER:-}" ]; then + printf '%s\n' "VER is empty — the caller failed to establish the release version. Refusing to decide — creating nothing." >&2 + return 1 + fi + if [ -z "${MERGE_SHA:-}" ]; then + printf '%s\n' "MERGE_SHA is empty — the caller failed to establish the merge commit. Refusing to decide — creating nothing." >&2 + return 1 + fi + if [ -z "${RELEASED:-}" ]; then + printf '%s\n' "RELEASED is empty — the caller failed to establish whether release '$VER' exists. Refusing to decide — creating nothing." >&2 + return 1 + fi + case "$RELEASED" in + yes | no) ;; + *) + printf '%s\n' "RELEASED='$RELEASED' — expected yes or no. Refusing to decide — creating nothing." >&2 + return 1 + ;; + esac + + # Row 1 comes first: deleting a tag under a standing release never makes + # that release safe to recreate. + if [ "$RELEASED" = yes ]; then + printf '%s\n' "release '$VER' already exists — this release already happened; refusing to re-release, creating nothing." >&2 + return 1 + fi + + # Row 2: an absent tag is the ordinary first run. + if [ -z "${TAG_SHAS:-}" ]; then + printf '%s\n' 'resume=no' + return 0 + fi + + # Row 3: compare each object name as a whole line. For an annotated tag the + # direct ref names the tag object and the peeled ref names MERGE_SHA. + while IFS= read -r sha; do + if [ "$sha" = "$MERGE_SHA" ]; then + printf '%s\n' "NOTICE: tag '$VER' already stands at this merge commit and no release exists — a previous run of this door tagged and then failed to publish. Resuming: the tag is not recreated; the artifact hook and the publish run." + printf '%s\n' 'resume=yes' + return 0 + fi + done <<<"$TAG_SHAS" + + # Row 4: the first object name is enough to diagnose the conflicting tag; + # MERGE_SHA is printed beside it so the operator sees both sides. + tag_sha="${TAG_SHAS%%$'\n'*}" + printf '%s\n' "tag '$VER' already exists at $tag_sha but this run would tag $MERGE_SHA — a manual tag won the race, or it names a different commit; refusing to re-release, creating nothing. Delete that tag, or re-tag the merge commit." >&2 + return 1 +} diff --git a/test/preflight.test.sh b/test/preflight.test.sh index 6cf2bb4..dc07abe 100755 --- a/test/preflight.test.sh +++ b/test/preflight.test.sh @@ -18,7 +18,13 @@ FOREIGN_SHA=2222222222222222222222222222222222222222 # preflight — run the pure decision # with exactly the four gathered facts in its environment. preflight() { - VER="$1" MERGE_SHA="$2" TAG_SHAS="$3" RELEASED="$4" bash "$PREFLIGHT" + ( + VER="$1" MERGE_SHA="$2" TAG_SHAS="$3" RELEASED="$4" + export VER MERGE_SHA TAG_SHAS RELEASED + # shellcheck source=lib/preflight.sh + . "$PREFLIGHT" + release_preflight + ) } preflight_stdout() {