feat(release): generate command-reference.md release asset from the Typer app (#498)#500
Conversation
…yper app (#498) The CLI's command reference exists only as hand-maintained copies (kbagent context, CLAUDE.md, commands-reference.md, and now the help.keboola.com CLI section from keboola/connection-docs#1015) -- every one a documented silent-drift risk. This adds the zero-drift source: a generator that introspects the live Typer/Click command tree, so the output cannot disagree with the shipped CLI. - scripts/gen_command_reference.py: walks the command tree (duck-typed via commands/repl._is_group -- Typer >=0.25 vendors Click, so isinstance against plain click classes fails), emits deterministic markdown: global options, one section per group, per-command option tables with required markers. Hidden commands/aliases (sl) are skipped, matching check_command_sync.py. 238 commands / 27 groups at v0.70.1. - release.yml: after the wheel build, generate the reference FROM the built wheel (uv run --no-project --with dist/*.whl) and upload command-reference.md as a release asset next to the wheel, so consumers fetch it pinned to a released version. - Makefile: gen-command-reference target. - cli.py: fix --allow-env-manage-token help text: default-deny shipped in 0.29.0, not 0.28.0 (changelog is authoritative) -- this exact string was the source of a wrong version in the help-docs PR. - tests/test_gen_command_reference.py: determinism, completeness cross-checked against check_command_sync.collect_commands(), required markers, hidden-alias and --help exclusion, version header. Closes #498.
| - name: Generate command-reference.md from the built wheel | ||
| # Introspects the Typer command tree of the EXACT artifact being | ||
| # shipped (issue #498), so the reference asset can never disagree with | ||
| # the released CLI. Downstream consumers (help.keboola.com CLI section, | ||
| # the Claude Code plugin) fetch it pinned to the release tag instead of | ||
| # hand-copying command lists. | ||
| run: | | ||
| tag="$TAG" | ||
| version="${tag#v}" | ||
| uv run --no-project --with "dist/keboola_cli-${version}-py3-none-any.whl" \ | ||
| python scripts/gen_command_reference.py --output dist/command-reference.md |
There was a problem hiding this comment.
🟡 Re-attaching a wheel to an older release now fails entirely
The release job runs the new reference generator from the checked-out tag (python scripts/gen_command_reference.py at .github/workflows/release.yml:129-130) before uploading, but for a tag that predates this change the script file does not exist at that commit, so the step errors and the whole job aborts before the wheel is attached.
Impact: The manual backfill path for older releases can no longer attach a wheel to them, which is the explicitly documented purpose of the manual trigger.
Why the backfill trigger breaks on pre-existing tags
The workflow_dispatch trigger is documented (.github/workflows/release.yml:19-20) as a way to "attach a wheel to an already-published release that predates this workflow (e.g. v0.59.0)". The job checks out the exact tag ref (ref: ${{ env.TAG }} at .github/workflows/release.yml:45). Since scripts/gen_command_reference.py is introduced by this PR, checking out any earlier tag will not contain the script, so the new step (.github/workflows/release.yml:120-130) fails with a missing-file error. Because this step is placed before the upload step (.github/workflows/release.yml:136-137), the failure prevents the wheel from being uploaded at all, regressing a path that worked before this change.
Prompt for agents
The new "Generate command-reference.md from the built wheel" step in .github/workflows/release.yml runs `python scripts/gen_command_reference.py`, but the job checks out the exact release tag (ref: env.TAG). For the documented backfill use case (workflow_dispatch on an older tag such as v0.59.0), that tag's commit does not contain scripts/gen_command_reference.py, so the step fails and prevents the wheel upload step that follows it. Make the reference-generation step tolerant of a missing script (e.g. guard with `if [ -f scripts/gen_command_reference.py ]; then ... fi`) and make the subsequent upload step upload command-reference.md only when it was actually produced, so backfilling wheels to pre-existing releases keeps working.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in d11e926. The generation step now skips cleanly when scripts/gen_command_reference.py is absent at the checked-out tag, and the upload step attaches command-reference.md only when it was produced — the wheel upload is unconditional, so pre-existing-tag backfills keep working. Good catch: this was the exact documented purpose of the workflow_dispatch path.
| def _walk( | ||
| group: click.Group, ctx: click.Context, prefix: tuple[str, ...] = () | ||
| ) -> list[tuple[tuple[str, ...], click.Command, click.Context]]: | ||
| """Collect (path, command, ctx) for every visible leaf, depth-first.""" | ||
| leaves: list[tuple[tuple[str, ...], click.Command, click.Context]] = [] | ||
| for name in group.list_commands(ctx): | ||
| cmd = group.get_command(ctx, name) | ||
| if cmd is None or cmd.hidden: | ||
| continue | ||
| path = (*prefix, name) | ||
| if _is_group(cmd): | ||
| with click.Context(cmd, parent=ctx, info_name=name) as sub_ctx: | ||
| leaves.extend(_walk(cmd, sub_ctx, path)) | ||
| else: | ||
| leaves.append((path, cmd, ctx)) | ||
| return leaves |
There was a problem hiding this comment.
🟡 Helper returns an unnamed multi-value tuple against project convention
The command-tree walker returns a list of three-element tuples bundling distinct values (scripts/gen_command_reference.py:84-99), whereas the repository's coding conventions forbid new bare tuples carrying more than two semantically distinct values and require a dataclass/NamedTuple instead.
Impact: Reviewers relying on the stated conventions will flag this; call sites unpack by position rather than by name, which the convention exists to prevent.
Convention reference
CONTRIBUTING.md "Code Quality Patterns" states: "Multiple values: return a @DataClass (or NamedTuple/BaseModel) -- never a bare tuple beyond two values, and even two-element tuples should use a dataclass when the values are semantically distinct" and "do not add new ones". _walk returns list[tuple[tuple[str, ...], click.Command, click.Context]] -- a three-value tuple of (path, command, context) unpacked positionally at scripts/gen_command_reference.py:131.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in d11e926 — _walk now returns a frozen LeafCommand dataclass (path/command/ctx) instead of the bare 3-tuple; the call site accesses fields by name. Output verified byte-identical.
…ass walker 1. release.yml: the job checks out the release tag, so a workflow_dispatch backfill of a tag predating scripts/gen_command_reference.py would fail the generation step and block the wheel upload (the documented purpose of the manual trigger). The step now skips cleanly when the script is absent, and command-reference.md uploads only when actually produced. 2. gen_command_reference.py: _walk returned a bare 3-tuple against the CONTRIBUTING.md multi-value-return rule; replaced with a frozen LeafCommand dataclass. Output byte-identical. 3. Test loader registers the script module in sys.modules before exec -- @DataClass under 'from __future__ import annotations' resolves its module namespace via sys.modules at class-creation time.
Backfills changelog entries for ten PRs merged since v0.66.1 without a version bump (#465 #486 #487 #488 #490 #492 #493 #494 #495 + #500), attributed to their bump windows (0.67.0 / 0.70.0 / 0.70.1 / 0.71.0), and aligns the version at 0.71.0 as the catch-up release: 0.67.0-0.70.1 were merged to main but never tagged or published, so auto-update users are still on 0.66.1. The v0.71.0 tag + GitHub Release follow.
Why — and why now
@jordanrburger raised the canonical-source question on the help-docs CLI section (keboola/connection-docs#1015): the new public docs hand-copy the command reference, and every existing reference surface in this repo (
kbagent context,CLAUDE.md,commands-reference.md) is also hand-maintained — convention #17's documented silent-drift list. A release is about to be cut (main is at 0.70.1, last release v0.66.1), which is exactly when the help docs start drifting. Landing this before the tag means the very first release the docs describe already carries the zero-drift asset.Closes #498.
What
scripts/gen_command_reference.py— introspects the live Typer/Click command tree and emits deterministic markdown: global options, one section per command group, per-command option tables with required markers and positional args. Hidden commands and aliases (sl) are skipped, matchingcheck_command_sync.py. Output at v0.70.1: 238 commands, 27 groups. Duck-typed viacommands/repl._is_group— Typer ≥0.25 vendors Click, soisinstanceagainst plainclickclasses fails (learned the hard way).release.yml— after the wheel build, the reference is generated from the built wheel (uv run --no-project --with dist/*.whl, same pattern as ci.yml's wheel smoke test) and uploaded ascommand-reference.mdnext to the wheel. The asset describes the exact artifact being shipped, by construction.Makefile—make gen-command-referencefor local use.cli.py) —--allow-env-manage-tokenhelp said "Default-deny since 0.28.0"; the changelog is unambiguous that both the flag and the default-deny shipped in 0.29.0. This exact help string was the source of the wrong version that surfaced in the help-docs review, which is a neat demonstration of why generated-from-source references matter: they propagate fixes too, not just drift.Tests
tests/test_gen_command_reference.py(7): byte-identical determinism; completeness cross-checked againstcheck_command_sync.collect_commands()(every visible leaf must appear); required-flag markers; hidden-alias and--helpexclusion; version header.Verification
uv run pytest tests/test_gen_command_reference.py→ 7 passedruff check/ruff format --check/ty checkclean; pre-commit hook passedKBAGENT_SKIP_UI_BUILD=1 uv run --no-project --with . python scripts/gen_command_reference.py→ correct output from the installed-package pathOut of scope (follow-ups)
context,commands-reference.md) from this generator — worth its own design pass.No version bump: release tooling + a help-text fix; the user cutting the release owns the version/notes.