Skip to content

Commit 7add14c

Browse files
committed
fix release script
1 parent 937aead commit 7add14c

4 files changed

Lines changed: 58 additions & 11 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ release-check:
113113

114114
release-artifacts:
115115
@if [ -z "$(RELEASE_TAG)" ]; then \
116-
echo "Error: RELEASE_TAG is required. Usage: make release RELEASE_TAG=v1.0.0"; \
116+
echo "Error: RELEASE_TAG is required. Usage: make release RELEASE_TAG=v1.0.0_2026-07-04"; \
117117
exit 1; \
118118
fi
119119
@printf "$(BLUE)==>$(RESET) Building and smoke testing release artifacts...\n"

docs/release-process.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ only when every package reports the same PEP 440 version.
1616
Run the complete release gate:
1717

1818
```bash
19-
make release RELEASE_TAG=v1.0.0
19+
make release RELEASE_TAG=v1.0.0_2026-07-04
2020
```
2121

2222
The gate:
@@ -32,13 +32,17 @@ The gate:
3232

3333
## Publish
3434

35-
Push the exact matching tag:
35+
Push a matching version tag. An optional ISO date suffix can distinguish the
36+
repository release event without changing the package version:
3637

3738
```bash
38-
git tag -s v1.0.0 -m "ACP Kit 1.0.0"
39-
git push origin v1.0.0
39+
git tag -s v1.0.0_2026-07-04 -m "ACP Kit 1.0.0"
40+
git push origin v1.0.0_2026-07-04
4041
```
4142

43+
Both `v1.0.0` and `v1.0.0_YYYY-MM-DD` validate against package version
44+
`1.0.0`. The suffix does not permit republishing an existing PyPI version.
45+
4246
GitHub Actions repeats the release gate, uploads the artifacts for inspection,
4347
and publishes through PyPI Trusted Publishing. Never publish from a dirty
4448
worktree or bypass a failed artifact smoke test.

scripts/release.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import tomllib
1111
import zipfile
1212
from dataclasses import dataclass
13+
from datetime import date
1314
from email.message import Message
1415
from email.parser import Parser
1516
from pathlib import Path
@@ -23,6 +24,7 @@
2324
r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)"
2425
r"(?:(?:a|b|rc)\d+)?$"
2526
)
27+
_RELEASE_DATE_PATTERN: Final[re.Pattern[str]] = re.compile(r"^\d{4}-\d{2}-\d{2}$")
2628

2729

2830
class ReleaseValidationError(RuntimeError):
@@ -128,14 +130,34 @@ def _check_root_extras(root: Path, version: str) -> None:
128130
)
129131

130132

133+
def _check_release_tag(tag: str, version: str) -> None:
134+
version_tag = f"v{version}"
135+
if tag == version_tag:
136+
return
137+
138+
dated_prefix = f"{version_tag}_"
139+
if tag.startswith(dated_prefix):
140+
release_date = tag.removeprefix(dated_prefix)
141+
if _RELEASE_DATE_PATTERN.fullmatch(release_date) is not None:
142+
try:
143+
date.fromisoformat(release_date)
144+
except ValueError:
145+
pass
146+
else:
147+
return
148+
149+
raise ReleaseValidationError(
150+
f"Release tag {tag!r} does not match workspace version {version!r}; "
151+
f"expected {version_tag!r} or {version_tag + '_YYYY-MM-DD'!r}."
152+
)
153+
154+
131155
def check_release(*, root: Path, tag: str | None) -> str:
132156
version = _workspace_version(root)
133157
_check_changelog(root, version)
134158
_check_root_extras(root, version)
135-
if tag is not None and tag != f"v{version}":
136-
raise ReleaseValidationError(
137-
f"Release tag {tag!r} does not match workspace version {version!r}."
138-
)
159+
if tag is not None:
160+
_check_release_tag(tag, version)
139161
print(f"Release metadata valid for {version}.")
140162
return version
141163

@@ -334,7 +356,10 @@ def _parser() -> argparse.ArgumentParser:
334356
subparsers = parser.add_subparsers(dest="command", required=True)
335357

336358
check_parser = subparsers.add_parser("check", help="Validate release metadata.")
337-
check_parser.add_argument("--tag", help="Expected git tag, for example v1.0.0.")
359+
check_parser.add_argument(
360+
"--tag",
361+
help="Expected git tag, for example v1.0.0 or v1.0.0_2026-07-04.",
362+
)
338363

339364
build_parser = subparsers.add_parser("build", help="Build and validate all artifacts.")
340365
build_parser.add_argument("--output-dir", type=Path, default=Path("dist"))
@@ -347,7 +372,11 @@ def _parser() -> argparse.ArgumentParser:
347372
prepare_parser = subparsers.add_parser(
348373
"prepare", help="Validate, build, and smoke test a tagged release."
349374
)
350-
prepare_parser.add_argument("--tag", required=True)
375+
prepare_parser.add_argument(
376+
"--tag",
377+
required=True,
378+
help="Git tag, for example v1.0.0 or v1.0.0_2026-07-04.",
379+
)
351380
prepare_parser.add_argument("--output-dir", type=Path, default=Path("dist"))
352381
return parser
353382

tests/test_release.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,22 @@ def test_release_metadata_matches_current_version_and_tag() -> None:
2727
assert f"Release metadata valid for {acpkit.__version__}." in result.stdout
2828

2929

30+
def test_release_metadata_accepts_dated_tag() -> None:
31+
result = _run_release("check", "--tag", f"v{acpkit.__version__}_2026-07-04")
32+
33+
assert result.returncode == 0
34+
assert f"Release metadata valid for {acpkit.__version__}." in result.stdout
35+
36+
3037
def test_release_metadata_rejects_mismatched_tag() -> None:
3138
result = _run_release("check", "--tag", "v999.0.0")
3239

3340
assert result.returncode == 1
3441
assert "does not match workspace version" in result.stderr
42+
43+
44+
def test_release_metadata_rejects_invalid_release_date() -> None:
45+
result = _run_release("check", "--tag", f"v{acpkit.__version__}_2026-02-30")
46+
47+
assert result.returncode == 1
48+
assert "does not match workspace version" in result.stderr

0 commit comments

Comments
 (0)