diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 30fcd3c..a561819 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,7 +16,56 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.12" + - name: Verify release version + run: | + python - <<'PY' + import os + import re + import tomllib + from pathlib import Path + + ref_type = os.environ.get("GITHUB_REF_TYPE") + tag_name = os.environ.get("GITHUB_REF_NAME", "") + if ref_type != "tag": + raise SystemExit( + "Publishing requires a tag ref. For workflow_dispatch, run this workflow from the release tag." + ) + + expected = tag_name[1:] if tag_name.startswith("v") else tag_name + if not expected: + raise SystemExit("Could not determine release version from tag.") + + with Path("pyproject.toml").open("rb") as file: + pyproject_version = tomllib.load(file)["project"]["version"] + + def read_version(path: str, pattern: str) -> str: + match = re.search(pattern, Path(path).read_text(encoding="utf-8"), re.MULTILINE) + if match is None: + raise SystemExit(f"Could not find package version in {path}.") + return match.group(1) + + versions = { + "pyproject.toml": pyproject_version, + "setup.py": read_version("setup.py", r'^VERSION = "([^"]+)"$'), + "audithub_sdk/__init__.py": read_version( + "audithub_sdk/__init__.py", r'^__version__ = "([^"]+)"$' + ), + } + + mismatches = { + path: version + for path, version in versions.items() + if version != expected + } + if mismatches: + details = "\n".join( + f"- {path}: {version} != tag {tag_name} ({expected})" + for path, version in mismatches.items() + ) + raise SystemExit(f"Release version mismatch:\n{details}") + print(f"Release version {expected} matches package metadata.") + PY - name: Build distributions run: | python -m pip install --upgrade pip diff --git a/.openapi-generator-ignore b/.openapi-generator-ignore index 7484ee5..c89134b 100644 --- a/.openapi-generator-ignore +++ b/.openapi-generator-ignore @@ -21,3 +21,15 @@ #docs/*.md # Then explicitly reverse the ignore rule for a single file: #!docs/README.md + +# Repository-maintained files. Generated source, docs, and generated tests are +# still replaced during regeneration. +README.md +AGENTS.md +pyproject.toml +setup.py +.openapi-generator-ignore +.github/ +scripts/ +audithub_sdk_ext/ +tests/ diff --git a/AGENTS.md b/AGENTS.md index 0429511..adad042 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,13 @@ It is intended to be the shared/core dependency for other Python repositories th - Package version currently used during generation: `0.1.0` - Transport template: `httpx` -Generation command: +Preferred regeneration command: + +```sh +scripts/regenerate-sdk.sh 0.1.0 +``` + +The script runs the equivalent of: ```sh openapi-generator generate \ @@ -28,6 +34,7 @@ openapi-generator generate \ ## Important Decisions From Prior Sessions - The repo should remain mostly generated output. Manual edits should stay limited to repository-specific files and metadata. +- `bump-my-version` is configured for release version bumps, but `README.md`'s generator command should only be updated during SDK regeneration. ## Files That May Be Manually Maintained @@ -35,15 +42,33 @@ openapi-generator generate \ - `AGENTS.md` - `pyproject.toml` - `setup.py` +- `.openapi-generator-ignore` +- `scripts/regenerate-sdk.sh` +- `audithub_sdk_ext/` +- `tests/` - `.github/workflows/python.yml` - `.github/workflows/publish.yml` -Generated source under `audithub_sdk/`, generated docs under `docs/`, generated tests under `test/`, and generator metadata under `.openapi-generator/` should generally be replaced by regeneration rather than hand-edited. +Generated source under `audithub_sdk/`, generated docs under `docs/`, generated tests under `test/`, and generator metadata under `.openapi-generator/` should generally be replaced by regeneration rather than hand-edited. The narrow exception is package-version strings updated by `bump-my-version` in `audithub_sdk/__init__.py`, `audithub_sdk/api_client.py`, and `audithub_sdk/configuration.py`. + +## Version Bumping + +`bump-my-version` configuration lives in `pyproject.toml`. + +Common commands: + +```sh +bump-my-version bump patch +bump-my-version bump --new-version 0.1.1 +``` + +The configured bump is for release-only version bumps. For API schema changes, use `scripts/regenerate-sdk.sh VERSION` instead. The configured bump creates a commit and `v{new_version}` tag. It updates `pyproject.toml`, `setup.py`, and runtime package-version strings in generated source. It intentionally does not update `README.md` or `AGENTS.md`; those generation-contract versions are updated by `scripts/regenerate-sdk.sh`. ## CI And Publishing - Test workflow: `.github/workflows/python.yml` - Publish workflow: `.github/workflows/publish.yml` +- Publish workflow is read-only with respect to package version files and fails unless the release tag matches `pyproject.toml`, `setup.py`, and `audithub_sdk/__init__.py`. - Publish workflow uses GitHub Actions trusted publishing to PyPI via OIDC. - PyPI publishing expects the GitHub repository to be registered as a trusted publisher for the `audithub-sdk` project. diff --git a/README.md b/README.md index b7da9cd..854320a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ The client code in this repository is generated from the live OpenAPI document: `https://audithub.dev.veridise.tools/api/v1/openapi.json` -The repository contents are produced with OpenAPI Generator 7.20.0 using: +The repository contents are produced with OpenAPI Generator 7.20.0. The current generated package version is `0.1.0`. + +The current generation command is: ```sh openapi-generator generate \ @@ -21,16 +23,50 @@ openapi-generator generate \ ## Regenerating -When the AuditHub API schema changes, regenerate the SDK from the repository root with the command above. +When the AuditHub API schema changes, regenerate the SDK from the repository root and pass the new SDK version: + +```sh +scripts/regenerate-sdk.sh 0.1.1 +``` + +The script passes that version as `packageVersion` to OpenAPI Generator, then syncs repository-maintained version references in: + +- `pyproject.toml` +- `setup.py` +- `README.md` +- `AGENTS.md` + +The version in this README's generator command is intentionally updated only when regenerating the SDK. Manual changes in this repo should stay limited to repository-specific files such as: - `README.md` - packaging metadata - CI workflows +- `scripts/regenerate-sdk.sh` Generated source, models, docs, and tests should be replaced by regeneration rather than edited by hand. +## Version Bumping + +`bump-my-version` is configured in `pyproject.toml` for release-only version bumps. For API schema changes, use `scripts/regenerate-sdk.sh VERSION` instead. The bump command updates package metadata and runtime package-version strings, commits the change, and creates a `vX.Y.Z` tag: + +```sh +uv tool install bump-my-version +``` + +```sh +bump-my-version bump patch +``` + +To set an exact version: + +```sh +bump-my-version bump --new-version 0.1.1 +``` + +The bump configuration intentionally does not update this README's generator command. That command is updated by `scripts/regenerate-sdk.sh` when the SDK is regenerated. + ## Installation From PyPI: @@ -99,3 +135,16 @@ To build distributions locally: ```sh python -m build ``` + +Publishing is handled by `.github/workflows/publish.yml` from a GitHub release tag. The workflow does not edit version files; it fails unless the tag version matches: + +- `pyproject.toml` +- `setup.py` +- `audithub_sdk/__init__.py` + +Use `vX.Y.Z` tags for releases, for example: + +```sh +git tag v0.1.1 +git push origin v0.1.1 +``` diff --git a/pyproject.toml b/pyproject.toml index 89ea689..a8c4616 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,41 @@ types-python-dateutil = ">= 2.8.19.14" mypy = ">= 1.5" +[tool.bumpversion] +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" +serialize = ["{major}.{minor}.{patch}"] +search = "{current_version}" +replace = "{new_version}" +regex = false +ignore_missing_version = false +tag = true +sign_tags = false +tag_name = "v{new_version}" +tag_message = "Bump version: {current_version} -> {new_version}" +commit = true +message = "Bump version: {current_version} -> {new_version}" + +[[tool.bumpversion.files]] +filename = "setup.py" +search = 'VERSION = "{current_version}"' +replace = 'VERSION = "{new_version}"' + +[[tool.bumpversion.files]] +filename = "audithub_sdk/__init__.py" +search = '__version__ = "{current_version}"' +replace = '__version__ = "{new_version}"' + +[[tool.bumpversion.files]] +filename = "audithub_sdk/api_client.py" +search = "OpenAPI-Generator/{current_version}/python" +replace = "OpenAPI-Generator/{new_version}/python" + +[[tool.bumpversion.files]] +filename = "audithub_sdk/configuration.py" +search = "SDK Package Version: {current_version}" +replace = "SDK Package Version: {new_version}" + + [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" diff --git a/scripts/regenerate-sdk.sh b/scripts/regenerate-sdk.sh new file mode 100755 index 0000000..bedfe84 --- /dev/null +++ b/scripts/regenerate-sdk.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: scripts/regenerate-sdk.sh VERSION + +Regenerates the AuditHub Python SDK from the live OpenAPI document using +VERSION as the packageVersion, then syncs repository-maintained version +references. +USAGE +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +if [[ $# -ne 1 ]]; then + usage >&2 + exit 2 +fi + +VERSION="$1" + +if [[ -z "$VERSION" || "$VERSION" == *","* || "$VERSION" =~ [[:space:]] ]]; then + echo "Version must be non-empty and must not contain commas or whitespace." >&2 + exit 2 +fi + +if ! command -v openapi-generator >/dev/null 2>&1; then + echo "openapi-generator is required. Install OpenAPI Generator 7.20.0 before regenerating." >&2 + exit 127 +fi + +PYTHON="${PYTHON:-python3}" +if ! command -v "$PYTHON" >/dev/null 2>&1; then + echo "$PYTHON is required to sync repository metadata." >&2 + exit 127 +fi + +openapi-generator generate \ + -i https://audithub.dev.veridise.tools/api/v1/openapi.json \ + -g python \ + -o . \ + --additional-properties="packageName=audithub_sdk,projectName=audithub-sdk,packageVersion=${VERSION},hideGenerationTimestamp=true,library=httpx" + +"$PYTHON" - "$VERSION" <<'PY' +from __future__ import annotations + +from pathlib import Path +import re +import sys + + +version = sys.argv[1] + + +def replace_exact(path: str, pattern: str, replacement, expected: int) -> None: + file_path = Path(path) + text = file_path.read_text(encoding="utf-8") + updated, count = re.subn(pattern, replacement, text, flags=re.MULTILINE) + if count != expected: + raise SystemExit(f"{path}: expected {expected} replacement(s), made {count}") + file_path.write_text(updated, encoding="utf-8") + + +replace_exact( + "pyproject.toml", + r'^(version = ")[^"]+(")$', + lambda match: f"{match.group(1)}{version}{match.group(2)}", + 1, +) +replace_exact( + "setup.py", + r'^(VERSION = ")[^"]+(")$', + lambda match: f"{match.group(1)}{version}{match.group(2)}", + 1, +) +replace_exact( + "README.md", + r"packageVersion=[^,]+", + f"packageVersion={version}", + 1, +) +replace_exact( + "AGENTS.md", + r"(Package version currently used during generation: `)[^`]+(`)", + lambda match: f"{match.group(1)}{version}{match.group(2)}", + 1, +) +replace_exact( + "AGENTS.md", + r"packageVersion=[^,]+", + f"packageVersion={version}", + 1, +) +PY