From 1f0fa899f5a0ed1b9bd46498334163bf43edd4fd Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 00:22:41 +0000 Subject: [PATCH 1/2] Publish tagged release notes in native GitHub Releases (#1111) The protected writer published only the identity table, so every native release needed a hand-made body edit, and the writer then refused its own completed release. A final release's body is now the identity table plus docs/releases/.md read from the verified tag commit through the contents API, with relative links resolved against the immutable tag. Missing notes, a mismatched heading or a link leaving the repository refuse before any write. Candidates and historical native- tags keep the identity table alone. The existing-release check compares that complete body. repair_legacy_notes (REPAIR_LEGACY_NOTES=true) replaces only an exact identity-only body from the earlier writer, after every other binding and the Latest check pass. It passes Latest as it already stands and reads the result back. It is how 3.4.0-3.8.0 get their notes. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/native-github-release.yml | 12 +++- scripts/ci/check-native-release-tags.sh | 56 ++++++++++++++++- scripts/ci/native-release-github-release.sh | 69 +++++++++++++++++---- 3 files changed, 121 insertions(+), 16 deletions(-) diff --git a/.github/workflows/native-github-release.yml b/.github/workflows/native-github-release.yml index 38a355abd..76bec56bb 100644 --- a/.github/workflows/native-github-release.yml +++ b/.github/workflows/native-github-release.yml @@ -13,8 +13,10 @@ name: Native GitHub Release # The Release is titled with the plain version. A candidate is a prerelease; # a final release is Latest only when the live latest alias names its index # in both registries. The job holds the rolling-alias lock, so no alias -# moves between that read and the write. An existing Release is left as it -# is. +# moves between that read and the write. A final release's body is the +# identity table plus docs/releases/.md from the tag commit (#1111). +# An existing Release is left as it is; repair_legacy_notes only replaces an +# exact identity-only body from the earlier writer with the tagged notes. on: workflow_dispatch: inputs: @@ -35,6 +37,11 @@ on: description: 'Final releases require the exact qualification manifest used for alias promotion; candidates leave empty' required: false type: string + repair_legacy_notes: + description: 'Replace an existing identity-only body (written before #1111) with the tagged notes; nothing else changes' + required: false + type: boolean + default: false permissions: contents: read @@ -82,4 +89,5 @@ jobs: RELEASE_RUN_ID: ${{ inputs.release_run_id }} RELEASE_RUN_ATTEMPT: ${{ inputs.release_run_attempt }} PROMOTION_EVIDENCE: ${{ inputs.promotion_evidence }} + REPAIR_LEGACY_NOTES: ${{ inputs.repair_legacy_notes }} run: bash scripts/ci/native-release-github-release.sh diff --git a/scripts/ci/check-native-release-tags.sh b/scripts/ci/check-native-release-tags.sh index 0e7d4307c..7ca66802f 100755 --- a/scripts/ci/check-native-release-tags.sh +++ b/scripts/ci/check-native-release-tags.sh @@ -292,11 +292,24 @@ commit=0123456789abcdef0123456789abcdef01234567 cat > "$scratch/gh" <<'FAKE' #!/usr/bin/env bash case "$1 $2" in - "release view") [[ -n "${EXISTING:-}" ]] || exit 1; printf '%s\n' "$EXISTING"; exit 0 ;; - "release create") + "release view") + [[ -n "${EXISTING:-}" ]] || exit 1 + if [[ -s "$GH_NOTES" && "${EDIT_STICKS:-yes}" == yes ]] && grep -q -- '^release edit ' "$GH_LOG"; then + jq -c --rawfile body "$GH_NOTES" '.body = $body' <<<"$EXISTING" + else printf '%s\n' "$EXISTING"; fi + exit 0 ;; + "release create"|"release edit") printf '%s\n' "$*" >> "$GH_LOG" while [[ $# -gt 0 && "$1" != --notes ]]; do shift; done printf '%s' "${2:-}" > "$GH_NOTES"; exit 0 ;; + "api -H") + [[ "$3" == 'Accept: application/vnd.github.raw' && "$4" == "repos/example/server/contents/docs/releases/"*".md?ref=$COMMIT" ]] || exit 1 + [[ "${TAGGED_NOTES-default}" != missing ]] || exit 1 + version=${4#repos/example/server/contents/docs/releases/}; version=${version%%.md\?*} + if [[ "${TAGGED_NOTES-default}" == default ]]; then + printf '# Durable Workflow Server %s (native)\n\nSee the [upgrade](3.0.0.md#upgrade), the\n[chart guide](../../k8s/UPGRADING.md), [this](#changes) and [PR 1](https://github.com/example/server/pull/1).\n' "$version" + else printf '%s\n' "$TAGGED_NOTES"; fi + exit 0 ;; "attestation verify") printf '%s\n' "$*" >> "$GH_ATTEST_LOG" [[ "${ATTEST:-ok}" == ok ]] || exit 1 @@ -345,11 +358,15 @@ chmod +x "$scratch/python3" newer_index=sha256:$(printf '%064d' 0 | tr 0 b) gh_release() { # release tag, latest state [, VAR=value ...] -> the Latest choice local tag=$1 state=$2; shift 2 - : > "$scratch/gh_calls"; : > "$scratch/gh_attest" + : > "$scratch/gh_calls"; : > "$scratch/gh_attest"; : > "$scratch/gh_notes" env -i PATH="$scratch:$PATH" GH="$scratch/gh" GH_LOG="$scratch/gh_calls" GH_ATTEST_LOG="$scratch/gh_attest" GH_NOTES="$scratch/gh_notes" \ DOCKER="$scratch/release-docker" "${images[@]}" GITHUB_REPOSITORY=example/server \ RELEASE_TAG="$tag" RELEASE_RUN_ID=41 RELEASE_RUN_ATTEMPT=1 PROMOTION_EVIDENCE=synthetic-selected-evidence RUN_ID=41 COMMIT="$commit" INDEX="$index" LATEST_STATE="$state" "$@" \ bash scripts/ci/native-release-github-release.sh >/dev/null 2>&1 || { echo FAILED; return; } + if grep -q -- '^release edit ' "$scratch/gh_calls"; then + grep -c -- '^release ' "$scratch/gh_calls" | grep -qx 1 || { echo FAILED; return; } + printf 'edit %s\n' "$(grep -oE -- '--latest=(true|false)$' "$scratch/gh_calls")"; return + fi grep -q -- '^release create ' "$scratch/gh_calls" || { echo none; return; } grep -q -- '--verify-tag --title ' "$scratch/gh_calls" || { echo FAILED; return; } tr '\n' ' ' < "$scratch/gh_calls" | grep -oE -- '--prerelease|--latest(=false)?( |$)' | tr -d ' ' | paste -sd' ' - @@ -396,6 +413,39 @@ want_release "existing release with another title" "$(gh_release 3.1.0 "$ours" L want_release "existing final marked prerelease" "$(gh_release 3.1.0 "$ours" LATEST_TAG=3.1.0 EXISTING="$(existing 3.1.0 3.1.0 false true "$notes_final")")" "$failed" want_release "existing final the alias owns but not Latest" "$(gh_release 3.1.0 "$ours" LATEST_TAG=3.0.2 EXISTING="$(existing 3.1.0 3.1.0 false false "$notes_final")")" "$failed" want_release "existing backport marked Latest" "$(gh_release 3.0.2 "$theirs" LATEST_TAG=3.0.2 EXISTING="$(existing 3.0.2 3.0.2 false false "$notes_backport")")" "$failed" +# A final Release carries docs/releases/.md from the tag commit +# (Server #1111), with relative links resolved against the immutable tag. +base=https://github.com/example/server/blob/3.1.0 +grep -qx '# Durable Workflow Server 3.1.0 (native)' <<<"$notes_final" || fail "a final Release must carry its tagged notes." +[[ "$(head -n1 <<<"$notes_final")" == 'Durable Workflow Server 3.1.0.' ]] || fail "the identity table must come first." +for link in "]($base/docs/releases/3.0.0.md#upgrade)" "]($base/k8s/UPGRADING.md)" "](#changes)" "](https://github.com/example/server/pull/1)"; do + grep -qF -- "$link" <<<"$notes_final" || fail "tagged notes must contain the link $link." +done +! grep -q '# Durable Workflow Server' <<<"$notes_candidate" || fail "a candidate keeps the identity table alone." +gh_release native-3.0.2 "$ours" >/dev/null +! grep -q '# Durable Workflow Server' "$scratch/gh_notes" || fail "a historical native- tag keeps the identity table alone." +want_release "final without tagged notes" "$(gh_release 3.1.0 "$ours" TAGGED_NOTES=missing)" "$failed" +[[ ! -s "$scratch/gh_calls" ]] || fail "Release was created without its tagged notes." +want_release "tagged notes for another version" "$(gh_release 3.1.0 "$ours" TAGGED_NOTES='# Durable Workflow Server 3.0.9 (native)')" "$failed" +want_release "tagged notes linking outside the repository" "$(gh_release 3.1.0 "$ours" TAGGED_NOTES=$'# Durable Workflow Server 3.1.0 (native)\n\n[x](../../../x.md)')" "$failed" +[[ ! -s "$scratch/gh_calls" ]] || fail "Release was created with a link outside the repository." +# REPAIR_LEGACY_NOTES replaces only an exact identity-only body, keeps +# Latest where it stands, and reads the result back. +legacy_final=${notes_final%%$'\n\n# Durable Workflow Server'*} +legacy_backport=${notes_backport%%$'\n\n# Durable Workflow Server'*} +want_release "legacy body, no repair asked" "$(gh_release 3.1.0 "$ours" LATEST_TAG=3.1.0 EXISTING="$(existing 3.1.0 3.1.0 false false "$legacy_final")")" "$failed" +want_release "legacy body repaired, Latest kept" "$(gh_release 3.1.0 "$ours" REPAIR_LEGACY_NOTES=true LATEST_TAG=3.1.0 EXISTING="$(existing 3.1.0 3.1.0 false false "$legacy_final")")" "edit --latest=true" +[[ "$(cat "$scratch/gh_notes")" == "$notes_final" ]] || fail "the repair must publish exactly the tagged notes." +want_release "legacy backport repaired, not Latest" "$(gh_release 3.0.2 "$theirs" REPAIR_LEGACY_NOTES=true LATEST_TAG=3.1.0 EXISTING="$(existing 3.0.2 3.0.2 false false "$legacy_backport")")" "edit --latest=false" +want_release "complete body with repair asked" "$(gh_release 3.1.0 "$ours" REPAIR_LEGACY_NOTES=true LATEST_TAG=3.1.0 EXISTING="$(existing 3.1.0 3.1.0 false false "$notes_final")")" "none" +want_release "repair of other notes" "$(gh_release 3.1.0 "$ours" REPAIR_LEGACY_NOTES=true LATEST_TAG=3.1.0 EXISTING="$(existing 3.1.0 3.1.0 false false "$legacy_final extra")")" "$failed" +want_release "repair with another title" "$(gh_release 3.1.0 "$ours" REPAIR_LEGACY_NOTES=true LATEST_TAG=3.1.0 EXISTING="$(existing 3.1.0 native-3.1.0 false false "$legacy_final")")" "$failed" +want_release "repair of a draft" "$(gh_release 3.1.0 "$ours" REPAIR_LEGACY_NOTES=true LATEST_TAG=3.1.0 EXISTING="$(existing 3.1.0 3.1.0 true false "$legacy_final")")" "$failed" +want_release "repair where Latest is wrong" "$(gh_release 3.1.0 "$ours" REPAIR_LEGACY_NOTES=true LATEST_TAG=3.0.2 EXISTING="$(existing 3.1.0 3.1.0 false false "$legacy_final")")" "$failed" +[[ ! -s "$scratch/gh_notes" ]] || fail "a refused repair must not edit the Release." +want_release "repair that does not read back" "$(gh_release 3.1.0 "$ours" REPAIR_LEGACY_NOTES=true EDIT_STICKS=no LATEST_TAG=3.1.0 EXISTING="$(existing 3.1.0 3.1.0 false false "$legacy_final")")" "$failed" +want_release "repair without tagged notes" "$(gh_release 3.1.0 "$ours" REPAIR_LEGACY_NOTES=true TAGGED_NOTES=missing LATEST_TAG=3.1.0 EXISTING="$(existing 3.1.0 3.1.0 false false "$legacy_final")")" "$failed" +want_release "candidate has nothing to repair" "$(gh_release 3.1.0-rc.1 "$ours" REPAIR_LEGACY_NOTES=true LATEST_TAG=3.0.2 EXISTING="$(existing 3.1.0-rc.1 3.1.0-rc.1 false true "$notes_candidate")")" "none" # The bindings come first: an existing Release does not excuse a failed one. want_release "existing release, unverifiable provenance" "$(gh_release 3.1.0 "$ours" ATTEST=fail LATEST_TAG=3.1.0 EXISTING="$(existing 3.1.0 3.1.0 false false "$notes_final")")" "$failed" want_release "existing release, commit off main" "$(gh_release 3.1.0 "$ours" COMPARE=diverged LATEST_TAG=3.1.0 EXISTING="$(existing 3.1.0 3.1.0 false false "$notes_final")")" "$failed" diff --git a/scripts/ci/native-release-github-release.sh b/scripts/ci/native-release-github-release.sh index 379441c3d..10f421df1 100755 --- a/scripts/ci/native-release-github-release.sh +++ b/scripts/ci/native-release-github-release.sh @@ -17,11 +17,13 @@ # across this step, so no alias moves between the read and the write. A # release candidate is a prerelease and never Latest. An existing Release # that records exactly this verified release is left unchanged, so a rerun -# cannot move Latest either; any other existing Release is refused. Any -# failed read stops the step before it writes. +# cannot move Latest either; any other existing Release is refused, except +# that REPAIR_LEGACY_NOTES=true replaces an exact identity-only body with the +# tagged notes. Any failed read stops the step before it writes. # # Env: RELEASE_TAG, RELEASE_RUN_ID, GITHUB_REPOSITORY, GHCR_IMAGE, -# DOCKERHUB_IMAGE. GH and DOCKER override the gh and docker commands. +# DOCKERHUB_IMAGE, optional REPAIR_LEGACY_NOTES. GH and DOCKER override the +# gh and docker commands. set -euo pipefail source scripts/ci/native-registry-absence.sh fail() { printf 'GitHub Release failed: %s\n' "$1" >&2; exit 1; } @@ -59,25 +61,70 @@ if [[ "$version" != *-* ]]; then done fi -notes=$(printf 'Durable Workflow Server %s.\n\n| | |\n| --- | --- |\n| Git tag | `%s` → `%s` |\n| Image index | `%s` in `%s` and `%s` |\n| Release run | https://github.com/%s/actions/runs/%s |\n' \ +identity=$(printf 'Durable Workflow Server %s.\n\n| | |\n| --- | --- |\n| Git tag | `%s` → `%s` |\n| Image index | `%s` in `%s` and `%s` |\n| Release run | https://github.com/%s/actions/runs/%s |\n' \ "$version" "$tag" "$commit" "$index" "$DOCKERHUB_IMAGE" "$GHCR_IMAGE" "$repo" "$run_id") +# A final release publishes its tagged notes (Server #1111): the identity +# table, then docs/releases/.md read from the verified tag commit, +# never from later main. Relative links resolve against the immutable tag so +# they work on the Release page. Candidates and the frozen historical +# native-X.Y.Z tags keep the identity table alone. Missing or mismatched +# notes refuse before any write. +notes=$identity +if [[ "$version" != *-* && "$tag" != native-* ]]; then + tagged=$("$gh_bin" api -H 'Accept: application/vnd.github.raw' \ + "repos/$repo/contents/docs/releases/$version.md?ref=$commit") \ + || fail "cannot read docs/releases/$version.md at $commit." + [[ "$(head -n1 <<<"$tagged")" == "# Durable Workflow Server $version (native)" ]] \ + || fail "docs/releases/$version.md at $commit does not start with its release heading." + # shellcheck disable=SC2016 # Perl, not shell, expands these. + tagged=$(BASE="https://github.com/$repo/blob/$tag" perl -pe ' + sub resolve { my @out; for (split m{/}, "docs/releases/$_[0]") { + if ($_ eq "..") { return undef unless @out; pop @out } elsif ($_ ne "." && $_ ne "") { push @out, $_ } } + return join "/", @out } + s{\]\((?![A-Za-z][A-Za-z0-9+.-]*:|#|/)([^)\s]+)\)}{ my $p = resolve($1); + defined $p ? "]($ENV{BASE}/$p)" : die "link $1 leaves the repository\n" }ge' <<<"$tagged") \ + || fail "docs/releases/$version.md at $commit has a link outside the repository." + notes=$(printf '%s\n\n%s\n' "$identity" "$tagged") +fi # An existing Release is accepted only when it is exactly the Release this # step would publish: published (not a draft), tag, plain title, candidate # state and these exact notes, and Latest exactly when the live alias says -# so, read under the same lock. It is never edited, so a rerun cannot move -# Latest; any other Release stops the step. +# so, read under the same lock. Any other Release stops the step. +# +# REPAIR_LEGACY_NOTES=true is the one edit: when everything else matches and +# the body is exactly the identity table an earlier writer published without +# notes, only the body is replaced with these notes and read back. Latest is +# passed as it already stands, so the repair cannot move it. if existing=$("$gh_bin" release view "$tag" --repo "$repo" --json tagName,name,isDraft,isPrerelease,body 2>/dev/null); then candidate=false [[ "$version" == *-* ]] && candidate=true - jq -e --arg tag "$tag" --arg title "$version" --argjson candidate "$candidate" --arg notes "$notes" ' - .tagName == $tag and .name == $title and .isDraft == false and .isPrerelease == $candidate - and ((.body // "") | sub("\\s+$"; "")) == ($notes | sub("\\s+$"; ""))' \ - <<<"$existing" >/dev/null \ - || fail "Release $tag exists but is not exactly this verified release (draft, tag, title, candidate state or notes)." + matches() { # expected body + jq -e --arg tag "$tag" --arg title "$version" --argjson candidate "$candidate" --arg notes "$1" ' + .tagName == $tag and .name == $title and .isDraft == false and .isPrerelease == $candidate + and ((.body // "") | gsub("\r"; "") | sub("\\s+$"; "")) == ($notes | sub("\\s+$"; ""))' \ + <<<"$existing" >/dev/null + } + repair=false + if ! matches "$notes"; then + if [[ "${REPAIR_LEGACY_NOTES:-}" == true && "$notes" != "$identity" ]] && matches "$identity"; then + repair=true + else + fail "Release $tag exists but is not exactly this verified release (draft, tag, title, candidate state or notes)." + fi + fi current=$("$gh_bin" api "repos/$repo/releases/latest" --jq .tag_name) || fail "cannot read the Latest Release." if [[ "$latest" == true && "$current" != "$tag" ]] || [[ "$latest" != true && "$current" == "$tag" ]]; then fail "Release $tag exists, but Latest is $current where the latest alias calls for $([[ "$latest" == true ]] && echo "$tag" || echo "another release")." fi + if [[ "$repair" == true ]]; then + "$gh_bin" release edit "$tag" --repo "$repo" --notes "$notes" "--latest=$latest" >/dev/null \ + || fail "cannot replace the legacy notes of Release $tag." + existing=$("$gh_bin" release view "$tag" --repo "$repo" --json tagName,name,isDraft,isPrerelease,body) \ + || fail "cannot read Release $tag back." + matches "$notes" || fail "Release $tag did not read back with its tagged notes." + printf 'Release %s now carries its tagged notes.\n' "$tag" + exit 0 + fi printf 'Release %s is exactly this verified release; left unchanged.\n' "$tag" exit 0 fi From fc14c8fce00de30c95ec2dc2cc0837c4f94ffc25 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 00:26:19 +0000 Subject: [PATCH 2/2] Read Latest back after a legacy-notes repair (#1111) Repairing an older release (3.4.0-3.7.0) must not move Latest off 3.8.0. After the body readback, the writer reads the Latest Release again and refuses if it changed. Co-Authored-By: Claude Opus 5.5 --- scripts/ci/check-native-release-tags.sh | 6 +++++- scripts/ci/native-release-github-release.sh | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/ci/check-native-release-tags.sh b/scripts/ci/check-native-release-tags.sh index 7ca66802f..600f018ff 100755 --- a/scripts/ci/check-native-release-tags.sh +++ b/scripts/ci/check-native-release-tags.sh @@ -321,7 +321,9 @@ case "$path" in repos/example/server/git/ref/tags/*) printf '%s\n' "${TAG_OBJECT:-commit $COMMIT}" ;; repos/example/server/git/tags/*) printf 'commit %s\n' "$COMMIT" ;; repos/example/server/compare/*) printf '%s\n' "${COMPARE:-ahead}" ;; - repos/example/server/releases/latest) printf '%s\n' "${LATEST_TAG:-}" ;; + repos/example/server/releases/latest) + if [[ -n "${LATEST_AFTER_EDIT:-}" ]] && grep -q -- '^release edit ' "$GH_LOG"; then printf '%s\n' "$LATEST_AFTER_EDIT" + else printf '%s\n' "${LATEST_TAG:-}"; fi ;; repos/example/server/actions/runs/*) printf '%s\n' "${RUN_LINE:-.github/workflows/native-release.yml workflow_dispatch completed success $RELEASE_TAG $COMMIT ${RUN_ATTEMPT:-1}}" ;; *) exit 1 ;; esac @@ -437,6 +439,8 @@ want_release "legacy body, no repair asked" "$(gh_release 3.1.0 "$ours" LATEST_T want_release "legacy body repaired, Latest kept" "$(gh_release 3.1.0 "$ours" REPAIR_LEGACY_NOTES=true LATEST_TAG=3.1.0 EXISTING="$(existing 3.1.0 3.1.0 false false "$legacy_final")")" "edit --latest=true" [[ "$(cat "$scratch/gh_notes")" == "$notes_final" ]] || fail "the repair must publish exactly the tagged notes." want_release "legacy backport repaired, not Latest" "$(gh_release 3.0.2 "$theirs" REPAIR_LEGACY_NOTES=true LATEST_TAG=3.1.0 EXISTING="$(existing 3.0.2 3.0.2 false false "$legacy_backport")")" "edit --latest=false" +want_release "older release repaired while a newer one stays Latest" "$(gh_release 3.0.2 "$theirs" REPAIR_LEGACY_NOTES=true LATEST_TAG=3.1.0 LATEST_AFTER_EDIT=3.1.0 EXISTING="$(existing 3.0.2 3.0.2 false false "$legacy_backport")")" "edit --latest=false" +want_release "repair that moves Latest" "$(gh_release 3.0.2 "$theirs" REPAIR_LEGACY_NOTES=true LATEST_TAG=3.1.0 LATEST_AFTER_EDIT=3.0.2 EXISTING="$(existing 3.0.2 3.0.2 false false "$legacy_backport")")" "$failed" want_release "complete body with repair asked" "$(gh_release 3.1.0 "$ours" REPAIR_LEGACY_NOTES=true LATEST_TAG=3.1.0 EXISTING="$(existing 3.1.0 3.1.0 false false "$notes_final")")" "none" want_release "repair of other notes" "$(gh_release 3.1.0 "$ours" REPAIR_LEGACY_NOTES=true LATEST_TAG=3.1.0 EXISTING="$(existing 3.1.0 3.1.0 false false "$legacy_final extra")")" "$failed" want_release "repair with another title" "$(gh_release 3.1.0 "$ours" REPAIR_LEGACY_NOTES=true LATEST_TAG=3.1.0 EXISTING="$(existing 3.1.0 native-3.1.0 false false "$legacy_final")")" "$failed" diff --git a/scripts/ci/native-release-github-release.sh b/scripts/ci/native-release-github-release.sh index 10f421df1..27ec5fec9 100755 --- a/scripts/ci/native-release-github-release.sh +++ b/scripts/ci/native-release-github-release.sh @@ -122,7 +122,9 @@ if existing=$("$gh_bin" release view "$tag" --repo "$repo" --json tagName,name,i existing=$("$gh_bin" release view "$tag" --repo "$repo" --json tagName,name,isDraft,isPrerelease,body) \ || fail "cannot read Release $tag back." matches "$notes" || fail "Release $tag did not read back with its tagged notes." - printf 'Release %s now carries its tagged notes.\n' "$tag" + after=$("$gh_bin" api "repos/$repo/releases/latest" --jq .tag_name) || fail "cannot read the Latest Release back." + [[ "$after" == "$current" ]] || fail "repairing $tag moved Latest from $current to $after." + printf 'Release %s now carries its tagged notes; Latest is still %s.\n' "$tag" "$current" exit 0 fi printf 'Release %s is exactly this verified release; left unchanged.\n' "$tag"