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 de17b25 [SPARK-59105][INFRA] Push release tags and GitHub Releases to
the apache repo, not the fork
de17b25 is described below
commit de17b251a8563888faf8779e74dfd8082302c1b3
Author: Hyukjin Kwon <[email protected]>
AuthorDate: Sat Aug 29 17:32:30 2026 +0900
[SPARK-59105][INFRA] Push release tags and GitHub Releases to the apache
repo, not the fork
### What changes were proposed in this pull request?
The release workflow runs in a committer's fork, but its tags and GitHub
Releases were being created on the **fork** (via the built-in `GITHUB_TOKEN`,
which can only write to the repo running the workflow). This redirects every
tag/release **write** to the canonical **`apache/spark-connect-rust`** repo:
- the RC tag push,
- the RC GitHub pre-release (`gh release create/upload/view`),
- the finalize check-out/download of the RC (now from apache),
- the permanent `v<version>` tag push,
- the RC pre-release/tag cleanup.
Cross-repo writes need a credential the built-in token can't provide, so
those operations now authenticate with a new **`RELEASE_GITHUB_TOKEN`** PAT
(write/`contents` access to `apache/spark-connect-rust`) stored as a fork
secret. The built-in `GITHUB_TOKEN` drops to `contents: read`. Release
**source** was already fetched from apache.
### Why are the changes needed?
The workflow only *runs* in a fork (for the publish tokens); the release
artifacts of record — tags and GitHub Releases — should live on the canonical
apache repo, not scattered across committer forks.
### Does this PR introduce _any_ user-facing change?
No change to released artifacts. It changes where the release tags / GitHub
Releases are created.
### How was this patch tested?
`release.yml` parses as valid YAML; verified no
`GITHUB_REPOSITORY`/`github.token` references remain in the tag/release paths,
`github.repository` appears only in the two dispatch guards, and every
tag/release op targets `${UPSTREAM_REPO}` with `RELEASE_GITHUB_TOKEN`.
### Setup required before a real release
Add a fork secret `RELEASE_GITHUB_TOKEN`: a PAT with write (`contents`)
access to `apache/spark-connect-rust` (a committer's classic PAT with
`public_repo` scope suffices, since the repo is public). Ensure no apache
tag-protection ruleset blocks the PAT from pushing `v*` tags.
> **Note (stacked):** this branch is based on the PR #84 branch
(SPARK-59101 / SPARK-59100), so until #84 merges this PR's diff also shows
those commits. Merge #84 first, then rebase this onto master.
This pull request and its description were written by Isaac.
Closes #87 from HyukjinKwon/release-tags-to-apache.
Authored-by: Hyukjin Kwon <[email protected]>
Signed-off-by: Hyukjin Kwon <[email protected]>
---
.github/workflows/release.yml | 80 +++++++++++++++++++++++++------------------
1 file changed, 46 insertions(+), 34 deletions(-)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 527461f..ffe851f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -42,24 +42,31 @@
# * Cut an RC (release-version + rc-count, finalize=false): build the wheels
+
# sdist -- versioned as the FINAL release-version, since the RC number
lives
# only in the tag/release name, never in the artifact version -- then
-# auto-create and push the tag `v<version>-rc<rc>` to this fork and attach
the
-# artifacts to a GitHub *pre-release* of that tag. crates.io/PyPI are NOT
-# touched, so an RC stays fully deletable.
+# auto-create and push the tag `v<version>-rc<rc>` to the canonical apache
repo
+# and attach the artifacts to a GitHub *pre-release* of that tag.
crates.io/PyPI
+# are NOT touched, so an RC stays fully deletable.
#
# * Finalize (finalize=true, naming an existing RC): check out the RC tag,
# `twine upload` the EXACT wheels+sdist from that RC's GitHub pre-release
to
# PyPI, `cargo publish` the identical source to crates.io, push a permanent
# `v<version>` git tag marking the released commit, then delete every
-# `v<version>-rc*` GitHub pre-release (and its tag) from this fork. GitHub
+# `v<version>-rc*` GitHub pre-release (and its tag) from the apache repo.
GitHub
# Releases are only RC staging; the official release lives on PyPI +
crates.io,
-# marked in git by the immutable `v<version>` tag.
+# marked in git by the immutable `v<version>` tag on the apache repo.
#
-# Required fork secrets for a real run: CARGO_REGISTRY_TOKEN (crates.io) and
-# PYPI_API_TOKEN (PyPI). Without them, only dry runs work. The default
GITHUB_TOKEN
-# creates/deletes the RC tags and GitHub Releases on the fork.
+# The workflow RUNS in a committer's fork, but all tags and GitHub Releases
land on
+# the canonical apache repo (never the fork). Required fork secrets for a real
run:
+# - CARGO_REGISTRY_TOKEN crates.io publish
+# - PYPI_API_TOKEN PyPI publish
+# - RELEASE_GITHUB_TOKEN a Personal Access Token with write (contents)
access to
+# apache/spark-connect-rust, used to push the tags
and
+# create/delete the GitHub Releases there. The
built-in
+# GITHUB_TOKEN can only write to the fork, so it is
NOT used
+# for these; it is read-only here.
+# Without these secrets, only dry runs work.
#
# To drop an RC without finalizing: delete its GitHub pre-release and the
-# `v<version>-rc<n>` tag from the fork (nothing was published to
PyPI/crates.io).
+# `v<version>-rc<n>` tag from the apache repo (nothing was published to
PyPI/crates.io).
name: Release
@@ -350,7 +357,9 @@ jobs:
if: needs.resolve.outputs.mode == 'rc'
runs-on: ubuntu-latest
permissions:
- contents: write # push the RC tag + create the GitHub Release on the
fork
+ # The tag push + GitHub Release go to the canonical apache repo via
+ # RELEASE_GITHUB_TOKEN; the built-in token only needs read
(checkout/artifacts).
+ contents: read
steps:
- uses: actions/checkout@v4
with:
@@ -362,9 +371,9 @@ jobs:
pattern: wheels-*
path: dist
merge-multiple: true
- - name: Create and push the RC tag to this fork
+ - name: Create and push the RC tag to the canonical apache repo
env:
- GH_TOKEN: ${{ github.token }}
+ GH_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
TAG: ${{ needs.resolve.outputs.rc_tag }}
VERSION: ${{ needs.resolve.outputs.version }}
run: |
@@ -376,12 +385,13 @@ jobs:
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}"
+ # Push the RC tag (and the objects it reaches) to the canonical
apache repo,
+ # 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/${UPSTREAM_REPO}.git"
"refs/tags/${TAG}"
- name: Create the GitHub pre-release with the artifacts
env:
- GH_TOKEN: ${{ github.token }}
+ GH_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
TAG: ${{ needs.resolve.outputs.rc_tag }}
VERSION: ${{ needs.resolve.outputs.version }}
run: |
@@ -389,10 +399,10 @@ jobs:
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
+ if gh release view "${TAG}" --repo "${UPSTREAM_REPO}" >/dev/null
2>&1; then
+ gh release upload "${TAG}" dist/* --repo "${UPSTREAM_REPO}"
--clobber
else
- gh release create "${TAG}" dist/* --repo "${GITHUB_REPOSITORY}" \
+ gh release create "${TAG}" dist/* --repo "${UPSTREAM_REPO}" \
--prerelease --title "${TAG}" --notes "${NOTES}"
fi
@@ -403,7 +413,9 @@ jobs:
if: needs.resolve.outputs.mode == 'finalize'
runs-on: ubuntu-latest
permissions:
- contents: write # delete the RC pre-releases + tags on the fork
+ # All writes (permanent tag push, RC pre-release/tag deletion) go to the
+ # canonical apache repo via RELEASE_GITHUB_TOKEN; the built-in token
only reads.
+ contents: read
steps:
- name: Abort window
run: |
@@ -416,19 +428,19 @@ jobs:
sleep 60
- name: Verify the RC pre-release exists
env:
- GH_TOKEN: ${{ github.token }}
+ GH_TOKEN: ${{ secrets.RELEASE_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."
+ if ! gh release view "${TAG}" --repo "${UPSTREAM_REPO}" >/dev/null
2>&1; then
+ echo "::error::RC GitHub pre-release ${TAG} not found on
${UPSTREAM_REPO}. 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 }}
+ # The RC tag lives on the canonical apache repo, where the RC was
cut.
+ repository: ${{ env.UPSTREAM_REPO }}
ref: ${{ needs.resolve.outputs.rc_tag }}
fetch-depth: 0
- name: Validate the RC source versions
@@ -457,12 +469,12 @@ jobs:
python-version: "3.11"
- name: Download the RC artifacts (the exact tested wheels + sdist)
env:
- GH_TOKEN: ${{ github.token }}
+ GH_TOKEN: ${{ secrets.RELEASE_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 '*'
+ gh release download "${TAG}" --repo "${UPSTREAM_REPO}" --dir dist
--pattern '*'
ls -l dist
- name: Publish wheels + sdist to PyPI
env:
@@ -504,14 +516,14 @@ jobs:
fi
- name: Tag the released commit with the permanent v<version> tag
env:
- GH_TOKEN: ${{ github.token }}
+ GH_TOKEN: ${{ secrets.RELEASE_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"
-
URL="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
+
URL="https://x-access-token:${GH_TOKEN}@github.com/${UPSTREAM_REPO}.git"
# HEAD is the RC tag's commit (the exact released source), checked
out above.
LOCAL_SHA=$(git rev-parse HEAD)
if ! git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
@@ -533,31 +545,31 @@ jobs:
if [ "${REMOTE_SHA}" = "${LOCAL_SHA}" ]; then
echo "Release tag ${TAG} already points at ${LOCAL_SHA}; nothing
to do."
else
- echo "::error::Release tag ${TAG} already exists on
${GITHUB_REPOSITORY} at ${REMOTE_SHA}, but the release commit is ${LOCAL_SHA}.
Refusing to move an immutable release tag."
+ echo "::error::Release tag ${TAG} already exists on
${UPSTREAM_REPO} at ${REMOTE_SHA}, but the release commit is ${LOCAL_SHA}.
Refusing to move an immutable release tag."
exit 1
fi
fi
- name: Remove all RC GitHub pre-releases + tags for this version
env:
- GH_TOKEN: ${{ github.token }}
+ GH_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
VERSION: ${{ needs.resolve.outputs.version }}
run: |
set -euo pipefail
# Every v<version>-rcN pre-release is RC staging; the official
release now lives
- # on PyPI + crates.io, so delete them all (and their tags) from this
fork.
+ # on PyPI + crates.io, so delete them all (and their tags) from the
apache repo.
# Escape dots: grep -E treats '.' as any char, so an unescaped 4.2.0
would also
# match tags like v4x2x0-rcN.
ESC_V="${VERSION//./\\.}"
# Fetch the tag list in its own command so a `gh` failure (rate
limit/network)
# aborts loudly under `set -e`, instead of being swallowed by the
`|| true` that
# is only meant to absorb grep's "no match" exit code.
- ALL_TAGS=$(gh release list --repo "${GITHUB_REPOSITORY}" --limit 200
--json tagName --jq '.[].tagName')
+ ALL_TAGS=$(gh release list --repo "${UPSTREAM_REPO}" --limit 200
--json tagName --jq '.[].tagName')
mapfile -t RCS < <(printf '%s\n' "${ALL_TAGS}" | grep -E
"^v${ESC_V}-rc[0-9]+$" || true)
if [ "${#RCS[@]}" -eq 0 ]; then
echo "No v${VERSION}-rc* GitHub releases to remove."
fi
for t in "${RCS[@]}"; do
echo "Deleting RC release + tag ${t}"
- gh release delete "${t}" --repo "${GITHUB_REPOSITORY}"
--cleanup-tag --yes
+ gh release delete "${t}" --repo "${UPSTREAM_REPO}" --cleanup-tag
--yes
done
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]