diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..952f8d1 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,14 @@ +changelog: + categories: + - title: Compatibility-significant changes + labels: + - compatibility + - schema + - generated-runtime + - conformance + - title: Fixes + labels: + - bug + - title: Other changes + labels: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8359302 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI + +on: + pull_request: + push: + branches: + - master + +jobs: + test-and-build: + name: Test, build, and prove artifact install + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Set up uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Install development dependencies + run: uv sync --all-extras --dev + + - name: Run tests + run: uv run pytest + + - name: Run lint + run: uv run ruff check src tests + + - name: Build package artifacts + run: uv build + + - name: Prove wheel installs outside the source tree + run: | + python -m venv .artifact-venv + . .artifact-venv/bin/activate + python -m pip install --upgrade pip + python -m pip install dist/*.whl + cd "$(mktemp -d)" + python - <<'PY' + import importlib.metadata + import command_generation + + print(importlib.metadata.version("command-generation")) + print(command_generation.command_package_schema_path()) + PY + + - name: Upload package artifacts + uses: actions/upload-artifact@v4 + with: + name: command-generation-dist + path: dist/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..73eb0a2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,76 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +permissions: + contents: write + +jobs: + release: + name: Build and publish semver artifacts + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Set up uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Validate tag matches package version + run: | + PACKAGE_VERSION="$(python - <<'PY' + import tomllib + + with open("pyproject.toml", "rb") as handle: + print(tomllib.load(handle)["project"]["version"]) + PY + )" + TAG_VERSION="${GITHUB_REF_NAME#v}" + if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then + echo "Tag ${GITHUB_REF_NAME} does not match pyproject version ${PACKAGE_VERSION}" >&2 + exit 1 + fi + + - name: Install development dependencies + run: uv sync --all-extras --dev + + - name: Run tests + run: uv run pytest + + - name: Run lint + run: uv run ruff check src tests + + - name: Build package artifacts + run: uv build + + - name: Prove wheel installs outside the source tree + run: | + python -m venv .artifact-venv + . .artifact-venv/bin/activate + python -m pip install --upgrade pip + python -m pip install dist/*.whl + cd "$(mktemp -d)" + python - <<'PY' + import importlib.metadata + import command_generation + + print(importlib.metadata.version("command-generation")) + print(command_generation.command_package_schema_path()) + PY + + - name: Publish GitHub release + uses: softprops/action-gh-release@v2 + with: + files: dist/* + generate_release_notes: true + fail_on_unmatched_files: true diff --git a/README.md b/README.md index 00a2184..fd05fb4 100644 --- a/README.md +++ b/README.md @@ -92,4 +92,6 @@ uv run python tests/primitive_conformance.py This package should become a semver-tagged maintainer dependency with CI-built wheel/sdist artifacts. Downstream repositories should be able to consume immutable package versions instead of Git source refs. Until that release path is in place, source installs may remain a development fallback, but generated runtimes still must not import this package at runtime. +Pull request CI builds wheel and sdist artifacts and proves the built wheel can be installed outside the source tree. Semver releases are created from `vMAJOR.MINOR.PATCH` tags, validate that the tag matches `pyproject.toml`, and attach the built artifacts to the GitHub Release. + See `docs/release-and-versioning.md` for the intended package and compatibility model. diff --git a/docs/release-and-versioning.md b/docs/release-and-versioning.md index 7886752..ddb773e 100644 --- a/docs/release-and-versioning.md +++ b/docs/release-and-versioning.md @@ -14,6 +14,14 @@ The ordinary downstream path should be: GitHub source installs are acceptable during development and transition, but they should not remain the ordinary compatibility signal. +## CI And Release Mechanics + +Pull request CI builds the package with `uv build`, uploads the `dist/` artifacts, and installs the built wheel into a fresh virtual environment from outside the source tree. That install proof is the ordinary guard against accidentally relying on editable-source imports, untracked files, or repository layout. + +Semver releases are cut by pushing a `vMAJOR.MINOR.PATCH` tag. The release workflow validates that the tag version matches `project.version` in `pyproject.toml`, reruns tests and lint, rebuilds wheel/sdist artifacts, proves wheel installation from `dist/`, and attaches the artifacts to the GitHub Release. + +Release notes are generated from merged PRs and classify compatibility-significant changes separately. PRs that change the command package IR schema, generated runtime layout, conformance semantics, target extension contract, or primitive behavior should use a compatibility label such as `schema`, `generated-runtime`, `conformance`, or `compatibility`. + ## Compatibility Signals Release notes should call out changes that affect consumers: diff --git a/tests/test_release_workflows.py b/tests/test_release_workflows.py new file mode 100644 index 0000000..dfccfc9 --- /dev/null +++ b/tests/test_release_workflows.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_ci_builds_and_proves_install_from_package_artifact() -> None: + workflow = (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") + + assert "uv build" in workflow + assert "python -m pip install dist/*.whl" in workflow + assert "cd \"$(mktemp -d)\"" in workflow + assert "import command_generation" in workflow + assert "actions/upload-artifact" in workflow + + +def test_release_workflow_publishes_semver_tag_artifacts() -> None: + workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text(encoding="utf-8") + + assert '"v*.*.*"' in workflow + assert "TAG_VERSION=\"${GITHUB_REF_NAME#v}\"" in workflow + assert "Tag ${GITHUB_REF_NAME} does not match pyproject version" in workflow + assert "uv build" in workflow + assert "python -m pip install dist/*.whl" in workflow + assert "softprops/action-gh-release" in workflow + assert "files: dist/*" in workflow + assert "generate_release_notes: true" in workflow + + +def test_release_notes_classify_compatibility_significant_changes() -> None: + release_config = (ROOT / ".github" / "release.yml").read_text(encoding="utf-8") + + assert "Compatibility-significant changes" in release_config + assert "schema" in release_config + assert "generated-runtime" in release_config + assert "conformance" in release_config