diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8ecd261..caf14a4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,11 +51,32 @@ jobs: run: 'just quality' env: UV_PYTHON_VERSION: '3.12' + build-check: + name: 'build-check' + runs-on: 'ubuntu-latest' + steps: + - name: 'checkout repository' + uses: 'actions/checkout@v6' + with: + persist-credentials: false + - name: 'install just' + uses: 'extractions/setup-just@v3' + - name: 'setup uv' + uses: 'astral-sh/setup-uv@v7' + with: + enable-cache: true + cache-dependency-glob: '**/pyproject.toml' + python-version: '3.12' + - name: 'run build validation' + run: 'just build-check' + env: + UV_PYTHON_VERSION: '3.12' tests: name: 'tests (${{ matrix.python-version }})' needs: - 'quality' - 'docs' + - 'build-check' runs-on: 'ubuntu-latest' strategy: matrix: diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml index 0bb9fdc..a4a7a29 100644 --- a/.github/workflows/gh-pages.yaml +++ b/.github/workflows/gh-pages.yaml @@ -5,10 +5,27 @@ name: 'GitHub Pages Documentation' push: branches: - 'main' - release: - types: - - 'published' + workflow_call: + inputs: + deploy-mode: + description: 'Deployment mode used to determine docs aliases' + required: false + default: 'push' + type: 'string' workflow_dispatch: + inputs: + deploy-mode: + description: 'Deployment mode used to determine docs aliases' + required: false + default: 'push' + type: 'choice' + options: + - 'push' + - 'release' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true permissions: contents: 'read' @@ -44,7 +61,7 @@ jobs: enable-cache: true cache-dependency-glob: 'pyproject.toml' python-version: '${{ matrix.python-version }}' - - name: 'Extract flepimop2 version' + - name: 'Extract op_system version' id: extract-version run: | cat > version.py << EOF @@ -59,10 +76,25 @@ jobs: rm version.py - name: 'Build API Reference' run: 'just api-reference' + - name: 'Determine deploy mode' + id: determine-mode + run: | + if [ "${EVENT_NAME}" = "workflow_call" ]; then + echo "deploy-mode=${CALL_MODE}" >> "${GITHUB_OUTPUT}" + elif [ "${EVENT_NAME}" = "workflow_dispatch" ] \ + && [ -n "${DISPATCH_MODE}" ]; then + echo "deploy-mode=${DISPATCH_MODE}" >> "${GITHUB_OUTPUT}" + else + echo "deploy-mode=push" >> "${GITHUB_OUTPUT}" + fi + env: + CALL_MODE: '${{ inputs.deploy-mode }}' + DISPATCH_MODE: '${{ github.event.inputs.deploy-mode }}' + EVENT_NAME: '${{ github.event_name }}' - name: 'Deploy to GitHub Pages' run: | uv run mike set-default latest - if [ "${EVENT_NAME}" == "release" ] || [[ $VERSION == 0* ]]; then + if [ "${DEPLOY_MODE}" = "release" ] || [[ $VERSION == 0* ]]; then ALIAS=latest else ALIAS=dev @@ -71,6 +103,6 @@ jobs: --push --update-aliases \ ${VERSION} ${ALIAS} env: - EVENT_NAME: "${{ github.event_name }}" + DEPLOY_MODE: "${{ steps.determine-mode.outputs.deploy-mode }}" GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" VERSION: "${{ steps.extract-version.outputs.version }}" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..71c6353 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,196 @@ +--- +name: 'Release' + +'on': + workflow_dispatch: + inputs: + publish-target: + description: 'Package publish target' + required: true + default: 'none' + type: 'choice' + options: + - 'none' + - 'testpypi' + - 'pypi' + create-github-release: + description: 'Create the GitHub release after publishing' + required: true + default: false + type: 'boolean' + deploy-docs: + description: 'Deploy versioned docs after creating the GitHub release' + required: true + default: false + type: 'boolean' + +permissions: + contents: 'read' + +jobs: + validate: + name: 'Validate release' + runs-on: 'ubuntu-latest' + outputs: + version: '${{ steps.validate-version.outputs.version }}' + docs-version: '${{ steps.validate-version.outputs.docs-version }}' + prerelease: '${{ steps.validate-version.outputs.prerelease }}' + steps: + - name: 'Install just' + run: 'sudo apt install just' + - name: 'Checkout repository' + uses: 'actions/checkout@v6' + with: + fetch-depth: 0 + persist-credentials: false + - name: 'Setup uv with python 3.12' + uses: 'astral-sh/setup-uv@v7.2.0' + with: + enable-cache: true + cache-dependency-glob: | + pyproject.toml + flepimop2-op_system/pyproject.toml + python-version: '3.12' + - name: 'Validate shared package version' + id: validate-version + run: 'uv run python scripts/release_validate.py --github-output "${GITHUB_OUTPUT}"' + - name: 'Run release build validation' + run: 'just build-all' + env: + UV_PYTHON_VERSION: '3.12' + - name: 'Build core release artifacts' + run: | + rm -rf release-dist + mkdir -p release-dist/op_system + uv run --with build --with twine python -m build --outdir release-dist/op_system + uv run --with twine python -m twine check --strict release-dist/op_system/* + - name: 'Build provider release artifacts' + run: | + mkdir -p release-dist/flepimop2-op_system + cd flepimop2-op_system + uv run --with build --with twine python -m build --outdir ../release-dist/flepimop2-op_system + uv run --with twine python -m twine check --strict ../release-dist/flepimop2-op_system/* + - name: 'Upload core package artifacts' + uses: 'actions/upload-artifact@v4' + with: + name: 'op-system-dist' + path: 'release-dist/op_system/*' + if-no-files-found: 'error' + - name: 'Upload provider package artifacts' + uses: 'actions/upload-artifact@v4' + with: + name: 'flepimop2-op-system-dist' + path: 'release-dist/flepimop2-op_system/*' + if-no-files-found: 'error' + + publish-core: + name: 'Publish op_system' + needs: + - 'validate' + runs-on: 'ubuntu-latest' + env: + PACKAGE_DIST_DIR: 'dist/op_system' + permissions: + id-token: 'write' + steps: + - name: 'Skip core publish' + if: "${{ github.event.inputs.publish-target == 'none' }}" + run: 'echo "publish-target=none; skipping op_system publish."' + - name: 'Download core package artifacts' + if: "${{ github.event.inputs.publish-target != 'none' }}" + uses: 'actions/download-artifact@v4' + with: + name: 'op-system-dist' + path: '${{ env.PACKAGE_DIST_DIR }}' + - name: 'Publish core package to TestPyPI' + if: "${{ github.event.inputs.publish-target == 'testpypi' }}" + uses: 'pypa/gh-action-pypi-publish@release/v1' + with: + packages-dir: '${{ env.PACKAGE_DIST_DIR }}' + repository-url: 'https://test.pypi.org/legacy/' + - name: 'Publish core package to PyPI' + if: "${{ github.event.inputs.publish-target == 'pypi' }}" + uses: 'pypa/gh-action-pypi-publish@release/v1' + with: + packages-dir: '${{ env.PACKAGE_DIST_DIR }}' + + publish-provider: + name: 'Publish flepimop2-op_system' + needs: + - 'validate' + - 'publish-core' + runs-on: 'ubuntu-latest' + env: + PACKAGE_DIST_DIR: 'dist/flepimop2-op_system' + permissions: + id-token: 'write' + steps: + - name: 'Skip provider publish' + if: "${{ github.event.inputs.publish-target == 'none' }}" + run: 'echo "publish-target=none; skipping flepimop2-op_system publish."' + - name: 'Download provider package artifacts' + if: "${{ github.event.inputs.publish-target != 'none' }}" + uses: 'actions/download-artifact@v4' + with: + name: 'flepimop2-op-system-dist' + path: '${{ env.PACKAGE_DIST_DIR }}' + - name: 'Publish provider package to TestPyPI' + if: "${{ github.event.inputs.publish-target == 'testpypi' }}" + uses: 'pypa/gh-action-pypi-publish@release/v1' + with: + packages-dir: '${{ env.PACKAGE_DIST_DIR }}' + repository-url: 'https://test.pypi.org/legacy/' + - name: 'Publish provider package to PyPI' + if: "${{ github.event.inputs.publish-target == 'pypi' }}" + uses: 'pypa/gh-action-pypi-publish@release/v1' + with: + packages-dir: '${{ env.PACKAGE_DIST_DIR }}' + + create-github-release: + name: 'Create GitHub release' + needs: + - 'validate' + - 'publish-provider' + if: >- + ${{ + github.event.inputs.create-github-release == 'true' && + (github.event.inputs.publish-target == 'none' || + needs.publish-provider.result == 'success') + }} + runs-on: 'ubuntu-latest' + permissions: + contents: 'write' + steps: + - name: 'Create release' + run: | + ARGS=( + release create "v${VERSION}" + --target "${GITHUB_SHA}" + --title "v${VERSION}" + --generate-notes + ) + if [ "${PRERELEASE}" = 'true' ]; then + ARGS+=(--prerelease) + fi + gh "${ARGS[@]}" + env: + GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + GITHUB_SHA: '${{ github.sha }}' + PRERELEASE: '${{ needs.validate.outputs.prerelease }}' + VERSION: '${{ needs.validate.outputs.version }}' + + deploy-docs: + name: 'Deploy release docs' + needs: + - 'create-github-release' + if: >- + ${{ + github.event.inputs.deploy-docs == 'true' && + needs.create-github-release.result == 'success' + }} + permissions: + contents: 'write' + uses: './.github/workflows/gh-pages.yaml' + with: + deploy-mode: 'release' + secrets: 'inherit' diff --git a/docs/development/creating-a-release.md b/docs/development/creating-a-release.md new file mode 100644 index 0000000..21b07db --- /dev/null +++ b/docs/development/creating-a-release.md @@ -0,0 +1,132 @@ +# Creating A Release + +This guide covers the `op_system` release process for this repository's two Python distributions: + +- `op_system` +- `flepimop2-op_system` + +The release workflow treats them as a single release unit. Both package versions must match, and the workflow publishes them sequentially under one GitHub release tag. + +## Prerequisites + +- You have write access to the [`ACCIDDA/op_system`](https://github.com/ACCIDDA/op_system) repository. +- The release version has already been updated everywhere it is declared: + - `pyproject.toml` + - `src/op_system/__init__.py` + - `flepimop2-op_system/pyproject.toml` +- Your local environment is synced with a supported Python version. +- You have [GitHub's `gh` CLI](https://cli.github.com/) installed and authenticated if you plan to dispatch workflows from the command line. + +## 1. Confirm The Shared Version + +The release workflow validates that all version declarations match before it builds or publishes anything. + +Today that means these three files must contain the same semantic version: + +- `pyproject.toml` +- `src/op_system/__init__.py` +- `flepimop2-op_system/pyproject.toml` + +If any of them differ, the `validate` job fails immediately. + +## 2. Run The Local Release Preflight + +Use the local pre-release target before dispatching the release workflow: + +```shell +just release-validate +``` + +That command does two things: + +- Validates that the release version matches in: + - `pyproject.toml` + - `src/op_system/__init__.py` + - `flepimop2-op_system/pyproject.toml` +- Runs `just build-all` to execute the clean-room build and install tests for both packages. + +The release workflow runs the same validation again on GitHub Actions before publishing. + +## 3. Run The Release Workflow + +Releases are created through the manual GitHub Actions workflow in `.github/workflows/release.yaml`. + +If you are testing the workflow before merging, add `--ref ` to the `gh workflow run` command. Without `--ref`, GitHub dispatches the workflow definition from the repository's default branch. + +### Dry Run + +Use this to validate shared versioning, build both packages, and upload release artifacts without publishing anything: + +```shell +gh workflow run release.yaml \ + --repo ACCIDDA/op_system \ + --ref \ + --field publish-target=none \ + --field create-github-release=false \ + --field deploy-docs=false +``` + +This runs the `validate`, `publish-core`, and `publish-provider` jobs, but the publish jobs become no-ops when `publish-target=none`. + +### TestPyPI + +Use this to publish both packages to TestPyPI without creating the GitHub release or deploying docs: + +```shell +gh workflow run release.yaml \ + --repo ACCIDDA/op_system \ + --ref \ + --field publish-target=testpypi \ + --field create-github-release=false \ + --field deploy-docs=false +``` + +The workflow always publishes in dependency order: + +1. `op_system` +2. `flepimop2-op_system` + +When testing from a branch, keep `create-github-release=false` and `deploy-docs=false`. The reusable docs workflow checks out `main`, so it is not intended for pre-merge branch testing. + +### PyPI + +Use this to publish both packages, create a GitHub release, and deploy the versioned documentation: + +```shell +gh workflow run release.yaml \ + --repo ACCIDDA/op_system \ + --field publish-target=pypi \ + --field create-github-release=true \ + --field deploy-docs=true +``` + +When `create-github-release=true`, the workflow creates a single `vX.Y.Z` GitHub release for the shared package version and uses GitHub's generated release notes. + +## 4. Documentation Deployment + +The release workflow calls `.github/workflows/gh-pages.yaml` as a reusable workflow when `deploy-docs=true`. + +That workflow supports two deployment modes: + +- `push`: for normal `main` branch documentation updates +- `release`: for a tagged release, which updates the release alias in `mike` + +If you want to deploy docs manually without running a release, you can dispatch `gh-pages.yaml` directly and choose the deploy mode in the GitHub Actions UI. + +## 5. Trusted Publishing Setup + +The publish jobs use PyPI Trusted Publishing rather than a stored API token. + +For `publish-target=testpypi`, configure the TestPyPI trusted publisher entry for: + +- Owner: `ACCIDDA` +- Repository: `op_system` +- Workflow file: `release.yaml` + +For `publish-target=pypi`, configure the PyPI trusted publisher entry for the same repository and workflow file. + +Because this repository publishes two distributions from one workflow, the same trusted publisher setup must be allowed to publish both package names on the selected index. + +## 6. Current Packaging Note + +The release workflow now handles the mechanics of validating, building, and sequencing both distributions together. Package-index acceptance still depends on the metadata inside each built distribution, so publishing `flepimop2-op_system` to a public index may still require follow-up packaging changes outside the workflow itself. diff --git a/flepimop2-op_system/README.md b/flepimop2-op_system/README.md new file mode 100644 index 0000000..c7feb9f --- /dev/null +++ b/flepimop2-op_system/README.md @@ -0,0 +1,5 @@ +# flepimop2-op_system + +`flepimop2-op_system` provides the `flepimop2` system adapter for `op_system`. + +It packages the `flepimop2.system.op_system` provider so `flepimop2` can load and execute RHS specifications compiled by the core `op_system` package. diff --git a/flepimop2-op_system/pyproject.toml b/flepimop2-op_system/pyproject.toml index 7b25700..3689a98 100644 --- a/flepimop2-op_system/pyproject.toml +++ b/flepimop2-op_system/pyproject.toml @@ -1,7 +1,8 @@ [project] name = "flepimop2-op_system" -version = "0.1.0" +version = "0.1.1" description = "flepimop2 system provider package for op_system" +readme = "README.md" requires-python = ">=3.11,<3.15" authors = [ { name = "Joshua Macdonald", email = "jmacdo16@jh.edu" }, @@ -10,12 +11,8 @@ authors = [ ] license = { file = "LICENSE" } dependencies = [ - # Declare the dependency normally... "op-system>=0.1.0", - - # flepimop2 is not in a registry, so keep it as a direct reference. - "flepimop2 @ git+https://github.com/ACCIDDA/flepimop2.git@main", - + "flepimop2>=0.1.0", "pydantic>=2.0,<3", "numpy>=1.26", "PyYAML>=6.0", @@ -41,10 +38,6 @@ Repository = "https://github.com/ACCIDDA/op_system" requires = ["hatchling"] build-backend = "hatchling.build" -# REQUIRED for hatchling to accept git/direct URL dependencies -[tool.hatch.metadata] -allow-direct-references = true - # Ensure hatchling can build from src/ layout and include the namespace package [tool.hatch.build.targets.wheel] packages = ["src/flepimop2"] diff --git a/justfile b/justfile index 221f37b..d3f6147 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,6 @@ run := "uv run" provider_dir := "flepimop2-op_system" -provider_run := run + " --project " + provider_dir + " --directory " + provider_dir +provider_run := run + " --directory " + provider_dir # Run all default tasks for local development default: dev docs @@ -104,6 +104,81 @@ mypy-provider: provider-sync [group('ci')] quality: ci-ruff mypy +# Build core package artifacts and validate metadata +[group('ci')] +build-check-core: + rm -rf dist + {{run}} --with build --with twine python -m build --wheel + {{run}} --with twine python -m twine check --strict dist/* + +# Build provider package artifacts and validate metadata +[group('ci')] +build-check-provider: + cd {{provider_dir}} && rm -rf dist + {{provider_run}} --with build --with twine python -m build --wheel + cd {{provider_dir}} && uv run --with twine python -m twine check --strict dist/* + +[group('ci')] +build-check: build-check-core build-check-provider + +# Install built core package into a clean environment and run tests against the wheel +[group('ci')] +build-test-core: + #!/usr/bin/env bash + set -euo pipefail + CLEANROOM="$(mktemp -d)" + trap 'rm -rf "${CLEANROOM}"' EXIT + uv export --frozen --only-group dev --no-emit-project --format requirements.txt --no-hashes --output-file "${CLEANROOM}/dev-requirements.txt" >/dev/null + {{run}} --with build python -m build --wheel --outdir "${CLEANROOM}/dist" + uv venv --python "${UV_PYTHON_VERSION:-3.12}" "${CLEANROOM}/venv" + uv pip install --python "${CLEANROOM}/venv/bin/python" "${CLEANROOM}/dist"/*.whl + uv pip install --python "${CLEANROOM}/venv/bin/python" -r "${CLEANROOM}/dev-requirements.txt" + cp pyproject.toml "${CLEANROOM}/pyproject.toml" + cp -R tests "${CLEANROOM}/tests" + cd "${CLEANROOM}" + "${CLEANROOM}/venv/bin/pytest" --import-mode=importlib tests --quiet --exitfirst + +# Install built provider package into a clean environment and run provider tests against the wheel +[group('ci')] +build-test-provider: + #!/usr/bin/env bash + set -euo pipefail + CLEANROOM="$(mktemp -d)" + trap 'rm -rf "${CLEANROOM}"' EXIT + cd {{provider_dir}} + uv export --frozen --only-group dev --no-emit-project --format requirements.txt --no-hashes --output-file "${CLEANROOM}/dev-requirements.txt" >/dev/null + uv run --with build python -m build --wheel --outdir "${CLEANROOM}/provider-dist" + cd .. + {{run}} --with build python -m build --wheel --outdir "${CLEANROOM}/core-dist" + uv venv --python "${UV_PYTHON_VERSION:-3.12}" "${CLEANROOM}/venv" + uv pip install --python "${CLEANROOM}/venv/bin/python" "flepimop2 @ git+https://github.com/ACCIDDA/flepimop2.git@main" + uv pip install --python "${CLEANROOM}/venv/bin/python" "${CLEANROOM}/core-dist"/*.whl + uv pip install --python "${CLEANROOM}/venv/bin/python" --no-deps "${CLEANROOM}/provider-dist"/*.whl + uv pip install --python "${CLEANROOM}/venv/bin/python" -r "${CLEANROOM}/dev-requirements.txt" + cp {{provider_dir}}/pyproject.toml "${CLEANROOM}/pyproject.toml" + cp -R {{provider_dir}}/src "${CLEANROOM}/src" + cp -R {{provider_dir}}/tests "${CLEANROOM}/tests" + cd "${CLEANROOM}" + export PATH="${CLEANROOM}/venv/bin:${PATH}" + "${CLEANROOM}/venv/bin/pytest" --import-mode=importlib tests --quiet --exitfirst + +[group('ci')] +build-test: build-test-core build-test-provider + +[group('ci')] +build-all-core: build-check-core build-test-core + +[group('ci')] +build-all-provider: build-check-provider build-test-provider + +[group('ci')] +build-all: build-all-core build-all-provider + +release-check: + {{run}} python scripts/release_validate.py + +release-validate: release-check build-all + # Build API reference for the documentation using `mkdocstrings` [group('docs')] api-reference: @@ -123,10 +198,12 @@ serve: api-reference [group('dev')] [unix] clean: + rm -rf dist rm -rf site rm -f uv.lock rm -rf .venv rm -rf .*_cache + rm -rf flepimop2-op_system/dist rm -f flepimop2-op_system/uv.lock rm -rf flepimop2-op_system/.venv rm -rf flepimop2-op_system/.*_cache diff --git a/mkdocs.yml b/mkdocs.yml index cfce0a4..df9f081 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,6 +43,8 @@ markdown_extensions: # Navigation structure nav: - Home: index.md + - Development: + - Creating A Release: development/creating-a-release.md - Guides: - Getting Started: guides/getting-started.md - API Reference: diff --git a/pyproject.toml b/pyproject.toml index 8a1a994..b28d7b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,7 @@ ignore = [ "tests/**/*" = ["INP001", "PLC2701", "S101"] "flepimop2-op_system/src/flepimop2/system/op_system/__init__.py" = ["ANN401", "RUF067"] "scripts/**/*" = [ + "N999", "T201", # `print` found ] diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..b4c0756 --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Repository utility scripts.""" diff --git a/scripts/release_validate.py b/scripts/release_validate.py new file mode 100644 index 0000000..5d5f0ec --- /dev/null +++ b/scripts/release_validate.py @@ -0,0 +1,108 @@ +"""Validate release version consistency for both distributable packages.""" + +from __future__ import annotations + +import argparse +import os +import pathlib +import re +import tomllib +from typing import Final + +REPO_ROOT: Final[pathlib.Path] = pathlib.Path(__file__).resolve().parents[1] +SEMVER_PATTERN: Final[re.Pattern[str]] = re.compile(r"^\d+\.\d+\.\d+$") +INIT_PATTERN: Final[re.Pattern[str]] = re.compile( + r'^__version__\s*=\s*"([^"]+)"', re.MULTILINE +) + + +def get_declared_versions() -> dict[str, str]: + """Read each release version declaration used by this repository. + + Returns: + A mapping from file path to declared release version. + + Raises: + SystemExit: If `src/op_system/__init__.py` does not define + `op_system.__version__`. + """ + core_version = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text("utf-8"))[ + "project" + ]["version"] + provider_version = tomllib.loads( + (REPO_ROOT / "flepimop2-op_system" / "pyproject.toml").read_text("utf-8") + )["project"]["version"] + + init_text = (REPO_ROOT / "src" / "op_system" / "__init__.py").read_text("utf-8") + init_match = INIT_PATTERN.search(init_text) + if init_match is None: + msg = "Unable to find op_system.__version__ in src/op_system/__init__.py." + raise SystemExit(msg) + + return { + "pyproject.toml": str(core_version), + "flepimop2-op_system/pyproject.toml": str(provider_version), + "src/op_system/__init__.py": init_match.group(1), + } + + +def validate_release_version() -> str: + """Validate that all release version declarations agree. + + Returns: + The shared release version. + + Raises: + SystemExit: If any declared versions differ or do not follow `X.Y.Z`. + """ + versions = get_declared_versions() + distinct_versions = sorted(set(versions.values())) + if len(distinct_versions) != 1: + rendered = ", ".join(f"{path}={version}" for path, version in versions.items()) + msg = f"Release versions do not match across packages: {rendered}" + raise SystemExit(msg) + + version = distinct_versions[0] + if SEMVER_PATTERN.fullmatch(version) is None: + msg = f"Release version must be semantic X.Y.Z, got {version!r}." + raise SystemExit(msg) + + return version + + +def write_github_outputs(version: str, output_path: pathlib.Path) -> None: + """Write workflow outputs for downstream GitHub Actions jobs.""" + docs_version = ".".join(version.split(".")[:2]) + prerelease = str(version.startswith("0.")).lower() + with output_path.open("a", encoding="utf-8") as fh: + fh.write(f"version={version}\n") + fh.write(f"docs-version={docs_version}\n") + fh.write(f"prerelease={prerelease}\n") + + +def main() -> None: + """Run release validation.""" + parser = argparse.ArgumentParser() + parser.add_argument( + "--github-output", + type=pathlib.Path, + default=None, + help="Optional path to a GitHub Actions output file.", + ) + args = parser.parse_args() + + version = validate_release_version() + print(f"Validated release version: {version}") + + output_path = args.github_output + if output_path is None: + raw_output = os.environ.get("GITHUB_OUTPUT") + if raw_output: + output_path = pathlib.Path(raw_output) + + if output_path is not None: + write_github_outputs(version, output_path) + + +if __name__ == "__main__": + main() diff --git a/src/op_system/__init__.py b/src/op_system/__init__.py index 0853393..1440444 100644 --- a/src/op_system/__init__.py +++ b/src/op_system/__init__.py @@ -36,7 +36,7 @@ # Versioning & capability metadata # ----------------------------------------------------------------------------- -__version__ = "0.1.0" +__version__ = "0.1.1" SUPPORTED_RHS_KINDS: tuple[str, ...] = ("expr", "transitions") # noqa: RUF067