Skip to content

Commit dea230f

Browse files
committed
ci: support to publish TestPyPI
1 parent 81ecf4a commit dea230f

File tree

3 files changed

+213
-0
lines changed

3 files changed

+213
-0
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: 'Set Developmental release version'
2+
description: 'Appends a developmental release suffix to the version in the [package] section of Cargo.toml.'
3+
4+
inputs:
5+
manifest_path:
6+
description: 'Path to the Cargo.toml file to modify'
7+
required: true
8+
9+
runs:
10+
using: "composite"
11+
steps:
12+
- name: Append developmental release suffix to Cargo.toml
13+
shell: bash
14+
env:
15+
MANIFEST_PATH: ${{ inputs.manifest_path }}
16+
DEV_VERSION_SUFFIX: -dev${{ github.run_number }}${{ github.run_attempt }}
17+
run: python "$GITHUB_ACTION_PATH/set_dev_version.py" "$MANIFEST_PATH" "$DEV_VERSION_SUFFIX"
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
from pathlib import Path
2+
import re
3+
import sys
4+
import tempfile
5+
6+
7+
SECTION_RE = re.compile(r"^\s*\[(?P<name>[^\[\]]+)\]\s*(?:#.*)?$")
8+
VERSION_RE = re.compile(r'^(?P<prefix>[ \t]*version\s*=\s*")(?P<version>[^"]+)(?P<suffix>".*)$')
9+
10+
11+
def find_section_bounds(lines: list[str], section_name: str, target_file: Path) -> tuple[int, int]:
12+
in_target_section = False
13+
section_start = None
14+
15+
for index, line in enumerate(lines):
16+
match = SECTION_RE.match(line)
17+
if not match:
18+
continue
19+
20+
current_section = match.group("name").strip()
21+
if in_target_section:
22+
return section_start, index
23+
24+
if current_section == section_name:
25+
in_target_section = True
26+
section_start = index + 1
27+
28+
if in_target_section:
29+
return section_start, len(lines)
30+
31+
raise RuntimeError(f"Section [{section_name}] not found in {target_file}")
32+
33+
34+
def append_suffix_to_version(lines: list[str], start: int, end: int, suffix: str, target_file: Path) -> bool:
35+
for index in range(start, end):
36+
match = VERSION_RE.match(lines[index])
37+
if not match:
38+
continue
39+
40+
current_version = match.group("version")
41+
if current_version.endswith(suffix):
42+
return False
43+
44+
lines[index] = (
45+
f"{match.group('prefix')}{current_version}{suffix}{match.group('suffix')}"
46+
)
47+
return True
48+
49+
raise RuntimeError(f"version entry not found in [package] section of {target_file}")
50+
51+
52+
def update_manifest(target_file: Path, suffix: str) -> bool:
53+
with target_file.open("r", encoding="utf-8", newline="") as file:
54+
lines = file.readlines()
55+
56+
package_start, package_end = find_section_bounds(lines, "package", target_file)
57+
changed = append_suffix_to_version(lines, package_start, package_end, suffix, target_file)
58+
59+
if changed:
60+
with target_file.open("w", encoding="utf-8", newline="") as file:
61+
file.writelines(lines)
62+
63+
return changed
64+
65+
66+
def assert_equal(actual: object, expected: object, message: str) -> None:
67+
if actual != expected:
68+
raise AssertionError(f"{message}: expected {expected!r}, got {actual!r}")
69+
70+
71+
def assert_raises(function, expected_message: str) -> None:
72+
try:
73+
function()
74+
except RuntimeError as error:
75+
if expected_message not in str(error):
76+
raise AssertionError(
77+
f"unexpected error message: expected to contain {expected_message!r}, got {str(error)!r}"
78+
) from error
79+
return
80+
81+
raise AssertionError("expected RuntimeError was not raised")
82+
83+
84+
def run_self_test() -> int:
85+
suffix = "-dev1234"
86+
valid_manifest = """[package]
87+
name = "example"
88+
version = "0.1.0"
89+
90+
[dependencies]
91+
serde = "1"
92+
"""
93+
94+
with tempfile.TemporaryDirectory() as temp_dir:
95+
manifest_path = Path(temp_dir) / "Cargo.toml"
96+
97+
manifest_path.write_text(
98+
valid_manifest,
99+
encoding="utf-8",
100+
newline="",
101+
)
102+
changed = update_manifest(manifest_path, suffix)
103+
updated_manifest = manifest_path.read_text(encoding="utf-8")
104+
105+
assert_equal(changed, True, "first update should modify the manifest")
106+
assert_equal(
107+
'version = "0.1.0-dev1234"' in updated_manifest,
108+
True,
109+
"version suffix should be appended in [package]",
110+
)
111+
assert_equal(
112+
'serde = "1"' in updated_manifest,
113+
True,
114+
"entries outside [package] should remain untouched",
115+
)
116+
117+
changed = update_manifest(manifest_path, suffix)
118+
assert_equal(changed, False, "second update should be idempotent")
119+
120+
missing_section_path = Path(temp_dir) / "missing-section.toml"
121+
missing_section_path.write_text(
122+
"""[dependencies]\nserde = \"1\"\n""",
123+
encoding="utf-8",
124+
newline="",
125+
)
126+
assert_raises(
127+
lambda: update_manifest(missing_section_path, suffix),
128+
"Section [package] not found",
129+
)
130+
131+
missing_version_path = Path(temp_dir) / "missing-version.toml"
132+
missing_version_path.write_text(
133+
"""[package]\nname = \"example\"\n""",
134+
encoding="utf-8",
135+
newline="",
136+
)
137+
assert_raises(
138+
lambda: update_manifest(missing_version_path, suffix),
139+
"version entry not found",
140+
)
141+
142+
print("self-test passed")
143+
return 0
144+
145+
146+
def main(argv: list[str] | None = None) -> int:
147+
args = list(sys.argv[1:] if argv is None else argv)
148+
149+
if args == ["--self-test"]:
150+
return run_self_test()
151+
152+
if len(args) != 2:
153+
raise SystemExit("usage: set_dev_version.py <manifest_path> <suffix> | --self-test")
154+
155+
update_manifest(Path(args[0]), args[1])
156+
return 0
157+
158+
159+
if __name__ == "__main__":
160+
raise SystemExit(main())

.github/workflows/ci-build.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ jobs:
191191
with:
192192
python-version: 3.x
193193

194+
- uses: ./.github/actions/python-dev-version
195+
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
196+
with:
197+
manifest_path: tsubakuro-rust-python/Cargo.toml
198+
194199
- name: Build wheels
195200
uses: PyO3/maturin-action@v1
196201
with:
@@ -240,6 +245,11 @@ jobs:
240245
with:
241246
python-version: 3.x
242247

248+
- uses: ./.github/actions/python-dev-version
249+
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
250+
with:
251+
manifest_path: tsubakuro-rust-python/Cargo.toml
252+
243253
- name: Build wheels
244254
uses: PyO3/maturin-action@v1
245255
with:
@@ -292,6 +302,11 @@ jobs:
292302
python-version: 3.13
293303
architecture: ${{ matrix.platform.python_arch }}
294304

305+
- uses: ./.github/actions/python-dev-version
306+
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
307+
with:
308+
manifest_path: tsubakuro-rust-python/Cargo.toml
309+
295310
- name: Install_Protobuf
296311
run: |
297312
choco install protoc -y
@@ -342,6 +357,11 @@ jobs:
342357
with:
343358
python-version: 3.x
344359

360+
- uses: ./.github/actions/python-dev-version
361+
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
362+
with:
363+
manifest_path: tsubakuro-rust-python/Cargo.toml
364+
345365
- name: Install Protoc
346366
run: |
347367
brew install protobuf
@@ -376,6 +396,15 @@ jobs:
376396
steps:
377397
- uses: actions/checkout@v6
378398

399+
- uses: actions/setup-python@v6
400+
with:
401+
python-version: 3.x
402+
403+
- uses: ./.github/actions/python-dev-version
404+
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
405+
with:
406+
manifest_path: tsubakuro-rust-python/Cargo.toml
407+
379408
- name: Build sdist
380409
uses: PyO3/maturin-action@v1
381410
with:
@@ -411,7 +440,14 @@ jobs:
411440
- name: Install uv
412441
uses: astral-sh/setup-uv@v7
413442

443+
- name: Publish to TestPyPI
444+
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
445+
run: uv publish --publish-url https://test.pypi.org/legacy/ 'wheels-*/*'
446+
env:
447+
UV_PUBLISH_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }}
448+
414449
- name: Publish to PyPI
450+
if: ${{ startsWith(github.ref, 'refs/tags/') }}
415451
run: uv publish 'wheels-*/*'
416452
env:
417453
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}

0 commit comments

Comments
 (0)