sync push creates duplicate configs for every config a dev branch inherits from main (even with zero local changes)
Summary
On a Keboola dev branch, sync push treats every config inherited from main (not materialized branch-locally) as added and creates a duplicate, even when there are no local edits at all. A single sync push on a freshly-pulled branch created 100 duplicate configurations. This makes sync push unsafe on any dev branch that inherits the bulk of its configs from main — i.e. the normal case for a small targeted change.
Environment
- kbagent v0.66.0
- Stack:
north-europe.azure
- Single-project workflow,
branch use dev branches (not git-branching)
- Project has the
storage-branches feature (real dev branches, not legacy fake-branch)
Steps to reproduce
Starting from a completely clean local state (to rule out a corrupted manifest):
# 1. Clean slate + pull main
rm -rf main <branch_dir> storage .keboola/manifest.json
kbagent sync init --project <alias>
kbagent sync pull --project <alias> # 66 configs, branchId 25376
kbagent sync diff --project <alias> # 0/0/66 unchanged ✅ clean baseline
# 2. Switch to an existing dev branch + pull (NO edits after this)
kbagent branch use --branch <dev_id> --project <alias>
kbagent sync pull --project <alias> # 68 configs into <branch_dir>/
# 3. Push with ZERO local changes — should be a no-op
kbagent --json sync push --project <alias>
Expected
With no local edits after a fresh pull, sync push should be a no-op (created: 0), exactly like the same sequence on main.
Actual
{ "status": "pushed", "created": 100, "updated": 0, "deleted": 0,
"errors": [ /* 3× keboola.orchestrator: "Creation of new Flows disabled" */ ] }
created: 100 — new duplicate configs, with no local changes.
- The only configs not duplicated were 3
keboola.orchestrator flows, blocked by an unrelated API permission ("Creation of new Flows disabled") — otherwise it would have been 103.
- Concrete example:
keboola.app-data-gateway had 1 config on the branch before push, 3 after (2 duplicates, same name Data Gateway, fresh ULIDs).
Dry-run reveals the same intent beforehand — 66 of 68 configs reported change_type: added with empty config_id, despite each having a valid ULID under branchId: <dev_id> in .keboola/manifest.json.
Proof the configs already exist on the branch
kbagent config detail --component-id keboola.app-data-gateway \
--config-id 01kc4xss8veqhgc92276scekpg --branch <dev_id> --project <alias>
# -> {id: 01kc4xss8veqhgc92276scekpg, name: "Data Gateway", version: 1,
# last_change: "Copied from default branch configuration ... version 35"}
The config resolves on the dev branch with the same ID as main. sync push still creates a duplicate.
Root cause (hypothesis)
The push diff engine decides "exists on remote branch?" from a branch-scoped listing that returns only branch-materialized configs, ignoring configs read-through from main. This is the documented Storage-API behaviour for buckets (kbagent's own gotchas.md: "storage buckets --branch ID returns only locally-modified buckets ... That is Storage API behaviour, not a CLI bug"). But sync push mishandles it: instead of treating an inherited config as read-through/unchanged, it classifies it as added, drops its manifest ULID, and POSTs a new config — producing duplicates. sync pull writes correct ULIDs for all 68 configs, so the pull/push pair is asymmetric.
Impact
- Data-corrupting. One
sync push on a partially-materialized branch silently creates N duplicate configs (100 here). Cleanup is hard: the push result does not reliably record the new ULIDs, so the only clean recovery was to delete the entire dev branch.
- Affects the most common workflow: create/checkout a dev branch, change one transformation, push.
--dry-run surfaces the added entries, but there is no scoping flag to push only genuinely-changed configs, and the danger (duplicate creation vs. harmless no-op) is not obvious from the dry-run output.
Suggested fix
sync push should treat a local config whose manifest ULID resolves on the target branch — including configs inherited/read-through from main — as unchanged when content matches, rather than added. The remote-existence check must account for branch inheritance, consistent with what sync pull and config detail --branch already do. At minimum, push should refuse to create a config whose manifest ULID already exists on the target branch, and warn loudly instead of silently duplicating.
Workaround
For single-config edits on such a branch, use the MCP tool call (update_config / update_sql_transformation), which targets the config ID on the active branch and lets KBC resolve inheritance server-side. Avoid sync push on any branch where sync push --dry-run reports added for configs that already exist on the branch.
sync pushcreates duplicate configs for every config a dev branch inherits from main (even with zero local changes)Summary
On a Keboola dev branch,
sync pushtreats every config inherited from main (not materialized branch-locally) asaddedand creates a duplicate, even when there are no local edits at all. A singlesync pushon a freshly-pulled branch created 100 duplicate configurations. This makessync pushunsafe on any dev branch that inherits the bulk of its configs from main — i.e. the normal case for a small targeted change.Environment
north-europe.azurebranch usedev branches (not git-branching)storage-branchesfeature (real dev branches, not legacy fake-branch)Steps to reproduce
Starting from a completely clean local state (to rule out a corrupted manifest):
Expected
With no local edits after a fresh pull,
sync pushshould be a no-op (created: 0), exactly like the same sequence onmain.Actual
{ "status": "pushed", "created": 100, "updated": 0, "deleted": 0, "errors": [ /* 3× keboola.orchestrator: "Creation of new Flows disabled" */ ] }created: 100— new duplicate configs, with no local changes.keboola.orchestratorflows, blocked by an unrelated API permission ("Creation of new Flows disabled") — otherwise it would have been 103.keboola.app-data-gatewayhad 1 config on the branch before push, 3 after (2 duplicates, same nameData Gateway, fresh ULIDs).Dry-run reveals the same intent beforehand — 66 of 68 configs reported
change_type: addedwith emptyconfig_id, despite each having a valid ULID underbranchId: <dev_id>in.keboola/manifest.json.Proof the configs already exist on the branch
The config resolves on the dev branch with the same ID as main.
sync pushstill creates a duplicate.Root cause (hypothesis)
The push diff engine decides "exists on remote branch?" from a branch-scoped listing that returns only branch-materialized configs, ignoring configs read-through from main. This is the documented Storage-API behaviour for buckets (kbagent's own
gotchas.md: "storage buckets --branch IDreturns only locally-modified buckets ... That is Storage API behaviour, not a CLI bug"). Butsync pushmishandles it: instead of treating an inherited config as read-through/unchanged, it classifies it asadded, drops its manifest ULID, and POSTs a new config — producing duplicates.sync pullwrites correct ULIDs for all 68 configs, so the pull/push pair is asymmetric.Impact
sync pushon a partially-materialized branch silently creates N duplicate configs (100 here). Cleanup is hard: the push result does not reliably record the new ULIDs, so the only clean recovery was to delete the entire dev branch.--dry-runsurfaces theaddedentries, but there is no scoping flag to push only genuinely-changed configs, and the danger (duplicate creation vs. harmless no-op) is not obvious from the dry-run output.Suggested fix
sync pushshould treat a local config whose manifest ULID resolves on the target branch — including configs inherited/read-through from main — asunchangedwhen content matches, rather thanadded. The remote-existence check must account for branch inheritance, consistent with whatsync pullandconfig detail --branchalready do. At minimum, push should refuse to create a config whose manifest ULID already exists on the target branch, and warn loudly instead of silently duplicating.Workaround
For single-config edits on such a branch, use the MCP tool call (
update_config/update_sql_transformation), which targets the config ID on the active branch and lets KBC resolve inheritance server-side. Avoidsync pushon any branch wheresync push --dry-runreportsaddedfor configs that already exist on the branch.