From cde86fe799e339a759520f3c3fa924fb7086a88b Mon Sep 17 00:00:00 2001 From: Petr Date: Sat, 22 Aug 2026 23:30:24 +0200 Subject: [PATCH 1/2] docs: stop bumping the version in feature PRs; releases become dedicated release PRs Parallel feature PRs (one AI session per issue) each bumping pyproject.toml collide on every merge and silently renumber releases -- the KNOWN_UNRELEASED list in scripts/generate_changelog.py is the accumulated damage of that pattern. Version bumps and changelog.py entries now happen ONLY in a dedicated release PR that batches everything merged since the last release. - CLAUDE.md Versioning: new 'Version bumps happen ONLY in a dedicated release PR' subsection with the two-step flow and the vNEXT placeholder convention for version-gated docs in feature PRs (version-gate-check rejects guessed numeric versions in per-PR CI, so the placeholder is the only workable tag before the release assigns a number). - CONTRIBUTING.md: new 'No version bumps in feature PRs' commit convention; per-command gotchas checklist and sync map updated for vNEXT; 'Releasing a new version' rewritten around the release PR -- new step 1 (collect merged PRs since the last tag as the exact scope of the changelog and release notes) and step 4 (replace every vNEXT, grep must come back empty); beta releases documented as the one deliberate exception where the bump rides the feature branch. --- CLAUDE.md | 35 +++++++++++++++++-- CONTRIBUTING.md | 93 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 103 insertions(+), 25 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e4615ed5..6ba7d246 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -153,7 +153,38 @@ is simply a separate distribution kbagent no longer manages. - `plugins/kbagent/.claude-plugin/plugin.json` must match. Run `make version-sync` (or `python scripts/sync_version.py`) to update it. - The pre-commit hook and CI automatically check version consistency. -**When bumping the version**: edit `pyproject.toml`, add a changelog entry to `src/keboola_agent_cli/changelog.py`, then run `make version-sync`. Do not edit `__init__.py` or `plugin.json` manually. CI enforces changelog completeness via `make changelog-check`. +### Version bumps happen ONLY in a dedicated release PR + +**Feature/fix PRs MUST NOT bump the version and MUST NOT add a `changelog.py` +entry.** PRs are routinely developed in parallel (separate AI sessions, one per +issue); when each of them bumps `pyproject.toml`, every merge is a version +conflict and the merge order silently renumbers releases. The historical +`KNOWN_UNRELEASED` list in `scripts/generate_changelog.py` -- 18 versions whose +content shipped with no release notes -- is exactly what that pattern produces. + +The flow is instead: + +1. **Feature PR**: merges to `main` with NO change to `pyproject.toml` + `version` and NO `changelog.py` entry. When the PR documents version-gated + behavior (gotchas.md, commands-reference.md, this file), it tags it with + the literal placeholder **`vNEXT`** -- `(since vNEXT)` / `vNEXT+`. + `make version-gate-check` ignores the placeholder (it only matches numeric + versions) but rejects a guessed numeric version with no changelog entry, + so a numeric future tag cannot even pass CI. +2. **Release PR** (its only job): bump `pyproject.toml`, review everything + merged since the last release (`git log v..origin/main`), write ONE + changelog entry covering all of it (one bullet per logical change, with + `(#PR)` references), replace every `vNEXT` placeholder with the real + version, run `make version-sync`. Merge, then tag -- the release pipeline + renders the GitHub release notes from the changelog entry + (`scripts/gen_release_notes.py`). Full checklist: `CONTRIBUTING.md` > + "Releasing a new version". + +**Inside the release PR**: edit `pyproject.toml`, add the changelog entry to `src/keboola_agent_cli/changelog.py`, then run `make version-sync`. Do not edit `__init__.py` or `plugin.json` manually. `make changelog-check` (release-time, needs `gh`) enforces changelog completeness in both directions. + +The one exception is a **beta/pre-release** (below): there the bump deliberately +rides the feature branch, because the pre-release tag and GitHub Release are cut +from that branch -- the branch temporarily *is* the release PR. ### Beta / pre-release versions (since 0.43.3) @@ -237,7 +268,7 @@ Full author checklist: see `CONTRIBUTING.md` > "Releasing a beta (pre-release) v - `plugins/kbagent/agents/keboola-expert.md` -- **highest risk** (Rule 6 VERSION GATE, tool selection matrix, inline gotchas) - `plugins/kbagent/skills/kbagent/SKILL.md` -- description triggers and workflow links (the auto-generated table is CI-checked, the rest is not) - `plugins/kbagent/skills/kbagent/references/commands-reference.md` - - `plugins/kbagent/skills/kbagent/references/gotchas.md` (every new gotcha **MUST** be tagged with `(since vX.Y.Z)`) + - `plugins/kbagent/skills/kbagent/references/gotchas.md` (every new gotcha **MUST** be tagged with a version -- `(since vNEXT)` in a feature PR, replaced with the real `(since vX.Y.Z)` by the release PR) - `plugins/kbagent/skills/kbagent/references/-workflow.md` (e.g. `semantic-layer-workflow.md`, `workspace-workflow.md`, `sync-workflow.md`) Forgetting any of these does not fail tests or lint -- it ships an AI agent that quietly recommends commands that do not exist on the user's installed kbagent version, or refuses commands that do. Treat the change as **not done** until every applicable file has been updated. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7435d8e5..9dfb4dca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -382,7 +382,7 @@ before the PR is mergeable. - [ ] **§3 Inline Gotchas** when behavior changed in a way the agent will get wrong by default (e.g. dev-branch auto-materialization, native column-type whitelisting). - [ ] **`plugins/kbagent/skills/kbagent/SKILL.md`** non-table portions -- update the `description:` trigger keywords when introducing a new topic area (so description-matching auto-triggers the skill); add a workflow row to the bottom table if you created a new `references/-workflow.md`. - [ ] **`plugins/kbagent/skills/kbagent/references/commands-reference.md`** -- add the new command bullet under the appropriate section. Hand-maintained, NOT auto-generated. (Yes, this partly duplicates the auto-generated SKILL.md table -- the reference carries denser per-command notes, the table is the at-a-glance picker.) -- [ ] **`plugins/kbagent/skills/kbagent/references/gotchas.md`** -- if the command's behavior is non-obvious, add an entry tagged with `(since vX.Y.Z)`. The version tag is **non-optional**; gotchas without versions are how AI agents end up recommending behavior that does not exist on older kbagent installs. +- [ ] **`plugins/kbagent/skills/kbagent/references/gotchas.md`** -- if the command's behavior is non-obvious, add an entry tagged with a version. In a feature PR that version is not known yet (the PR does not bump the version), so tag with the literal placeholder `(since vNEXT)` -- the release PR replaces every `vNEXT` with the version actually being released. Never guess a numeric version: `make version-gate-check` (per-PR CI) rejects any `(since vX.Y.Z)` whose version has no `changelog.py` entry. The version tag is **non-optional**; gotchas without versions are how AI agents end up recommending behavior that does not exist on older kbagent installs. - [ ] **`plugins/kbagent/skills/kbagent/references/-workflow.md`** -- create a new file if the command introduces a new workflow or topic area (existing examples: `workspace-workflow.md`, `branch-workflow.md`, `sync-workflow.md`, `storage-files-workflow.md`, `storage-types-workflow.md`). Single-command additions go into an existing workflow file. - [ ] **`plugins/kbagent/.claude-plugin/CLAUDE.md`** -- only update when the high-level delegation strategy changes (e.g. new "when NOT to delegate" cases). Most command additions do not touch this. - [ ] **`plugins/kbagent/commands/keboola.md`** -- only update if the `/keboola` slash-command UX changes. Most command additions do not touch this. @@ -454,8 +454,8 @@ release checklist below. | File | When to update | CI catches drift? | |------|----------------|-------------------| -| `pyproject.toml` (`version`) | Every release | -- (single source of truth) | -| `src/keboola_agent_cli/changelog.py` | Every release | YES (`make changelog-check`, both directions -- every release has an entry AND every entry has a release) | +| `pyproject.toml` (`version`) | Every release -- in the dedicated release PR ONLY, never a feature PR | -- (single source of truth) | +| `src/keboola_agent_cli/changelog.py` | Every release -- in the dedicated release PR ONLY, never a feature PR | YES (`make changelog-check`, both directions -- every release has an entry AND every entry has a release) | | `src/keboola_agent_cli/commands/context.py` (`AGENT_CONTEXT`) | Adding/removing/renaming commands; significant flag changes | NO | | `src/keboola_agent_cli/server/routers/.py` | Adding/removing/renaming commands -- `kbagent serve` mirrors the CLI 1:1 for external consumers (Web UI, scheduled agents, third-party apps). Skip only for terminal-only / kbagent-infrastructure commands; document skip in PR | NO -- callers get HTTP 404 instead of "command works in CLI but not via API" silent gap | | `CLAUDE.md` (`## All CLI Commands`) | Adding/removing/renaming commands | NO | @@ -466,7 +466,7 @@ release checklist below. | `plugins/kbagent/skills/kbagent/SKILL.md` -- table | Auto-generated by `make skill-gen` | YES (`make skill-check`; pre-commit auto-stages) | | `plugins/kbagent/skills/kbagent/SKILL.md` -- description / rules / workflow links | New topic area in `description` triggers; new workflow file added to bottom table | NO | | `plugins/kbagent/skills/kbagent/references/commands-reference.md` | Adding/removing/renaming commands; flag changes | NO | -| `plugins/kbagent/skills/kbagent/references/gotchas.md` | New non-obvious behavior -- always tag with `(since vX.Y.Z)` | PARTLY (`make version-gate-check` proves the tagged version EXISTS; whether it is the RIGHT one still needs review) | +| `plugins/kbagent/skills/kbagent/references/gotchas.md` | New non-obvious behavior -- always tag with a version (`(since vNEXT)` in feature PRs; the release PR rewrites it to `(since vX.Y.Z)`) | PARTLY (`make version-gate-check` proves a numeric tag names a released version; `vNEXT` leftovers are caught by the release checklist grep, not CI) | | `plugins/kbagent/skills/kbagent/references/-workflow.md` | New workflow / topic area introduced | NO | | `plugins/kbagent/skills//` (e.g. `kbagent-promotion-pipeline`) | Adding a **sibling skill** -- a self-contained skill directory next to `kbagent/`, used when the topic ships executable `scripts/` + tests or needs its own `description` triggers rather than being one more `references/*.md`. Must ALSO be linked from `kbagent/SKILL.md`'s bottom table, otherwise an agent already inside the `kbagent` skill can never discover it | NO -- `make skill-check` only regenerates `kbagent/SKILL.md` and never looks at sibling skills | @@ -479,6 +479,12 @@ to catch this before the change ships. - **No `Co-Authored-By`** lines in commit messages - **No AI attribution footers** in PR descriptions +- **No version bumps in feature PRs** -- `pyproject.toml`'s `version` and + `src/keboola_agent_cli/changelog.py` are touched ONLY by a dedicated release + PR (see [Releasing a new version](#releasing-a-new-version)). Parallel PRs + each bumping the version collide on merge and silently renumber releases. + Tag any new version-gated documentation with the `vNEXT` placeholder + (`(since vNEXT)`); the release PR replaces it with the real version. - **Conventional commits**: `feat:`, `fix:`, `chore:`, `docs:`, `test:`, `refactor:` - **One logical change per commit** -- don't mix unrelated fixes - **Pre-commit hook must pass** -- `ruff check` + `ruff format --check`. Install via `make hooks` @@ -573,10 +579,26 @@ ask you to address it before merge. ## Releasing a new version -A "release" is whenever you bump `pyproject.toml`'s version. Tag a feature -branch, walk this checklist end-to-end, then merge to `main`. The point of -steps 5-8 is that **CI will not catch you** if you skip them; they are the -manual safety net for the silent-drift risks summarized in the +A release is a **dedicated release PR** -- the only place `pyproject.toml`'s +`version` and `changelog.py` are ever touched. Feature PRs merge to `main` +without any version change; the release PR then batches **everything merged +since the last release** into one version bump, one changelog entry, and one +set of release notes. + +> **Why not bump in each feature PR?** PRs are developed in parallel (often +> one AI session per issue). When each bumps the version, every merge is a +> `pyproject.toml`/`changelog.py` conflict, and resolving the conflicts by +> merging all of them renumbers releases after the fact. The +> `KNOWN_UNRELEASED` list in `scripts/generate_changelog.py` -- 18 versions +> whose content shipped silently, folded into a later release's wheel with no +> release notes -- is the accumulated damage of exactly that pattern. + +Open the release PR from its own branch, walk this checklist end-to-end, +merge it, then tag the resulting `main` commit (the release pipeline renders +the GitHub release notes from the changelog entry via +`scripts/gen_release_notes.py`). The point of steps 7-10 is that **CI will not +catch you** if you skip them; they are the manual safety net for the +silent-drift risks summarized in the [Plugin synchronization map](#plugin-synchronization-map) above. > **Want to ship a beta first?** You can. PEP 440 pre-release versions @@ -586,23 +608,40 @@ manual safety net for the silent-drift risks summarized in the > [Releasing a beta (pre-release) version](#releasing-a-beta-pre-release-version) > below for the workflow. -1. **Edit `pyproject.toml`** -- bump `version = "X.Y.Z"`. Single source of truth; everything else derives from it. -2. **Add a changelog entry** to `src/keboola_agent_cli/changelog.py` -- one entry per release, no exceptions. CI fails (`make changelog-check`) if this is missing. Author it as the file's docstring describes: **one logical change per bullet** (split the release into several list items rather than one mega-paragraph), each starting with a recognised prefix (`BREAKING:`, `New:`, `Fix:`, `Change:`, `Note:`, `Security:`, ...) and leading with a self-contained first sentence. `kbagent changelog` shows only that first sentence per version by default (the rest is revealed by `--full`), so a buried headline or a single wall-of-text bullet reads as an unscannable blob. -3. **Run `make version-sync`** -- propagates the new version to `plugins/kbagent/.claude-plugin/plugin.json`. The pre-commit hook does this automatically on `git commit`, but running it explicitly lets you eyeball the diff. -4. **Run `make skill-gen`** -- regenerates the decision table in `SKILL.md`. Idempotent if no commands changed since the previous release. -5. **Manually review `plugins/kbagent/agents/keboola-expert.md`**: +1. **Collect the raw material** -- find the last released tag + (`gh release list --limit 1` or `git describe --tags --abbrev=0`), then list + every PR merged since it: + ```bash + git log v..origin/main --oneline --first-parent + ``` + (or `gh pr list --state merged --base main --search "merged:>"`). + Those merged PRs are **exactly** the scope of the release: the changelog + entry and the release notes must cover each of them, and nothing else. +2. **Edit `pyproject.toml`** -- bump `version = "X.Y.Z"`. Single source of truth; everything else derives from it. This is the release PR's defining change -- if you are doing this in a feature PR, stop and read the section intro above. +3. **Add a changelog entry** to `src/keboola_agent_cli/changelog.py` -- ONE entry for the new version, covering **every PR merged since the last release** (step 1), no exceptions. CI fails (`make changelog-check`) if this is missing. Author it as the file's docstring describes: **one logical change per bullet** (split the release into several list items rather than one mega-paragraph), each starting with a recognised prefix (`BREAKING:`, `New:`, `Fix:`, `Change:`, `Note:`, `Security:`, ...), carrying its `(#PR)` reference, and leading with a self-contained first sentence. `kbagent changelog` shows only that first sentence per version by default (the rest is revealed by `--full`), so a buried headline or a single wall-of-text bullet reads as an unscannable blob. +4. **Replace every `vNEXT` placeholder** left behind by the feature PRs with the version being released, then verify none survive: + ```bash + grep -rn "vNEXT" CLAUDE.md docs/ plugins/ src/keboola_agent_cli/commands/context.py + ``` + The grep MUST come back empty before merging. This is not CI-enforced (the + plugin is served from the repo, so a leftover `(since vNEXT)` ships to + agents as an unusable version gate) -- the release PR is the only place it + can be fixed. +5. **Run `make version-sync`** -- propagates the new version to `plugins/kbagent/.claude-plugin/plugin.json`. The pre-commit hook does this automatically on `git commit`, but running it explicitly lets you eyeball the diff. +6. **Run `make skill-gen`** -- regenerates the decision table in `SKILL.md`. Idempotent if no commands changed since the previous release. +7. **Manually review `plugins/kbagent/agents/keboola-expert.md`**: - **§1 Rule 6 VERSION GATE examples** -- if any feature this release shipped (or any feature shipped in a previous release that you missed) was previously missing-and-now-present, document it with the right minimum version. Remove stale "since X.Y.Z" mentions that no longer matter to live users. - **§2 Tool Selection Matrix** -- did you add a new write/destructive command *group* since last release? Is it present with one `First choice / Fallback / NEVER` row (per group, not per command)? Mind the hard 70 000 B prompt budget: trim stale content rather than raising the cap. New commands inside an existing group need no new row. - **§3 Inline Gotchas** -- new behavior the agent would get wrong by default? Add it. -6. **Manually review `plugins/kbagent/skills/kbagent/references/gotchas.md`** -- every behavior introduced or changed this release that an AI agent would not infer from `--help` should have its own `(since vX.Y.Z)` entry. The version tag is non-optional. -7. **Manually review `CLAUDE.md` `## All CLI Commands`** -- diff against `kbagent --help` output (and against `kbagent context`). Hand-maintained; CI does not catch drift here. -8. **Manually review `plugins/kbagent/skills/kbagent/references/commands-reference.md`** -- same drill. Hand-maintained, no CI coverage. -9. **Run `make check`** -- lint + format + skill freshness + version sync + changelog completeness + error-code enum + full test suite. -10. **Run `make test-e2e`** if you changed any command -- requires `E2E_API_TOKEN` and `E2E_URL`. -11. **Open a PR** -- list every plugin file you touched in the description so reviewers can spot what was missed. Plugin files do not auto-show up in CI failures the way Python files do; reviewers are the second line of defence. -12. **Merge via `gh pr merge`** -- never push directly to `main` (the branch is protected; this would fail anyway). - -If any of steps 5-8 reveal "I should have done this in the PR that introduced +8. **Manually review `plugins/kbagent/skills/kbagent/references/gotchas.md`** -- every behavior introduced or changed this release that an AI agent would not infer from `--help` should have its own `(since vX.Y.Z)` entry (freshly rewritten from `vNEXT` in step 4, or added now if a feature PR forgot one). The version tag is non-optional. +9. **Manually review `CLAUDE.md` `## All CLI Commands`** -- diff against `kbagent --help` output (and against `kbagent context`). Hand-maintained; CI does not catch drift here. +10. **Manually review `plugins/kbagent/skills/kbagent/references/commands-reference.md`** -- same drill. Hand-maintained, no CI coverage. +11. **Run `make check`** -- lint + format + skill freshness + version sync + changelog completeness + error-code enum + full test suite. +12. **Run `make test-e2e`** if any command changed since the last release -- requires `E2E_API_TOKEN` and `E2E_URL`. +13. **Open the release PR** -- link the merged PRs it covers (step 1) and list every plugin file you touched in the description so reviewers can spot what was missed. Plugin files do not auto-show up in CI failures the way Python files do; reviewers are the second line of defence. +14. **Merge via `gh pr merge`**, then tag the resulting `main` commit and publish the release -- never push directly to `main` (the branch is protected; this would fail anyway). + +If any of steps 7-10 reveal "I should have done this in the PR that introduced the command, not at release time", **also patch the per-command checklist** above so the next contributor catches the gap earlier. @@ -610,7 +649,15 @@ above so the next contributor catches the gap earlier. Beta and release-candidate versions follow PEP 440: `X.Y.Zb1`, `X.Y.Zb2`, `X.Y.Zrc1`, ... -- not the SemVer `-beta.1` form (hatchling and uv require -PEP 440 syntax in `pyproject.toml`'s `version` field). Two gates keep stable +PEP 440 syntax in `pyproject.toml`'s `version` field). + +A beta is the **one exception** to "version bumps only in the release PR": +the pre-release tag and GitHub Release are cut from the feature branch itself, +so the bump deliberately rides that branch -- for the duration of the beta, +the feature branch *is* the release PR. `main` stays on the stable channel +until the stable release PR ships the final version. + +Two gates keep stable users safe from accidentally landing on a beta: 1. **Version string itself.** PEP 440 marks any pre-release suffix as such; From 8f988a246133b377a90f493840e1639c9f7cd0ee Mon Sep 17 00:00:00 2001 From: Petr Date: Sat, 22 Aug 2026 23:35:19 +0200 Subject: [PATCH 2/2] docs(plugin): resolve the two stale release-step placeholders in gotchas.md Both '(Release step: once this ships, tag this sentence (since vX.Y.Z)...)' parentheticals in the storage-descriptions gotcha referred to behavior that has since merged: the table-detail human-mode Description column (#642) and the describe-batch --from-file shape rejection (#645). Neither commit is contained in the v0.88.0 tag and main is bumped to 0.89.0, so both are tagged (since v0.89.0) -- a version version-gate-check resolves via its existing changelog entry. This is a live instance of the leftover-placeholder failure mode this PR's release checklist step 4 now guards against. --- .../kbagent/skills/kbagent/references/gotchas.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/plugins/kbagent/skills/kbagent/references/gotchas.md b/plugins/kbagent/skills/kbagent/references/gotchas.md index f69048a0..7dca2fd0 100644 --- a/plugins/kbagent/skills/kbagent/references/gotchas.md +++ b/plugins/kbagent/skills/kbagent/references/gotchas.md @@ -2720,10 +2720,10 @@ write descriptive metadata onto storage objects. Three behaviors are easy to mis always returns `legacy_column_descriptions`, and warns in human mode when legacy keys remain (it never writes -- safe under a read-only token or `--deny-writes`). Its human-mode Columns table shows a `Description` - column. On 0.88.0 it did NOT -- there the descriptions were readable only - through `--json` `column_details[].description`, so a blank-looking terminal - table on that version does not mean the write failed. (Release step: once this - ships, tag this sentence `(since vX.Y.Z)` with the version that carried it.) Unknown column names now fail fast BEFORE any write; the old + column *(since v0.89.0)*. On 0.88.0 it did NOT -- there the descriptions were + readable only through `--json` `column_details[].description`, so a + blank-looking terminal table on that version does not mean the write + failed. Unknown column names now fail fast BEFORE any write; the old flat write accepted typos silently. Table and bucket descriptions are unaffected: still `KBC.description` (provider=user) on the object's metadata. - **`describe-batch` is partial-failure-tolerant.** Item-level errors are @@ -2735,10 +2735,9 @@ write descriptive metadata onto storage objects. Three behaviors are easy to mis `--from-file` whose shape is wrong (a `tables:` / `buckets:` / `columns:` section that is a list instead of a mapping of ID to description, a column entry that is not a mapping, a document that is not a mapping at all) is a - usage error — the whole file is rejected before the first write with - `INVALID_ARGUMENT` and exit 2, naming the offending key and its actual type. - Nothing is half-applied. (Release step: once this ships, tag this sentence - `(since vX.Y.Z)` with the version that carried it.) + usage error *(since v0.89.0)* — the whole file is rejected before the first + write with `INVALID_ARGUMENT` and exit 2, naming the offending key and its + actual type. Nothing is half-applied. - **Description-field precedence: metadata wins.** When both the native Storage API `description` field and a user-provided `KBC.description` (provider=user) metadata entry are present, `storage bucket-detail` / `storage table-detail`