Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/native-github-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/<version>.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:
Expand All @@ -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
Expand Down Expand Up @@ -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
62 changes: 58 additions & 4 deletions scripts/ci/check-native-release-tags.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -308,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
Expand Down Expand Up @@ -345,11 +360,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' ' -
Expand Down Expand Up @@ -396,6 +415,41 @@ 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/<version>.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 "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"
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"
Expand Down
71 changes: 60 additions & 11 deletions scripts/ci/native-release-github-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -59,25 +61,72 @@
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/<version>.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" '

Check warning on line 102 in scripts/ci/native-release-github-release.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_server&issues=AaHSUBGMR8whoTq6bIda&open=AaHSUBGMR8whoTq6bIda&pullRequest=1183
.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."
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"
exit 0
fi
Expand Down
Loading