Skip to content

Commit 422381c

Browse files
authored
chore: Supporting immutable releases (#5905)
* chore: Supporting immutable releases with updated workflow * fix: Refactoring for dedicated bash scripts * docs: Fixing docs * fix: Addressing review comment * fix: Addressing shellcheck findings * fix: Pulling in changes from working implementation * chore: Adding some testing from external workflow run
1 parent 8c473ae commit 422381c

16 files changed

Lines changed: 742 additions & 174 deletions

.github/scripts/release/check-release-exists.sh

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,44 +10,50 @@ set -euo pipefail
1010
# GITHUB_OUTPUT: Path to GitHub output file
1111

1212
function main {
13-
# Validate required environment variables
14-
: "${VERSION:?ERROR: VERSION is a required environment variable}"
15-
: "${GH_TOKEN:?ERROR: GH_TOKEN is a required environment variable}"
16-
: "${GITHUB_OUTPUT:?ERROR: GITHUB_OUTPUT is a required environment variable}"
17-
18-
printf 'Checking if release exists for tag: %s\n' "$VERSION"
19-
20-
# Check if release exists using gh CLI (only care about exit code)
21-
if ! gh release view "$VERSION" > /dev/null 2>&1; then
22-
printf 'exists=false\n' >> "$GITHUB_OUTPUT"
23-
printf 'Release not found for tag %s\n' "$VERSION"
24-
exit 1
25-
fi
26-
27-
# Get release details
28-
local release_json
29-
release_json=$(gh release view "$VERSION" --json 'id,uploadUrl,isDraft')
30-
31-
local release_id
32-
local upload_url
33-
local is_draft
34-
35-
release_id=$(jq -r '.id' <<< "$release_json")
36-
upload_url=$(jq -r '.uploadUrl' <<< "$release_json")
37-
is_draft=$(jq -r '.isDraft' <<< "$release_json")
38-
39-
# Write to GitHub output
40-
printf 'exists=true\n' >> "$GITHUB_OUTPUT"
41-
printf 'release_id=%s\n' "$release_id" >> "$GITHUB_OUTPUT"
42-
printf 'upload_url=%s\n' "$upload_url" >> "$GITHUB_OUTPUT"
43-
printf 'is_draft=%s\n' "$is_draft" >> "$GITHUB_OUTPUT"
44-
45-
echo "Found existing release:"
46-
printf ' Release ID: %s\n' "$release_id"
47-
printf ' Draft: %s\n' "$is_draft"
48-
printf ' Upload URL: %s\n' "${upload_url%\{*}"
49-
50-
return 0
13+
# Validate required environment variables
14+
: "${VERSION:?ERROR: VERSION is a required environment variable}"
15+
: "${GH_TOKEN:?ERROR: GH_TOKEN is a required environment variable}"
16+
: "${GITHUB_OUTPUT:?ERROR: GITHUB_OUTPUT is a required environment variable}"
17+
18+
printf 'Checking if release exists for tag: %s\n' "$VERSION"
19+
20+
# Check if release exists using gh CLI (only care about exit code)
21+
if ! gh release view "$VERSION" >/dev/null 2>&1; then
22+
{
23+
printf 'exists=false\n'
24+
printf 'is_draft=false\n'
25+
printf 'release_id=\n'
26+
} >>"$GITHUB_OUTPUT"
27+
printf 'Release not found for tag %s\n' "$VERSION"
28+
return 0
29+
fi
30+
31+
# Get release details
32+
local release_json
33+
release_json=$(gh release view "$VERSION" --json 'id,uploadUrl,isDraft')
34+
35+
local release_id
36+
local upload_url
37+
local is_draft
38+
39+
release_id=$(jq -r '.id' <<<"$release_json")
40+
upload_url=$(jq -r '.uploadUrl' <<<"$release_json")
41+
is_draft=$(jq -r '.isDraft' <<<"$release_json")
42+
43+
# Write to GitHub output
44+
{
45+
printf 'exists=true\n'
46+
printf 'release_id=%s\n' "$release_id"
47+
printf 'upload_url=%s\n' "$upload_url"
48+
printf 'is_draft=%s\n' "$is_draft"
49+
} >>"$GITHUB_OUTPUT"
50+
51+
echo "Found existing release:"
52+
printf ' Release ID: %s\n' "$release_id"
53+
printf ' Draft: %s\n' "$is_draft"
54+
printf ' Upload URL: %s\n' "${upload_url%\{*}"
55+
56+
return 0
5157
}
5258

5359
main "$@"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
# Script to enforce that a git ref is a full 40-character commit SHA.
6+
# This prevents branch names from being used as release targets, which
7+
# could allow new commits to be pulled into the release between draft
8+
# creation and publishing.
9+
#
10+
# Usage: enforce-commit-sha.sh <ref>
11+
# Or via environment variable:
12+
# REF=abc123... enforce-commit-sha.sh
13+
14+
function main {
15+
local ref="${1:-${REF:-}}"
16+
17+
if [[ -z "$ref" ]]; then
18+
echo "ERROR: No ref provided. Pass as argument or set REF env var." >&2
19+
exit 1
20+
fi
21+
22+
if [[ ! "$ref" =~ ^[0-9a-f]{40}$ ]]; then
23+
echo "ERROR: target_commitish must be a full 40-character commit SHA, got: '$ref'" >&2
24+
echo "This prevents new commits from being pulled into the release between draft creation and publishing." >&2
25+
echo "When creating the draft release, select a specific commit instead of a branch name." >&2
26+
exit 1
27+
fi
28+
29+
printf 'Valid commit SHA: %s\n' "$ref"
30+
31+
return 0
32+
}
33+
34+
main "$@"

.github/scripts/release/get-version.sh

Lines changed: 0 additions & 71 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
# Script to import a base64-encoded GPG private key and export the
6+
# public key for release artifact verification.
7+
#
8+
# Usage: import-gpg-key.sh <output_dir>
9+
# Environment variables:
10+
# SIGNING_GPG_PRIVATE_KEY: Base64-encoded GPG private key
11+
# GITHUB_ENV: Path to GitHub environment file
12+
13+
function main {
14+
local output_dir="${1:?ERROR: output directory is required as first argument}"
15+
16+
: "${SIGNING_GPG_PRIVATE_KEY:?ERROR: SIGNING_GPG_PRIVATE_KEY is a required environment variable}"
17+
: "${GITHUB_ENV:?ERROR: GITHUB_ENV is a required environment variable}"
18+
19+
base64 --decode <<<"${SIGNING_GPG_PRIVATE_KEY}" | gpg --batch --import
20+
21+
local gpg_fingerprint
22+
gpg_fingerprint=$(gpg --list-secret-keys --keyid-format LONG | awk '/^sec/{sub(/.*\//, "", $2); print $2; exit}')
23+
24+
printf 'GPG_FINGERPRINT=%s\n' "${gpg_fingerprint}" >>"${GITHUB_ENV}"
25+
gpg --armor --export "${gpg_fingerprint}" >"${output_dir}/terragrunt-signing-key.asc"
26+
27+
printf 'Imported GPG key: %s\n' "${gpg_fingerprint}"
28+
29+
return 0
30+
}
31+
32+
main "$@"
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
# Script to resolve the release version and target ref from a
6+
# workflow_dispatch input by looking up the existing draft release.
7+
#
8+
# Usage: resolve-version-ref.sh
9+
# Environment variables:
10+
# INPUT_VERSION: Version provided via workflow_dispatch (required)
11+
# GH_TOKEN: GitHub token for authentication (required)
12+
# GITHUB_OUTPUT: Path to GitHub output file
13+
14+
function main {
15+
: "${INPUT_VERSION:?ERROR: INPUT_VERSION is a required environment variable}"
16+
: "${GH_TOKEN:?ERROR: GH_TOKEN is a required environment variable}"
17+
: "${GITHUB_OUTPUT:?ERROR: GITHUB_OUTPUT is a required environment variable}"
18+
19+
local version="$INPUT_VERSION"
20+
21+
# Look up the release to get target_commitish
22+
local release_json
23+
if ! release_json=$(gh release view "$version" --json 'isDraft,targetCommitish' 2>/dev/null); then
24+
echo "ERROR: No release found for version $version" >&2
25+
exit 1
26+
fi
27+
28+
local is_draft
29+
is_draft=$(jq -r '.isDraft' <<<"$release_json")
30+
if [[ "$is_draft" != "true" ]]; then
31+
echo "ERROR: Release $version is already published. Cannot rebuild a published release." >&2
32+
exit 1
33+
fi
34+
35+
local ref
36+
ref=$(jq -r '.targetCommitish' <<<"$release_json")
37+
38+
{
39+
printf 'version=%s\n' "$version"
40+
printf 'ref=%s\n' "$ref"
41+
} >>"$GITHUB_OUTPUT"
42+
echo "Resolved version: $version, ref: $ref"
43+
44+
return 0
45+
}
46+
47+
main "$@"
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env bats
2+
3+
setup() {
4+
SCRIPT="${BATS_TEST_DIRNAME}/../check-release-exists.sh"
5+
STUB_DIR="$(mktemp -d)"
6+
cp "${BATS_TEST_DIRNAME}/helpers/gh-stub.sh" "${STUB_DIR}/gh"
7+
chmod +x "${STUB_DIR}/gh"
8+
export PATH="${STUB_DIR}:${PATH}"
9+
10+
GITHUB_OUTPUT="$(mktemp)"
11+
export GITHUB_OUTPUT
12+
export GH_TOKEN=fake
13+
export VERSION=v1.0.0
14+
}
15+
16+
teardown() {
17+
rm -rf "$STUB_DIR"
18+
rm -f "$GITHUB_OUTPUT"
19+
}
20+
21+
@test "writes exists=false and exits 0 when no release found" {
22+
export GH_STUB_RESPONSE=""
23+
export GH_STUB_EXIT=1
24+
25+
run "$SCRIPT"
26+
[ "$status" -eq 0 ]
27+
grep -q "^exists=false$" "$GITHUB_OUTPUT"
28+
grep -q "^is_draft=false$" "$GITHUB_OUTPUT"
29+
grep -q "^release_id=$" "$GITHUB_OUTPUT"
30+
}
31+
32+
@test "reports draft release" {
33+
export GH_STUB_RESPONSE='{"id":123,"uploadUrl":"https://uploads/repos/o/r/releases/123/assets{?name,label}","isDraft":true}'
34+
35+
run "$SCRIPT"
36+
[ "$status" -eq 0 ]
37+
grep -q "^exists=true$" "$GITHUB_OUTPUT"
38+
grep -q "^release_id=123$" "$GITHUB_OUTPUT"
39+
grep -q "^is_draft=true$" "$GITHUB_OUTPUT"
40+
}
41+
42+
@test "reports non-draft (published) release" {
43+
export GH_STUB_RESPONSE='{"id":456,"uploadUrl":"https://uploads/repos/o/r/releases/456/assets{?name,label}","isDraft":false}'
44+
45+
run "$SCRIPT"
46+
[ "$status" -eq 0 ]
47+
grep -q "^exists=true$" "$GITHUB_OUTPUT"
48+
grep -q "^release_id=456$" "$GITHUB_OUTPUT"
49+
grep -q "^is_draft=false$" "$GITHUB_OUTPUT"
50+
}
51+
52+
@test "fails when VERSION missing" {
53+
unset VERSION
54+
run "$SCRIPT"
55+
[ "$status" -ne 0 ]
56+
[[ "$output" == *"VERSION"* ]]
57+
}
58+
59+
@test "fails when GH_TOKEN missing" {
60+
unset GH_TOKEN
61+
run "$SCRIPT"
62+
[ "$status" -ne 0 ]
63+
[[ "$output" == *"GH_TOKEN"* ]]
64+
}
65+
66+
@test "fails when GITHUB_OUTPUT missing" {
67+
unset GITHUB_OUTPUT
68+
run "$SCRIPT"
69+
[ "$status" -ne 0 ]
70+
[[ "$output" == *"GITHUB_OUTPUT"* ]]
71+
}

0 commit comments

Comments
 (0)