Skip to content

sync push creates duplicate configs for every config a dev branch inherits from main (even with zero local changes) #482

Description

@frantisekrehor

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingepic/write-protectionSub-issue of #63 (Write Protection + Single-Config Sync)

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions