Skip to content

feat(release): generate command-reference.md release asset from the Typer app (#498)#500

Merged
padak merged 2 commits into
mainfrom
feat/gen-command-reference
Jul 20, 2026
Merged

feat(release): generate command-reference.md release asset from the Typer app (#498)#500
padak merged 2 commits into
mainfrom
feat/gen-command-reference

Conversation

@padak

@padak padak commented Jul 20, 2026

Copy link
Copy Markdown
Member

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, matching check_command_sync.py. Output at v0.70.1: 238 commands, 27 groups. Duck-typed via commands/repl._is_group — Typer ≥0.25 vendors Click, so isinstance against plain click classes 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 as command-reference.md next to the wheel. The asset describes the exact artifact being shipped, by construction.
  • Makefilemake gen-command-reference for local use.
  • Drive-by fix (cli.py)--allow-env-manage-token help 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 against check_command_sync.collect_commands() (every visible leaf must appear); required-flag markers; hidden-alias and --help exclusion; version header.

Verification

  • uv run pytest tests/test_gen_command_reference.py → 7 passed
  • ruff check / ruff format --check / ty check clean; pre-commit hook passed
  • Local simulation of the release-job invocation: KBAGENT_SKIP_UI_BUILD=1 uv run --no-project --with . python scripts/gen_command_reference.py → correct output from the installed-package path
  • Two consecutive runs diff-identical

Out of scope (follow-ups)

  • The connection-docs side of the loop (scheduled action pulling the asset from the latest release into an auto-PR) — tracked in the PR #1015 discussion.
  • Retiring/regenerating the hand-maintained surfaces (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.


Open in Devin Review

…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.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Open in Devin Review

Comment on lines +120 to +130
- 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +84 to +99
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@padak
padak merged commit 1c56bb8 into main Jul 20, 2026
4 checks passed
@padak
padak deleted the feat/gen-command-reference branch July 20, 2026 14:24
padak added a commit that referenced this pull request Jul 20, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate command-reference.md from the Typer app (zero-drift reference for help docs + plugin)

1 participant