-
Notifications
You must be signed in to change notification settings - Fork 26
Implement Schema Version Update Automation #276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e20b4f5
b76ccbe
df23da2
afa2878
bddd540
ac72d63
cbe17db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| name: Release | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| action: | ||
| description: "Release action to perform" | ||
| required: true | ||
| type: choice | ||
| options: | ||
| - major | ||
| - minor | ||
| - patch | ||
| - set-version | ||
| version: | ||
| description: "Version to set (required only for set-version)" | ||
| required: false | ||
|
|
||
| jobs: | ||
| release: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Set up Python | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.11" | ||
|
|
||
| - name: Install dependencies | ||
| run: pip install semver | ||
|
|
||
| - name: Run release script | ||
| run: | | ||
| if [ "${{ inputs.action }}" = "set-version" ]; then | ||
| if [ -z "${{ inputs.version }}" ]; then | ||
| echo "Version is required for set-version" | ||
| exit 1 | ||
| fi | ||
| python tools/spec_release.py set=${{ inputs.version }} | ||
| else | ||
| python tools/spec_release.py ${{ inputs.action }} | ||
| fi | ||
|
|
||
| - name: Commit version updates | ||
| run: | | ||
| git config user.name "github-actions" | ||
| git config user.email "github-actions@github.com" | ||
| git add . | ||
| git commit -m "chore(release): bump version (${{ inputs.action }})" || echo "No changes to commit" | ||
|
|
||
| - name: Push changes | ||
| run: git push | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This pushes directly to the branch rather than creating a PR for review. I'd prefer if the workflow created a new PR instead, so the version bump can be reviewed and approved before merging. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| name: Test Python Release | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Set up Python | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.11" | ||
|
|
||
| - name: Install dependencies | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| pip install pytest semver | ||
|
|
||
| - name: Run tests | ||
| run: | | ||
| PYTHONPATH=$PYTHONPATH:. pytest -v tests/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import pytest | ||
| from semver import VersionInfo | ||
| from tools.spec_release import compute_next_version | ||
|
|
||
| # start release | ||
|
|
||
| def test_start_minor_release_from_stable(): | ||
| v = VersionInfo.parse("1.2.3") | ||
| new = compute_next_version(v, "minor") | ||
| assert new.major == 1 | ||
| assert new.minor == 3 # bump minor | ||
| assert new.patch == 0 | ||
|
|
||
| def test_start_major_release_from_stable(): | ||
| v = VersionInfo.parse("1.2.3") | ||
| new = compute_next_version(v, "major") | ||
| assert new.major == 2 # bump major | ||
| assert new.minor == 0 | ||
| assert new.patch == 0 | ||
|
|
||
| def test_start_release_invalid_action(): | ||
| v = VersionInfo.parse("1.2.3") | ||
| with pytest.raises(SystemExit): | ||
| compute_next_version(v, "invalid-action") | ||
|
|
||
| # patch release | ||
|
|
||
| def test_patch_release_from_stable(): | ||
| v = VersionInfo.parse("1.2.3") | ||
| new = compute_next_version(v, "patch") | ||
| assert str(new) == "1.2.4" | ||
|
|
||
| # set version | ||
|
|
||
| def test_set_version_valid(): | ||
| v = compute_next_version(VersionInfo.parse("0.0.0"), "set", "2.0.1") | ||
| assert str(v) == "2.0.1" | ||
|
|
||
| def test_set_version_invalid(): | ||
| with pytest.raises(SystemExit): | ||
| compute_next_version(VersionInfo.parse("0.0.0"), "set", "not-a-version") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| #!/usr/bin/env python3 | ||
|
|
||
| import sys | ||
| from pathlib import Path | ||
| from semver import VersionInfo | ||
|
|
||
| VERSION_FILE = Path("version.txt") | ||
|
|
||
| SCHEMA_DIRS = ["schemas", "custom"] | ||
| CONFORMANCE_DIRS = ["conformance", "custom"] | ||
| DOC_FILES = ["cloudevents-binding.md", "spec.md", "links.md"] | ||
| README_FILE = "README.md" | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is not yet a drop-in replacement for
|
||
| # Utilities | ||
|
|
||
| def read_version() -> VersionInfo: | ||
| if not VERSION_FILE.exists(): | ||
| sys.exit(f"{VERSION_FILE} not found") | ||
| try: | ||
| return VersionInfo.parse(VERSION_FILE.read_text().strip()) | ||
| except ValueError as e: | ||
| sys.exit(f"Invalid version: {e}") | ||
|
|
||
| def write_version(version: VersionInfo): | ||
| VERSION_FILE.write_text(f"{version}\n") | ||
|
|
||
| def replace_all(files, old: str, new: str): | ||
| for f in files: | ||
| p = Path(f) | ||
| if p.exists(): | ||
| p.write_text(p.read_text().replace(old, new)) | ||
|
|
||
| def find_files(dirs, suffix=".json"): | ||
| return [ | ||
| f for d in dirs if Path(d).exists() | ||
| for f in Path(d).rglob(f"*{suffix}") | ||
| ] | ||
|
|
||
| # Version transitions | ||
|
|
||
| def compute_next_version(old: VersionInfo, action: str, value=None) -> VersionInfo: | ||
| """ | ||
| Compute next version based on the action: | ||
| - major: bump major, reset minor & patch | ||
| - minor: bump minor, reset patch | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The original issue (#218) and previous discussions mentioned handling For example: if old.prerelease == 'draft':
# Handle draft -> release transition
return old.replace(prerelease=None) |
||
| - patch: bump patch | ||
| - set: set to exact version | ||
| """ | ||
| if action == "set": | ||
| try: | ||
| return VersionInfo.parse(value) | ||
| except ValueError as e: | ||
| sys.exit(f"Invalid version: {e}") | ||
|
|
||
| if action == "major": | ||
| return old.bump_major() | ||
| if action == "minor": | ||
| return old.bump_minor() | ||
| if action == "patch": | ||
| return old.bump_patch() | ||
|
|
||
| sys.exit(f"Unknown action: {action}") | ||
|
|
||
| # Repository updates | ||
|
|
||
| def update_repository(old: VersionInfo, new: VersionInfo): | ||
| old_v, new_v = str(old), str(new) | ||
|
|
||
| # Update schema references | ||
| replace_all( | ||
| find_files(SCHEMA_DIRS), | ||
| f"https://cdevents.dev/{old_v}/schema/", | ||
| f"https://cdevents.dev/{new_v}/schema/", | ||
| ) | ||
|
|
||
| # Update conformance files | ||
| replace_all( | ||
| find_files(CONFORMANCE_DIRS), | ||
| f'"version": "{old_v}"', | ||
| f'"version": "{new_v}"', | ||
| ) | ||
|
|
||
| # Update documentation files | ||
| replace_all( | ||
| DOC_FILES, | ||
| f'"version": "{old_v}"', | ||
| f'"version": "{new_v}"', | ||
| ) | ||
|
|
||
| # Update README | ||
| replace_all([README_FILE], f"v{old_v}", f"v{new_v}") | ||
|
|
||
| # CLI | ||
ImJustHenry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def parse_action(): | ||
| """ | ||
| Get release action from command-line arguments. | ||
| Usage: | ||
| python release.py major | ||
| python release.py minor | ||
| python release.py patch | ||
| python release.py set=X.Y.Z | ||
| """ | ||
| if len(sys.argv) < 2: | ||
| sys.exit("Usage: release.py <major|minor|patch|set=X.Y.Z>") | ||
|
|
||
| arg = sys.argv[1].lower() | ||
|
|
||
| if arg in ("major", "minor", "patch"): | ||
| return arg, None | ||
|
|
||
| if arg.startswith("set="): | ||
| return "set", arg.split("=", 1)[1] | ||
|
|
||
| sys.exit(f"Unknown action: {arg}") | ||
|
|
||
| # Main | ||
|
|
||
| def main(): | ||
| action, value = parse_action() | ||
| old = read_version() | ||
| new = compute_next_version(old, action, value) | ||
| update_repository(old, new) | ||
| write_version(new) | ||
| print(f"Version updated: {old} -> {new}") | ||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The version input is passed directly to the Python script without validation. Consider adding input validation to ensure the version string matches expected semver format before passing it to the script, e.g.: