Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/release.yml
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 }}
Copy link
Member

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.:

- name: Validate version input
  if: inputs.action == 'set-version'
  run: |
    if ! echo "${{ inputs.version }}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?$'; then
      echo "Invalid version format"
      exit 1
    fi

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
Copy link
Member

Choose a reason for hiding this comment

The 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.

28 changes: 28 additions & 0 deletions .github/workflows/release_tests.yml
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/
41 changes: 41 additions & 0 deletions tests/test_release.py
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")
Empty file added tools/__init__.py
Empty file.
128 changes: 128 additions & 0 deletions tools/spec_release.py
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"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is not yet a drop-in replacement for tools/spec-release.sh, it would be good to:

  • in the shell script, remove the part that is covered by this and replace it with an invocation to the python implementation
  • or extend this PR to provide full coverage for the original bash script

# 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original issue (#218) and previous discussions mentioned handling -draft suffixes in versions. The current implementation only handles clean semver versions. Should we add support for the -draft prerelease suffix, or has the decision been made to move away from that pattern?

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

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()