Skip to content

Commit a9a8f6b

Browse files
build(python): give RC artifacts unique versions (#557)
* build(python): derive RC artifact version from tag * build(python): harden release workflow * fix(python): honor dispatched release tag * fix(python): align manual RC releases
1 parent 9e83dea commit a9a8f6b

3 files changed

Lines changed: 117 additions & 10 deletions

File tree

.github/workflows/release_python_binding.yml

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# under the License.
1717

1818
# Publish the paimon Python binding to PyPI.
19-
# Trigger: push tag only (e.g. v0.1.0).
19+
# Trigger: release tag push or manual dispatch.
2020
# Pre-release tags (containing '-') publish to TestPyPI; release tags publish to PyPI.
2121
#
2222
# Token auth: add secrets PYPI_API_TOKEN / TEST_PYPI_API_TOKEN for publishing.
@@ -29,21 +29,37 @@ on:
2929
- "v[0-9]+.[0-9]+.[0-9]+"
3030
- "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+"
3131
workflow_dispatch:
32+
inputs:
33+
tag:
34+
description: Release tag, for example v0.3.0-rc2
35+
required: true
36+
type: string
3237

3338
concurrency:
34-
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
39+
group: ${{ github.workflow }}-${{ inputs.tag || github.ref_name }}
3540
cancel-in-progress: true
3641

3742
permissions:
3843
contents: read
3944

45+
env:
46+
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
47+
4048
jobs:
4149
sdist:
4250
runs-on: ubuntu-latest
4351
steps:
4452
- uses: actions/checkout@v7
53+
with:
54+
ref: ${{ env.RELEASE_TAG }}
55+
56+
- name: Set Python release version
57+
run: >
58+
python3 scripts/python_release_version.py
59+
--tag "${RELEASE_TAG}"
60+
--pyproject bindings/python/pyproject.toml
4561
46-
- uses: PyO3/maturin-action@v1
62+
- uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
4763
with:
4864
working-directory: bindings/python
4965
command: sdist
@@ -69,8 +85,17 @@ jobs:
6985
- { os: ubuntu-latest, target: "aarch64", manylinux: "manylinux_2_28" }
7086
steps:
7187
- uses: actions/checkout@v7
88+
with:
89+
ref: ${{ env.RELEASE_TAG }}
90+
91+
- name: Set Python release version
92+
shell: bash
93+
run: >
94+
python3 scripts/python_release_version.py
95+
--tag "${RELEASE_TAG}"
96+
--pyproject bindings/python/pyproject.toml
7297
73-
- uses: PyO3/maturin-action@v1
98+
- uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
7499
with:
75100
working-directory: bindings/python
76101
target: ${{ matrix.target }}
@@ -143,7 +168,7 @@ jobs:
143168
permissions:
144169
contents: read
145170
needs: [sdist, wheels]
146-
if: startsWith(github.ref, 'refs/tags/')
171+
if: startsWith(inputs.tag || github.ref_name, 'v')
147172
steps:
148173
- uses: actions/download-artifact@v8
149174
with:
@@ -152,17 +177,17 @@ jobs:
152177
path: bindings/python/dist
153178

154179
- name: Publish to TestPyPI
155-
if: contains(github.ref, '-')
156-
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
180+
if: contains(env.RELEASE_TAG, '-')
181+
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
157182
with:
158183
repository-url: https://test.pypi.org/legacy/
159184
skip-existing: true
160185
packages-dir: bindings/python/dist
161186
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
162187

163188
- name: Publish to PyPI
164-
if: ${{ !contains(github.ref, '-') }}
165-
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
189+
if: ${{ !contains(env.RELEASE_TAG, '-') }}
190+
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
166191
with:
167192
skip-existing: true
168193
packages-dir: bindings/python/dist

docs/src/release/verifying-a-release-candidate.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ For any user-facing feature included in a release, we aim to ensure it is functi
115115
- **Python binding:** The RC is published to **TestPyPI**; install the client from TestPyPI and write your own test cases to verify:
116116

117117
```bash
118-
pip install -i https://test.pypi.org/simple/ pypaimon-rust==${RELEASE_VERSION}
118+
pip install -i https://test.pypi.org/simple/ pypaimon-rust==${RELEASE_VERSION}rc${RC_NUM}
119119
```
120120

121121
- **Go binding:** The RC is published as a Go module tag `bindings/go/v${RELEASE_VERSION}-rc${RC_NUM}`; see [Go Binding](https://paimon.apache.org/docs/rust/go-binding/) for usage. Add it to your Go project and write test cases to verify:

scripts/python_release_version.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#!/usr/bin/env python3
2+
3+
# Licensed to the Apache Software Foundation (ASF) under one or more
4+
# contributor license agreements. See the NOTICE file distributed with
5+
# this work for additional information regarding copyright ownership.
6+
# The ASF licenses this file to You under the Apache License, Version 2.0
7+
# (the "License"); you may not use this file except in compliance with
8+
# the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
"""Set a unique Python version for release-candidate artifacts."""
19+
20+
from __future__ import annotations
21+
22+
import argparse
23+
import re
24+
import sys
25+
import tomllib
26+
from pathlib import Path
27+
28+
29+
TAG_PATTERN = re.compile(r"^v(?P<base>\d+\.\d+\.\d+)(?:-rc(?P<rc>[1-9]\d*))?$")
30+
DYNAMIC_VERSION = 'dynamic = ["version"]'
31+
32+
33+
def workspace_version(root: Path) -> str:
34+
with (root / "Cargo.toml").open("rb") as source:
35+
return str(tomllib.load(source)["workspace"]["package"]["version"])
36+
37+
38+
def python_version(tag: str, expected_base: str) -> str:
39+
match = TAG_PATTERN.fullmatch(tag)
40+
if match is None:
41+
raise ValueError(f"invalid release tag: {tag}")
42+
base = match.group("base")
43+
if base != expected_base:
44+
raise ValueError(f"tag {base} does not match workspace {expected_base}")
45+
rc = match.group("rc")
46+
return base if rc is None else f"{base}rc{rc}"
47+
48+
49+
def update_pyproject(path: Path, version: str, expected_base: str) -> None:
50+
if version == expected_base:
51+
return
52+
content = path.read_text(encoding="utf-8")
53+
if content.count(DYNAMIC_VERSION) != 1:
54+
raise ValueError(f"unexpected dynamic version setting in {path}")
55+
path.write_text(
56+
content.replace(DYNAMIC_VERSION, f'version = "{version}"', 1),
57+
encoding="utf-8",
58+
)
59+
60+
61+
def main() -> int:
62+
parser = argparse.ArgumentParser(description=__doc__)
63+
parser.add_argument("--tag", required=True)
64+
parser.add_argument("--pyproject", type=Path)
65+
args = parser.parse_args()
66+
67+
root = Path(__file__).resolve().parents[1]
68+
try:
69+
base = workspace_version(root)
70+
version = python_version(args.tag, base)
71+
if args.pyproject is not None:
72+
update_pyproject(root / args.pyproject, version, base)
73+
except (KeyError, OSError, ValueError, tomllib.TOMLDecodeError) as error:
74+
print(f"Python release version failed: {error}", file=sys.stderr)
75+
return 1
76+
77+
print(version)
78+
return 0
79+
80+
81+
if __name__ == "__main__":
82+
sys.exit(main())

0 commit comments

Comments
 (0)