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: 6 additions & 6 deletions .github/workflows/finalize-prebuilt-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ jobs:
echo "ERROR: tag resolves to $object_sha, expected $EXPECTED_SOURCE_SHA" >&2
exit 1
}
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
release_json="$(scripts/get-release-by-tag.sh "$RELEASE_TAG")"
[ "$(jq -r '.draft' <<<"$release_json")" = true ] || {
echo "ERROR: release $RELEASE_TAG must remain a draft until finalization succeeds" >&2
exit 1
Expand Down Expand Up @@ -257,7 +257,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
release_json="$(scripts/get-release-by-tag.sh "$RELEASE_TAG")"
[ "$(jq -r '.draft' <<<"$release_json")" = true ] || {
echo "ERROR: release ceased to be a draft before mutation" >&2
exit 1
Expand All @@ -280,7 +280,7 @@ jobs:
for asset in release-assets/*; do basename "$asset"; done
printf '%s\n' "$STAGING_ASSET"
} | LC_ALL=C sort > "$expected_draft"
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
release_json="$(scripts/get-release-by-tag.sh "$RELEASE_TAG")"
[ "$(jq -r '.draft' <<<"$release_json")" = true ]
jq -r '.assets[].name' <<<"$release_json" | LC_ALL=C sort > "$actual_draft"
diff -u "$expected_draft" "$actual_draft" || {
Expand All @@ -299,7 +299,7 @@ jobs:
actual="$(mktemp)"
trap 'rm -f "$expected" "$actual"' EXIT
for asset in release-assets/*; do basename "$asset"; done | LC_ALL=C sort > "$expected"
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
release_json="$(scripts/get-release-by-tag.sh "$RELEASE_TAG")"
[ "$(jq -r '.draft' <<<"$release_json")" = true ]
jq -r '.assets[].name' <<<"$release_json" | LC_ALL=C sort > "$actual"
if ! diff -u "$expected" "$actual"; then
Expand All @@ -309,7 +309,7 @@ jobs:
fi
publication_state=""
if ! gh release edit "$RELEASE_TAG" --draft=false; then
if ! publication_state="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"; then
if ! publication_state="$(scripts/get-release-by-tag.sh "$RELEASE_TAG")"; then
echo "ERROR: publication result is ambiguous; no recovery mutation attempted" >&2
exit 1
fi
Expand All @@ -331,7 +331,7 @@ jobs:
if [ -n "$publication_state" ]; then
release_json="$publication_state"
else
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
release_json="$(scripts/get-release-by-tag.sh "$RELEASE_TAG")"
fi
[ "$(jq -r '.draft' <<<"$release_json")" = false ]
jq -r '.assets[].name' <<<"$release_json" | LC_ALL=C sort > "$actual"
Expand Down
23 changes: 23 additions & 0 deletions scripts/get-release-by-tag.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Resolve both published and draft GitHub releases by their exact tag.
set -euo pipefail

if [ "$#" -ne 1 ]; then
echo "Usage: GITHUB_REPOSITORY=owner/repo $0 <tag>" >&2
exit 2
fi

repository="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
tag="$1"

# GitHub's GET /releases/tags/{tag} endpoint excludes draft releases. Listing
# releases is the documented authenticated path that includes drafts; slurp
# keeps pagination correct for repositories with more than 100 releases.
gh api --paginate --slurp "repos/$repository/releases?per_page=100" \
| jq -cer --arg tag "$tag" '
[.[][] | select(.tag_name == $tag)]
| if length == 1 then .[0]
elif length == 0 then error("release tag not found: " + $tag)
else error("duplicate release tag: " + $tag)
end
'
17 changes: 16 additions & 1 deletion tests/test_release_finalizer_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
UPDATER_SIGNATURES = ROOT / "crates/terraphim_update/src/signature.rs"
VALIDATOR_PATH = ROOT / "scripts/validate-release-inputs.py"
VALIDATOR_SOURCE = VALIDATOR_PATH.read_text()
RELEASE_LOOKUP = ROOT / "scripts/get-release-by-tag.sh"

SPEC = importlib.util.spec_from_file_location("release_input_validator", VALIDATOR_PATH)
assert SPEC is not None and SPEC.loader is not None
Expand Down Expand Up @@ -83,7 +84,9 @@ def test_workflow_is_main_only_review_bound_and_fail_closed(self):
workflow.index('gh release edit "$RELEASE_TAG" --draft=false'),
)
failed_publish = workflow.index('if ! gh release edit "$RELEASE_TAG" --draft=false')
state_query = workflow.index('if ! publication_state="$(gh api', failed_publish)
state_query = workflow.index(
'if ! publication_state="$(scripts/get-release-by-tag.sh', failed_publish
)
restore_staging = workflow.index(
'gh release upload "$RELEASE_TAG" "staging/$STAGING_ASSET" --clobber',
failed_publish,
Expand All @@ -92,6 +95,18 @@ def test_workflow_is_main_only_review_bound_and_fail_closed(self):
self.assertIn("publication result is ambiguous; no recovery mutation attempted", workflow)
self.assertIn("publication state is unknown; no recovery mutation attempted", workflow)
self.assertIn("publish command failed after GitHub committed publication", workflow)
self.assertNotIn("/releases/tags/$RELEASE_TAG", workflow)
self.assertGreaterEqual(
workflow.count('scripts/get-release-by-tag.sh "$RELEASE_TAG"'), 6
)

def test_draft_release_lookup_is_paginated_and_exact(self):
lookup = RELEASE_LOOKUP.read_text()
self.assertIn("gh api --paginate --slurp", lookup)
self.assertIn("select(.tag_name == $tag)", lookup)
self.assertIn('length == 1 then .[0]', lookup)
self.assertIn('error("release tag not found: " + $tag)', lookup)
self.assertIn('error("duplicate release tag: " + $tag)', lookup)

def test_archive_signer_uses_the_client_trusted_primary_key(self):
pinned = PINNED_KEY.read_text().strip()
Expand Down
Loading