diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 6d7cd44..f0bcfa7 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -83,11 +83,17 @@ jobs: python -m pip install dist/*.whl - name: Validate consumer manifest run: python scripts/validate_consumers.py - - name: Install consumers independently + - name: Install consumers without dependency resolution run: | - python -m pip install compatibility/consumers/atlas_click - python -m pip install compatibility/consumers/beacon_typer - python -m pip install "compatibility/consumers/cinder_automation[observability]" + python -m pip install --no-deps compatibility/consumers/atlas_click + python -m pip install --no-deps compatibility/consumers/beacon_typer + python -m pip install --no-deps "compatibility/consumers/cinder_automation[observability]" + - name: Verify the installed framework matches the source release + run: | + expected="$(tr -d '\r\n' < VERSION)" + actual="$(python -c 'from importlib.metadata import version; print(version("base-cli"))')" + test "$actual" = "$expected" + python -m pip check - name: Run downstream compatibility suites run: | for tests in compatibility/consumers/*/tests; do diff --git a/compatibility/README.md b/compatibility/README.md index f13c0d9..e60093f 100644 --- a/compatibility/README.md +++ b/compatibility/README.md @@ -16,20 +16,25 @@ outcomes. `scripts/validate_consumers.py` validates the manifest, package metadata, and required compatibility documentation. The `Reference consumers` workflow -builds and installs the base-cli wheel first, installs each consumer with its -own dependencies, and runs each consumer's tests. This catches import, -packaging, adapter, and contract regressions without relying on repository -source imports. +builds and installs the base-cli wheel first, installs each consumer without +dependency resolution, verifies that the installed distribution still matches +the source release, and runs each consumer's tests. This prevents pip from +silently replacing the release candidate with an older published wheel. The downstream job records a dated JSON result as the `base-cli-compatibility-evidence-` artifact. It binds the result to the framework revision and version and lists the exact fixture and matrix that -passed. See [`adoption-evidence.md`](../docs/adoption-evidence.md) for the -claim and permission boundary. +passed. The record remains `schema_version: 1`; `source_version` identifies +the checked-out `VERSION`, while `framework_version` identifies the installed +distribution. These are additive metadata fields, so readers should not reject +the record merely because a newer record contains additional fields. See +[`adoption-evidence.md`](../docs/adoption-evidence.md) for the claim and +permission boundary. The same workflow runs the Typer adapter and Beacon fixture against Typer -0.25.1, 0.26.0, and 0.27.1 on Python 3.10 through 3.14. This matrix covers the -transition from Click's public command classes to Typer's vendored Click fork. +0.25.1, 0.26.0, 0.27.1, and 0.27.2 on Python 3.10 through 3.14. This matrix +covers the transition from Click's public command classes to Typer's vendored +Click fork. Run the same checks locally: @@ -37,9 +42,17 @@ Run the same checks locally: python scripts/validate_consumers.py python -m build --wheel python -m pip install dist/base_cli-*.whl -for consumer in compatibility/consumers/*; do python -m pip install "$consumer"; done +for consumer in compatibility/consumers/*; do python -m pip install --no-deps "$consumer"; done +python -m pip check for tests in compatibility/consumers/*/tests; do python -m pytest "$tests"; done ``` +The fixture metadata targets `base-cli>=0.5,<0.6`. Dependency-resolving +installation therefore requires the 0.5 package to be available from the +configured index. The workflow instead installs the reviewed wheel first, +installs each fixture with `--no-deps`, verifies the installed framework +version, and then runs `pip check` so the compatibility result cannot silently +fall back to an older published framework. + The fixtures are not customer claims. A permissioned public adopter can be added as a separate record while retaining the same downstream contract tests. diff --git a/compatibility/consumers/atlas_click/README.md b/compatibility/consumers/atlas_click/README.md index feca45d..8804e3c 100644 --- a/compatibility/consumers/atlas_click/README.md +++ b/compatibility/consumers/atlas_click/README.md @@ -6,4 +6,5 @@ without rebuilding the Click tree or changing the inventory output contract. Install with `python -m pip install .`, run `atlas-consumer --help`, and execute `atlas-consumer --quiet inventory`. The package is pinned to the supported -`base-cli` 0.3 minor window; tests run against the installed wheel in CI. +`base-cli` 0.5 minor window; tests run against the installed release-candidate +wheel in CI. diff --git a/compatibility/consumers/atlas_click/pyproject.toml b/compatibility/consumers/atlas_click/pyproject.toml index ea1d77b..2864a9b 100644 --- a/compatibility/consumers/atlas_click/pyproject.toml +++ b/compatibility/consumers/atlas_click/pyproject.toml @@ -7,7 +7,7 @@ name = "base-cli-compat-consumer-atlas" version = "0.1.0" description = "Independent Click consumer compatibility fixture for base-cli" requires-python = ">=3.10" -dependencies = ["base-cli>=0.3,<0.5", "click>=8.1"] +dependencies = ["base-cli>=0.5,<0.6", "click>=8.1,<8.6"] [project.scripts] atlas-consumer = "atlas_click.cli:main" diff --git a/compatibility/consumers/beacon_typer/README.md b/compatibility/consumers/beacon_typer/README.md index 5dcd9c0..c7c8237 100644 --- a/compatibility/consumers/beacon_typer/README.md +++ b/compatibility/consumers/beacon_typer/README.md @@ -5,7 +5,8 @@ proves that `base_cli.attach_typer()` preserves Typer's generated help, validation, and callback behavior while adding the `base-cli` framework lifecycle. Install with `python -m pip install .`, run `beacon-consumer --help`, and execute -`beacon-consumer --quiet deploy --service api --replicas 3`. The supported Typer -range is explicit in package metadata and is exercised by the compatibility -workflow across Typer 0.25.1, 0.26.0, and 0.27.1 on Python 3.10 through 3.14. -Run `python -m pytest tests` to repeat the downstream tests locally. +`beacon-consumer --quiet deploy --service api --replicas 3`. The package targets +the `base-cli` 0.5 minor window. The supported Typer range is explicit in +package metadata and is exercised by the compatibility workflow across Typer +0.25.1, 0.26.0, 0.27.1, and 0.27.2 on Python 3.10 through 3.14. Run +`python -m pytest tests` to repeat the downstream tests locally. diff --git a/compatibility/consumers/beacon_typer/pyproject.toml b/compatibility/consumers/beacon_typer/pyproject.toml index 9cda485..8c27d5a 100644 --- a/compatibility/consumers/beacon_typer/pyproject.toml +++ b/compatibility/consumers/beacon_typer/pyproject.toml @@ -7,7 +7,7 @@ name = "base-cli-compat-consumer-beacon" version = "0.1.0" description = "Independent Typer consumer compatibility fixture for base-cli" requires-python = ">=3.10" -dependencies = ["base-cli>=0.3,<0.5", "typer>=0.25.1,<0.28"] +dependencies = ["base-cli>=0.5,<0.6", "typer>=0.25.1,<0.28"] [project.scripts] beacon-consumer = "beacon_typer.cli:main" diff --git a/compatibility/consumers/cinder_automation/README.md b/compatibility/consumers/cinder_automation/README.md index b984574..2c69565 100644 --- a/compatibility/consumers/cinder_automation/README.md +++ b/compatibility/consumers/cinder_automation/README.md @@ -9,4 +9,5 @@ Install with `python -m pip install '.[observability]'`, run `cinder-consumer --help`, and start with `cinder-consumer --quiet --dry-run --target warehouse`. Exporters are optional; the command remains successful when no provider is configured. +The package targets the supported `base-cli` 0.5 minor window. Run `python -m pytest tests` to repeat the dry-run and JSON contract tests. diff --git a/compatibility/consumers/cinder_automation/pyproject.toml b/compatibility/consumers/cinder_automation/pyproject.toml index 955afb0..767ade4 100644 --- a/compatibility/consumers/cinder_automation/pyproject.toml +++ b/compatibility/consumers/cinder_automation/pyproject.toml @@ -7,7 +7,7 @@ name = "base-cli-compat-consumer-cinder" version = "0.1.0" description = "Independent automation consumer compatibility fixture for base-cli" requires-python = ">=3.10" -dependencies = ["base-cli>=0.3,<0.5", "click>=8.1"] +dependencies = ["base-cli>=0.5,<0.6", "click>=8.1,<8.6"] [project.optional-dependencies] observability = ["opentelemetry-api>=1.24,<2"] diff --git a/scripts/record_compatibility_evidence.py b/scripts/record_compatibility_evidence.py index 83ab9db..86f100f 100644 --- a/scripts/record_compatibility_evidence.py +++ b/scripts/record_compatibility_evidence.py @@ -7,6 +7,8 @@ import os import subprocess from datetime import datetime, timezone +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as distribution_version from pathlib import Path from typing import Any @@ -30,14 +32,28 @@ def _version() -> str: return (ROOT / "VERSION").read_text(encoding="utf-8").splitlines()[0].strip() +def _installed_version() -> str: + try: + return distribution_version("base-cli") + except PackageNotFoundError as exc: + raise SystemExit("compatibility evidence requires an installed base-cli distribution") from exc + + def main() -> None: now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") + source_version = _version() + installed_version = _installed_version() + if installed_version != source_version: + raise SystemExit( + f"compatibility evidence version mismatch: source {source_version}, installed {installed_version}" + ) record: dict[str, Any] = { "schema_version": 1, "status": "passed", "recorded_at": now, "source_revision": _revision(), - "framework_version": _version(), + "source_version": source_version, + "framework_version": installed_version, "workflow": os.environ.get("GITHUB_WORKFLOW", "local"), "run_id": os.environ.get("GITHUB_RUN_ID"), "run_attempt": os.environ.get("GITHUB_RUN_ATTEMPT"),