fix: resume stranded merge-door publishes
Some checks failed
CI / action-exercise (pull_request) Successful in 12s
CI / self-guards (pull_request) Successful in 16s
CI / release-exercise (pull_request) Successful in 20s
CI / docs-sync-exercise (pull_request) Successful in 12s
labels / labels (pull_request) Successful in 13s
Refs guard / refs-not-closing (pull_request) Successful in 10s
CI / test (pull_request) Failing after 58s
Some checks failed
CI / action-exercise (pull_request) Successful in 12s
CI / self-guards (pull_request) Successful in 16s
CI / release-exercise (pull_request) Successful in 20s
CI / docs-sync-exercise (pull_request) Successful in 12s
labels / labels (pull_request) Successful in 13s
Refs guard / refs-not-closing (pull_request) Successful in 10s
CI / test (pull_request) Failing after 58s
This commit is contained in:
parent
153a408e10
commit
7bd331a44d
2 changed files with 82 additions and 13 deletions
35
.github/workflows/release.yml
vendored
35
.github/workflows/release.yml
vendored
|
|
@ -205,31 +205,40 @@ jobs:
|
|||
fi
|
||||
changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md"
|
||||
cat "$RUNNER_TEMP/notes.md"
|
||||
- name: nothing may exist yet — no tag, no release (re-runs refuse loudly)
|
||||
- name: preflight — resume this merge, refuse every other collision
|
||||
id: preflight
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VER: ${{ steps.facts.outputs.ver }}
|
||||
# What makes a re-run of a completed ceremony refuse instead of
|
||||
# clobber, and what catches a manual tag racing the merge.
|
||||
MERGE_SHA: ${{ github.sha }}
|
||||
# The pure table in lib/preflight.sh distinguishes a stranded run of
|
||||
# this door from a completed release or a tag at another commit (#273).
|
||||
run: |
|
||||
if git ls-remote --exit-code origin "refs/tags/$VER" >/dev/null 2>&1; then
|
||||
echo "tag '$VER' already exists — this release already happened, or a manual tag won the race; refusing to re-release, creating nothing." >&2
|
||||
exit 1
|
||||
fi
|
||||
tag_read_rc=0
|
||||
tag_refs="$(git ls-remote --exit-code origin "refs/tags/$VER" "refs/tags/$VER^{}")" || tag_read_rc=$?
|
||||
case "$tag_read_rc" in
|
||||
0) tag_shas="$(printf '%s\n' "$tag_refs" | awk 'NF { print $1 }')" ;;
|
||||
2) tag_shas="" ;;
|
||||
*)
|
||||
echo "could not read tag '$VER' from origin (git ls-remote exit $tag_read_rc) — refusing rather than assuming it does not exist." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
# shellcheck source=/dev/null
|
||||
. "$CEREMONY_DIR/lib/forge.sh"
|
||||
forge_select ""
|
||||
if ! exists="$(forge_release_exists "$VER")"; then
|
||||
if ! released="$(forge_release_exists "$VER")"; then
|
||||
echo "could not read whether release '$VER' exists — refusing rather than assuming it does not (#191)." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$exists" = yes ]; then
|
||||
echo "release '$VER' already exists — refusing to re-release, creating nothing." >&2
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck source=/dev/null
|
||||
. "$CEREMONY_DIR/lib/preflight.sh"
|
||||
out="$(TAG_SHAS="$tag_shas" RELEASED="$released" release_preflight)"
|
||||
printf '%s\n' "$out"
|
||||
printf '%s\n' "$out" | grep '^resume=' >> "$GITHUB_OUTPUT"
|
||||
- name: tag the merge commit — same job as the publish, on purpose
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
if: steps.decide.outputs.ceremony == 'yes' && steps.preflight.outputs.resume != 'yes'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VER: ${{ steps.facts.outputs.ver }}
|
||||
|
|
|
|||
|
|
@ -1185,6 +1185,66 @@ release_read 200 '{"id":41,"tag_name":"1.2.3","draft":false}'
|
|||
forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" >/dev/null 2>&1
|
||||
check "a published same-tag release is never deleted" 1 "" grep -q '^DELETE ' "$WRITES"
|
||||
|
||||
# The merge door's preflight step is extracted and executed. Its network
|
||||
# edges are stubbed at the boundary, while the real pure decision library
|
||||
# consumes the gathered facts (#273).
|
||||
MERGE_PREFLIGHT="$TMP/merge-preflight.sh"
|
||||
{
|
||||
printf '%s\n' '#!/usr/bin/env bash' 'set -e'
|
||||
yq -r '.jobs.release-on-merge.steps[] | select(.id == "preflight") | .run' \
|
||||
"$ROOT/.github/workflows/release.yml"
|
||||
} >"$MERGE_PREFLIGHT"
|
||||
chmod +x "$MERGE_PREFLIGHT"
|
||||
|
||||
mkdir -p "$TMP/merge-preflight-lib/lib" "$TMP/merge-preflight-bin"
|
||||
ln -s "$ROOT/lib/preflight.sh" "$TMP/merge-preflight-lib/lib/preflight.sh"
|
||||
# shellcheck disable=SC2016 # expanded when the generated helper runs
|
||||
printf '%s\n' \
|
||||
'forge_select() { :; }' \
|
||||
'forge_release_exists() { case "$RELEASE_EXISTS" in error) return 1 ;; *) echo "$RELEASE_EXISTS" ;; esac; }' \
|
||||
>"$TMP/merge-preflight-lib/lib/forge.sh"
|
||||
# shellcheck disable=SC2016 # expanded when the PATH stub is executed
|
||||
printf '%s\n' \
|
||||
'#!/usr/bin/env bash' \
|
||||
'case "$GIT_LS_REMOTE" in' \
|
||||
' absent) exit 2 ;;' \
|
||||
' error) exit 128 ;;' \
|
||||
' *) printf "%s\n" "$GIT_LS_REMOTE" ;;' \
|
||||
'esac' >"$TMP/merge-preflight-bin/git"
|
||||
chmod +x "$TMP/merge-preflight-bin/git"
|
||||
|
||||
merge_preflight_extracted() { [ "$(wc -l <"$MERGE_PREFLIGHT")" -ge 8 ]; }
|
||||
run_merge_preflight() {
|
||||
: >"$TMP/merge-preflight-output"
|
||||
CEREMONY_DIR="$TMP/merge-preflight-lib" \
|
||||
GITHUB_OUTPUT="$TMP/merge-preflight-output" \
|
||||
PATH="$TMP/merge-preflight-bin:$PATH" \
|
||||
VER=1.2.3 MERGE_SHA=1111111111111111111111111111111111111111 \
|
||||
RELEASE_EXISTS="$1" GIT_LS_REMOTE="$2" "$MERGE_PREFLIGHT"
|
||||
}
|
||||
merge_preflight_output_is() {
|
||||
run_merge_preflight "$1" "$2" >/dev/null && \
|
||||
[ "$(cat "$TMP/merge-preflight-output")" = "$3" ]
|
||||
}
|
||||
|
||||
check "the merge door's preflight is extracted" 0 "" merge_preflight_extracted
|
||||
check "the merge door refuses an unreadable release state" 1 \
|
||||
"refusing rather than assuming" run_merge_preflight error absent
|
||||
check "the merge door refuses an unreadable tag state" 1 \
|
||||
"could not read tag '1.2.3'" run_merge_preflight no error
|
||||
check "ls-remote exit 2 is tag absence and proceeds" 0 "" \
|
||||
merge_preflight_output_is no absent resume=no
|
||||
check "a matching peeled ref records a resume" 0 "" \
|
||||
merge_preflight_output_is no \
|
||||
$'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\trefs/tags/1.2.3\n1111111111111111111111111111111111111111\trefs/tags/1.2.3^{}' \
|
||||
resume=yes
|
||||
tag_step_uses_preflight() {
|
||||
yq -r '.jobs.release-on-merge.steps[] | select(.name | test("tag the merge commit")) | .if' \
|
||||
"$ROOT/.github/workflows/release.yml" | grep -q 'steps.preflight.outputs.resume'
|
||||
}
|
||||
check "the merge-door tag step is conditioned on the preflight output" 0 "" \
|
||||
tag_step_uses_preflight
|
||||
|
||||
# The tag door's published-release guard is extracted and executed, like the
|
||||
# other workflow-shell contracts in this repository. The tag itself is this
|
||||
# door's premise, so only a published release refuses; adding the merge door's
|
||||
|
|
|
|||
Loading…
Reference in a new issue