diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1956421..ec250b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,7 @@ name: CI on: push: branches: [main] + tags: ["v*"] pull_request: jobs: @@ -18,3 +19,24 @@ jobs: python-version: ${{ matrix.python-version }} - run: pip install -e ".[dev]" - run: pytest tests/ -v + + # The package version and the git tag drifted for three releases: tags said 1.x + # while pyproject said 0.1.0. This fails the tag build rather than shipping a + # release that misreports itself. + version-matches-tag: + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Compare __version__ with the tag + run: | + tag="${GITHUB_REF_NAME#v}" + pkg=$(python -c "import sys; sys.path.insert(0, 'src'); import algolia_agent; print(algolia_agent.__version__)") + echo "tag=$tag __version__=$pkg" + if [ "$tag" != "$pkg" ]; then + echo "::error::tag ${GITHUB_REF_NAME} does not match __version__ ${pkg}" + exit 1 + fi diff --git a/README.md b/README.md index 8a13c24..fbce3b6 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,21 @@ algolia-agent delete --confirm ``` Add `--json` to any command except `init` for machine-readable output. +`algolia-agent --version` reports the installed version. + +## Releasing + +`src/algolia_agent/__init__.py` holds `__version__` and everything derives from it: +`pyproject.toml` reads it via `[tool.setuptools.dynamic]`, `--version` reports it, and +the client sends it in the `User-Agent`. To cut a release, bump that one value, merge, +then tag from `main`: + +```bash +git checkout main && git pull +git tag vX.Y.Z && git push origin vX.Y.Z +``` + +CI fails the tag build if the tag and `__version__` disagree. ## Config formats diff --git a/pyproject.toml b/pyproject.toml index e19faf9..8f91bf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "algolia-agent" -version = "0.1.0" +dynamic = ["version"] description = "CLI for Algolia Agent Studio" readme = "README.md" requires-python = ">=3.10" @@ -23,6 +23,9 @@ dev = ["pytest>=7"] [project.scripts] algolia-agent = "algolia_agent.cli:main" +[tool.setuptools.dynamic] +version = { attr = "algolia_agent.__version__" } + [tool.setuptools.packages.find] where = ["src"] diff --git a/src/algolia_agent/__init__.py b/src/algolia_agent/__init__.py index e69de29..e7aa3e6 100644 --- a/src/algolia_agent/__init__.py +++ b/src/algolia_agent/__init__.py @@ -0,0 +1,7 @@ +"""CLI for the Algolia Agent Studio REST API.""" + +# Single source of truth for the version. pyproject.toml reads it via +# [tool.setuptools.dynamic], the CLI reports it through --version, and the client +# sends it in the User-Agent — so a release cannot disagree with itself. CI checks +# this value against the git tag whenever a v* tag is pushed. +__version__ = "1.3.0" diff --git a/src/algolia_agent/cli.py b/src/algolia_agent/cli.py index 0ad528e..377abc3 100644 --- a/src/algolia_agent/cli.py +++ b/src/algolia_agent/cli.py @@ -21,6 +21,7 @@ from InquirerPy import inquirer +from . import __version__ from .client import AgentAPIError, AlgoliaAgentClient from .template import extract_variables, render @@ -1180,6 +1181,8 @@ def build_parser() -> argparse.ArgumentParser: prog="algolia-agent", description="Algolia Agent Studio CLI", ) + parser.add_argument("--version", action="version", + version=f"algolia-agent {__version__}") parser.add_argument("--app-id", help="Algolia Application ID (overrides env/dotenv)") parser.add_argument("--api-key", help="Algolia API Key (overrides env/dotenv)") diff --git a/src/algolia_agent/client.py b/src/algolia_agent/client.py index 3f7b5ea..ac75d43 100644 --- a/src/algolia_agent/client.py +++ b/src/algolia_agent/client.py @@ -19,6 +19,11 @@ _RETRY_BACKOFF = 1.0 # seconds; doubles each retry +from . import __version__ + +_USER_AGENT = f"algolia-agent-cli/{__version__}" + + def _load_dotenv(path: Path) -> dict[str, str]: """Parse a .env file and return key/value pairs. Ignores comments and blank lines.""" result = {} @@ -66,7 +71,7 @@ def _request(self, path: str, method: str = "GET", body: dict | None = None) -> req.add_header("x-algolia-api-key", self.api_key) req.add_header("Content-Type", "application/json") req.add_header("Accept", "application/json") - req.add_header("User-Agent", "algolia-agent-cli/0.1.0") + req.add_header("User-Agent", _USER_AGENT) delay = _RETRY_BACKOFF for attempt in range(_MAX_RETRIES): try: @@ -151,7 +156,7 @@ def list_indices(self) -> list[str]: req.add_header("x-algolia-application-id", self.app_id) req.add_header("x-algolia-api-key", self.api_key) req.add_header("Accept", "application/json") - req.add_header("User-Agent", "algolia-agent-cli/0.1.0") + req.add_header("User-Agent", _USER_AGENT) try: with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp: result = json.loads(resp.read()) diff --git a/tests/test_client.py b/tests/test_client.py index 920615e..3853066 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -384,3 +384,38 @@ def test_timeout_error_retries_then_succeeds(client): with patch("time.sleep"): result = client.get_agent("abc") assert result == agent + + +# ── version reporting ───────────────────────────────────────────────────────── + +def test_user_agent_reports_the_package_version(): + """The User-Agent was frozen at 0.1.0 through three tagged releases.""" + import re + + import algolia_agent + from algolia_agent.client import _USER_AGENT + + assert re.fullmatch(r"\d+\.\d+\.\d+", algolia_agent.__version__), algolia_agent.__version__ + assert _USER_AGENT == f"algolia-agent-cli/{algolia_agent.__version__}" + + +def test_user_agent_header_is_actually_sent(client): + import algolia_agent + + with patch("urllib.request.urlopen", return_value=_mock_response({"data": []})) as m: + client.list_agents() + req = m.call_args.args[0] + assert req.get_header("User-agent") == f"algolia-agent-cli/{algolia_agent.__version__}" + + +def test_installed_metadata_matches_dunder_version(): + """pyproject reads the version from the package, so these cannot diverge.""" + from importlib.metadata import PackageNotFoundError, version + + import algolia_agent + + try: + installed = version("algolia-agent") + except PackageNotFoundError: + pytest.skip("package not installed") + assert installed == algolia_agent.__version__