diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index aad85f5c..7afe7b35 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "kbagent", - "version": "0.81.0", + "version": "0.82.0", "source": "./plugins/kbagent", "description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces", "category": "development" diff --git a/.github/workflows/release-kbagent.yml b/.github/workflows/release-kbagent.yml index 9bf33052..9d627ee0 100644 --- a/.github/workflows/release-kbagent.yml +++ b/.github/workflows/release-kbagent.yml @@ -501,6 +501,13 @@ jobs: env: VERSION: ${{ needs.version.outputs.VERSION }} steps: + # NOTE for whoever gets this job working: `wingetcreate update` only bumps + # the version and installer URL -- it does NOT write locale fields. The + # FIRST successful submission must therefore set `License: Apache-2.0` + # itself, because every later run inherits whatever that manifest said. + # Do not copy the existing `Keboola.KeboolaCLI` manifest: that is the + # legacy Go CLI from keboola/keboola-as-code, a different product under a + # different (MIT) licence. `Keboola.KeboolaCLI2` has never been submitted. - name: Submit to winget-pkgs shell: bash env: diff --git a/README.md b/README.md index 59174a85..d8b58f9e 100644 --- a/README.md +++ b/README.md @@ -330,4 +330,4 @@ make hooks # install pre-commit hook ## License -MIT +[Apache License 2.0](LICENSE) diff --git a/build/package/homebrew/keboola-cli2.rb.tmpl b/build/package/homebrew/keboola-cli2.rb.tmpl index 3281af78..eb3e455b 100644 --- a/build/package/homebrew/keboola-cli2.rb.tmpl +++ b/build/package/homebrew/keboola-cli2.rb.tmpl @@ -6,7 +6,7 @@ class KeboolaCli2 < Formula desc "AI-friendly CLI for managing Keboola projects (kbagent)" homepage "https://github.com/keboola/cli" version "{VERSION}" - license "MIT" + license "Apache-2.0" on_macos do # Apple Silicon only (single macOS build env). Gate on arch so Intel Macs get a diff --git a/build/package/nfpm.yaml b/build/package/nfpm.yaml index 2ef96e55..22952df5 100644 --- a/build/package/nfpm.yaml +++ b/build/package/nfpm.yaml @@ -19,7 +19,7 @@ description: | Self-contained native binary; no Python runtime required. vendor: "Keboola" homepage: "https://github.com/keboola/cli" -license: "MIT" +license: "Apache-2.0" contents: - src: ${BIN_PATH} diff --git a/plugins/kbagent/.claude-plugin/plugin.json b/plugins/kbagent/.claude-plugin/plugin.json index dcfc0ac2..2f400caa 100644 --- a/plugins/kbagent/.claude-plugin/plugin.json +++ b/plugins/kbagent/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "kbagent", - "version": "0.81.0", + "version": "0.82.0", "description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces", "author": { "name": "Keboola", @@ -8,5 +8,5 @@ }, "homepage": "https://github.com/keboola/cli", "repository": "https://github.com/keboola/cli", - "license": "MIT" + "license": "Apache-2.0" } diff --git a/pyproject.toml b/pyproject.toml index c10487ac..3ef971df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [project] name = "keboola-cli" -version = "0.81.0" +version = "0.82.0" description = "AI-friendly CLI for managing Keboola projects" readme = "README.md" requires-python = ">=3.12" -license = "MIT" +license = "Apache-2.0" authors = [ { name = "Keboola", email = "dev@keboola.com" }, ] diff --git a/src/keboola_agent_cli/changelog.py b/src/keboola_agent_cli/changelog.py index a8933226..e35f56f0 100644 --- a/src/keboola_agent_cli/changelog.py +++ b/src/keboola_agent_cli/changelog.py @@ -24,6 +24,18 @@ # Ordered newest-first. Each value is a list of brief one-line descriptions. CHANGELOG: dict[str, list[str]] = { + "0.82.0": [ + "Fix: kbagent is licensed under **Apache 2.0**, and the packaging metadata now says " + "so. #544 added an Apache 2.0 `LICENSE` file, but `pyproject.toml`, the deb/rpm " + "package, the Homebrew formula, the Claude Code plugin manifest and the README all " + "still declared MIT -- so v0.79.0 through v0.81.0 shipped a wheel telling PyPI one " + "licence while the LICENSE file in the same distribution said another, and the " + "Chocolatey package pointed its `licenseUrl` at the Apache text under an MIT " + "declaration. Nothing caught it because each file was individually valid. The wheel " + "now carries `License-Expression: Apache-2.0`, and a test pins all five declaration " + "sites to the LICENSE file so they cannot drift apart again. No code change; if you " + "vendored kbagent under the belief it was MIT, re-check against Apache 2.0.", + ], "0.81.0": [ "New (#390): `kbagent doctor` gains an `mcp_tool_tasks` check that finds scheduled " "agent tasks still using the deprecated `--type mcp_tool` action, which is removed " diff --git a/tests/test_license_consistency.py b/tests/test_license_consistency.py new file mode 100644 index 00000000..ea01ba4d --- /dev/null +++ b/tests/test_license_consistency.py @@ -0,0 +1,95 @@ +"""The declared licence must match the LICENSE file, everywhere it is declared. + +kbagent declares its licence in five places that nothing kept in sync: the +Python distribution metadata, the deb/rpm package, the Homebrew formula, the +Claude Code plugin manifest and the README. #544 added an Apache 2.0 `LICENSE` +file while all five still said MIT, and v0.79.0 shipped that contradiction -- +the wheel told PyPI one licence while the file in the same repo said another. +No CI check noticed, because each file is individually valid. +""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).parent.parent +LICENSE_PATH = REPO_ROOT / "LICENSE" + +#: SPDX identifier the repository is licensed under. Change this ONLY together +#: with the LICENSE file itself -- every assertion below hangs off it. +EXPECTED_SPDX = "Apache-2.0" + +#: How each packaging file spells the same licence. +_DECLARATIONS: tuple[tuple[str, str], ...] = ( + ("pyproject.toml", rf'^license = "{re.escape(EXPECTED_SPDX)}"$'), + ("build/package/nfpm.yaml", rf'^license: "{re.escape(EXPECTED_SPDX)}"$'), + ( + "build/package/homebrew/keboola-cli2.rb.tmpl", + rf'^\s*license "{re.escape(EXPECTED_SPDX)}"$', + ), +) + + +def test_license_file_is_the_expected_licence() -> None: + """Anchor: the other assertions are only meaningful against the real file.""" + text = LICENSE_PATH.read_text(encoding="utf-8") + assert "Apache License" in text + assert "Version 2.0" in text + + +@pytest.mark.parametrize(("relative_path", "pattern"), _DECLARATIONS) +def test_packaging_file_declares_the_same_licence(relative_path: str, pattern: str) -> None: + text = (REPO_ROOT / relative_path).read_text(encoding="utf-8") + assert re.search(pattern, text, re.MULTILINE), ( + f"{relative_path} does not declare {EXPECTED_SPDX}; it must match the LICENSE file" + ) + + +def test_plugin_manifest_declares_the_same_licence() -> None: + manifest = json.loads( + (REPO_ROOT / "plugins/kbagent/.claude-plugin/plugin.json").read_text(encoding="utf-8") + ) + assert manifest["license"] == EXPECTED_SPDX + + +def test_readme_does_not_still_claim_mit() -> None: + """The README is what a human reads before the metadata.""" + readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8") + section = readme.split("## License", 1) + assert len(section) == 2, "README has no License section" + body = section[1][:200] + assert "MIT" not in body + assert "Apache" in body + + +def test_chocolatey_points_its_license_url_at_the_repo_license() -> None: + """The nuspec has no SPDX field; its licenceUrl is the whole declaration. + + A URL to the LICENSE file self-updates with the repository, which is why + this channel never went stale -- but only as long as it points THERE and + not at a hard-coded licence page. + """ + nuspec = (REPO_ROOT / "build/package/chocolatey/keboola-cli2.nuspec").read_text( + encoding="utf-8" + ) + assert "https://github.com/keboola/cli/blob/main/LICENSE" in nuspec + + +def test_no_packaging_file_still_says_mit() -> None: + """Catch a sixth declaration site added later without updating this test.""" + offenders = [] + for relative_path in ( + "pyproject.toml", + "build/package/nfpm.yaml", + "build/package/homebrew/keboola-cli2.rb.tmpl", + "plugins/kbagent/.claude-plugin/plugin.json", + "README.md", + ): + text = (REPO_ROOT / relative_path).read_text(encoding="utf-8") + if re.search(r"\bMIT\b", text): + offenders.append(relative_path) + assert not offenders, f"still declaring MIT: {offenders}" diff --git a/uv.lock b/uv.lock index e08389fa..0eb9138e 100644 --- a/uv.lock +++ b/uv.lock @@ -590,7 +590,7 @@ wheels = [ [[package]] name = "keboola-cli" -version = "0.81.0" +version = "0.82.0" source = { editable = "." } dependencies = [ { name = "croniter" },