This is an automated email from the ASF dual-hosted git repository.

XiaoHongbo-Hope pushed a commit to branch codex/prepare-0.4.0-release
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git

commit 64f74ee06687be39ccec1d1552e16ec23776f585
Author: xiaohongbo <[email protected]>
AuthorDate: Thu Sep 3 17:59:48 2026 +0800

    build: harden the release process
---
 .asf.yaml                                         |   2 +-
 .github/workflows/release-go-binding.yml          | 165 ++++++++++++++++++----
 .github/workflows/release-rust.yml                |   2 +-
 bindings/go/RELEASE.md                            |  39 +++--
 docs/src/release/creating-a-release.md            |  21 +--
 docs/src/release/verifying-a-release-candidate.md |   2 +-
 scripts/release.sh                                |  64 +++++++--
 7 files changed, 233 insertions(+), 62 deletions(-)

diff --git a/.asf.yaml b/.asf.yaml
index 33ebc904..e2aba478 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -47,7 +47,7 @@ github:
       branches:
         includes:
           - "~DEFAULT_BRANCH"
-          - "release/*"
+          - "release-*"
           - "rel/*"
         excludes: []
       bypass_teams:
diff --git a/.github/workflows/release-go-binding.yml 
b/.github/workflows/release-go-binding.yml
index 455b0ae0..f9868dfa 100644
--- a/.github/workflows/release-go-binding.yml
+++ b/.github/workflows/release-go-binding.yml
@@ -38,6 +38,10 @@ on:
 permissions:
   contents: read
 
+concurrency:
+  group: ${{ github.workflow }}-${{ inputs.version || github.ref_name }}
+  cancel-in-progress: false
+
 jobs:
   validate:
     runs-on: ubuntu-latest
@@ -46,6 +50,7 @@ jobs:
       source_ref: ${{ steps.meta.outputs.source_ref }}
       tag: ${{ steps.meta.outputs.tag }}
       source_sha: ${{ steps.meta.outputs.source_sha }}
+      tag_object: ${{ steps.meta.outputs.tag_object }}
     steps:
       - uses: actions/checkout@v7
         with:
@@ -55,44 +60,91 @@ jobs:
       - name: Validate release version and tag
         id: meta
         shell: bash
+        env:
+          INPUT_VERSION: ${{ inputs.version }}
+          INPUT_SOURCE_REF: ${{ inputs.source_ref }}
         run: |
-          event_name='${{ github.event_name }}'
+          set -euo pipefail
+          event_name="${GITHUB_EVENT_NAME}"
           version=''
           source_ref=''
 
           if [[ "$event_name" == "workflow_dispatch" ]]; then
-            version='${{ inputs.version }}'
-            source_ref='${{ inputs.source_ref }}'
+            version="$INPUT_VERSION"
+            source_ref="$INPUT_SOURCE_REF"
           else
-            tag_name='${{ github.ref_name }}'
-            source_ref='${{ github.sha }}'
+            tag_name="$GITHUB_REF_NAME"
+            source_ref="$GITHUB_SHA"
             version="$tag_name"
           fi
 
-          if [[ ! "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$ 
]]; then
-            echo "invalid version: $version; expected vX.Y.Z or vX.Y.Z-rc.N" 
>&2
+          if [[ ! "$version" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)(-rc[1-9][0-9]*)?$ 
]]; then
+            echo "invalid version: $version; expected vX.Y.Z or vX.Y.Z-rcN" >&2
             exit 1
           fi
+          tag_version="${BASH_REMATCH[1]}"
           if [[ -z "$source_ref" ]]; then
             echo "source_ref must not be empty" >&2
             exit 1
           fi
 
-          git rev-parse --verify "$source_ref^{commit}" >/dev/null 2>&1 || {
+          source_sha=$(git rev-parse --verify "$source_ref^{commit}") || {
             echo "source_ref does not resolve to a commit: $source_ref" >&2
             exit 1
           }
 
-          tag="bindings/go/$version"
-          if git rev-parse "$tag" >/dev/null 2>&1; then
-            echo "tag already exists: $tag" >&2
+          workspace_version=$(
+            cargo metadata --locked --format-version 1 --no-deps |
+              jq -er '.packages[] | select(.name == "paimon") | .version'
+          )
+          if [[ "$tag_version" != "$workspace_version" ]]; then
+            echo "version $tag_version does not match workspace 
$workspace_version" >&2
             exit 1
           fi
 
+          tag="bindings/go/$version"
+          tag_ref="refs/tags/$tag"
+          tag_object=''
+          if git show-ref --verify --quiet "$tag_ref"; then
+            tag_object=$(git rev-parse "$tag_ref")
+            if [[ "$(git cat-file -t "$tag_ref")" != "tag" ]]; then
+              echo "existing release tag is not annotated: $tag" >&2
+              exit 1
+            fi
+            tag_commit=$(git rev-parse "$tag_ref^{commit}")
+            tag_line=$(git rev-list --parents -n 1 "$tag_commit")
+            if [[ "$tag_commit" != "$source_sha" && "$tag_line" != 
"$tag_commit $source_sha" ]]; then
+              echo "existing tag $tag is not based on $source_sha" >&2
+              exit 1
+            fi
+            if [[ "$tag_commit" != "$source_sha" ]]; then
+              while IFS= read -r path; do
+                case "$path" in
+                  bindings/go/LICENSE | \
+                    bindings/go/NOTICE | \
+                    bindings/go/libpaimon_c.linux.amd64.so.zst | \
+                    bindings/go/libpaimon_c.linux.arm64.so.zst | \
+                    bindings/go/libpaimon_c.darwin.amd64.dylib.zst | \
+                    bindings/go/libpaimon_c.darwin.arm64.dylib.zst | \
+                    bindings/go/THIRD-PARTY-LICENSES.linux.amd64.html | \
+                    bindings/go/THIRD-PARTY-LICENSES.linux.arm64.html | \
+                    bindings/go/THIRD-PARTY-LICENSES.darwin.amd64.html | \
+                    bindings/go/THIRD-PARTY-LICENSES.darwin.arm64.html) ;;
+                  *)
+                    echo "existing tag $tag changes unexpected path: $path" >&2
+                    exit 1
+                    ;;
+                esac
+              done < <(git diff-tree --no-commit-id --name-only -r 
"$source_sha" "$tag_commit")
+            fi
+            echo "existing tag $tag is based on the requested source; the 
release can resume"
+          fi
+
           echo "tag=$tag" >> "$GITHUB_OUTPUT"
           echo "version=$version" >> "$GITHUB_OUTPUT"
           echo "source_ref=$source_ref" >> "$GITHUB_OUTPUT"
-          echo "source_sha=$(git rev-parse "$source_ref^{commit}")" >> 
"$GITHUB_OUTPUT"
+          echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
+          echo "tag_object=$tag_object" >> "$GITHUB_OUTPUT"
 
   build:
     needs: validate
@@ -236,16 +288,50 @@ jobs:
           python3 scripts/verify_go_release_artifacts.py --artifacts-dir 
release/go
 
       - name: Create release tag commit
+        id: tag_state
         env:
           TAG: ${{ needs.validate.outputs.tag }}
           VERSION: ${{ needs.validate.outputs.version }}
           SOURCE_REF: ${{ needs.validate.outputs.source_ref }}
           SOURCE_SHA: ${{ needs.validate.outputs.source_sha }}
+          EXPECTED_TAG_OBJECT: ${{ needs.validate.outputs.tag_object }}
         shell: bash
         run: |
+          set -euo pipefail
           git config user.name 'github-actions[bot]'
           git config user.email 
'41898282+github-actions[bot]@users.noreply.github.com'
 
+          release_files=(
+            LICENSE
+            NOTICE
+            libpaimon_c.linux.amd64.so.zst
+            libpaimon_c.linux.arm64.so.zst
+            libpaimon_c.darwin.amd64.dylib.zst
+            libpaimon_c.darwin.arm64.dylib.zst
+            THIRD-PARTY-LICENSES.linux.amd64.html
+            THIRD-PARTY-LICENSES.linux.arm64.html
+            THIRD-PARTY-LICENSES.darwin.amd64.html
+            THIRD-PARTY-LICENSES.darwin.arm64.html
+          )
+          tag_ref="refs/tags/$TAG"
+          current_tag_object=''
+          if git show-ref --verify --quiet "$tag_ref"; then
+            current_tag_object=$(git rev-parse "$tag_ref")
+          fi
+          if [[ "$current_tag_object" != "$EXPECTED_TAG_OBJECT" ]]; then
+            echo "tag $TAG changed after validation" >&2
+            exit 1
+          fi
+          if [[ -n "$EXPECTED_TAG_OBJECT" ]]; then
+            echo "Reusing artifacts from existing tag: $TAG"
+            for file in "${release_files[@]}"; do
+              git show "${EXPECTED_TAG_OBJECT}^{commit}:bindings/go/${file}" > 
"release/go/${file}"
+            done
+            python3 scripts/verify_go_release_artifacts.py --artifacts-dir 
release/go
+            echo "tag_object=$EXPECTED_TAG_OBJECT" >> "$GITHUB_OUTPUT"
+            exit 0
+          fi
+
           cp release/go/libpaimon_c.*.zst bindings/go/
           cp \
             release/go/LICENSE \
@@ -264,24 +350,55 @@ jobs:
 
           git tag -a "$TAG" -m "Release Go binding ${VERSION} from 
${SOURCE_REF}@${SOURCE_SHA}"
           git push origin "refs/tags/$TAG"
+          tag_object=$(git rev-parse "refs/tags/$TAG")
+          echo "tag_object=$tag_object" >> "$GITHUB_OUTPUT"
 
       - name: Create GitHub release
         env:
           TAG: ${{ needs.validate.outputs.tag }}
           VERSION: ${{ needs.validate.outputs.version }}
+          TAG_OBJECT: ${{ steps.tag_state.outputs.tag_object }}
           GH_TOKEN: ${{ github.token }}
         working-directory: release/go
         run: |
-          gh release create "$TAG" \
-            --title "Release Go binding $VERSION" \
-            --generate-notes \
-            LICENSE \
-            NOTICE \
-            libpaimon_c.linux.amd64.so.zst \
-            libpaimon_c.linux.arm64.so.zst \
-            libpaimon_c.darwin.amd64.dylib.zst \
-            libpaimon_c.darwin.arm64.dylib.zst \
-            THIRD-PARTY-LICENSES.linux.amd64.html \
-            THIRD-PARTY-LICENSES.linux.arm64.html \
-            THIRD-PARTY-LICENSES.darwin.amd64.html \
+          set -euo pipefail
+          remote_tag_object=$(
+            git ls-remote --refs origin "refs/tags/$TAG" |
+              awk 'NR == 1 { print $1 }'
+          )
+          if [[ "$remote_tag_object" != "$TAG_OBJECT" ]]; then
+            echo "remote tag $TAG changed before GitHub Release creation" >&2
+            exit 1
+          fi
+          release_files=(
+            LICENSE
+            NOTICE
+            libpaimon_c.linux.amd64.so.zst
+            libpaimon_c.linux.arm64.so.zst
+            libpaimon_c.darwin.amd64.dylib.zst
+            libpaimon_c.darwin.arm64.dylib.zst
+            THIRD-PARTY-LICENSES.linux.amd64.html
+            THIRD-PARTY-LICENSES.linux.arm64.html
+            THIRD-PARTY-LICENSES.darwin.amd64.html
             THIRD-PARTY-LICENSES.darwin.arm64.html
+          )
+          prerelease=false
+          release_flags=(--verify-tag --latest=false)
+          if [[ "$VERSION" == *-rc* ]]; then
+            prerelease=true
+            release_flags+=(--prerelease)
+          fi
+          if gh release view "$TAG" >/dev/null 2>&1; then
+            echo "Updating assets on existing GitHub release: $TAG"
+            gh release edit "$TAG" \
+              --title "Release Go binding $VERSION" \
+              --prerelease="$prerelease" \
+              --latest=false
+            gh release upload "$TAG" "${release_files[@]}" --clobber
+          else
+            gh release create "$TAG" \
+              --title "Release Go binding $VERSION" \
+              --generate-notes \
+              "${release_flags[@]}" \
+              "${release_files[@]}"
+          fi
diff --git a/.github/workflows/release-rust.yml 
b/.github/workflows/release-rust.yml
index a71f0f3a..f7d58233 100644
--- a/.github/workflows/release-rust.yml
+++ b/.github/workflows/release-rust.yml
@@ -49,7 +49,7 @@ jobs:
         shell: bash
         run: |
           set -euo pipefail
-          if [[ ! "${GITHUB_REF_NAME}" =~ 
^v([0-9]+\.[0-9]+\.[0-9]+)(-rc[0-9]+)?$ ]]; then
+          if [[ ! "${GITHUB_REF_NAME}" =~ 
^v([0-9]+\.[0-9]+\.[0-9]+)(-rc[1-9][0-9]*)?$ ]]; then
             echo "::error::invalid release tag: ${GITHUB_REF_NAME}"
             exit 1
           fi
diff --git a/bindings/go/RELEASE.md b/bindings/go/RELEASE.md
index 2c1c19f6..668ad17b 100644
--- a/bindings/go/RELEASE.md
+++ b/bindings/go/RELEASE.md
@@ -44,19 +44,19 @@ In practice, the release relationship should look like this:
 3. build the Go embedded libraries from the same baseline,
 4. create a Go release commit that contains the generated `libpaimon_c.*.zst`
    files, and
-5. tag that commit as `bindings/go/vX.Y.Z` or `bindings/go/vX.Y.Z-rc.N`.
+5. tag that commit as `bindings/go/vX.Y.Z` or `bindings/go/vX.Y.Z-rcN`.
 
 This keeps a clear link to the Rust release baseline while allowing the Go
 submodule tag to contain the embedded artifacts required by Go module 
consumers.
 
 ## Recommended Sequence
 
-- For an ASF-style release process, first complete the source release steps for
-  the Rust project.
-- After the corresponding release candidate has been approved, run the Go
-  binding release workflow to publish the convenience Go module tag.
-- If you publish release candidates for the Go binding, base them on the same
-  reviewed Rust release-candidate commit.
+- For an ASF-style release process, first prepare the Rust source release.
+- Pushing a root Rust RC tag automatically publishes the Go RC convenience tag
+  from the same source commit. After the RC is approved, pushing the final root
+  tag does the same for the final Go version.
+- For a manual recovery or standalone Go release, use the same reviewed Rust
+  release commit or immutable tag as `source_ref`.
 
 ## Before Release
 
@@ -64,9 +64,11 @@ submodule tag to contain the embedded artifacts required by 
Go module consumers.
 - Confirm the target version is decided, for example `v0.1.0`.
 - Confirm the Rust release baseline commit for this Go release is recorded.
 - Decide the exact `source_ref` to pass into the release workflow.
+- Confirm the base version in `version` matches the Paimon workspace version at
+  `source_ref`; after `main` is bumped, it cannot be used to publish the 
previous version.
 - Confirm whether this Go release is based on a Rust GA tag or an approved Rust
   release-candidate commit.
-- Use `vX.Y.Z-rc.N` for release candidates, for example `v0.1.0-rc.1`.
+- Use `vX.Y.Z-rcN` for release candidates, for example `v0.1.0-rc1`.
 - Use `vX.Y.Z` for the final general-availability release.
 - Confirm the version has not already been used as a Go submodule tag:
   `bindings/go/v0.1.0`.
@@ -82,10 +84,12 @@ submodule tag to contain the embedded artifacts required by 
Go module consumers.
 
 ## Publish
 
-- Open GitHub Actions and run `Release Go Binding`.
-- Provide the release version, for example `v0.1.0`.
-- Provide `source_ref`, for example `main`, `v0.1.0-rc.1`, or a specific 
commit SHA.
-- For release candidates, publish `bindings/go/vX.Y.Z-rc.N` first and verify 
it before the final tag.
+- For a coordinated Rust release, pushing the root `vX.Y.Z-rcN` or `vX.Y.Z`
+  tag starts `Release Go Binding` automatically with that exact source commit.
+- For a manual recovery or standalone Go release, open GitHub Actions and run
+  `Release Go Binding`. Provide the release version, for example `v0.1.0`, and
+  `source_ref`, for example `main`, `v0.1.0-rc1`, or a specific commit SHA.
+- For release candidates, publish `bindings/go/vX.Y.Z-rcN` first and verify it 
before the final tag.
 - Make sure the workflow is started from the intended Rust release baseline
   branch or commit lineage.
 - Wait for all matrix builds to finish successfully:
@@ -95,7 +99,8 @@ submodule tag to contain the embedded artifacts required by 
Go module consumers.
   - `darwin/arm64`
 - Confirm each build job generates and verifies the target-specific legal 
report.
 - Confirm the workflow creates the annotated tag `bindings/go/v0.1.0`.
-- Confirm the workflow publishes a GitHub release for that tag.
+- Confirm the workflow publishes a GitHub release for that tag, marks an RC
+  release as a pre-release, and does not replace the root project's latest 
release.
 
 ## After Release
 
@@ -130,7 +135,11 @@ import paimon "github.com/apache/paimon-rust/bindings/go"
 ## Rollback Notes
 
 - Do not reuse a broken Go module version once it has been observed externally.
-- Do not delete and recreate an existing `bindings/go/vX.Y.Z-rc.N` or 
`bindings/go/vX.Y.Z` tag.
+- Do not delete and recreate an existing `bindings/go/vX.Y.Z-rcN` or 
`bindings/go/vX.Y.Z` tag.
 - Publish a new patch version instead, for example move from `v0.1.0` to 
`v0.1.1`.
-- If an RC is bad, publish the next RC, for example move from `v0.1.0-rc.1` to 
`v0.1.0-rc.2`.
+- If an RC is bad, publish the next RC, for example move from `v0.1.0-rc1` to 
`v0.1.0-rc2`.
 - If the workflow fails before pushing the tag, fix the issue and rerun with 
the same intended version.
+- If the workflow fails after pushing the tag, rerun it with the same version 
and
+  original source commit SHA or immutable tag. It verifies that the existing 
tag
+  contains only the expected release artifacts on top of that source, restores
+  the tagged artifacts, and resumes GitHub Release creation.
diff --git a/docs/src/release/creating-a-release.md 
b/docs/src/release/creating-a-release.md
index 57553a05..0474b1d9 100644
--- a/docs/src/release/creating-a-release.md
+++ b/docs/src/release/creating-a-release.md
@@ -39,11 +39,11 @@ The release process consists of:
 
 When a version tag is pushed, GitHub Actions automatically publishes the 
language-specific artifacts:
 
-| Component        | Tag Pattern              | Published To      | 
Pre-release (`-rc`) Behavior          |
-|------------------|--------------------------|-------------------|---------------------------------------|
-| Rust crates      | `v0.1.0`                 | crates.io         | Dry-run 
only                          |
-| Python binding   | `v0.1.0`                 | PyPI              | Publishes 
to TestPyPI                 |
-| Go binding       | `v0.1.0`                 | Go module proxy   | Publishes 
as `bindings/go/vX.Y.Z-rcN` |
+| Component        | Root Tag Pattern     | Published To                    | 
Release-candidate Behavior                                 |
+|------------------|----------------------|---------------------------------|------------------------------------------------------------|
+| Rust crates      | `vX.Y.Z[-rcN]`       | crates.io                       | 
Validates packages and performs a dry run only             |
+| Python binding   | `vX.Y.Z[-rcN]`       | PyPI / TestPyPI                 | 
Publishes the corresponding PEP 440 RC version to TestPyPI |
+| Go binding       | `vX.Y.Z[-rcN]`       | Go module tag and GitHub Release | 
Creates `bindings/go/vX.Y.Z-rcN` as a GitHub pre-release   |
 
 The Release Manager's primary responsibility is managing the **source 
release** (tarball + signature) and coordinating the community vote. Language 
artifact publishing is handled by CI once the final tag is pushed.
 
@@ -244,9 +244,12 @@ After pushing, verify in [GitHub 
Actions](https://github.com/apache/paimon-rust/
 
 ### Create source release artifacts
 
-From the repository root (on the release branch, at the commit you tagged):
+From the repository root, check out and verify the exact RC tag before 
building:
 
 ```bash
+git switch --detach ${RC_TAG}
+git tag -v ${RC_TAG}
+test "$(git rev-parse HEAD)" = "$(git rev-parse "${RC_TAG}^{commit}")"
 ./scripts/release.sh ${RELEASE_VERSION}
 ```
 
@@ -256,7 +259,7 @@ This creates the following under `dist/`:
 - `paimon-rust-${RELEASE_VERSION}.tar.gz.asc` — GPG signature
 - `paimon-rust-${RELEASE_VERSION}.tar.gz.sha512` — SHA-512 checksum
 
-The script automatically generates the archive from `HEAD` via `git archive`, 
signs it with your GPG key, and verifies the signature.
+The script verifies that the version matches `Cargo.toml` at `HEAD` and that 
`HEAD` has an exact signed release tag. It then generates the archive via `git 
archive`, signs it with the Git-configured signing key when available, and 
verifies the signature.
 
 ### Stage artifacts to SVN
 
@@ -266,7 +269,7 @@ Upload the source release to the ASF dev area:
 svn checkout https://dist.apache.org/repos/dist/dev/paimon/ paimon-dist-dev 
--depth=immediates
 cd paimon-dist-dev
 mkdir paimon-rust-${RELEASE_VERSION}-rc${RC_NUM}
-cp ../paimon-rust-${RELEASE_VERSION}.tar.gz* 
paimon-rust-${RELEASE_VERSION}-rc${RC_NUM}/
+cp ../dist/paimon-rust-${RELEASE_VERSION}.tar.gz* 
paimon-rust-${RELEASE_VERSION}-rc${RC_NUM}/
 svn add paimon-rust-${RELEASE_VERSION}-rc${RC_NUM}
 svn commit -m "Add paimon-rust ${RELEASE_VERSION} RC${RC_NUM}"
 ```
@@ -297,7 +300,7 @@ The release candidate is available at:
 
https://dist.apache.org/repos/dist/dev/paimon/paimon-rust-${RELEASE_VERSION}-rc${RC_NUM}/
 
 Git tag:
-https://github.com/apache/paimon-rust/releases/tag/${RC_TAG}
+https://github.com/apache/paimon-rust/tree/${RC_TAG}
 
 KEYS for signature verification:
 https://downloads.apache.org/paimon/KEYS
diff --git a/docs/src/release/verifying-a-release-candidate.md 
b/docs/src/release/verifying-a-release-candidate.md
index b8c5c896..736eeecb 100644
--- a/docs/src/release/verifying-a-release-candidate.md
+++ b/docs/src/release/verifying-a-release-candidate.md
@@ -89,7 +89,7 @@ cd paimon-rust-${RELEASE_VERSION}
 Build the workspace:
 
 ```bash
-cargo build --workspace --release
+cargo build --locked --workspace --release
 ```
 
 For Python binding, see `bindings/python/`. For Go binding, see `bindings/go/`.
diff --git a/scripts/release.sh b/scripts/release.sh
index 6c95f089..67edfc2c 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -21,23 +21,60 @@
 #
 # Run from repo root. Check out the release tag first (e.g. git checkout 
v0.1.0-rc1).
 # Usage: ./scripts/release.sh [version]
-#   If version is omitted, it is read from Cargo.toml 
(workspace.package.version).
+#   If version is omitted, it is read from Cargo.toml at HEAD 
(workspace.package.version).
 
-set -e
+set -euo pipefail
 
 REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
 cd "$REPO_ROOT"
 
-if [ -n "$1" ]; then
-  VERSION="$1"
-else
-  VERSION=$(grep -E '^version\s*=' Cargo.toml | head -1 | sed 
's/.*"\([^"]*\)".*/\1/')
-  if [ -z "$VERSION" ]; then
-    echo "Could not read version from Cargo.toml. Pass version as argument: $0 
<version>"
-    exit 1
-  fi
+if [ "$#" -gt 1 ]; then
+  echo "Usage: $0 [version]"
+  exit 1
+fi
+
+WORKSPACE_VERSION=$(
+  git show HEAD:Cargo.toml |
+    awk '
+      /^\[workspace\.package\]$/ { in_workspace_package = 1; next }
+      in_workspace_package && /^\[/ { exit }
+      in_workspace_package && /^version[[:space:]]*=/ {
+        if (match($0, /"[^"]+"/)) {
+          print substr($0, RSTART + 1, RLENGTH - 2)
+          exit
+        }
+      }
+    '
+)
+if [ -z "$WORKSPACE_VERSION" ]; then
+  echo "Could not read workspace version from Cargo.toml at HEAD"
+  exit 1
+fi
+
+VERSION="${1:-$WORKSPACE_VERSION}"
+if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+  echo "Invalid release version: ${VERSION}; expected X.Y.Z"
+  exit 1
+fi
+if [ "$VERSION" != "$WORKSPACE_VERSION" ]; then
+  echo "Release version ${VERSION} does not match workspace 
${WORKSPACE_VERSION} at HEAD"
+  exit 1
 fi
 
+VERSION_PATTERN=${VERSION//./\\.}
+RELEASE_TAG=$(
+  git tag --points-at HEAD |
+    grep -E "^v${VERSION_PATTERN}(-rc[1-9][0-9]*)?$" |
+    head -1 || true
+)
+if [ -z "$RELEASE_TAG" ]; then
+  echo "HEAD must have an exact v${VERSION} or v${VERSION}-rcN release tag"
+  exit 1
+fi
+
+echo "Verifying signed release tag: ${RELEASE_TAG}"
+git tag -v "$RELEASE_TAG"
+
 PREFIX="paimon-rust-${VERSION}"
 DIST_DIR="${REPO_ROOT}/dist"
 TARBALL="${PREFIX}.tar.gz"
@@ -56,7 +93,12 @@ else
 fi
 
 echo "Signing with GPG: ${TARBALL}.asc"
-(cd "$DIST_DIR" && gpg --armor --detach-sig "$TARBALL")
+SIGNING_KEY=$(git config --get user.signingkey || true)
+if [ -n "$SIGNING_KEY" ]; then
+  (cd "$DIST_DIR" && gpg --local-user "$SIGNING_KEY" --armor --detach-sig 
"$TARBALL")
+else
+  (cd "$DIST_DIR" && gpg --armor --detach-sig "$TARBALL")
+fi
 
 echo "Verifying signature"
 (cd "$DIST_DIR" && gpg --verify "${TARBALL}.asc" "$TARBALL")

Reply via email to