Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/ISSUE_TEMPLATE/support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
name: Adoption support
about: Request migration or downstream compatibility help
title: "[Support] "
labels: ""
assignees: ""
---

## What are you adopting?

Describe the CLI, its Click/Typer version, and the intended production use case.

## Reproduction

Provide a minimal command and the exact exit code. Remove credentials, tokens,
customer data, and private paths.

## Environment

- `base-cli` version:
- Python version:
- Operating system:
- Click/Typer and optional integration versions:

## Evidence

Attach or link a redacted log/run bundle and say whether the failure occurs
against an installed wheel. Security-sensitive reports belong in `SECURITY.md`.
43 changes: 43 additions & 0 deletions .github/workflows/compatibility.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Reference consumers

on:
push:
pull_request:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
downstream:
name: Install and test independent consumers
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Install framework build and test dependencies
run: python -m pip install ".[dev,typer,rich,telemetry]"
- name: Build and install the framework wheel
run: |
python -m build --wheel
python -m pip uninstall --yes base-cli
python -m pip install dist/*.whl
- name: Validate consumer manifest
run: python scripts/validate_consumers.py
- name: Install consumers independently
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]"
- name: Run downstream compatibility suites
run: |
for tests in compatibility/consumers/*/tests; do
python -m pytest "$tests"
done
1 change: 1 addition & 0 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ on:
- "scripts/validate_installed_package.py"
- "scripts/validate_examples.py"
- "examples/**"
- "compatibility/**"
- "docs/releasing.md"
- ".github/workflows/package.yml"
push:
Expand Down
3 changes: 2 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ include SECURITY.md
include VERSION
include base_manifest.yaml
include pyproject.toml
recursive-include .github *.yml
recursive-include .github *.yml *.md
recursive-include docs *.md
recursive-include examples *.py *.md *.toml
recursive-include compatibility *.py *.md *.toml *.json
recursive-include lib/python/base_cli *.py py.typed
recursive-include scripts *.py
recursive-include tests *.py
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ catalog](examples/README.md). It covers a minimal command, nested/plugin Click,
Typer, and automation/observability flows; each example has its own packaging,
tests, completion, release, and troubleshooting guidance.

Teams evaluating adoption can follow the [adopter readiness and migration
guide](docs/adopter-readiness.md) and run the three independent
[downstream compatibility consumers](compatibility/README.md).

## Minimal Command

```python
Expand Down
34 changes: 34 additions & 0 deletions compatibility/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Downstream compatibility consumers

These are three independent, maintainable consumer fixtures used as adoption
evidence until a non-Base team grants permission for a public case study. They
are deliberately separate packages with separate names, entry points, and test
suites; none imports another fixture or Base product code.

| Consumer | Shape | Use case | Compatibility outcome |
| --- | --- | --- | --- |
| [Atlas](consumers/atlas_click/README.md) | Existing Click group | Inventory/status automation | Existing tree remains intact after `attach()`. |
| [Beacon](consumers/beacon_typer/README.md) | Typed Typer app | Deployment command | Typer validation/help remains native through `attach_typer()`. |
| [Cinder](consumers/cinder_automation/README.md) | Native `App` | Scheduled reconciliation | Dry-run and JSON output are deterministic and safe. |

## How the evidence is retained

`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.

Run the same checks locally:

```bash
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 tests in compatibility/consumers/*/tests; do python -m pytest "$tests"; done
```

The fixtures are not customer claims. A permissioned public adopter can be
substituted in the manifest while retaining the same downstream contract tests.
9 changes: 9 additions & 0 deletions compatibility/consumers/atlas_click/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Atlas consumer fixture

Atlas represents a team with an established Click group and a monitoring
script. The fixture proves that `base_cli.attach()` adds lifecycle behavior
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.
19 changes: 19 additions & 0 deletions compatibility/consumers/atlas_click/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[build-system]
requires = ["setuptools>=68,<77"]
build-backend = "setuptools.build_meta"

[project]
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.4", "click>=8.1"]

[project.scripts]
atlas-consumer = "atlas_click.cli:main"

[tool.setuptools]
package-dir = {"" = "src"}

[tool.setuptools.packages.find]
where = ["src"]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Atlas downstream Click consumer fixture."""
45 changes: 45 additions & 0 deletions compatibility/consumers/atlas_click/src/atlas_click/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Atlas: a pre-existing Click tree adopting base-cli lifecycle services."""

from __future__ import annotations

import click
import base_cli


@click.group(name="atlas-consumer", help="Inventory resources managed by Atlas.")
def cli() -> None:
"""The consumer owns this tree and its command names."""


@cli.command()
@click.option(
"--format",
"output_format",
type=click.Choice(base_cli.output_format_choices().split("|")),
default="json",
show_default=True,
)
@click.option("--api-key", hidden=True)
def inventory(output_format: str, api_key: str | None) -> None:
"""Return a small inventory suitable for a monitoring job."""

context = base_cli.get_current_context()
context.log.info("Atlas inventory requested")
del api_key
base_cli.render_records(
({"consumer": "atlas", "resource": "catalog", "status": "ready"},),
requested_format=output_format,
columns=(("CONSUMER", "consumer"), ("RESOURCE", "resource"), ("STATUS", "status")),
rich=context.rich,
)


command = base_cli.attach(cli, sensitive_parameters={"api_key"})


def main() -> int:
return base_cli.run_app(command)


if __name__ == "__main__":
raise SystemExit(main())
26 changes: 26 additions & 0 deletions compatibility/consumers/atlas_click/tests/test_consumer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations

import tempfile
from pathlib import Path

import base_cli

from atlas_click.cli import command


def test_existing_click_tree_keeps_its_machine_contract() -> None:
with tempfile.TemporaryDirectory() as directory:
result = base_cli.testing.invoke(
command,
["--quiet", "inventory"],
home=Path(directory),
)

assert result.exit_code == 0, result.output
assert '"consumer":"atlas"' in result.stdout


def test_help_retains_consumer_command_name() -> None:
result = base_cli.testing.invoke(command, ["--help"])
assert result.exit_code == 0, result.output
assert "inventory" in result.stdout
10 changes: 10 additions & 0 deletions compatibility/consumers/beacon_typer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Beacon consumer fixture

Beacon represents a typed deployment CLI that already uses Typer. The fixture
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. Run `python -m pytest tests` to repeat the downstream tests locally.
19 changes: 19 additions & 0 deletions compatibility/consumers/beacon_typer/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[build-system]
requires = ["setuptools>=68,<77"]
build-backend = "setuptools.build_meta"

[project]
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.4", "typer>=0.12,<0.26"]

[project.scripts]
beacon-consumer = "beacon_typer.cli:main"

[tool.setuptools]
package-dir = {"" = "src"}

[tool.setuptools.packages.find]
where = ["src"]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Beacon downstream Typer consumer fixture."""
43 changes: 43 additions & 0 deletions compatibility/consumers/beacon_typer/src/beacon_typer/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Beacon: typed deployment commands with the optional Typer adapter."""

from __future__ import annotations

import base_cli
import typer


cli = typer.Typer(help="Deploy Beacon services with typed parameters.")


@cli.callback()
def callback() -> None:
"""Keep Typer's callback and generated help behavior."""


@cli.command()
def deploy(
service: str = typer.Option(..., help="Service to deploy."),
replicas: int = typer.Option(1, min=1, max=20, help="Desired replica count."),
token: str | None = typer.Option(None, hidden=True),
) -> None:
"""Publish the requested deployment plan."""

context = base_cli.get_current_context()
context.log.info("Beacon deployment requested for %s", service)
del token
typer.echo(f"deploy {service} replicas={replicas}")


command = base_cli.attach_typer(
cli,
name="beacon-consumer",
sensitive_parameters={"token"},
)


def main() -> int:
return base_cli.run_app(command)


if __name__ == "__main__":
raise SystemExit(main())
25 changes: 25 additions & 0 deletions compatibility/consumers/beacon_typer/tests/test_consumer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

import tempfile
from pathlib import Path

import base_cli

from beacon_typer.cli import command


def test_typer_deployment_preserves_typed_parameters() -> None:
with tempfile.TemporaryDirectory() as directory:
result = base_cli.testing.invoke(
command,
["--quiet", "deploy", "--service", "api", "--replicas", "3"],
home=Path(directory),
)

assert result.exit_code == 0, result.output
assert result.stdout.strip() == "deploy api replicas=3"


def test_typer_rejects_invalid_replica_count() -> None:
result = base_cli.testing.invoke(command, ["deploy", "--service", "api", "--replicas", "0"])
assert result.exit_code != 0
12 changes: 12 additions & 0 deletions compatibility/consumers/cinder_automation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Cinder consumer fixture

Cinder represents a scheduled automation worker that needs a safe dry-run,
deterministic records, and an optional OpenTelemetry provider. The fixture
proves that a native `base_cli.App` can expose both a command-level record
contract and the versioned lifecycle JSON envelope provided by `base-cli`.

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.
Run `python -m pytest tests` to repeat the dry-run and JSON contract tests.
22 changes: 22 additions & 0 deletions compatibility/consumers/cinder_automation/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[build-system]
requires = ["setuptools>=68,<77"]
build-backend = "setuptools.build_meta"

[project]
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.4", "click>=8.1"]

[project.optional-dependencies]
observability = ["opentelemetry-api>=1.24,<2"]

[project.scripts]
cinder-consumer = "cinder_automation.cli:main"

[tool.setuptools]
package-dir = {"" = "src"}

[tool.setuptools.packages.find]
where = ["src"]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Cinder downstream automation consumer fixture."""
Loading
Loading