From d4a040147b6bf5a9fe58347f83aa33173b9351ca Mon Sep 17 00:00:00 2001 From: pos Date: Tue, 6 Oct 2026 21:14:22 +0200 Subject: [PATCH 1/4] M2.25: expose installed package version --- src/storyforge/version.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/storyforge/version.py diff --git a/src/storyforge/version.py b/src/storyforge/version.py new file mode 100644 index 0000000..47f00c9 --- /dev/null +++ b/src/storyforge/version.py @@ -0,0 +1,8 @@ +from importlib.metadata import PackageNotFoundError, version + + +def storyforge_version() -> str: + try: + return version("storyforge") + except PackageNotFoundError: + return "0+unknown" From 74efd7b5de88bbe210ed43e7cdf280f630de16b6 Mon Sep 17 00:00:00 2001 From: pos Date: Tue, 6 Oct 2026 21:14:25 +0200 Subject: [PATCH 2/4] M2.25: add CLI version contract --- src/storyforge/cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/storyforge/cli.py b/src/storyforge/cli.py index 2ba2da8..ed2570e 100644 --- a/src/storyforge/cli.py +++ b/src/storyforge/cli.py @@ -22,6 +22,7 @@ from .provider_config import load_provider_config from .provider_runtime import resolve_role_providers from .http_transport import json_post_transport +from .version import storyforge_version TEMPLATE = { @@ -44,6 +45,7 @@ def main() -> int: parser = argparse.ArgumentParser(prog="storyforge") + parser.add_argument("--version", action="version", version=f"%(prog)s {storyforge_version()}") sub = parser.add_subparsers(dest="command", required=True) vm = sub.add_parser("validate-migrations") From a35c26ad9c4e88d7a219ba886c03037751d37258 Mon Sep 17 00:00:00 2001 From: pos Date: Tue, 6 Oct 2026 21:14:36 +0200 Subject: [PATCH 3/4] M2.25: enforce package and tag version contract --- .github/workflows/release-contract.yml | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/release-contract.yml diff --git a/.github/workflows/release-contract.yml b/.github/workflows/release-contract.yml new file mode 100644 index 0000000..297a474 --- /dev/null +++ b/.github/workflows/release-contract.yml @@ -0,0 +1,42 @@ +name: Release contract + +on: + pull_request: + paths: + - pyproject.toml + - src/storyforge/version.py + - src/storyforge/cli.py + - .github/workflows/release-contract.yml + push: + tags: ["v*"] + +permissions: + contents: read + +jobs: + version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install package + run: pip install . + - name: Verify installed CLI version + run: | + python - <<'PY' + import tomllib, subprocess + expected = tomllib.load(open("pyproject.toml", "rb"))["project"]["version"] + actual = subprocess.check_output(["storyforge", "--version"], text=True).strip() + assert actual == f"storyforge {expected}", (actual, expected) + PY + - name: Verify release tag matches package version + if: startsWith(github.ref, 'refs/tags/v') + run: | + python - <<'PY' + import os, tomllib + expected = "v" + tomllib.load(open("pyproject.toml", "rb"))["project"]["version"] + actual = os.environ["GITHUB_REF_NAME"] + assert actual == expected, f"tag {actual!r} != package {expected!r}" + PY From e55654e7262c0914fe05a772e0e1e2f0b1fa5ad6 Mon Sep 17 00:00:00 2001 From: pos Date: Tue, 6 Oct 2026 21:14:38 +0200 Subject: [PATCH 4/4] M2.25: document release version contract --- docs/release-contract.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/release-contract.md diff --git a/docs/release-contract.md b/docs/release-contract.md new file mode 100644 index 0000000..fbabff1 --- /dev/null +++ b/docs/release-contract.md @@ -0,0 +1,15 @@ +# Release and version contract + +StoryForge has one package version: `project.version` in `pyproject.toml`. + +The installed CLI exposes exactly that version: + +``` +storyforge --version +``` + +A version release tag must be `v`. The release-contract workflow rejects a tag whose version differs from the Python package. + +The OCI workflow already derives version tags from Git refs. Combining the two contracts means a successful `vX.Y.Z` release has the same semantic version in the Python package, CLI, Git tag, and OCI tag. + +Development pushes to `main` may still publish the moving `main` and SHA OCI tags. Immutable version tags are created only from matching Git version tags.