Conversation
keboola-pr-reviewer-bot
left a comment
There was a problem hiding this comment.
Verdict: needs_human (risk 3/5) · profile keboola-mcp-server
Large, well-tested branch-resolution refactor with several cross-cutting behavior changes that warrant a human owner's sign-off.
Impact flags: possible rollback re-introduction — see Check Run summary.
There was a problem hiding this comment.
Review of #800 — fix(branch): report the branch each command uses, from one place (#766)
Generated by
kbagent-pr-reviewersubagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed viamake check, not duplicated here.
Summary
This PR centralizes branch resolution behind a single new module, effective_branch.py (resolve_branch() / record_branch()), replacing three previously-uncoordinated call sites (the commands/_helpers.py helper, ~36 branch_id or project.active_branch_id service one-liners, and separate sync/workspace resolvers). It also makes every command that picks a branch report it: a Target:/Source: line on stderr in human mode, and a targets key in the --json envelope (success and error), including under --dry-run. tests/test_effective_branch.py (403 new lines) enforces via AST inspection that no code outside effective_branch.py reads ProjectConfig.active_branch_id directly. Verdict: APPROVE. The mechanism is well designed (thread-safe recorder, dedup logic for repeated production resolution, clean 3-layer separation), all mandatory documentation surfaces from the Plugin synchronization map are updated correctly, make check passes clean, and I reproduced the claimed Target:/targets behavior live against a real project in both human and --json modes.
Verdict
- Verdict: APPROVE
- Blocking findings: 0
- Non-blocking findings: 1
- Nits: 0
Blocking findings
(none)
Non-blocking findings
[NB-1] tests/test_e2e.py — no dedicated E2E assertion for the targets/Target: mechanism
CONTRIBUTING.md requires E2E coverage for CLI command behavior. This PR changes output behavior across ~20 command groups but adds no tests/test_e2e.py entry that asserts a targets key (or Target: line) appears against a real Keboola project. The PR description states a manual harness validated every command against a recording HTTP transport in three response modes, which is solid verification, but it is not committed/automated. I independently reproduced the behavior live (see Verification log) and it matches the spec exactly, so this is not a correctness concern — just a coverage gap for regression protection going forward.
Nits
(none)
Verification log
- Read
CONTRIBUTING.md§"Checklist: Adding a New CLI Command", §"Plugin synchronization map", §"Releasing a new version" ✓ - Read
CLAUDE.mdconvention #17 and## All CLI Commands(via system context) ✓ - Read
plugins/kbagent/agents/keboola-expert.md§1 (non-negotiable rules) and §3 (inline gotchas) ✓ gh auth status→ authenticated assoustruh✓gh pr view 800 --json title,body,files,...→ state OPEN, +1138/-542, 50 files, conventionalfix(branch):prefix matches a behavior fix ✓git rev-parse --abbrev-ref HEAD→fix/report-resolved-branch-target, matches<branch>✓ (working tree clean, at PR head 4390d58)- Local
mainwas stale (pointed at a pre-0.95.0-release commit); re-diffed againstorigin/main(merge-base confirms the PR branched cleanly off currentorigin/mainat 2342bba) — the file list matchesgh pr viewexactly (50 files, no version/changelog files touched) ✓ - Layer-violation greps (typer/formatter in
services/, httpx incommands/, formatter/typer in clients) → all empty except atyper.testing.CliRunnerimport in a test file (expected) ✓ no violation - Magic-number / raw-error-code-literal / bare-except /
print()/ token-leak greps on the diff → only one hit, a rawerror_code="API_ERROR"string, located intests/test_effective_branch.py:3109— confirmedscripts/check_error_codes.pydeliberately excludestests/(string comparisons in assertions are fine), so not a finding ✓ uv run --frozen make check→ full pass: ruff check/format clean,ty checkclean (0 diagnostics, ignoring one pre-existing unrelatedhatchlingimport warning inscripts/hatch_build.py),SKILL.mdup to date, version/plugin.json in sync,check_version_gates.pyall 361 gates resolve,check_command_sync.py→ "OK: all 281 CLI commands are registered (OPERATION_REGISTRY) and documented" (no new commands added by this PR, consistent with the diff), changelog completeness OK,check_error_codes.pyOK,check_sentinel_guards.pyOK,check_file_size.py→ only pre-existing/disclosed soft-ceiling warnings (commands/data_app.py809/800 lines, matching the PR description's own disclosure), 6936 passed, 15 skipped, 0 failures ✓- Manually walked the
Plugin synchronization maprows relevant to this PR (no new commands, soOPERATION_REGISTRY/hint-definition rows don't apply):commands/context.py(AGENT_CONTEXT) updated forbranch use,workspace list/detail,sync clone,semantic-layerversion-tag fixes, and the new--jsontargetsshape in the Tips section ✓;CLAUDE.md## All CLI Commandsupdated (effective_branch.pyin Project Structure, convention #19,branch usecomment) ✓;keboola-expert.md§3 gotcha added ("Which branch did a command use? (vNEXT+)") ✓, §2 matrix correctly left untouched (no new write/destructive command group) ✓;plugins/kbagent/skills/kbagent/references/gotchas.mdnew top-of-file entry tagged(since vNEXT, #766)correctly (version not yet released) ✓;commands-reference.mdandbranch-workflow.mdupdated ✓;docs/merge-requests-layer1.mdexample line updated to the newSource:format ✓ wc -c plugins/kbagent/agents/keboola-expert.md→ 62943 bytes, under the 70000 B budget;uv run --frozen pytest tests/test_agent_prompt.py -q→ 42 passed ✓- Traced
resolve_branch()/record_branch()call-site migration: confirmed the OLD_helpers.resolve_branch(config_store, formatter, project, branch, ...)'s alias-auto-resolution branch (used when--projectwas omitted) was dead code — every call site inorigin/maindiscarded the returned alias via_, effective_branch = resolve_branch(...)and always passed an already-resolvedprojectstring, so dropping it is not a behavior regression ✓ - Spot-checked the documented "Behavior changes" section against the diff:
--branch 0now means production uniformly (config_service.py,flow_service.py,notification_service.py,schedule_service.pyall switched frombranch_id or project.active_branch_id— which treated0as falsy — toresolve_branch(), which special-casesbranch == 0) ✓;flow_service.pydelete_flow/remove_flow_scheduleuse the resolvedeffective_branch(not the raw--branchvalue) forwould_delete.branch_id/branch_id✓;workspace list/workspace detailnow apply the active branch instead of forcing production with anInfo:banner (commands/workspace.py,services/workspace_service.py) ✓;config clonewithin one alias under an active branch now copies into that branch instead of refusing (services/_config_clone.py, verified against new tests intest_config_clone_service.py::test_branch_flag_under_an_active_branch_copies_into_that_branchand::test_a_second_alias_of_the_project_keeps_its_own_active_branch) ✓ - Confirmed no stale references survive to the removed
Info: Using production branch for readbanner outside the historicalchangelog.pyentry (which is correctly left untouched as a historical record) and a#766-referencing comment intest_workspace_cli.py✓ - Confirmed
record_targets()is opened only incli.py'smain()callback, gated onctx.invoked_subcommand not in (None, "repl", "serve"); verified the REPL re-invokes the whole click app per line (click_app(full_argv, standalone_mode=False)incommands/repl.py) so each line still gets its own record via a freshmain()call;serveis excluded because uvicorn's per-request threads would share one process-lifetime record — this is directly tested byTestCommands::test_serve_opens_no_record✓ - Reproduced the feature live against a registered test project with read-only commands, in human and
--jsonmode, with and without an active branch. The output matched the PR description. - Tallied new/changed test functions: 34 total, 31 in the new
tests/test_effective_branch.py(covering resolution order, dedup, thread-safety, output formatting,--json/error envelopes,--dry-run, ordering-before-confirmation-prompts,serveexclusion, and the AST guard itself), 2 intest_config_clone_service.py, 1 intest_workspace_cli.py— solid coverage of the new mechanism at both the service and CLI layer ✓
Open questions for the author
(none)
resolve_branch() in the new effective_branch.py is now the only code that applies the `branch use` active branch. It replaces the command helper and the service lines that applied the active branch without notice. It records each choice, and OutputFormatter reports it: a Target: or Source: line on stderr, and `targets` in the --json envelope, also for --dry-run. A test fails on a new direct read of active_branch_id.
4390d58 to
0c28702
Compare
Closes #766. Replaces #769.
Why
kbagent branch usesaves an active branch for a project. Commands use it when--branchis not given.resolve_branch()incommands/_helpers.py(56 calls). It printed anInfo:line, but only in human mode.branch_id or project.active_branch_id. They printed nothing.workspace createand most config and flow commands wrote to a development branch with no notice (branch use: active branch persists silently across sessions and is never echoed on config read/write, so config update can target a dev branch unnoticed #766). With--json, no command reported the branch.--branch(Data Streams, data apps, branch metadata) never apply the active branch, and they did not report their branch either.flowchart LR subgraph before["Before"] C1["commands: resolve_branch()<br/>Info: line, human mode only"] --> A1["API call"] S1["services: branch_id or active_branch_id<br/>no output"] --> A1 R1["sync and workspace resolvers<br/>no output"] --> A1 endflowchart LR subgraph after["After"] C2["commands"] --> R["resolve_branch()<br/>effective_branch.py"] S2["services"] --> R Y2["sync and workspace resolvers"] --> R R -->|branch ID| A2["API call"] R -->|record| F["OutputFormatter"] F --> H["Target: / Source: line on stderr"] F --> J["targets in the --json envelope"] endWhat changed
resolve_branch()moves fromcommands/_helpers.pyto the neweffective_branch.pyand keeps its name. It is now the only code that applies the active branch. Services cannot import fromcommands/, so the function had to move to a module that every layer can import.OutputFormatterreports each record once:--json: atargetskey in the success envelope and in the error envelope.dataanderrordo not change.--branchor uses the active branch reports its branch, also with--dry-run. A command that never uses the active branch reports production and names the active branch that it did not use.Source:names a branch that a command reads from or merges from:config clonestorage create-table --source-branch-idandstorage clone-tablemerge-request mergeand an armedmerge-request auto-mergealso report production as their target.workspace create --uicreates the config in the active branch but runs its job on production.branch useandbranch createsave the branch name next to the ID (active_branch_nameinconfig.json), so the line can show it.kbagent serve, the REPL shell and the SDK do not open it, so thereresolve_branch()only returns the ID.tests/test_effective_branch.pyfails on a new read ofProjectConfig.active_branch_idoutsideeffective_branch.py. A read that only shows or resets the active branch must be on its list, with a reason.AGENT_CONTEXT,CLAUDE.md(convention 19 and Project Structure),CONTRIBUTING.md,keboola-expert.md,gotchas.md(since vNEXT),commands-reference.md,branch-workflow.md,docs/merge-requests-layer1.md, and the REST parameter descriptions for workspaces.Output
{ "status": "ok", "targets": [ { "role": "target", "project_alias": "prod", "branch_id": 456, "branch_name": "feature-x", "branch_source": "active_branch", "active_branch": {"branch_id": 456, "branch_name": "feature-x"} } ], "data": {} }branch_sourceisexplicit,active_branch,git_mapping,manifest,merge_requestorproduction. Notargetskey means that the command chose no branch. It does not mean production.Behavior changes
flow delete --dry-runandflow schedule-remove --dry-runput the resolved branch inwould_delete.branch_id. Before, they put the raw--branchvalue there, so the value was null under an active branch.workspace listandworkspace detailno longer printInfo: Using production branch for read. That line was wrong: the workspace service has used the active branch since v0.42.0. The listing does not change.config clonewithin one alias, with--branchunder an active branch, now copies into that branch. Before, it refused because of a--target-branchthat the caller did not give.--branch 0now means production for every command. The API clients always sent 0 to the production endpoint. Before, the config, flow, schedule and notification commands used the active branch for--branch 0.Not in this PR
kbagent serveresponses do not carrytargets. REST reporting belongs to Specify the complete kbagent command-line interface #791.workspace create --uicreates the sandbox config in the active branch but runs its job on production.workspace from-transformationreads the transformation from production but creates the workspace in the active branch.sync pushinto a tree whose first manifest branch is a development branch (aftersync clone --branch) writes data apps to production.project description-getandproject description-setalways use the default branch and have no branch option, so they report nothing.commands/data_app.pygoes over its soft size ceiling (809 of 800 code lines). This is a warning only.Testing
tests/test_effective_branch.py: the resolution order, the record rules, the output text and the envelopes. It also runs CLI paths: the service path ofworkspace detail,flow delete --dry-run, the line order before a prompt, the lines ofmerge-request merge, andserve, which opens no record. It also contains the guard for new reads ofactive_branch_id.tests/test_config_clone_service.py: the clone within one alias under an active branch, and a second alias of the same project that keeps its own active branch.--projectagainst a recording HTTP transport, in three response modes. Each command ran with an active branch, with--branch, with--branch 0and with--dry-run. No request went to a branch that the command did not report. With--branch 0, no request went to the active branch.--dry-runcommands ran against a test project with an active branch.make checktargets pass, exceptchangelog-check, which runs only at release time.