HyukjinKwon commented on code in PR #84:
URL: https://github.com/apache/spark-connect-rust/pull/84#discussion_r3886002728


##########
.github/workflows/release.yml:
##########
@@ -233,28 +303,234 @@ jobs:
           name: wheels-sdist
           path: dist/*.tar.gz
 
-  publish-wheels:
-    name: Publish wheel + sdist to PyPI
-    needs: [build-wheels, build-sdist]
-    if: github.event_name == 'push' || (github.event_name == 
'workflow_dispatch' && inputs.dry_run == false)
+  # ---- Dry run: rehearse the crates.io packaging without publishing ----
+  dryrun-crates:
+    name: Dry-run crates.io packaging
+    needs: resolve
+    if: needs.resolve.outputs.mode == 'dryrun'
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/setup-python@v5
+      - uses: actions/checkout@v4
         with:
-          python-version: "3.11"
+          repository: ${{ env.UPSTREAM_REPO }}
+          ref: ${{ needs.resolve.outputs.sha }}
+      - name: Install Rust toolchain
+        run: |
+          rustup toolchain install stable --profile minimal
+          rustup default stable
+      - name: Install protoc
+        run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
+      - name: cargo publish --dry-run (dependency-ordered)
+        run: |
+          set -euo pipefail
+          cargo publish --dry-run \
+            -p apache-spark-connect-proto \
+            -p apache-spark-connect-core \
+            -p apache-spark-connect
+
+  # ---- Cut an RC: tag + GitHub pre-release with the artifacts (no 
crates.io/PyPI) ----
+  publish-rc:
+    name: Cut RC GitHub pre-release
+    needs: [resolve, build-wheels, build-sdist]
+    if: needs.resolve.outputs.mode == 'rc'
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write  # push the RC tag + create the GitHub Release on the 
fork
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          repository: ${{ env.UPSTREAM_REPO }}
+          ref: ${{ needs.resolve.outputs.sha }}
+          fetch-depth: 0  # full history so the tag push carries all reachable 
objects
       - uses: actions/download-artifact@v4
         with:
-          # Matches every wheels-<target> artifact plus wheels-sdist.
           pattern: wheels-*
           path: dist
           merge-multiple: true
-      - name: Publish to PyPI
-        # Fork-based release: authenticate with the committer's PyPI token via 
twine
-        # (avoids third-party publish actions; ASF Actions policy allowlists 
actions/*).
+      - name: Create and push the RC tag to this fork
+        env:
+          GH_TOKEN: ${{ github.token }}
+          TAG: ${{ needs.resolve.outputs.rc_tag }}
+          VERSION: ${{ needs.resolve.outputs.version }}
+        run: |
+          set -euo pipefail
+          git config user.name "${{ github.actor }}"
+          git config user.email "${{ github.actor }}@users.noreply.github.com"
+          if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
+            echo "Local tag ${TAG} already exists; reusing."
+          else
+            git tag -a "${TAG}" -m "Release candidate ${TAG} of 
pyspark-client-rust ${VERSION}"
+          fi
+          # Push the RC tag (and the objects it reaches) to THIS fork, so the 
GitHub
+          # pre-release below can hang off it. Force so re-cutting the same RC 
is idempotent.
+          git push --force 
"https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"; 
"refs/tags/${TAG}"
+      - name: Create the GitHub pre-release with the artifacts
+        env:
+          GH_TOKEN: ${{ github.token }}
+          TAG: ${{ needs.resolve.outputs.rc_tag }}
+          VERSION: ${{ needs.resolve.outputs.version }}
+        run: |
+          set -euo pipefail
+          ls -l dist
+          NOTES="Release candidate ${TAG} of pyspark-client-rust 
${VERSION}."$'\n\n'"> Not published to PyPI or crates.io. Install directly from 
a wheel/sdist asset below, e.g. \`pip install <asset-url>\`. This pre-release 
is deleted when the RC is finalized (or dropped)."
+          # Idempotent: a re-run updates the existing release and clobbers its 
assets.
+          if gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" >/dev/null 
2>&1; then
+            gh release upload "${TAG}" dist/* --repo "${GITHUB_REPOSITORY}" 
--clobber
+          else
+            gh release create "${TAG}" dist/* --repo "${GITHUB_REPOSITORY}" \
+              --prerelease --title "${TAG}" --notes "${NOTES}"
+          fi
+
+  # ---- Finalize: promote the named RC into the official release 
(IRREVERSIBLE) ----
+  finalize:
+    name: Finalize release to PyPI + crates.io (IRREVERSIBLE)
+    needs: resolve
+    if: needs.resolve.outputs.mode == 'finalize'
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write  # delete the RC pre-releases + tags on the fork
+    steps:
+      - name: Abort window
+        run: |
+          echo 
"=============================================================================="
+          echo " CONVERTING RC ${{ needs.resolve.outputs.rc_tag }} INTO THE 
OFFICIAL RELEASE"
+          echo " ${{ needs.resolve.outputs.version }} -> PyPI + crates.io. 
THIS IS IRREVERSIBLE."
+          echo " Cancel this workflow now if you did not intend to finalize."
+          echo " Continuing in 60 seconds..."
+          echo 
"=============================================================================="
+          sleep 60
+      - name: Verify the RC pre-release exists
+        env:
+          GH_TOKEN: ${{ github.token }}
+          TAG: ${{ needs.resolve.outputs.rc_tag }}
+        run: |
+          set -euo pipefail
+          if ! gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" 
>/dev/null 2>&1; then
+            echo "::error::RC GitHub pre-release ${TAG} not found on 
${GITHUB_REPOSITORY}. Cut the RC first."
+            exit 1
+          fi
+      - name: Check out the RC tag (the exact released source)
+        uses: actions/checkout@v4
+        with:
+          # The RC tag lives on the fork where the RC was cut (this 
repository).
+          repository: ${{ github.repository }}
+          ref: ${{ needs.resolve.outputs.rc_tag }}
+          fetch-depth: 0
+      - name: Validate the RC source versions
+        run: |
+          set -euo pipefail
+          V="${{ needs.resolve.outputs.version }}"
+          CARGO_VER=$(grep -m1 '^version' Cargo.toml | awk -F'"' '{print $2}')
+          PYPROJECT_VER=$(grep -m1 '^version' pyproject.toml | awk -F'"' 
'{print $2}')
+          echo "RC tag source: Cargo=${CARGO_VER} pyproject=${PYPROJECT_VER}; 
release-version=${V}"
+          # Cargo and pyproject must both equal the release-version (agreement 
+ expected value).
+          if [ "$CARGO_VER" != "$V" ] || [ "$PYPROJECT_VER" != "$V" ]; then
+            echo "::error::RC tag ${{ needs.resolve.outputs.rc_tag }} declares 
Cargo=${CARGO_VER} / pyproject=${PYPROJECT_VER}, not ${V}."
+            exit 1
+          fi
+          # Path-dep pins must match, or `cargo publish` ships a stale hard 
dep (same check as resolve).
+          BAD=$(grep -rEn 'path = "\.\./spark-connect[^"]*"' 
crates/*/Cargo.toml \
+                | grep -E 'version = "' \
+                | grep -v "version = \"${CARGO_VER}\"" || true)
+          if [ -n "$BAD" ]; then
+            echo "::error::internal path-dep version pin(s) in the RC source 
do not match ${CARGO_VER}:"
+            echo "$BAD"
+            exit 1
+          fi
+      - uses: actions/setup-python@v5
+        with:
+          python-version: "3.11"
+      - name: Download the RC artifacts (the exact tested wheels + sdist)
+        env:
+          GH_TOKEN: ${{ github.token }}
+          TAG: ${{ needs.resolve.outputs.rc_tag }}
+        run: |
+          set -euo pipefail
+          mkdir -p dist
+          gh release download "${TAG}" --repo "${GITHUB_REPOSITORY}" --dir 
dist --pattern '*'
+          ls -l dist
+      - name: Publish wheels + sdist to PyPI
         env:
           TWINE_USERNAME: __token__
           TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
         run: |
+          set -euo pipefail
           python -m pip install --upgrade twine
           # --skip-existing makes a re-run after a partial upload safe.
           twine upload --skip-existing dist/*
+      - name: Install Rust toolchain
+        run: |
+          rustup toolchain install stable --profile minimal
+          rustup default stable
+      - name: Install protoc
+        run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
+      - name: Publish crates to crates.io (from the identical RC source)
+        env:
+          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+        run: |
+          set -euo pipefail
+          CRATES=(apache-spark-connect-proto apache-spark-connect-core 
apache-spark-connect)
+          VER="${{ needs.resolve.outputs.version }}"
+          # Skip any crate already on crates.io at this version so a re-run is 
idempotent.
+          pkgs=()
+          for c in "${CRATES[@]}"; do
+            if curl -sf -H "User-Agent: spark-connect-rust-release 
([email protected])" \
+                 "https://crates.io/api/v1/crates/${c}/${VER}"; >/dev/null; then
+              echo "${c} ${VER} is already on crates.io — skipping"
+            else
+              pkgs+=(-p "$c")
+            fi
+          done
+          if [ ${#pkgs[@]} -eq 0 ]; then
+            echo "All crates already published at ${VER}; nothing to do."
+          else
+            # cargo publishes in dependency order and waits for the index 
between crates.
+            cargo publish "${pkgs[@]}"
+          fi
+      - name: Tag the released commit with the permanent v<version> tag
+        env:
+          GH_TOKEN: ${{ github.token }}
+          TAG: v${{ needs.resolve.outputs.version }}
+          VERSION: ${{ needs.resolve.outputs.version }}
+        run: |
+          set -euo pipefail
+          git config user.name "${{ github.actor }}"
+          git config user.email "${{ github.actor }}@users.noreply.github.com"
+          # HEAD is the RC tag's commit (the exact released source), checked 
out above.
+          if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
+            echo "Local tag ${TAG} already exists; reusing."
+          else
+            git tag -a "${TAG}" -m "pyspark-client-rust ${VERSION}"
+          fi
+          # Push the permanent release tag to this fork. NOT forced: a release 
tag is
+          # immutable, so if ${TAG} already points at this commit the push is 
a no-op,
+          # and if it somehow exists elsewhere we must not silently move it.
+          if git push 
"https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"; 
"refs/tags/${TAG}"; then

Review Comment:
   **The `else` masks every `git push` failure as "tag already exists" (same 
bug-class you just fixed for `gh release list`).** Any non-zero exit — a 
genuine "already exists at a different commit" rejection, but also a transient 
network/auth failure — falls into the `else` and prints "already exists ... 
leaving it untouched", and the step (and finalize) reports success. Two 
consequences: (1) a stale `v${VERSION}` tag pointing at a *different* commit is 
silently accepted while PyPI/crates.io have just received *this* commit's 
artifacts — the git tag then marks the wrong source; (2) a network blip is 
reported as success with no tag pushed. Consider checking whether the remote 
tag exists and points at `HEAD` first (`git ls-remote --tags`), erroring on a 
mismatch, and only treating the exact-same-commit case as a no-op.



##########
.github/workflows/release.yml:
##########
@@ -57,118 +95,150 @@ concurrency:
   group: release
   cancel-in-progress: false
 
+env:
+  # The release source is always fetched from this canonical apache repository.
+  UPSTREAM_REPO: apache/spark-connect-rust
+
 jobs:
-  validate-version:
-    name: Validate version
+  resolve:
+    name: Resolve release mode
     runs-on: ubuntu-latest
+    # In the canonical apache repo, only dry runs are allowed (release-version 
and
+    # rc-count empty). A fork may perform real runs. This mirrors apache/spark.
+    # NOTE: the `env` context is not available in a job-level `if:`, so the 
canonical
+    # repo is spelled out literally here; it must match `env.UPSTREAM_REPO` 
below.
+    if: >-

Review Comment:
   **Minor:** dispatching from `apache/spark-connect-rust` *with* a 
`release-version` makes this `if:` false, so `resolve` and every job that needs 
it are skipped — the run shows as a green, all-skipped no-op with nothing 
explaining that real releases must come from a fork. (The dry-run path from 
apache still works.) This mirrors apache/spark, so it may be intentional; if 
you want a clearer signal, a tiny always-run guard step that fails with an 
explanatory message when `github.repository == 'apache/spark-connect-rust' && 
release-version != ''` would save a confused committer.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to