forked from heavy-duty/ceremony
fix: resume stranded merge-door publishes
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
|
fi
|
||||||
changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md"
|
changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md"
|
||||||
cat "$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'
|
if: steps.decide.outputs.ceremony == 'yes'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
VER: ${{ steps.facts.outputs.ver }}
|
VER: ${{ steps.facts.outputs.ver }}
|
||||||
# What makes a re-run of a completed ceremony refuse instead of
|
MERGE_SHA: ${{ github.sha }}
|
||||||
# clobber, and what catches a manual tag racing the merge.
|
# 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: |
|
run: |
|
||||||
if git ls-remote --exit-code origin "refs/tags/$VER" >/dev/null 2>&1; then
|
tag_read_rc=0
|
||||||
echo "tag '$VER' already exists — this release already happened, or a manual tag won the race; refusing to re-release, creating nothing." >&2
|
tag_refs="$(git ls-remote --exit-code origin "refs/tags/$VER" "refs/tags/$VER^{}")" || tag_read_rc=$?
|
||||||
exit 1
|
case "$tag_read_rc" in
|
||||||
fi
|
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
|
# shellcheck source=/dev/null
|
||||||
. "$CEREMONY_DIR/lib/forge.sh"
|
. "$CEREMONY_DIR/lib/forge.sh"
|
||||||
forge_select ""
|
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
|
echo "could not read whether release '$VER' exists — refusing rather than assuming it does not (#191)." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ "$exists" = yes ]; then
|
# shellcheck source=/dev/null
|
||||||
echo "release '$VER' already exists — refusing to re-release, creating nothing." >&2
|
. "$CEREMONY_DIR/lib/preflight.sh"
|
||||||
exit 1
|
out="$(TAG_SHAS="$tag_shas" RELEASED="$released" release_preflight)"
|
||||||
fi
|
printf '%s\n' "$out"
|
||||||
|
printf '%s\n' "$out" | grep '^resume=' >> "$GITHUB_OUTPUT"
|
||||||
- name: tag the merge commit — same job as the publish, on purpose
|
- 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:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
VER: ${{ steps.facts.outputs.ver }}
|
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
|
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"
|
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
|
# 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
|
# 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
|
# door's premise, so only a published release refuses; adding the merge door's
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue