Skip to content

Commit cd63f1f

Browse files
authored
Merge pull request #16 from project-tsurugi/ci-pypi
ci: support to publish TestPyPI
2 parents 13ae3ab + d8eecf0 commit cd63f1f

File tree

3 files changed

+231
-0
lines changed

3 files changed

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