Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ same release controls as the projects it publishes.
| `python-tests.yml`, `python-versions.yml`, `rust-tests.yml` | Test matrices |
| `dev-security.yml` | Dependency review and workflow lint (`actionlint`, `zizmor`), the security checks `dev` requires |
| `stable-guard.yml` | Checks a pull request into `stable` is release-ready: a MAJOR.MINOR.PATCH version, matching chart versions, no git dependencies |
| `release-check.yml` | Runs on `stable` only: reads the version, refuses a partly published release, checks merged-PR provenance and that the `release` environment is protected |
| `release-check.yml` | Runs on `stable` only: reads the version, refuses a partly published release, checks merged-PR provenance and that the `release` environment is protected. The version comes from the package manifest (`pyproject.toml`, `Cargo.toml`), or from a `VERSION` file at the repository root in a repository without one, such as a Go or Docker-only repository; where both exist they must agree |
| `maturin-wheels.yml`, `crates.yml`, `docker.yml` | Build and publish artifacts |
| `docker-dev.yml` | `dev` channel images on GHCR (not releases) |
| `publish.yml` | Creates the tag and GitHub release with the release App, after everything is published |
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "release-workflows"
version = "1.1.0"
version = "1.2.0"
requires-python = ">=3.11"
dependencies = ["packaging", "pyyaml"]

Expand Down
22 changes: 17 additions & 5 deletions scripts/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def python_versions(root):
return versions


def version(root):
def manifest_version(root):
pyproject = load(root / "pyproject.toml")
project = pyproject.get("project", {})
if "version" in project:
Expand All @@ -95,10 +95,22 @@ def version(root):
if "version" in tool.get("poetry", {}):
return tool["poetry"]["version"]
cargo = root / tool.get("maturin", {}).get("manifest-path", "Cargo.toml")
found = cargo_version(cargo)
if found is None:
sys.exit(f"no static version in {root}")
return found
return cargo_version(cargo)


def version(root):
"""The release version: the package manifest's, or a VERSION file's for a repository without one (Docker or Go).

Where both exist they must agree, so a VERSION file can't drift from the manifest it duplicates.
"""
declared = manifest_version(root)
path = root / "VERSION"
from_file = path.read_text().strip() if path.is_file() else None
if declared is None and from_file is None:
sys.exit(f"no static version in {root}: add a VERSION file or a version to the package manifest")
if declared is not None and from_file is not None and declared != from_file:
sys.exit(f"VERSION {from_file} does not match manifest version {declared}")
return declared if declared is not None else from_file


def emit(**outputs):
Expand Down
41 changes: 41 additions & 0 deletions tests/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,44 @@ def test_release_refuses_git_crates(repo, cargo):
def test_release_allows_registry_dependencies_with_markers(repo):
root = repo('[project]\nname = "x"\nversion = "1.0.0"\ndependencies = ["radixtarget>=3; python_version >= \'3.11\'", "y[extra]~=1.0"]\n')
assert run(root, "release").returncode == 0


def bare(tmp_path, files):
for name, body in files.items():
(tmp_path / name).write_text(body)
subprocess.run(["git", "init", "-q"], cwd=tmp_path, check=True)
subprocess.run(["git", "add", "."], cwd=tmp_path, check=True)
return tmp_path


def test_release_reads_a_version_file_without_a_package_manifest(tmp_path):
root = bare(tmp_path, {"VERSION": "1.7.2\n", "go.mod": "module example.com/x\n"})
assert outputs(run(root, "release")) == {
"version": "1.7.2",
"tag": "v1.7.2",
"major": "1",
"minor": "7",
"pypi_name": "",
"crate_name": "",
}


def test_release_refuses_a_prerelease_version_file(tmp_path):
result = run(bare(tmp_path, {"VERSION": "1.7.2-rc1\n"}), "release")
assert result.returncode != 0 and "MAJOR.MINOR.PATCH" in result.stderr


def test_release_refuses_a_version_file_that_disagrees_with_the_manifest(repo):
result = run(repo('[project]\nname = "x"\nversion = "1.2.3"\n', {"VERSION": "1.2.4\n"}), "release")
assert result.returncode != 0 and "does not match manifest version 1.2.3" in result.stderr


def test_release_accepts_a_version_file_that_matches_the_manifest(repo):
root = repo('[project]\nname = "x"\nversion = "1.2.3"\n', {"VERSION": "1.2.3\n"})
subprocess.run(["git", "init", "-q"], cwd=root, check=True)
assert outputs(run(root, "release"))["version"] == "1.2.3"


def test_release_names_the_missing_version_source(tmp_path):
result = run(bare(tmp_path, {"go.mod": "module example.com/x\n"}), "release")
assert result.returncode != 0 and "add a VERSION file" in result.stderr
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading