Skip to content
Closed
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
652 changes: 652 additions & 0 deletions .github/scripts/release/publish-github-release.sh

Large diffs are not rendered by default.

95 changes: 95 additions & 0 deletions .github/scripts/release/resolve-release-metadata.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env bash

set -euo pipefail

usage() {
echo "Usage: $0 BASE_SHA HEAD_SHA GITHUB_OUTPUT" >&2
}

fail() {
echo "resolve-release-metadata: $*" >&2
exit 1
}

if [[ $# -ne 3 ]]; then
usage
exit 2
fi

base_ref=$1
head_ref=$2
github_output=$3

git rev-parse --is-inside-work-tree >/dev/null 2>&1 ||
fail "The current directory is not a Git worktree."

base_sha=$(git rev-parse --verify "${base_ref}^{commit}" 2>/dev/null) ||
fail "BASE_SHA does not identify a commit: ${base_ref}."
head_sha=$(git rev-parse --verify "${head_ref}^{commit}" 2>/dev/null) ||
fail "HEAD_SHA does not identify a commit: ${head_ref}."

git merge-base --is-ancestor "$base_sha" "$head_sha" ||
fail "BASE_SHA must be an ancestor of HEAD_SHA."

diff_fields=()
while IFS= read -r -d '' field; do
diff_fields+=("$field")
done < <(
git diff \
--name-status \
--no-renames \
-z \
"$base_sha" \
"$head_sha" \
-- releases/
)

if [[ ${#diff_fields[@]} -ne 2 ]]; then
fail \
"The release range must contain exactly one release-file change; found $(( ${#diff_fields[@]} / 2 ))."
fi

status=${diff_fields[0]}
notes_file=${diff_fields[1]}

[[ "$status" == "A" ]] ||
fail "The only release-file change must add a new file; found status ${status} for ${notes_file}."

if [[ ! "$notes_file" =~ ^releases/(v[0-9]+\.[0-9]+\.[0-9]+(-RC[0-9]+)?)$ ]]; then
fail "The release notes filename is invalid: ${notes_file}."
fi

tag=${BASH_REMATCH[1]}
version=${tag#v}

tree_entry=$(git ls-tree "$head_sha" -- "$notes_file")
read -r notes_mode notes_type _ <<<"$tree_entry"
[[ "$notes_mode" == "100644" && "$notes_type" == "blob" ]] ||
fail "The release notes path must be a regular, nonexecutable Git file: ${notes_file}."

notes_size=$(git cat-file -s "${head_sha}:${notes_file}" 2>/dev/null) ||
fail "The release notes file is not present at HEAD_SHA: ${notes_file}."
[[ "$notes_size" -gt 0 ]] ||
fail "The release notes file must not be empty: ${notes_file}."

if git show-ref --verify --quiet "refs/tags/${tag}"; then
fail "The release tag already exists: ${tag}."
fi

if [[ "$tag" == *-RC* ]]; then
prerelease=true
else
prerelease=false
fi

output_dir=$(dirname -- "$github_output")
[[ -d "$output_dir" ]] ||
fail "The GitHub output directory does not exist: ${output_dir}."

{
printf 'tag=%s\n' "$tag"
printf 'version=%s\n' "$version"
printf 'notes_file=%s\n' "$notes_file"
printf 'commit=%s\n' "$head_sha"
printf 'prerelease=%s\n' "$prerelease"
} >>"$github_output"
157 changes: 157 additions & 0 deletions .github/scripts/release/wait-for-maven-central.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#!/usr/bin/env bash

set -euo pipefail

# Exit status 0 means the artifact is visible. Exit status 1 is reserved for a
# bounded poll whose final response was a definitive 404. Exit status 2 means
# the result is ambiguous or the request/configuration is invalid.

usage() {
echo "Usage: $0 VERSION ATTEMPTS DELAY_SECONDS [EXPECTED_COMMIT]" >&2
}

fail() {
echo "wait-for-maven-central: $*" >&2
exit 2
}

is_nonnegative_integer() {
[[ "$1" =~ ^[0-9]+$ ]]
}

read_pom_commit() {
local pom_file=$1

python3 - "$pom_file" <<'PY'
import sys
import xml.etree.ElementTree as ET

try:
root = ET.parse(sys.argv[1]).getroot()
except (ET.ParseError, OSError):
raise SystemExit(1)

namespace = ""
if root.tag.startswith("{"):
namespace = root.tag.split("}", 1)[0] + "}"

scm = root.find(f"{namespace}scm")
tag = None if scm is None else scm.find(f"{namespace}tag")
if tag is None or tag.text is None or not tag.text.strip():
raise SystemExit(1)

print(tag.text.strip())
PY
}

if [[ $# -lt 3 || $# -gt 4 ]]; then
usage
exit 2
fi

version=$1
attempts=$2
delay_seconds=$3
expected_commit=${4:-}

[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-RC[0-9]+)?$ ]] ||
fail "The version is invalid: ${version}."
is_nonnegative_integer "$attempts" && [[ "$attempts" -gt 0 ]] ||
fail "ATTEMPTS must be a positive integer."
is_nonnegative_integer "$delay_seconds" ||
fail "DELAY_SECONDS must be a nonnegative integer."
if [[ -n "$expected_commit" ]]; then
expected_commit=$(printf '%s' "$expected_commit" | tr '[:upper:]' '[:lower:]')
[[ "$expected_commit" =~ ^[0-9a-f]{40}$ ]] ||
fail "EXPECTED_COMMIT must be a full 40-character commit SHA."
command -v python3 >/dev/null 2>&1 ||
fail "python3 is required to verify Maven POM provenance."
fi

curl_bin=${CURL_BIN:-curl}
central_base_url=${MAVEN_CENTRAL_BASE_URL:-https://repo1.maven.org/maven2}
connect_timeout=${MAVEN_CONNECT_TIMEOUT_SECONDS:-10}
request_timeout=${MAVEN_REQUEST_TIMEOUT_SECONDS:-30}

is_nonnegative_integer "$connect_timeout" && [[ "$connect_timeout" -gt 0 ]] ||
fail "MAVEN_CONNECT_TIMEOUT_SECONDS must be a positive integer."
is_nonnegative_integer "$request_timeout" && [[ "$request_timeout" -gt 0 ]] ||
fail "MAVEN_REQUEST_TIMEOUT_SECONDS must be a positive integer."
command -v "$curl_bin" >/dev/null 2>&1 ||
fail "curl is not available: ${curl_bin}."

artifact_url="${central_base_url%/}/io/temporal/temporal-sdk/${version}/temporal-sdk-${version}.pom"
response_file=$(mktemp)
trap 'rm -f -- "$response_file"' EXIT

for ((attempt = 1; attempt <= attempts; attempt++)); do
http_status=
curl_exit=0

if http_status=$(
"$curl_bin" \
--silent \
--show-error \
--location \
--output "$response_file" \
--write-out '%{http_code}' \
--connect-timeout "$connect_timeout" \
--max-time "$request_timeout" \
"$artifact_url"
); then
curl_exit=0
else
curl_exit=$?
fi

if [[ "$curl_exit" -ne 0 ]]; then
case "$curl_exit" in
5 | 6 | 7 | 18 | 28 | 35 | 52 | 55 | 56 | 92)
reason="network error (curl exit ${curl_exit})"
exhausted_status=2
retryable=true
;;
*)
fail "curl failed permanently with exit ${curl_exit} for ${artifact_url}."
;;
esac
elif [[ "$http_status" == "200" ]]; then
if [[ -n "$expected_commit" ]]; then
if ! published_commit=$(read_pom_commit "$response_file"); then
fail "Maven Central returned a malformed POM or one without scm.tag for io.temporal:temporal-sdk:${version}."
fi
published_commit=$(printf '%s' "$published_commit" | tr '[:upper:]' '[:lower:]')
[[ "$published_commit" == "$expected_commit" ]] ||
fail \
"Maven Central contains io.temporal:temporal-sdk:${version} from commit ${published_commit}, expected ${expected_commit}."
fi
echo "Maven Central contains io.temporal:temporal-sdk:${version}${expected_commit:+ from commit ${expected_commit}}."
exit 0
elif [[ "$http_status" == "404" ]]; then
reason="HTTP ${http_status}"
exhausted_status=1
retryable=true
elif [[ "$http_status" == "408" || "$http_status" == "425" || "$http_status" == "429" || "$http_status" =~ ^5[0-9][0-9]$ ]]; then
reason="HTTP ${http_status}"
exhausted_status=2
retryable=true
elif [[ "$http_status" =~ ^4[0-9][0-9]$ ]]; then
fail "Maven Central returned permanent HTTP ${http_status} for ${artifact_url}."
else
fail "Maven Central returned unexpected HTTP status ${http_status:-<empty>} for ${artifact_url}."
fi

if [[ "$retryable" == "true" && "$attempt" -lt "$attempts" ]]; then
echo \
"Maven Central is not ready for io.temporal:temporal-sdk:${version} (${reason}); retrying in ${delay_seconds}s (${attempt}/${attempts})." \
>&2
if [[ "$delay_seconds" -gt 0 ]]; then
sleep "$delay_seconds"
fi
fi
done

echo \
"wait-for-maven-central: Maven Central did not expose io.temporal:temporal-sdk:${version} after ${attempts} attempts; last result: ${reason}." \
>&2
exit "$exhausted_status"
76 changes: 67 additions & 9 deletions .github/workflows/build-native-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ on:
description: "Git ref from which to release"
required: true
default: "main"
version:
type: string
description: "Exact release version without the leading v"
required: false
default: ""
upload_artifact:
type: boolean
description: "Upload the native test server executable as an artifact"
Expand All @@ -21,16 +26,21 @@ on:
inputs:
ref:
type: string
description: "Git ref from which to release"
description: "Immutable Git commit from which to build"
required: true
default: "main"
version:
type: string
description: "Exact release version without the leading v"
required: false
default: ""
upload_artifact:
type: boolean
description: "Upload the native test server executable as an artifact"
required: false
default: false
env:
INPUT_REF: ${{ inputs.ref }}
RELEASE_VERSION: ${{ inputs.version }}

jobs:
build_native_images:
Expand Down Expand Up @@ -68,6 +78,14 @@ jobs:
submodules: recursive
ref: ${{ env.INPUT_REF }}

- name: Validate release version
if: env.RELEASE_VERSION != ''
run: |
if [[ ! "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-RC[0-9]+)?$ ]]; then
echo "::error::Invalid release version: $RELEASE_VERSION"
exit 1
fi

- name: Set up Java
if: matrix.os_family != 'linux'
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
Expand All @@ -82,37 +100,77 @@ jobs:
- name: Build native test server (non-Docker)
if: matrix.os_family != 'linux'
run: |
./gradlew -PnativeBuild :temporal-test-server:nativeCompile
if [[ -n "$RELEASE_VERSION" ]]; then
./gradlew "-PreleaseVersion=$RELEASE_VERSION" -PnativeBuild :temporal-test-server:nativeCompile
else
./gradlew -PnativeBuild :temporal-test-server:nativeCompile
fi

- name: Build native test server (Docker non-musl)
if: matrix.os_family == 'linux' && matrix.musl == false
run: |
IMAGE_ID_FILE="$(mktemp)"
docker build --iidfile "$IMAGE_ID_FILE" ./docker/native-image
for attempt in 1 2 3; do
: > "$IMAGE_ID_FILE"
if docker build --iidfile "$IMAGE_ID_FILE" ./docker/native-image; then
break
fi
if [[ "$attempt" -eq 3 ]]; then
echo "::error::Docker build failed after $attempt attempts."
exit 1
fi
delay=$((attempt * 20))
echo "::warning::Docker build attempt $attempt failed; retrying in ${delay}s."
sleep "$delay"
done
IMAGE_ID="$(cat "$IMAGE_ID_FILE")"
version_arg=""
if [[ -n "$RELEASE_VERSION" ]]; then
version_arg="-PreleaseVersion=$RELEASE_VERSION"
fi
docker run \
--rm -w /github/workspace -v "$(pwd):/github/workspace" \
"$IMAGE_ID" \
sh -c "./gradlew -PnativeBuild :temporal-test-server:nativeCompile"
sh -c "./gradlew $version_arg -PnativeBuild :temporal-test-server:nativeCompile"

- name: Build native test server (Docker musl)
if: matrix.os_family == 'linux' && matrix.musl == true
run: |
IMAGE_ID_FILE="$(mktemp)"
docker build --iidfile "$IMAGE_ID_FILE" ./docker/native-image-musl
for attempt in 1 2 3; do
: > "$IMAGE_ID_FILE"
if docker build --iidfile "$IMAGE_ID_FILE" ./docker/native-image-musl; then
break
fi
if [[ "$attempt" -eq 3 ]]; then
echo "::error::Docker build failed after $attempt attempts."
exit 1
fi
delay=$((attempt * 20))
echo "::warning::Docker build attempt $attempt failed; retrying in ${delay}s."
sleep "$delay"
done
IMAGE_ID="$(cat "$IMAGE_ID_FILE")"
version_arg=""
if [[ -n "$RELEASE_VERSION" ]]; then
version_arg="-PreleaseVersion=$RELEASE_VERSION"
fi
docker run \
--rm -w /github/workspace -v "$(pwd):/github/workspace" \
"$IMAGE_ID" \
sh -c "./gradlew -PnativeBuild -PnativeBuildMusl :temporal-test-server:nativeCompile"
sh -c "./gradlew $version_arg -PnativeBuild -PnativeBuildMusl :temporal-test-server:nativeCompile"

- name: Verify build did not modify tracked files
run: git diff --exit-code

# path ends in a wildcard because on windows the file ends in '.exe'
- name: Upload executable to workflow
if: ${{ inputs.upload_artifact }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: ${{ matrix.musl && format('{0}_{1}_musl', matrix.os_family, matrix.arch) || format('{0}_{1}', matrix.os_family, matrix.arch)}}
name: ${{ matrix.musl && format('native-{0}_{1}_musl', matrix.os_family, matrix.arch) || format('native-{0}_{1}', matrix.os_family, matrix.arch)}}
path: |
temporal-test-server/build/native/nativeCompile/temporal-test-server*
if-no-files-found: error
retention-days: 1
overwrite: true
retention-days: 30
Loading
Loading