Skip to content

fix(branch): report the branch each command uses, from one place (#766) - #800

Open
soustruh wants to merge 1 commit into
mainfrom
fix/report-resolved-branch-target
Open

soustruh wants to merge 1 commit into
mainfrom
fix/report-resolved-branch-target

Conversation

@soustruh

Copy link
Copy Markdown
Contributor

Closes #766. Replaces #769.

Why

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
  end
Loading
flowchart 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"]
  end
Loading

What changed

  • resolve_branch() moves from commands/_helpers.py to the new effective_branch.py and keeps its name. It is now the only code that applies the active branch. Services cannot import from commands/, so the function had to move to a module that every layer can import.
  • The function returns the branch ID and records the project, the branch and the source of the choice. OutputFormatter reports each record once:
    • human mode: one line on stderr, before the first API call to the project and before any confirmation prompt.
    • --json: a targets key in the success envelope and in the error envelope. data and error do not change.
  • Every command that has --branch or 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:
    • the source project of config clone
    • the branch of a merge request
    • the branch where notification commands read config names
    • the source branch of storage create-table --source-branch-id and storage clone-table
  • merge-request merge and an armed merge-request auto-merge also report production as their target.
  • A command that uses two branches reports both. For example, workspace create --ui creates the config in the active branch but runs its job on production.
  • branch use and branch create save the branch name next to the ID (active_branch_name in config.json), so the line can show it.
  • The record is open only while a CLI command runs. kbagent serve, the REPL shell and the SDK do not open it, so there resolve_branch() only returns the ID.
  • tests/test_effective_branch.py fails on a new read of ProjectConfig.active_branch_id outside effective_branch.py. A read that only shows or resets the active branch must be on its list, with a reason.
  • Docs: 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

Target: project 'prod', branch 456 'feature-x' (from 'kbagent branch use')
Target: project 'prod', branch 789 (from the command line)
Target: project 'prod', production (active branch 456 'feature-x' not used; pass --branch 456 to use it)
Target: project 'prod', branch 388 (from .keboola/branch-mapping.json)
Source: project 'prod', branch 456 'feature-x' (from 'kbagent branch use')
{
  "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_source is explicit, active_branch, git_mapping, manifest, merge_request or production. No targets key means that the command chose no branch. It does not mean production.

Behavior changes

  • flow delete --dry-run and flow schedule-remove --dry-run put the resolved branch in would_delete.branch_id. Before, they put the raw --branch value there, so the value was null under an active branch.
  • workspace list and workspace detail no longer print Info: 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 clone within one alias, with --branch under an active branch, now copies into that branch. Before, it refused because of a --target-branch that the caller did not give.
  • --branch 0 now 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 serve responses do not carry targets. REST reporting belongs to Specify the complete kbagent command-line interface #791.
  • Commands that use a different branch for one of their steps. This PR reports both branches and does not change the behavior. Separate issues:
    • workspace create --ui creates the sandbox config in the active branch but runs its job on production.
    • workspace from-transformation reads the transformation from production but creates the workspace in the active branch.
    • sync push into a tree whose first manifest branch is a development branch (after sync clone --branch) writes data apps to production.
  • project description-get and project description-set always use the default branch and have no branch option, so they report nothing.
  • commands/data_app.py goes 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 of workspace detail, flow delete --dry-run, the line order before a prompt, the lines of merge-request merge, and serve, which opens no record. It also contains the guard for new reads of active_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.
  • A harness ran every command that has --project against a recording HTTP transport, in three response modes. Each command ran with an active branch, with --branch, with --branch 0 and 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.
  • Read-only and --dry-run commands ran against a test project with an active branch.
  • All make check targets pass, except changelog-check, which runs only at release time.

@keboola-pr-reviewer-bot keboola-pr-reviewer-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.

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.

@soustruh soustruh left a comment •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review of #800 — fix(branch): report the branch each command uses, from one place (#766)

Generated by kbagent-pr-reviewer subagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed via make 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.md convention #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 as soustruh ✓
  • gh pr view 800 --json title,body,files,... → state OPEN, +1138/-542, 50 files, conventional fix(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 main was stale (pointed at a pre-0.95.0-release commit); re-diffed against origin/main (merge-base confirms the PR branched cleanly off current origin/main at 2342bba) — the file list matches gh pr view exactly (50 files, no version/changelog files touched) ✓
  • Layer-violation greps (typer/formatter in services/, httpx in commands/, formatter/typer in clients) → all empty except a typer.testing.CliRunner import 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 raw error_code="API_ERROR" string, located in tests/test_effective_branch.py:3109 — confirmed scripts/check_error_codes.py deliberately excludes tests/ (string comparisons in assertions are fine), so not a finding ✓
  • uv run --frozen make check → full pass: ruff check/format clean, ty check clean (0 diagnostics, ignoring one pre-existing unrelated hatchling import warning in scripts/hatch_build.py), SKILL.md up to date, version/plugin.json in sync, check_version_gates.py all 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.py OK, check_sentinel_guards.py OK, check_file_size.py → only pre-existing/disclosed soft-ceiling warnings (commands/data_app.py 809/800 lines, matching the PR description's own disclosure), 6936 passed, 15 skipped, 0 failures ✓
  • Manually walked the Plugin synchronization map rows relevant to this PR (no new commands, so OPERATION_REGISTRY/hint-definition rows don't apply): commands/context.py (AGENT_CONTEXT) updated for branch use, workspace list/detail, sync clone, semantic-layer version-tag fixes, and the new --json targets shape in the Tips section ✓; CLAUDE.md ## All CLI Commands updated (effective_branch.py in Project Structure, convention #19, branch use comment) ✓; 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.md new top-of-file entry tagged (since vNEXT, #766) correctly (version not yet released) ✓; commands-reference.md and branch-workflow.md updated ✓; docs/merge-requests-layer1.md example line updated to the new Source: 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 --project was omitted) was dead code — every call site in origin/main discarded the returned alias via _, effective_branch = resolve_branch(...) and always passed an already-resolved project string, so dropping it is not a behavior regression ✓
  • Spot-checked the documented "Behavior changes" section against the diff: --branch 0 now means production uniformly (config_service.py, flow_service.py, notification_service.py, schedule_service.py all switched from branch_id or project.active_branch_id — which treated 0 as falsy — to resolve_branch(), which special-cases branch == 0) ✓; flow_service.py delete_flow/remove_flow_schedule use the resolved effective_branch (not the raw --branch value) for would_delete.branch_id/branch_id ✓; workspace list/workspace detail now apply the active branch instead of forcing production with an Info: banner (commands/workspace.py, services/workspace_service.py) ✓; config clone within one alias under an active branch now copies into that branch instead of refusing (services/_config_clone.py, verified against new tests in test_config_clone_service.py::test_branch_flag_under_an_active_branch_copies_into_that_branch and ::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 read banner outside the historical changelog.py entry (which is correctly left untouched as a historical record) and a #766-referencing comment in test_workspace_cli.py ✓
  • Confirmed record_targets() is opened only in cli.py's main() callback, gated on ctx.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) in commands/repl.py) so each line still gets its own record via a fresh main() call; serve is excluded because uvicorn's per-request threads would share one process-lifetime record — this is directly tested by TestCommands::test_serve_opens_no_record ✓
  • Reproduced the feature live against a registered test project with read-only commands, in human and --json mode, 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, serve exclusion, and the AST guard itself), 2 in test_config_clone_service.py, 1 in test_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.
@soustruh
soustruh force-pushed the fix/report-resolved-branch-target branch from 4390d58 to 0c28702 Compare September 26, 2026 07:21

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants