#!/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 }