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

HyukjinKwon pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark-connect-rust.git


The following commit(s) were added to refs/heads/master by this push:
     new 04be01a  [SPARK-59077][INFRA] Make the release workflow safe for a 
direct release
04be01a is described below

commit 04be01ab5e52e451aa10268f96581581ba8e9c2b
Author: Hyukjin Kwon <[email protected]>
AuthorDate: Fri Aug 28 16:52:10 2026 +0900

    [SPARK-59077][INFRA] Make the release workflow safe for a direct release
    
    ## What changes were proposed in this pull request?
    
    Hardens `.github/workflows/release.yml` so a committer can cut a release 
directly (via a `v*` tag push or a `dry_run=false` dispatch) without the risk 
of a botched, irreversible publish.
    
    - **Dry-run now actually rehearses the crate publish.** Previously a 
`dry_run=true` dispatch only built wheels — the crate publish was never 
exercised, so the first proof that `cargo publish` worked was the real, 
irreversible release. `publish-crates` now always runs and, in dry-run, 
executes `cargo publish --dry-run -p apache-spark-connect-proto -p 
apache-spark-connect-core -p apache-spark-connect`. cargo (>= 1.90) orders and 
verify-builds these together against the local workspace,  [...]
    - **Idempotent re-runs.** The real crate publish skips any crate whose 
current version is already on crates.io, and wheels/sdist upload with `twine 
upload --skip-existing`. A re-run after a partial failure no longer aborts on 
"already exists".
    - **Portable, complete artifacts.** Wheels are built from a target matrix: 
Linux `x86_64` + `aarch64` cross-linked against an old glibc via `--zig` 
(manylinux2014), macOS `universal2`, and Windows `x64`, plus a source 
distribution (sdist) as the install fallback. This replaces the single 
`ubuntu-latest` wheel whose glibc floor tracked the runner (and which PyPI 
could reject), and the arm64-only macOS wheel.
    - **`concurrency` guard** so two releases never overlap and a publish in 
flight is never cancelled.
    - **Stronger version gate.** `validate-version` now also asserts the 
internal path-dep version pins (`spark-connect-core`/`spark-connect` → `proto`) 
track the workspace version.
    - **Crate metadata.** The three published crates inherit 
`repository`/`homepage`/`authors` from the workspace, fixing the `manifest has 
no ... repository` warning so the crates.io pages link back to the repo.
    
    The workflow stays first-party-actions-only (`actions/*` + 
`twine`/`maturin` via pip), consistent with the existing ASF Actions-policy 
note in the file.
    
    ## Why are the changes needed?
    
    A "dry run" that skips the crate publish gives false confidence: crates.io 
versions are permanent (yank-only), and the previous workflow offered no 
rehearsal and no safe recovery from a partial publish. The Linux wheel was also 
tied to the runner's glibc with no aarch64/sdist coverage.
    
    ## Does this PR introduce _any_ user-facing change?
    
    No. This only changes the release/CI workflow and crate packaging metadata; 
it does not change any client API.
    
    ## How was this patch tested?
    
    Locally rehearsed the crate side offline against a populated cargo cache 
(network to crates.io is the only piece a runner adds):
    
    - `cargo package -p apache-spark-connect-proto -p apache-spark-connect-core 
-p apache-spark-connect` packaged all three and verify-built each in dependency 
order, resolving `core`/`connect` against the **locally-packaged** siblings — 
the same resolution `cargo publish --dry-run` uses.
    - Inspected the shipped `Cargo.toml`: path deps stripped, `version` pins 
kept, and `repository`/`homepage`/`authors` now present (no more "manifest has 
no repository" warning).
    - `validate-version` version/tag/path-pin logic checked against the current 
`4.2.0`.
    - `release.yml` validated as well-formed YAML.
    
    Wheel/sdist build steps use standard `maturin` invocations and are 
exercised by the workflow's own dry-run.
    
    This pull request and its description were written by Isaac.
    
    Closes #79 from HyukjinKwon/release-actions-review.
    
    Authored-by: Hyukjin Kwon <[email protected]>
    Signed-off-by: Hyukjin Kwon <[email protected]>
---
 .github/workflows/release.yml         | 138 +++++++++++++++++++++++++++++-----
 crates/spark-connect-core/Cargo.toml  |   3 +
 crates/spark-connect-proto/Cargo.toml |   3 +
 crates/spark-connect/Cargo.toml       |   3 +
 pyproject.toml                        |   8 +-
 5 files changed, 133 insertions(+), 22 deletions(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0f99673..fede7a5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -30,8 +30,11 @@
 #   4. Dispatch this workflow with dry_run=true to rehearse (default), then 
push
 #      a matching `v<version>` tag (or dispatch with dry_run=false) to publish.
 #
-# A dry run builds and validates artifacts without publishing. A tag push (or 
an
-# explicit dry_run=false dispatch) publishes to crates.io and PyPI.
+# A dry run fully rehearses BOTH sides without publishing: it packages and
+# verify-builds every crate in dependency order (`cargo publish --dry-run`) and
+# builds every wheel and the sdist. A tag push (or an explicit dry_run=false
+# dispatch) additionally publishes to crates.io and PyPI. Both publish steps 
are
+# idempotent, so a re-run after a partial failure skips what already landed.
 
 name: Release
 
@@ -48,6 +51,12 @@ on:
 permissions:
   contents: read
 
+# Only one release may run at a time, and a publish in flight is never 
cancelled
+# (cancelling mid-publish is what leaves crates.io/PyPI half-updated).
+concurrency:
+  group: release
+  cancel-in-progress: false
+
 jobs:
   validate-version:
     name: Validate version
@@ -56,6 +65,7 @@ jobs:
       - uses: actions/checkout@v4
       - name: Cargo and pyproject versions agree (and match the tag, if any)
         run: |
+          set -euo pipefail
           CARGO_VER=$(grep -m1 '^version' Cargo.toml | awk -F'"' '{print $2}')
           PYPROJECT_VER=$(grep -m1 '^version' pyproject.toml | awk -F'"' 
'{print $2}')
           echo "Cargo.toml=${CARGO_VER}  pyproject.toml=${PYPROJECT_VER}"
@@ -63,6 +73,17 @@ jobs:
             echo "::error::Cargo.toml (${CARGO_VER}) and pyproject.toml 
(${PYPROJECT_VER}) versions differ"
             exit 1
           fi
+          # Internal path-dependency version pins (e.g. spark-connect-core -> 
proto)
+          # must track the workspace version, or `cargo publish` embeds a stale
+          # requirement into the published crate.
+          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) do not match 
${CARGO_VER}:"
+            echo "${BAD}"
+            exit 1
+          fi
           if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
             TAG="${GITHUB_REF##*/}"
             if [[ "${TAG}" != "v${CARGO_VER}" ]]; then
@@ -74,7 +95,6 @@ jobs:
   publish-crates:
     name: Publish Rust crates to crates.io
     needs: validate-version
-    if: github.event_name == 'push' || (github.event_name == 
'workflow_dispatch' && inputs.dry_run == false)
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
@@ -84,36 +104,88 @@ jobs:
           rustup default stable
       - name: Install protoc
         run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
-      - name: cargo publish (dependency order)
+      - name: cargo publish (dependency-ordered; dry-run unless releasing)
         env:
           CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+          # true only for an explicit dry-run dispatch; tag pushes and an 
explicit
+          # dry_run=false dispatch both evaluate to "false" and publish for 
real.
+          DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && 
inputs.dry_run }}
         run: |
-          # Publish leaf-first so each crate's path+version dependencies 
already
-          # exist on crates.io. Allow the index a moment to catch up between 
crates.
-          for crate in apache-spark-connect-proto apache-spark-connect-core 
apache-spark-connect; do
-            echo "::group::cargo publish -p ${crate}"
-            cargo publish -p "${crate}"
+          set -euo pipefail
+          # Leaf-first; cargo (>=1.90) orders and verify-builds these together,
+          # resolving siblings from the local workspace.
+          CRATES=(apache-spark-connect-proto apache-spark-connect-core 
apache-spark-connect)
+          VER=$(grep -m1 '^version' Cargo.toml | awk -F'"' '{print $2}')
+
+          if [[ "${DRY_RUN}" == "true" ]]; then
+            # Rehearse the real publish. Because siblings resolve from the 
local
+            # workspace, this works even before anything exists on crates.io.
+            pkgs=(); for c in "${CRATES[@]}"; do pkgs+=(-p "$c"); done
+            echo "::group::cargo publish --dry-run ${pkgs[*]}"
+            cargo publish --dry-run "${pkgs[@]}"
             echo "::endgroup::"
-            sleep 30
+            echo "Dry run only — nothing published."
+            exit 0
+          fi
+
+          # Real publish. Skip any crate whose current version is already on
+          # crates.io so a re-run after a partial failure 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."
+            exit 0
+          fi
+          echo "::group::cargo publish ${pkgs[*]}"
+          # cargo publishes in dependency order and waits for the index between
+          # crates, so no manual sleep is needed.
+          cargo publish "${pkgs[@]}"
+          echo "::endgroup::"
 
   build-wheels:
-    name: Build wheel (${{ matrix.os }})
+    name: Build wheel (${{ matrix.target }})
     needs: validate-version
     runs-on: ${{ matrix.os }}
     strategy:
       fail-fast: false
       matrix:
-        os: [ubuntu-latest, macos-latest, windows-latest]
+        include:
+          - os: ubuntu-latest
+            target: x86_64-unknown-linux-gnu
+            zig: true
+          - os: ubuntu-latest
+            target: aarch64-unknown-linux-gnu
+            zig: true
+          - os: macos-latest
+            target: universal2-apple-darwin
+          - os: windows-latest
+            target: x86_64-pc-windows-msvc
     steps:
       - uses: actions/checkout@v4
       - uses: actions/setup-python@v5
         with:
           python-version: "3.11"
       - name: Install Rust toolchain
+        shell: bash
         run: |
           rustup toolchain install stable --profile minimal
           rustup default stable
+      - name: Add Rust target(s)
+        shell: bash
+        run: |
+          case "${{ matrix.target }}" in
+            universal2-apple-darwin)
+              rustup target add x86_64-apple-darwin aarch64-apple-darwin ;;
+            *)
+              rustup target add "${{ matrix.target }}" ;;
+          esac
       - name: Install protoc
         shell: bash
         run: |
@@ -123,19 +195,47 @@ jobs:
             Windows) choco install protoc -y ;;
           esac
       - name: Build abi3 wheel with maturin
-        # abi3-py39 produces a single wheel per platform valid for CPython 
3.9+.
+        # abi3-py39 produces a single wheel per (platform, arch) valid for
+        # CPython 3.9+. `--zig` cross-links the Linux wheels against an old 
glibc
+        # (manylinux2014), so they install on essentially any modern Linux 
rather
+        # than only on the runner's glibc.
         shell: bash
         run: |
           python -m pip install --upgrade maturin
-          maturin build --release --out dist
+          args=(--release --out dist --target "${{ matrix.target }}")
+          if [[ "${{ matrix.zig }}" == "true" ]]; then
+            python -m pip install ziglang
+            args+=(--zig)
+          fi
+          maturin build "${args[@]}"
       - uses: actions/upload-artifact@v4
         with:
-          name: wheels-${{ matrix.os }}
+          name: wheels-${{ matrix.target }}
           path: dist/*.whl
 
+  build-sdist:
+    name: Build source distribution
+    needs: validate-version
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: "3.11"
+      - name: Build sdist with maturin
+        # sdist packages source only (no compile), so protoc is not needed 
here.
+        # It is the install fallback for platforms without a prebuilt wheel.
+        run: |
+          python -m pip install --upgrade maturin
+          maturin sdist --out dist
+      - uses: actions/upload-artifact@v4
+        with:
+          name: wheels-sdist
+          path: dist/*.tar.gz
+
   publish-wheels:
-    name: Publish wheel to PyPI
-    needs: build-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)
     runs-on: ubuntu-latest
     steps:
@@ -144,6 +244,7 @@ jobs:
           python-version: "3.11"
       - uses: actions/download-artifact@v4
         with:
+          # Matches every wheels-<target> artifact plus wheels-sdist.
           pattern: wheels-*
           path: dist
           merge-multiple: true
@@ -155,4 +256,5 @@ jobs:
           TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
         run: |
           python -m pip install --upgrade twine
-          twine upload dist/*
+          # --skip-existing makes a re-run after a partial upload safe.
+          twine upload --skip-existing dist/*
diff --git a/crates/spark-connect-core/Cargo.toml 
b/crates/spark-connect-core/Cargo.toml
index cd714c3..6f4c1d8 100644
--- a/crates/spark-connect-core/Cargo.toml
+++ b/crates/spark-connect-core/Cargo.toml
@@ -3,6 +3,9 @@ name = "apache-spark-connect-core"
 version.workspace = true
 edition.workspace = true
 license.workspace = true
+repository.workspace = true
+homepage.workspace = true
+authors.workspace = true
 description = "Spark Connect client transport: gRPC channel, retries, 
reattach, artifacts, config, errors"
 
 [lib]
diff --git a/crates/spark-connect-proto/Cargo.toml 
b/crates/spark-connect-proto/Cargo.toml
index 2ba3f8d..604ff89 100644
--- a/crates/spark-connect-proto/Cargo.toml
+++ b/crates/spark-connect-proto/Cargo.toml
@@ -3,6 +3,9 @@ name = "apache-spark-connect-proto"
 version.workspace = true
 edition.workspace = true
 license.workspace = true
+repository.workspace = true
+homepage.workspace = true
+authors.workspace = true
 description = "Generated Spark Connect protobuf/gRPC types (spark.connect.*)"
 
 [lib]
diff --git a/crates/spark-connect/Cargo.toml b/crates/spark-connect/Cargo.toml
index 5042122..92ad84f 100644
--- a/crates/spark-connect/Cargo.toml
+++ b/crates/spark-connect/Cargo.toml
@@ -3,6 +3,9 @@ name = "apache-spark-connect"
 version.workspace = true
 edition.workspace = true
 license.workspace = true
+repository.workspace = true
+homepage.workspace = true
+authors.workspace = true
 description = "Pure-Rust Spark Connect DataFrame client mirroring the PySpark 
API surface"
 
 [lib]
diff --git a/pyproject.toml b/pyproject.toml
index fb9f434..0ec2245 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,10 +32,10 @@ dependencies = [
 ]
 # cloudpickle is vendored at pyspark/cloudpickle (matching Apache Spark's own
 # bundling), so it is intentionally NOT an external dependency.
-
-[project.optional-dependencies]
-grpc = ["grpcio>=1.48.0"]
-all = ["grpcio>=1.48.0"]
+#
+# No optional-dependencies: unlike upstream PySpark's `connect` extra, this
+# package has no pure-Python gRPC client — the Rust extension 
(pyspark._pyspark)
+# speaks gRPC via tonic, so grpcio is never imported at runtime.
 
 [project.urls]
 Homepage = "https://spark.apache.org/";


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

Reply via email to