diff --git a/.github/scripts/package-native-release.sh b/.github/scripts/package-native-release.sh new file mode 100755 index 0000000000..ab758c95f8 --- /dev/null +++ b/.github/scripts/package-native-release.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Packages each native test server binary into a reproducible release archive. +: "${RELEASE_VERSION:?RELEASE_VERSION is required.}" + +native_directory=${1:?Native artifact directory is required.} +asset_directory=${2:?Release asset directory is required.} +platforms=( + linux_amd64_musl + linux_amd64 + macOS_amd64 + macOS_arm64 + linux_arm64 + windows_amd64 +) + +native_directory=$(cd "$native_directory" && pwd) +mkdir -p "$asset_directory" +asset_directory=$(cd "$asset_directory" && pwd) +staging_directory=$(mktemp -d) +trap 'rm -rf "$staging_directory"' EXIT + +for platform in "${platforms[@]}"; do + source_directory="$native_directory/release-native-$platform" + mapfile -t binaries < <( + find "$source_directory" -type f -name 'temporal-test-server*' + ) + if [[ "${#binaries[@]}" -ne 1 ]]; then + echo "::error::Expected one native executable in $source_directory." + exit 1 + fi + + archive_root="temporal-test-server_${RELEASE_VERSION}_${platform}" + package_directory="$staging_directory/$archive_root" + mkdir "$package_directory" + if [[ "$platform" == windows_* ]]; then + executable="$package_directory/temporal-test-server.exe" + install -m 0755 "${binaries[0]}" "$executable" + touch -t 198001010000 "$package_directory" "$executable" + ( + cd "$staging_directory" + zip -X -qr "$asset_directory/$archive_root.zip" "$archive_root" + ) + else + executable="$package_directory/temporal-test-server" + install -m 0755 "${binaries[0]}" "$executable" + touch -t 198001010000 "$package_directory" "$executable" + COPYFILE_DISABLE=1 tar -cf - -C "$staging_directory" "$archive_root" \ + | gzip -n > "$asset_directory/$archive_root.tar.gz" + fi + rm -rf "$package_directory" +done + +( + cd "$asset_directory" + sha256sum ./*.tar.gz ./*.zip | sort -k2 > SHA256SUMS +) diff --git a/.github/scripts/release.sh b/.github/scripts/release.sh new file mode 100755 index 0000000000..115e1c2e9b --- /dev/null +++ b/.github/scripts/release.sh @@ -0,0 +1,397 @@ +#!/usr/bin/env bash +set -euo pipefail +shopt -s nullglob + +# Reports a workflow-formatted error and exits. +fail() { + echo "::error::$*" >&2 + exit 1 +} + +# Fails when a required environment variable is empty. +require() { + [[ -n "${!1:-}" ]] || fail "$1 is required." +} + +# Writes release metadata to GitHub Actions or standard output. +write_output() { + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + printf '%s=%s\n' "$1" "$2" >> "$GITHUB_OUTPUT" + else + printf '%s=%s\n' "$1" "$2" + fi +} + +# Lists the valid version headings at a given commit. +changelog_versions() { + git show "$1:CHANGELOG.md" \ + | sed -nE 's/^## \[([0-9]+\.[0-9]+\.[0-9]+(-RC[0-9]+)?)\] - [0-9]{4}-[0-9]{2}-[0-9]{2}$/\1/p' \ + | sort +} + +# Lists every semantic-version token used in a level-two heading. +changelog_version_tokens() { + git show "$1:CHANGELOG.md" \ + | sed -nE 's/^## \[([0-9]+\.[0-9]+\.[0-9]+(-RC[0-9]+)?)\].*$/\1/p' \ + | sort +} + +# Extracts one version's changelog section at a given commit. +changelog_section() { + git show "$1:CHANGELOG.md" | awk -v heading="## [$2] - " -v include="${3:-false}" ' + !seen && index($0, heading) == 1 { + date = substr($0, length(heading) + 1) + if (date !~ /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/) next + seen = 1 + capture = 1 + if (include == "true") lines[++count] = $0 + next + } + capture && /^## / { capture = 0 } + capture { lines[++count] = $0 } + END { + first = 1 + while (first <= count && lines[first] == "") first++ + while (count >= first && lines[count] == "") count-- + for (line = first; line <= count; line++) print lines[line] + } + ' +} + +# Prints one version's exact dated heading at a given commit. +changelog_heading() { + changelog_section "$1" "$2" true | sed -n '1p' +} + +# Finds the first-parent commit that added an exact heading line. +changelog_heading_commit() { + local head=$1 + local heading=$2 + local pattern + + pattern=$(printf '%s\n' "$heading" | sed 's/[][\\.^$*+?(){}|]/\\&/g') + git log -1 --first-parent --format=%H -G"^$pattern$" "$head" -- CHANGELOG.md +} + +# Rejects malformed or repeated semantic-version headings at a commit. +validate_version_headings() { + local commit=$1 + local duplicate + local -a tokens versions + + mapfile -t tokens < <(changelog_version_tokens "$commit") + mapfile -t versions < <(changelog_versions "$commit") + diff -q <(printf '%s\n' "${tokens[@]}") \ + <(printf '%s\n' "${versions[@]}") >/dev/null \ + || fail "Version headings at $commit must use ## [X.Y.Z] - YYYY-MM-DD." + duplicate=$(printf '%s\n' "${tokens[@]}" | uniq -d | sed -n '1p') + [[ -z "$duplicate" ]] \ + || fail "Changelog version $duplicate appears more than once at $commit." +} + +# Validates a changelog transition and prints its newly added version, if any. +changelog_transition() { + local base=$1 + local head=$2 + local version + local -a added base_versions head_versions + + validate_version_headings "$head" + mapfile -t base_versions < <(changelog_versions "$base") + mapfile -t head_versions < <(changelog_versions "$head") + for version in "${base_versions[@]}"; do + diff -q <(changelog_section "$base" "$version" true) \ + <(changelog_section "$head" "$version" true) >/dev/null \ + || fail "Published changelog section $version was changed." + done + + mapfile -t added < <(comm -13 \ + <(printf '%s\n' "${base_versions[@]}") \ + <(printf '%s\n' "${head_versions[@]}")) + [[ "${#added[@]}" -le 1 ]] \ + || fail "A release commit must add exactly one versioned changelog section." + printf '%s' "${added[0]:-}" +} + +# Validates a changelog transition and identifies a release candidate. +candidate() { + require EVENT_NAME + require HEAD_SHA + local notes=${1:?Release notes output path is required.} + local base dispatch_head draft_release existing heading release_base release_commit tag version + + if [[ "$EVENT_NAME" == workflow_dispatch ]]; then + require MANUAL_DRAFT_RELEASE + require MANUAL_REF + require MANUAL_VERSION + [[ "$MANUAL_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-RC[0-9]+)?$ ]] \ + || fail "Manual release version must look like 1.2.3 or 1.2.3-RC1." + case "$MANUAL_DRAFT_RELEASE" in + true) draft_release=1 ;; + false) draft_release=0 ;; + *) fail "Manual draft selection must be true or false." ;; + esac + + dispatch_head=$(git rev-parse --verify "$HEAD_SHA^{commit}") + validate_version_headings "$dispatch_head" + heading=$(changelog_heading "$dispatch_head" "$MANUAL_VERSION") + [[ -n "$heading" ]] \ + || fail "Version $MANUAL_VERSION was not found on $MANUAL_REF." + release_commit=$(changelog_heading_commit "$dispatch_head" "$heading") + [[ -n "$release_commit" ]] \ + || fail "Version $MANUAL_VERSION was not introduced on $MANUAL_REF." + BASE_SHA=$(git rev-parse --verify "$release_commit^1") + HEAD_SHA=$release_commit + else + require BASE_SHA + fi + + if [[ "$BASE_SHA" =~ ^0+$ ]]; then + BASE_SHA=$(git rev-parse "$HEAD_SHA^") + fi + base=$(git rev-parse --verify "$BASE_SHA^{commit}") + head=$(git rev-parse --verify "$HEAD_SHA^{commit}") + if [[ "$EVENT_NAME" == pull_request ]]; then + base=$(git merge-base "$base" "$head") \ + || fail "The pull request branches must have a common ancestor." + else + git merge-base --is-ancestor "$base" "$head" \ + || fail "The base commit must be an ancestor of the release commit." + fi + + version=$(changelog_transition "$base" "$head") + if [[ -z "$version" ]]; then + write_output release false + return + fi + + if [[ "$EVENT_NAME" == workflow_dispatch ]]; then + [[ "$version" == "$MANUAL_VERSION" ]] \ + || fail "Commit $head does not introduce version $MANUAL_VERSION." + diff -q <(changelog_section "$head" "$version" true) \ + <(changelog_section "$dispatch_head" "$version" true) >/dev/null \ + || fail "Release notes for $version changed after commit $head." + elif [[ "$EVENT_NAME" == push ]]; then + heading=$(changelog_heading "$head" "$version") + release_commit=$(changelog_heading_commit "$head" "$heading") + [[ -n "$release_commit" ]] \ + || fail "The commit that introduced version $version was not found." + git merge-base --is-ancestor "$base" "$release_commit" \ + || fail "Version $version was introduced before this push." + release_base=$(git rev-parse --verify "$release_commit^1") + [[ $(changelog_transition "$release_base" "$release_commit") == "$version" ]] \ + || fail "Commit $release_commit does not introduce version $version." + diff -q <(changelog_section "$release_commit" "$version" true) \ + <(changelog_section "$head" "$version" true) >/dev/null \ + || fail "Release notes for $version changed after commit $release_commit." + head=$release_commit + fi + changelog_section "$head" "$version" > "$notes" + [[ -s "$notes" ]] || fail "Release notes for $version are empty." + git show "$head:CHANGELOG.md" | awk -v release="## [$version]" ' + /^## \[Unreleased\]$/ { unreleased = NR } + index($0, release) == 1 { candidate = NR } + END { exit !(unreleased && candidate && unreleased < candidate) } + ' || fail "[Unreleased] must remain above the new release section." + + tag="v$version" + if existing=$(git rev-parse --verify "refs/tags/$tag^{commit}" 2>/dev/null); then + [[ $(git cat-file -t "refs/tags/$tag") == commit ]] \ + || fail "Release tag $tag must be a lightweight tag." + [[ ("$EVENT_NAME" == push || "$EVENT_NAME" == workflow_dispatch) && \ + "$existing" == "$head" ]] \ + || fail "Release tag $tag already exists at $existing." + fi + write_output release true + [[ "$EVENT_NAME" == workflow_dispatch ]] \ + && write_output draft_release "$draft_release" + write_output version "$version" + write_output tag "$tag" + write_output commit "$head" + write_output prerelease "$([[ "$version" == *-RC* ]] && echo true || echo false)" +} + +# Tests the project and verifies its primary local Maven publication. +build_maven() { + require RELEASE_COMMIT + require RELEASE_VERSION + local repository=${1:?Local Maven repository path is required.} + local pom="$repository/io/temporal/temporal-sdk/$RELEASE_VERSION/temporal-sdk-$RELEASE_VERSION.pom" + + ./gradlew --no-daemon \ + "-PreleaseVersion=$RELEASE_VERSION" \ + "-PreleaseCommit=$RELEASE_COMMIT" \ + "-Dmaven.repo.local=$repository" \ + build publishToMavenLocal + git diff --exit-code + [[ -f "$pom" ]] || fail "The temporal-sdk POM was not generated." + grep -Fq "$RELEASE_COMMIT" "$pom" \ + || fail "The generated POM does not identify the release commit." +} + +# Reports whether the exact release is already visible on Maven Central. +central_state() { + local pom="$RUNNER_TEMP/temporal-sdk-central.pom" + local url="https://repo1.maven.org/maven2/io/temporal/temporal-sdk/$RELEASE_VERSION/temporal-sdk-$RELEASE_VERSION.pom" + local status curl_status + set +e + status=$(curl --silent --show-error --output "$pom" --write-out '%{http_code}' "$url") + curl_status=$? + set -e + [[ "$curl_status" -eq 0 ]] || fail "Maven Central could not be read." + case "$status" in + 200) + grep -Fq "$RELEASE_COMMIT" "$pom" \ + || fail "Maven Central contains this version from another commit." + echo published + ;; + 404) echo absent ;; + *) fail "Maven Central returned HTTP $status." ;; + esac +} + +# Publishes the signed staging repository and waits for Maven Central. +publish_maven() { + require GRADLE_USER_HOME + require MAVEN_RETRY_REQUIRED + require RELEASE_COMMIT + require RELEASE_VERSION + require RUNNER_TEMP + local state variable signing_directory status attempt + + state=$(central_state) + [[ "$state" == absent ]] || return + case "$MAVEN_RETRY_REQUIRED" in + false) ;; + true) + [[ "${MAVEN_RETRY_COMMIT:-}" == "$RELEASE_COMMIT" ]] \ + || fail "Inspect Sonatype, then set MAVEN_RETRY_COMMIT to $RELEASE_COMMIT before rerunning." + ;; + *) fail "MAVEN_RETRY_REQUIRED must be true or false." ;; + esac + for variable in KEY KEY_ID KEY_PASSWORD RH_PASSWORD RH_USER; do + [[ -n "${!variable:-}" ]] || fail "Release secret $variable is not configured." + done + + umask 077 + signing_directory="$RUNNER_TEMP/release-gnupg" + signing_key="$signing_directory/secring.gpg" + properties="$GRADLE_USER_HOME/gradle.properties" + mkdir -p "$GRADLE_USER_HOME" "$signing_directory" + trap 'rm -f "$properties" "$signing_key"' EXIT + printf '%s' "$KEY" | base64 --decode > "$signing_key" + { + printf 'signing.keyId = %s\n' "$KEY_ID" + printf 'signing.password = %s\n' "$KEY_PASSWORD" + printf 'signing.secretKeyRingFile = %s\n' "$signing_key" + printf 'ossrhUsername = %s\n' "$RH_USER" + printf 'ossrhPassword = %s\n' "$RH_PASSWORD" + } > "$properties" + + set +e + ./gradlew --no-daemon \ + "-PreleaseVersion=$RELEASE_VERSION" \ + "-PreleaseCommit=$RELEASE_COMMIT" \ + publishToSonatype closeAndReleaseSonatypeStagingRepository + status=$? + set -e + [[ "$status" -eq 0 ]] \ + || echo "::warning::Gradle failed; checking Central before declaring an ambiguous publication." + + for attempt in {1..90}; do + state=$(central_state) + [[ "$state" == published ]] && return + [[ "$attempt" -eq 90 ]] || sleep 20 + done + fail "Maven publication is ambiguous. Inspect Sonatype before authorizing a retry." +} + +# Creates or validates the GitHub release, tag, notes, and assets. +publish_github() { + require GITHUB_REPOSITORY + require GITHUB_STEP_SUMMARY + require PRERELEASE + require RELEASE_COMMIT + require RELEASE_TAG + local notes=${1:?Release notes path is required.} + local assets=${2:?Release asset directory is required.} + local draft_release=${MANUAL_DRAFT_RELEASE:-${DRAFT_RELEASE:-1}} + local requested_draft=false + local actual_draft maven_retry_required=false published_assets release summary_title tag + local release_assets=("$assets"/*) + case "$draft_release" in + 0) ;; + 1) requested_draft=true ;; + *) fail "DRAFT_RELEASE must be 0, 1, or unset." ;; + esac + [[ "${#release_assets[@]}" -gt 0 ]] || fail "No GitHub release assets were produced." + + if ! release=$(gh release view --repo "$GITHUB_REPOSITORY" "$RELEASE_TAG" \ + --json body,isDraft,isPrerelease,name,targetCommitish,url 2>/dev/null); then + create=(release create "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \ + --title "$RELEASE_TAG" --target "$RELEASE_COMMIT" --notes-file "$notes") + [[ "$requested_draft" == true ]] && create+=(--draft) + [[ "$PRERELEASE" == true ]] && create+=(--prerelease) + create+=("${release_assets[@]}") + gh "${create[@]}" + release=$(gh release view --repo "$GITHUB_REPOSITORY" "$RELEASE_TAG" \ + --json body,isDraft,isPrerelease,name,targetCommitish,url) + elif [[ $(jq -r .isDraft <<<"$release") == false ]]; then + maven_retry_required=true + fi + + jq -e --arg tag "$RELEASE_TAG" --argjson prerelease "$PRERELEASE" \ + --rawfile notes "$notes" \ + 'def normalized: gsub("\r"; "") | sub("\n+$"; ""); + .name == $tag and ((.body | normalized) == ($notes | normalized)) and + .isPrerelease == $prerelease' <<<"$release" >/dev/null + actual_draft=$(jq -r .isDraft <<<"$release") + if tag=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" 2>/dev/null); then + jq -e --arg commit "$RELEASE_COMMIT" \ + '.object.type == "commit" and .object.sha == $commit' <<<"$tag" >/dev/null \ + || fail "Release tag $RELEASE_TAG must be lightweight and point to $RELEASE_COMMIT." + else + [[ "$actual_draft" == true ]] || fail "Published release tag $RELEASE_TAG is missing." + jq -e --arg commit "$RELEASE_COMMIT" \ + '.targetCommitish == $commit' <<<"$release" >/dev/null + fi + + published_assets=$(mktemp -d) + trap "rm -rf -- '$published_assets'" EXIT + gh release download --repo "$GITHUB_REPOSITORY" "$RELEASE_TAG" --dir "$published_assets" + diff -qr "$assets" "$published_assets" + + if [[ "$requested_draft" == false && "$actual_draft" == true ]]; then + gh release edit --repo "$GITHUB_REPOSITORY" "$RELEASE_TAG" --draft=false + release=$(gh release view --repo "$GITHUB_REPOSITORY" "$RELEASE_TAG" \ + --json body,isDraft,isPrerelease,name,targetCommitish,url) + actual_draft=$(jq -r .isDraft <<<"$release") + [[ "$actual_draft" == false ]] || fail "GitHub release $RELEASE_TAG remains a draft." + tag=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG") + jq -e --arg commit "$RELEASE_COMMIT" \ + '.object.type == "commit" and .object.sha == $commit' <<<"$tag" >/dev/null \ + || fail "Release tag $RELEASE_TAG must be lightweight and point to $RELEASE_COMMIT." + fi + + write_output draft "$actual_draft" + write_output maven_retry_required "$maven_retry_required" + summary_title="Release published" + [[ "$actual_draft" == true ]] && summary_title="Release drafted" + { + echo "## $summary_title" + echo + echo "- GitHub: $(jq -r .url <<<"$release")" + echo "- Commit: \`$RELEASE_COMMIT\`" + } >> "$GITHUB_STEP_SUMMARY" +} + +# Dispatches the requested release operation. +command=${1:-} +shift || true +case "$command" in + candidate) candidate "$@" ;; + build-maven) build_maven "$@" ;; + publish-maven) publish_maven "$@" ;; + publish-github) publish_github "$@" ;; + *) fail "Unknown release command: $command" ;; +esac diff --git a/.github/scripts/test-release.sh b/.github/scripts/test-release.sh new file mode 100755 index 0000000000..a160c8bb92 --- /dev/null +++ b/.github/scripts/test-release.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Exercises changelog validation and deterministic packaging without external services. +script_directory=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +release_script="$script_directory/release.sh" +package_script="$script_directory/package-native-release.sh" +temporary_directory=$(mktemp -d) +trap 'rm -rf "$temporary_directory"' EXIT + +repository="$temporary_directory/repository" +mkdir "$repository" +git -C "$repository" init -q +git -C "$repository" config user.email test@example.com +git -C "$repository" config user.name "Release Test" +printf '# Changelog\n\n## [Unreleased]\n\n### Fixed\n- Fixed it.\n' \ + > "$repository/CHANGELOG.md" +git -C "$repository" add CHANGELOG.md +git -C "$repository" commit -qm base +base=$(git -C "$repository" rev-parse HEAD) +( + cd "$repository" + BASE_SHA="$base" HEAD_SHA="$base" EVENT_NAME=pull_request \ + GITHUB_OUTPUT="$temporary_directory/no-release-output" \ + "$release_script" candidate "$temporary_directory/no-release-notes" +) +grep -Fxq 'release=false' "$temporary_directory/no-release-output" + +git -C "$repository" switch -qc feature +printf '# Changelog\n\n## [Unreleased]\n\n### Fixed\n- Fixed it.\n- Fixed on the feature branch.\n' \ + > "$repository/CHANGELOG.md" +git -C "$repository" commit -qam feature +feature=$(git -C "$repository" rev-parse HEAD) +git -C "$repository" switch -q - +printf 'The target branch advanced.\n' > "$repository/README.md" +git -C "$repository" add README.md +git -C "$repository" commit -qm advance +target=$(git -C "$repository" rev-parse HEAD) +( + cd "$repository" + BASE_SHA="$target" HEAD_SHA="$feature" EVENT_NAME=pull_request \ + GITHUB_OUTPUT="$temporary_directory/divergent-output" \ + "$release_script" candidate "$temporary_directory/divergent-notes" +) +grep -Fxq 'release=false' "$temporary_directory/divergent-output" + +git -C "$repository" switch -qc malformed +printf '# Changelog\n\n## [Unreleased]\n\n## [1.41.0] - TBD\n\nWrong notes.\n' \ + > "$repository/CHANGELOG.md" +git -C "$repository" commit -qam malformed +malformed=$(git -C "$repository" rev-parse HEAD) +if (cd "$repository" && BASE_SHA="$target" HEAD_SHA="$malformed" EVENT_NAME=pull_request \ + "$release_script" candidate "$temporary_directory/malformed-notes" 2>/dev/null); then + echo "Malformed release headings must be rejected." >&2 + exit 1 +fi +printf '# Changelog\n\n## [Unreleased]\n\n## [1.41.0] - 2026-10-03\n\nCorrect notes.\n' \ + > "$repository/CHANGELOG.md" +git -C "$repository" commit -qam corrected +corrected=$(git -C "$repository" rev-parse HEAD) +( + cd "$repository" + BASE_SHA="$malformed" HEAD_SHA="$corrected" EVENT_NAME=push \ + GITHUB_OUTPUT="$temporary_directory/corrected-output" \ + "$release_script" candidate "$temporary_directory/corrected-notes" +) +grep -Fxq "commit=$corrected" "$temporary_directory/corrected-output" +grep -Fxq 'Correct notes.' "$temporary_directory/corrected-notes" +git -C "$repository" switch -q - + +printf '# Changelog\n\n## [Unreleased]\n\n## [1.41.0] - 2026-10-03\n\n### Fixed\n- Fixed it.\n' \ + > "$repository/CHANGELOG.md" +git -C "$repository" commit -qam release +head=$(git -C "$repository" rev-parse HEAD) +( + cd "$repository" + BASE_SHA="$base" HEAD_SHA="$head" EVENT_NAME=pull_request \ + GITHUB_OUTPUT="$temporary_directory/output" \ + "$release_script" candidate "$temporary_directory/notes" +) +grep -Fxq 'release=true' "$temporary_directory/output" +grep -Fxq 'version=1.41.0' "$temporary_directory/output" +grep -Fxq '### Fixed' "$temporary_directory/notes" +git -C "$repository" tag -a v1.41.0 -m "Annotated release tag" "$head" +if (cd "$repository" && BASE_SHA="$base" HEAD_SHA="$head" EVENT_NAME=push \ + "$release_script" candidate "$temporary_directory/annotated-notes" 2>/dev/null); then + echo "Annotated release tags must be rejected." >&2 + exit 1 +fi +git -C "$repository" tag -d v1.41.0 >/dev/null +printf '# Changelog\n\n## [Unreleased]\n\nReference: `## [1.41.0] - 2026-10-03`\n\n## [1.41.0] - 2026-10-03\n\n### Fixed\n- Fixed it.\n' \ + > "$repository/CHANGELOG.md" +printf 'Later change.\n' > "$repository/README.md" +git -C "$repository" add README.md +git -C "$repository" commit -qm later +later=$(git -C "$repository" rev-parse HEAD) +( + cd "$repository" + BASE_SHA="$base" HEAD_SHA="$later" EVENT_NAME=push \ + GITHUB_OUTPUT="$temporary_directory/push-output" \ + "$release_script" candidate "$temporary_directory/push-notes" +) +grep -Fxq "commit=$head" "$temporary_directory/push-output" +diff -q "$temporary_directory/notes" "$temporary_directory/push-notes" +git -C "$repository" branch releases/1.41.x "$later" +( + cd "$repository" + EVENT_NAME=workflow_dispatch HEAD_SHA="$later" \ + MANUAL_DRAFT_RELEASE=true MANUAL_REF=releases/1.41.x MANUAL_VERSION=1.41.0 \ + GITHUB_OUTPUT="$temporary_directory/manual-output" \ + "$release_script" candidate "$temporary_directory/manual-notes" +) +grep -Fxq 'release=true' "$temporary_directory/manual-output" +grep -Fxq "commit=$head" "$temporary_directory/manual-output" +grep -Fxq 'draft_release=1' "$temporary_directory/manual-output" +diff -q "$temporary_directory/notes" "$temporary_directory/manual-notes" +( + cd "$repository" + EVENT_NAME=workflow_dispatch HEAD_SHA="$later" \ + MANUAL_DRAFT_RELEASE=false MANUAL_REF=main MANUAL_VERSION=1.41.0 \ + GITHUB_OUTPUT="$temporary_directory/manual-public-output" \ + "$release_script" candidate "$temporary_directory/manual-public-notes" +) +grep -Fxq 'draft_release=0' "$temporary_directory/manual-public-output" +printf '# Changelog\n\n## [Unreleased]\n\n## [1.41.0] - 2026-10-04\n\n### Fixed\n- Fixed it.\n' \ + > "$repository/CHANGELOG.md" +git -C "$repository" commit -qam rewrite +rewrite=$(git -C "$repository" rev-parse HEAD) +if (cd "$repository" && BASE_SHA="$head" HEAD_SHA="$rewrite" EVENT_NAME=push \ + "$release_script" candidate "$temporary_directory/rewrite-notes" 2>/dev/null); then + echo "Published changelog edits must be rejected." >&2 + exit 1 +fi + +native="$temporary_directory/native" +for platform in linux_amd64_musl linux_amd64 macOS_amd64 macOS_arm64 linux_arm64 windows_amd64; do + mkdir -p "$native/release-native-$platform" + printf 'binary for %s' "$platform" > "$native/release-native-$platform/temporal-test-server" +done +RELEASE_VERSION=1.41.0 "$package_script" "$native" "$temporary_directory/first" +RELEASE_VERSION=1.41.0 "$package_script" "$native" "$temporary_directory/second" +diff -qr "$temporary_directory/first" "$temporary_directory/second" +[[ $(find "$temporary_directory/first" -type f | wc -l) -eq 7 ]] +(cd "$temporary_directory/first" && sha256sum -c SHA256SUMS) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 885fe8a227..17d16d7ec9 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,10 +1,68 @@ -# sdk-java Github Workflows +# sdk-java GitHub workflows -## Prepare Release (prepare-release.yml) +## Releases -This is a [manually triggered](https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow) workflow that uses the gradle build files already present in the sdk-java repository to prepare release artifacts for publication. This workflow takes a tag string and a git ref to use in peparing a release. There is an expectation that if preparing a given release (e.g. v1.2.3) then there exists a file in the repository, releases/ (i.e. releases/v1.2.3) containing release notes. This file must be present on the ref passed to the workflow invocation. +[`release.yml`](release.yml) publishes a release from an exact commit after a +release pull request is merged. Normal releases require two human decisions: -This workflow requires five secrets: +1. Review and merge a pull request that promotes the desired entries from the + `CHANGELOG.md` `[Unreleased]` section into one new + `## [X.Y.Z] - YYYY-MM-DD` section. +2. Approve the `release-publication` GitHub environment after the workflow has + tested the Maven publications and built the native test server executables. + Approval must be given within 30 days or GitHub automatically fails the + waiting job. + +The protected `publish` job depends on both validation paths, so GitHub does +not request approval until they succeed. GitHub marks the job as **Waiting** +and sends the configured required reviewers a deployment review notification; +open the workflow run and select **Review deployments** to approve or reject +it. Notification delivery outside GitHub depends on each reviewer's settings +or an optional Slack or Microsoft Teams deployment integration. + +The workflow creates and verifies the GitHub release before publishing any +Maven artifacts. Draft GitHub releases are the default and leave Maven +unpublished. Release candidates are always bound to the full merge commit SHA. +RC versions use headings such as +`## [1.41.0-RC1] - 2026-10-03` and are published as GitHub prereleases. + +Leave the `release-publication` environment variable `DRAFT_RELEASE` unset, or +set it to `1`, to keep the GitHub release as a draft for final inspection. Set +it to `0` to publish the GitHub release and then Maven in the same run. Other +values fail the publication job. To finish a draft later, manually run the +**Release** workflow for the same version with the draft option cleared. The +workflow validates and publishes the existing GitHub draft before publishing +Maven. Publishing the draft in the GitHub UI first is also safe; rerun the +workflow afterward to publish Maven. + +An ordinary pull request that only adds entries beneath `[Unreleased]` runs the +candidate check but does not start a release. + +To run an existing release candidate manually, open the **Release** workflow, +select **Run workflow** on `main` or a supported backport branch, enter the +version without its leading `v`, and choose whether to hold the GitHub Release +as a draft. The version must already have a versioned `CHANGELOG.md` section. +The workflow finds the exact first-parent commit on the selected branch that +introduced that section and runs it through the same validation, build, +approval, and publication jobs as an automatic release. The manual draft +selection overrides `DRAFT_RELEASE` for that run. Maven is published only after +the GitHub release is public. + +Maintainers can check the release scripts locally with +`.github/scripts/test-release.sh`. +The local check requires Bash 4 or newer and GNU `sha256sum`; on macOS these +are available from the Homebrew `bash` and `coreutils` packages. + +### One-time repository setup + +Configure the existing `release-publication` environment with required +reviewers. GitHub permits up to six users or teams with repository read access, +and one listed reviewer must approve. Preventing self-review is recommended. +Restrict its deployment branch policy to `main` and the supported backport +patterns `releases/*`, `v*.*.x`, `*.*.x`, and `release_*_*_x`. Add an explicit +pattern for each additional slash-separated level used below `releases/`. +Make these secrets available to the workflow, preferably as environment +secrets: - `JAR_SIGNING_KEY` - `JAR_SIGNING_KEY_ID` @@ -12,27 +70,27 @@ This workflow requires five secrets: - `RH_PASSWORD` - `RH_USER` - The results of running this workflow are - - - A *DRAFT* Github release will be created - - Signed jars *STAGED* to the Sonatype Nexus artifact repository - - To complete the release, the releaser should - - - Validate and publish the Github release - - Approve and publish the jars via the Sonatype UI +The signing key is the base64-encoded secret key ring used by Gradle signing. +The RH credentials must be authorized to publish `io.temporal` through the +Sonatype staging API. -### Testing +### Recovery -This workflow does not publish release artifacts in a way that is externally -visible and thus it is safe to execute at any time as long as the resulting -draft release and unpublished jars are cleaned up. +Jobs before GitHub publication are safe to rerun. A draft GitHub release keeps +Maven unpublished. Once the GitHub release is public, a rerun continues after +Maven publication when the `temporal-sdk` POM is on Maven Central and its +`scm.tag` matches the release commit. -Workflows can also be invoked from the `gh` cli. To invoke this workflow and watch its progress +If a Sonatype request fails and the release does not become visible on Central, +the workflow stops with an ambiguous-publication error. Inspect Sonatype before +rerunning. Do not authorize another staging generation until the earlier one is +known to be inactive. After that inspection, set the `release-publication` +environment variable `MAVEN_RETRY_COMMIT` to the exact 40-character release +commit and rerun the workflow. Clear the variable after the release; its value +is bound to that commit and cannot authorize another candidate. A published +version or GitHub tag that points to another commit is a permanent error and +must not be replaced. -```.sh -$ gh workflow run --repo temporalio/sdk-java --field tag=v1.2.3 prepare-release.yml -$ gh run list --workflow prepare-release.yml --repo temporalio/sdk-java -$ # Note ID of your workflow run in the output of the command above -$ gh run watch --repo temporalio/sdk-java -``` +A run that finds an already-public GitHub release but no matching Central POM +uses the same recovery gate, even when it is a fresh manual run. Promote drafts +through the workflow to keep the first Maven publication attempt unambiguous. diff --git a/.github/workflows/build-native-image.yml b/.github/workflows/build-native-image.yml index 33515cde6c..0c5e120065 100644 --- a/.github/workflows/build-native-image.yml +++ b/.github/workflows/build-native-image.yml @@ -17,6 +17,11 @@ on: description: "Upload the native test server executable as an artifact" required: false default: false + artifact_prefix: + type: string + description: "Optional prefix for uploaded artifact names" + required: false + default: "" workflow_call: inputs: ref: @@ -29,6 +34,11 @@ on: description: "Upload the native test server executable as an artifact" required: false default: false + artifact_prefix: + type: string + description: "Optional prefix for uploaded artifact names" + required: false + default: "" env: INPUT_REF: ${{ inputs.ref }} @@ -111,8 +121,9 @@ jobs: 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: ${{ format('{0}{1}', inputs.artifact_prefix, matrix.musl && format('{0}_{1}_musl', matrix.os_family, matrix.arch) || format('{0}_{1}', matrix.os_family, matrix.arch)) }} path: | temporal-test-server/build/native/nativeCompile/temporal-test-server* if-no-files-found: error + overwrite: true retention-days: 1 diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml deleted file mode 100644 index b43ce62c88..0000000000 --- a/.github/workflows/prepare-release.yml +++ /dev/null @@ -1,185 +0,0 @@ -name: Prepare release -defaults: - run: - shell: bash -euo pipefail -O nullglob {0} -on: - workflow_dispatch: - inputs: - tag: - type: string - description: "Release version tag (e.g. v1.2.3)" - required: true - ref: - type: string - description: "Git ref from which to release" - required: true - default: "main" - do_build_native_images: - type: boolean - description: "Native Test Server" - required: true - default: "true" - do_publish_jars: - type: boolean - description: "Publish Java Artifacts" - required: true - default: "true" - -permissions: - contents: read - -env: - INPUT_REF: ${{ github.event.inputs.ref }} - INPUT_TAG: ${{ github.event.inputs.tag }} - -jobs: - create_draft_release: - name: Create Github draft release - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Audit gh version - run: gh --version - - - name: Check for existing release - id: check_release - run: | - echo "::echo::on" - gh release view --repo "$GITHUB_REPOSITORY" "$INPUT_TAG" \ - && echo "::set-output name=already_exists::true" \ - || echo "::set-output name=already_exists::false" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Checkout repo - if: steps.check_release.outputs.already_exists == 'false' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ env.INPUT_REF }} - - - name: Create release - if: steps.check_release.outputs.already_exists == 'false' - run: > - gh release create - "$INPUT_REF" - --draft - --repo "$GITHUB_REPOSITORY" - --title "$INPUT_TAG" - --target "$INPUT_REF" - --notes-file releases/"$INPUT_TAG" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - publish_java_artifacts: - name: Publish Java Artifacts - if: github.event.inputs.do_publish_jars == 'true' - runs-on: ubuntu-latest - needs: create_draft_release - steps: - - name: Checkout repo - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ env.INPUT_REF }} - - # Our custom gradle version sniffing builds the maven release artifact - # names out of the git tag ... but the repo isn't tagged (yet) so add a - # tag to the _local_ clone just to get the right jar names. This tag - # does not get pushed back to the origin. Once the artifacts have been - # inspected and verified, the manual act of publishing the draft GH - # release creates the tag. - - name: Temporary tag - run: git tag "$INPUT_TAG" - - - name: Set up Java - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - with: - java-version: "23" - distribution: "temurin" - - - name: Set up Gradle - uses: gradle/actions/setup-gradle@ac396bf1a80af16236baf54bd7330ae21dc6ece5 # v6 - - - name: Set up signing key - run: mkdir -p "$HOME/.gnupg" && echo -n "$KEY" | base64 -d > "$HOME/.gnupg/secring.gpg" - env: - KEY: ${{ secrets.JAR_SIGNING_KEY }} - - # Prefer env variables here rather than inline ${{ secrets.FOO }} to - # decrease the likelihood that secrets end up printed to stdout. - - name: Set up secret gradle properties - run: | - mkdir -p "$HOME/.gradle" - envsubst >"$HOME/.gradle/gradle.properties" < temporal-test-server_1.2.3_linux_amd64 - # the name of the directory created becomes the basename of the archive (*.tar.gz or *.zip) and - # the root directory of the contents of the archive. - - name: Rename dirs - run: | - version="$(sed 's/^v//'<<<"$INPUT_TAG")" - for dir in *; do mv "$dir" "temporal-test-server_${version}_${dir}"; done - - - name: Tar (linux, macOS) - run: for dir in *{linux,macOS}*; do tar cvzf "${dir}.tar.gz" "$dir"; done - - - name: Zip (windows) - run: for dir in *windows*; do zip -r "${dir}.zip" "$dir"; done - - - name: Upload release archives - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: release-archives - path: | - *.zip - *.tar.gz - if-no-files-found: error - retention-days: 1 - - - name: Upload - run: | - until gh release upload --clobber --repo $GITHUB_REPOSITORY "$INPUT_TAG" *.zip *.tar.gz; do - echo "Failed to upload release artifacts. Will retry in 20s" - sleep 20 - done - timeout-minutes: 10 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index 8628e4b274..ca8b4d3053 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -15,6 +15,7 @@ on: - 'main' paths-ignore: - 'releases/**' + - 'CHANGELOG.md' - 'docker/buildkite/**' - '.buildkite/**' - '.github/**' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..1661e4c24b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,234 @@ +name: Release + +defaults: + run: + shell: bash -euo pipefail {0} + +on: + pull_request: + branches: [main, "releases/**", "v*.*.x", "*.*.x", "release_*_*_x"] + paths: [CHANGELOG.md] + push: + branches: [main, "releases/**", "v*.*.x", "*.*.x", "release_*_*_x"] + paths: [CHANGELOG.md] + workflow_dispatch: + inputs: + version: + description: Version already present in CHANGELOG.md, without a leading v + required: true + type: string + draft_release: + description: Hold the GitHub Release as a draft and leave Maven unpublished + required: true + default: true + type: boolean + +permissions: + contents: read + +jobs: + candidate: + name: Resolve release candidate + runs-on: ubuntu-latest + outputs: + commit: ${{ steps.metadata.outputs.commit }} + draft_release: ${{ steps.metadata.outputs.draft_release }} + prerelease: ${{ steps.metadata.outputs.prerelease }} + release: ${{ steps.metadata.outputs.release }} + tag: ${{ steps.metadata.outputs.tag }} + version: ${{ steps.metadata.outputs.version }} + steps: + - name: Checkout candidate + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + + - name: Validate changelog transition + id: metadata + env: + BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} + EVENT_NAME: ${{ github.event_name }} + HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + MANUAL_DRAFT_RELEASE: ${{ inputs.draft_release }} + MANUAL_REF: ${{ github.ref_name }} + MANUAL_VERSION: ${{ inputs.version }} + run: .github/scripts/release.sh candidate "$RUNNER_TEMP/release-notes.md" + + - name: Upload exact release notes + if: >- + (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && + steps.metadata.outputs.release == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: release-notes-${{ steps.metadata.outputs.commit }} + path: ${{ runner.temp }}/release-notes.md + if-no-files-found: error + overwrite: true + retention-days: 90 + + - name: Summarize candidate + if: steps.metadata.outputs.release == 'true' + env: + RELEASE_COMMIT: ${{ steps.metadata.outputs.commit }} + RELEASE_TAG: ${{ steps.metadata.outputs.tag }} + run: | + { + echo "## Release candidate" + echo + echo "- Tag: \`$RELEASE_TAG\`" + echo "- Commit: \`$RELEASE_COMMIT\`" + } >> "$GITHUB_STEP_SUMMARY" + + build_maven: + name: Test and inspect Maven publications + if: >- + (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && + needs.candidate.outputs.release == 'true' + needs: candidate + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - name: Checkout exact release commit + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + ref: ${{ needs.candidate.outputs.commit }} + submodules: recursive + + - name: Set up Java + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: "23" + distribution: temurin + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@ac396bf1a80af16236baf54bd7330ae21dc6ece5 # v6 + + - name: Test and inspect Maven publications + env: + RELEASE_COMMIT: ${{ needs.candidate.outputs.commit }} + RELEASE_VERSION: ${{ needs.candidate.outputs.version }} + run: >- + .github/scripts/release.sh build-maven + "$RUNNER_TEMP/maven-repository" + + build_native: + name: Build native test server + if: >- + (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && + needs.candidate.outputs.release == 'true' + needs: candidate + uses: ./.github/workflows/build-native-image.yml + with: + artifact_prefix: release-native- + ref: ${{ needs.candidate.outputs.commit }} + upload_artifact: true + + package_native: + name: Package native release assets + if: >- + (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && + needs.candidate.outputs.release == 'true' + needs: [candidate, build_native] + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + steps: + - name: Checkout exact release commit + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + ref: ${{ needs.candidate.outputs.commit }} + + - name: Download native executables + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + pattern: release-native-* + path: native + + - name: Package native executables + env: + RELEASE_VERSION: ${{ needs.candidate.outputs.version }} + run: .github/scripts/package-native-release.sh native release-assets + + - name: Preserve exact release assets + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: release-assets-${{ needs.candidate.outputs.commit }} + path: release-assets + if-no-files-found: error + overwrite: true + retention-days: 90 + + publish: + name: Publish ${{ needs.candidate.outputs.tag }} + if: >- + (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && + needs.candidate.outputs.release == 'true' + needs: [candidate, build_maven, package_native] + runs-on: ubuntu-latest + timeout-minutes: 150 + concurrency: + group: sdk-java-release-publication + cancel-in-progress: false + environment: + name: release-publication + permissions: + actions: read + contents: write + env: + RELEASE_COMMIT: ${{ needs.candidate.outputs.commit }} + RELEASE_TAG: ${{ needs.candidate.outputs.tag }} + RELEASE_VERSION: ${{ needs.candidate.outputs.version }} + steps: + - name: Checkout exact release commit + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ needs.candidate.outputs.commit }} + submodules: recursive + + - name: Download release inputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + pattern: ${{ format('*-{0}', needs.candidate.outputs.commit) }} + path: release-inputs + + - name: Publish or draft GitHub release + id: github_release + env: + DRAFT_RELEASE: ${{ vars.DRAFT_RELEASE }} + GH_TOKEN: ${{ github.token }} + MANUAL_DRAFT_RELEASE: ${{ needs.candidate.outputs.draft_release }} + PRERELEASE: ${{ needs.candidate.outputs.prerelease }} + run: >- + .github/scripts/release.sh publish-github + "release-inputs/release-notes-$RELEASE_COMMIT/release-notes.md" + "release-inputs/release-assets-$RELEASE_COMMIT" + + - name: Set up Java + if: steps.github_release.outputs.draft == 'false' + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: "23" + distribution: temurin + + - name: Set up Gradle + if: steps.github_release.outputs.draft == 'false' + uses: gradle/actions/setup-gradle@ac396bf1a80af16236baf54bd7330ae21dc6ece5 # v6 + + - name: Publish and verify Maven artifacts + if: steps.github_release.outputs.draft == 'false' + env: + GRADLE_USER_HOME: ${{ runner.temp }}/release-gradle-home + KEY: ${{ secrets.JAR_SIGNING_KEY }} + KEY_ID: ${{ secrets.JAR_SIGNING_KEY_ID }} + KEY_PASSWORD: ${{ secrets.JAR_SIGNING_KEY_PASSWORD }} + MAVEN_RETRY_REQUIRED: ${{ steps.github_release.outputs.maven_retry_required }} + MAVEN_RETRY_COMMIT: ${{ vars.MAVEN_RETRY_COMMIT }} + RH_PASSWORD: ${{ secrets.RH_PASSWORD }} + RH_USER: ${{ secrets.RH_USER }} + run: .github/scripts/release.sh publish-maven diff --git a/gradle/publishing.gradle b/gradle/publishing.gradle index fdde671b93..7de6d16287 100644 --- a/gradle/publishing.gradle +++ b/gradle/publishing.gradle @@ -1,4 +1,21 @@ +import java.time.Duration + +def releaseCommit = rootProject.findProperty('releaseCommit')?.toString()?.toLowerCase() +if (releaseCommit != null && !(releaseCommit ==~ /^[0-9a-f]{40}$/)) { + throw new GradleException( + "Invalid releaseCommit '${releaseCommit}'. Expected a full 40-character commit SHA.") +} + nexusPublishing { + if (releaseCommit != null) { + repositoryDescription = "sdk-java:${releaseCommit}" + } + transitionCheckOptions { + // Central transitions can take several minutes. + maxRetries = 180 + delayBetween = Duration.ofSeconds(10) + } + // to release to sonatype use ./gradlew publishToSonatype repositories { sonatype { @@ -53,6 +70,9 @@ subprojects { connection = 'scm:git@github.com:temporalio/sdk-java.git' developerConnection = 'scm:git@github.com:temporalio/sdk-java.git' url = 'https://github.com/temporalio/sdk-java.git' + if (releaseCommit != null) { + tag = releaseCommit + } } licenses { diff --git a/gradle/versioning.gradle b/gradle/versioning.gradle index 7cdddffae3..e4017d5234 100644 --- a/gradle/versioning.gradle +++ b/gradle/versioning.gradle @@ -21,6 +21,15 @@ ext.getTag = { -> // 0.20.2-RC1-g000a42a -> 0.20.2-SNAPSHOT // 0.20.2-RC1-somepostfix-g000a42a -> 0.20.2-SNAPSHOT ext.getVersionName = { -> + if (rootProject.hasProperty('releaseVersion')) { + String releaseVersion = rootProject.property('releaseVersion').toString() + if (!(releaseVersion ==~ /^\d+[.]\d+[.]\d+(?:-RC\d+)?$/)) { + throw new GradleException( + "Invalid releaseVersion '${releaseVersion}'. Expected X.Y.Z or X.Y.Z-RCN.") + } + return releaseVersion + } + String tag = getTag() // The last element of describe should start with g according to git describe format @@ -57,4 +66,4 @@ version = getVersionName() subprojects { group = 'io.temporal' version = getVersionName() -} \ No newline at end of file +}