From 5038f19257e5708788d7b233c85a7a23d5a42e38 Mon Sep 17 00:00:00 2001 From: Petr Date: Wed, 12 Aug 2026 01:12:01 +0200 Subject: [PATCH 1/2] fix(license): declare Apache-2.0 everywhere, matching the LICENSE file pyproject.toml, build/package/nfpm.yaml, the Homebrew formula, the Claude Code plugin manifest and the README. So v0.79.0 onwards shipped a wheel telling PyPI one licence while the LICENSE file inside the same distribution said another, and the Chocolatey package pointed its licenseUrl at the Apache text under an MIT declaration. Nothing caught it because every file was individually valid -- the drift only exists between them. tests/test_license_consistency.py now pins all five declaration sites to the LICENSE file, including a catch-all that fails if any of them says MIT again. Verified on the real artifact, not just the source: `uv build --wheel` now emits `License-Expression: Apache-2.0` and `License-File: LICENSE`. No code change. --- .claude-plugin/marketplace.json | 2 +- README.md | 2 +- build/package/homebrew/keboola-cli2.rb.tmpl | 2 +- build/package/nfpm.yaml | 2 +- plugins/kbagent/.claude-plugin/plugin.json | 4 +- pyproject.toml | 4 +- src/keboola_agent_cli/changelog.py | 12 +++ tests/test_license_consistency.py | 82 +++++++++++++++++++++ uv.lock | 2 +- 9 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 tests/test_license_consistency.py 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/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..5fd05b25 --- /dev/null +++ b/tests/test_license_consistency.py @@ -0,0 +1,82 @@ +"""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_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" }, From 8c16a5bc1eb0f2595c776b5297750773bd1fffbb Mon Sep 17 00:00:00 2001 From: Petr Date: Wed, 12 Aug 2026 01:27:14 +0200 Subject: [PATCH 2/2] docs(license): cover the two channels the consistency test cannot reach Devin review of #577. Both turned out to need documentation rather than a fix, but one needed checking rather than assuming. WinGet: `Keboola.KeboolaCLI` DOES exist in microsoft/winget-pkgs and declares 'MIT License' -- but it is the legacy Go CLI from keboola/keboola-as-code, a different product whose own LICENSE is MIT. Our id is `Keboola.KeboolaCLI2`, which has never been submitted (the winget job has never succeeded). So there is no kbagent manifest carrying a wrong licence today. The forward risk is real though: `wingetcreate update` only bumps version and installer URL, so the FIRST successful submission has to set License: Apache-2.0 itself. Noted in the workflow step, next to the code someone will edit when they fix it. Chocolatey: the nuspec has no SPDX field, only `licenseUrl`, which points at the repository's LICENSE and therefore self-updated when that file changed -- which is why this channel never went stale. Pinned by a test so it keeps pointing there rather than at some hard-coded licence page. --- .github/workflows/release-kbagent.yml | 7 +++++++ tests/test_license_consistency.py | 13 +++++++++++++ 2 files changed, 20 insertions(+) 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/tests/test_license_consistency.py b/tests/test_license_consistency.py index 5fd05b25..ea01ba4d 100644 --- a/tests/test_license_consistency.py +++ b/tests/test_license_consistency.py @@ -66,6 +66,19 @@ def test_readme_does_not_still_claim_mit() -> None: 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 = []