Skip to content

Commit 391002f

Browse files
authored
fix(release): resolve draft releases by tag (#20)
1 parent 7b03a10 commit 391002f

3 files changed

Lines changed: 45 additions & 7 deletions

File tree

.github/workflows/finalize-prebuilt-release.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ jobs:
100100
echo "ERROR: tag resolves to $object_sha, expected $EXPECTED_SOURCE_SHA" >&2
101101
exit 1
102102
}
103-
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
103+
release_json="$(scripts/get-release-by-tag.sh "$RELEASE_TAG")"
104104
[ "$(jq -r '.draft' <<<"$release_json")" = true ] || {
105105
echo "ERROR: release $RELEASE_TAG must remain a draft until finalization succeeds" >&2
106106
exit 1
@@ -257,7 +257,7 @@ jobs:
257257
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
258258
run: |
259259
set -euo pipefail
260-
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
260+
release_json="$(scripts/get-release-by-tag.sh "$RELEASE_TAG")"
261261
[ "$(jq -r '.draft' <<<"$release_json")" = true ] || {
262262
echo "ERROR: release ceased to be a draft before mutation" >&2
263263
exit 1
@@ -280,7 +280,7 @@ jobs:
280280
for asset in release-assets/*; do basename "$asset"; done
281281
printf '%s\n' "$STAGING_ASSET"
282282
} | LC_ALL=C sort > "$expected_draft"
283-
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
283+
release_json="$(scripts/get-release-by-tag.sh "$RELEASE_TAG")"
284284
[ "$(jq -r '.draft' <<<"$release_json")" = true ]
285285
jq -r '.assets[].name' <<<"$release_json" | LC_ALL=C sort > "$actual_draft"
286286
diff -u "$expected_draft" "$actual_draft" || {
@@ -299,7 +299,7 @@ jobs:
299299
actual="$(mktemp)"
300300
trap 'rm -f "$expected" "$actual"' EXIT
301301
for asset in release-assets/*; do basename "$asset"; done | LC_ALL=C sort > "$expected"
302-
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
302+
release_json="$(scripts/get-release-by-tag.sh "$RELEASE_TAG")"
303303
[ "$(jq -r '.draft' <<<"$release_json")" = true ]
304304
jq -r '.assets[].name' <<<"$release_json" | LC_ALL=C sort > "$actual"
305305
if ! diff -u "$expected" "$actual"; then
@@ -309,7 +309,7 @@ jobs:
309309
fi
310310
publication_state=""
311311
if ! gh release edit "$RELEASE_TAG" --draft=false; then
312-
if ! publication_state="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"; then
312+
if ! publication_state="$(scripts/get-release-by-tag.sh "$RELEASE_TAG")"; then
313313
echo "ERROR: publication result is ambiguous; no recovery mutation attempted" >&2
314314
exit 1
315315
fi
@@ -331,7 +331,7 @@ jobs:
331331
if [ -n "$publication_state" ]; then
332332
release_json="$publication_state"
333333
else
334-
release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")"
334+
release_json="$(scripts/get-release-by-tag.sh "$RELEASE_TAG")"
335335
fi
336336
[ "$(jq -r '.draft' <<<"$release_json")" = false ]
337337
jq -r '.assets[].name' <<<"$release_json" | LC_ALL=C sort > "$actual"

scripts/get-release-by-tag.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env bash
2+
# Resolve both published and draft GitHub releases by their exact tag.
3+
set -euo pipefail
4+
5+
if [ "$#" -ne 1 ]; then
6+
echo "Usage: GITHUB_REPOSITORY=owner/repo $0 <tag>" >&2
7+
exit 2
8+
fi
9+
10+
repository="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
11+
tag="$1"
12+
13+
# GitHub's GET /releases/tags/{tag} endpoint excludes draft releases. Listing
14+
# releases is the documented authenticated path that includes drafts; slurp
15+
# keeps pagination correct for repositories with more than 100 releases.
16+
gh api --paginate --slurp "repos/$repository/releases?per_page=100" \
17+
| jq -cer --arg tag "$tag" '
18+
[.[][] | select(.tag_name == $tag)]
19+
| if length == 1 then .[0]
20+
elif length == 0 then error("release tag not found: " + $tag)
21+
else error("duplicate release tag: " + $tag)
22+
end
23+
'

tests/test_release_finalizer_contract.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
UPDATER_SIGNATURES = ROOT / "crates/terraphim_update/src/signature.rs"
1818
VALIDATOR_PATH = ROOT / "scripts/validate-release-inputs.py"
1919
VALIDATOR_SOURCE = VALIDATOR_PATH.read_text()
20+
RELEASE_LOOKUP = ROOT / "scripts/get-release-by-tag.sh"
2021

2122
SPEC = importlib.util.spec_from_file_location("release_input_validator", VALIDATOR_PATH)
2223
assert SPEC is not None and SPEC.loader is not None
@@ -83,7 +84,9 @@ def test_workflow_is_main_only_review_bound_and_fail_closed(self):
8384
workflow.index('gh release edit "$RELEASE_TAG" --draft=false'),
8485
)
8586
failed_publish = workflow.index('if ! gh release edit "$RELEASE_TAG" --draft=false')
86-
state_query = workflow.index('if ! publication_state="$(gh api', failed_publish)
87+
state_query = workflow.index(
88+
'if ! publication_state="$(scripts/get-release-by-tag.sh', failed_publish
89+
)
8790
restore_staging = workflow.index(
8891
'gh release upload "$RELEASE_TAG" "staging/$STAGING_ASSET" --clobber',
8992
failed_publish,
@@ -92,6 +95,18 @@ def test_workflow_is_main_only_review_bound_and_fail_closed(self):
9295
self.assertIn("publication result is ambiguous; no recovery mutation attempted", workflow)
9396
self.assertIn("publication state is unknown; no recovery mutation attempted", workflow)
9497
self.assertIn("publish command failed after GitHub committed publication", workflow)
98+
self.assertNotIn("/releases/tags/$RELEASE_TAG", workflow)
99+
self.assertGreaterEqual(
100+
workflow.count('scripts/get-release-by-tag.sh "$RELEASE_TAG"'), 6
101+
)
102+
103+
def test_draft_release_lookup_is_paginated_and_exact(self):
104+
lookup = RELEASE_LOOKUP.read_text()
105+
self.assertIn("gh api --paginate --slurp", lookup)
106+
self.assertIn("select(.tag_name == $tag)", lookup)
107+
self.assertIn('length == 1 then .[0]', lookup)
108+
self.assertIn('error("release tag not found: " + $tag)', lookup)
109+
self.assertIn('error("duplicate release tag: " + $tag)', lookup)
95110

96111
def test_archive_signer_uses_the_client_trusted_primary_key(self):
97112
pinned = PINNED_KEY.read_text().strip()

0 commit comments

Comments
 (0)