Skip to content

Commit 40b9a5b

Browse files
committed
spec_release.py with tests and CI
1 parent e1d95cf commit 40b9a5b

7 files changed

Lines changed: 225 additions & 118 deletions

File tree

.github/workflows/release.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
action:
7+
description: "Release action to perform"
8+
required: true
9+
type: choice
10+
options:
11+
- major
12+
- minor
13+
- patch
14+
- set-version
15+
version:
16+
description: "Version to set (required only for set-version)"
17+
required: false
18+
19+
jobs:
20+
release:
21+
runs-on: ubuntu-latest
22+
23+
steps:
24+
- name: Checkout repository
25+
uses: actions/checkout@v5
26+
with:
27+
fetch-depth: 0
28+
29+
- name: Set up Python
30+
uses: actions/setup-python@v5
31+
with:
32+
python-version: "3.11"
33+
34+
- name: Install dependencies
35+
run: pip install semver
36+
37+
- name: Run release script
38+
run: |
39+
if [ "${{ inputs.action }}" = "set-version" ]; then
40+
if [ -z "${{ inputs.version }}" ]; then
41+
echo "Version is required for set-version"
42+
exit 1
43+
fi
44+
python tools/release.py set=${{ inputs.version }}
45+
else
46+
python tools/release.py ${{ inputs.action }}
47+
48+
- name: Commit version updates
49+
run: |
50+
git config user.name "github-actions"
51+
git config user.email "github-actions@github.com"
52+
git add .
53+
git commit -m "chore(release): bump version (${{ inputs.action }})" || echo "No changes to commit"
54+
55+
- name: Push changes
56+
run: git push

.github/workflows/version-bump.yml

Lines changed: 0 additions & 40 deletions
This file was deleted.
7.51 KB
Binary file not shown.

tests/test_release.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import pytest
2+
from semver import VersionInfo
3+
from tools.spec_release import compute_next_version
4+
5+
# start release
6+
7+
def test_start_minor_release_from_stable():
8+
v = VersionInfo.parse("1.2.3")
9+
new = compute_next_version(v, "minor")
10+
assert new.major == 1
11+
assert new.minor == 3 # bump minor
12+
assert new.patch == 0
13+
14+
def test_start_major_release_from_stable():
15+
v = VersionInfo.parse("1.2.3")
16+
new = compute_next_version(v, "major")
17+
assert new.major == 2 # bump major
18+
assert new.minor == 0
19+
assert new.patch == 0
20+
21+
def test_start_release_invalid_action():
22+
v = VersionInfo.parse("1.2.3")
23+
with pytest.raises(SystemExit):
24+
compute_next_version(v, "invalid-action")
25+
26+
# patch release
27+
28+
def test_patch_release_from_stable():
29+
v = VersionInfo.parse("1.2.3")
30+
new = compute_next_version(v, "patch")
31+
assert str(new) == "1.2.4"
32+
33+
# set version
34+
35+
def test_set_version_valid():
36+
v = compute_next_version(VersionInfo.parse("0.0.0"), "set", "2.0.1")
37+
assert str(v) == "2.0.1"
38+
39+
def test_set_version_invalid():
40+
with pytest.raises(SystemExit):
41+
compute_next_version(VersionInfo.parse("0.0.0"), "set", "not-a-version")

tools/__init__.py

Whitespace-only changes.

tools/bump_version.py

Lines changed: 0 additions & 78 deletions
This file was deleted.

tools/spec_release.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env python3
2+
3+
import sys
4+
from pathlib import Path
5+
from semver import VersionInfo
6+
7+
VERSION_FILE = Path("version.txt")
8+
9+
SCHEMA_DIRS = ["schemas", "custom"]
10+
CONFORMANCE_DIRS = ["conformance", "custom"]
11+
DOC_FILES = ["cloudevents-binding.md", "spec.md", "links.md"]
12+
README_FILE = "README.md"
13+
14+
# Utilities
15+
16+
def read_version() -> VersionInfo:
17+
if not VERSION_FILE.exists():
18+
sys.exit(f"{VERSION_FILE} not found")
19+
try:
20+
return VersionInfo.parse(VERSION_FILE.read_text().strip())
21+
except ValueError as e:
22+
sys.exit(f"Invalid version: {e}")
23+
24+
def write_version(version: VersionInfo):
25+
VERSION_FILE.write_text(f"{version}\n")
26+
27+
def replace_all(files, old: str, new: str):
28+
for f in files:
29+
p = Path(f)
30+
if p.exists():
31+
p.write_text(p.read_text().replace(old, new))
32+
33+
def find_files(dirs, suffix=".json"):
34+
return [
35+
f for d in dirs if Path(d).exists()
36+
for f in Path(d).rglob(f"*{suffix}")
37+
]
38+
39+
# Version transitions
40+
41+
def compute_next_version(old: VersionInfo, action: str, value=None) -> VersionInfo:
42+
"""
43+
Compute next version based on the action:
44+
- major: bump major, reset minor & patch
45+
- minor: bump minor, reset patch
46+
- patch: bump patch
47+
- set: set to exact version
48+
"""
49+
if action == "set":
50+
try:
51+
return VersionInfo.parse(value)
52+
except ValueError as e:
53+
sys.exit(f"Invalid version: {e}")
54+
55+
if action == "major":
56+
return old.bump_major()
57+
if action == "minor":
58+
return old.bump_minor()
59+
if action == "patch":
60+
return old.bump_patch()
61+
62+
sys.exit(f"Unknown action: {action}")
63+
64+
# Repository updates
65+
66+
def update_repository(old: VersionInfo, new: VersionInfo):
67+
old_v, new_v = str(old), str(new)
68+
69+
# Update schema references
70+
replace_all(
71+
find_files(SCHEMA_DIRS),
72+
f"https://cdevents.dev/{old_v}/schema/",
73+
f"https://cdevents.dev/{new_v}/schema/",
74+
)
75+
76+
# Update conformance files
77+
replace_all(
78+
find_files(CONFORMANCE_DIRS),
79+
f'"version": "{old_v}"',
80+
f'"version": "{new_v}"',
81+
)
82+
83+
# Update documentation files
84+
replace_all(
85+
DOC_FILES,
86+
f'"version": "{old_v}"',
87+
f'"version": "{new_v}"',
88+
)
89+
90+
# Update README
91+
replace_all([README_FILE], f"v{old_v}", f"v{new_v}")
92+
93+
# CLI
94+
95+
def parse_action():
96+
"""
97+
Get release action from command-line arguments.
98+
Usage:
99+
python release.py major
100+
python release.py minor
101+
python release.py patch
102+
python release.py set=X.Y.Z
103+
"""
104+
if len(sys.argv) < 2:
105+
sys.exit("Usage: release.py <major|minor|patch|set=X.Y.Z>")
106+
107+
arg = sys.argv[1].lower()
108+
109+
if arg in ("major", "minor", "patch"):
110+
return arg, None
111+
112+
if arg.startswith("set="):
113+
return "set", arg.split("=", 1)[1]
114+
115+
sys.exit(f"Unknown action: {arg}")
116+
117+
# Main
118+
119+
def main():
120+
action, value = parse_action()
121+
old = read_version()
122+
new = compute_next_version(old, action, value)
123+
update_repository(old, new)
124+
write_version(new)
125+
print(f"Version updated: {old} -> {new}")
126+
127+
if __name__ == "__main__":
128+
main()

0 commit comments

Comments
 (0)