Skip to content

sync push stamps pull_config_hash from disk while diff compares an API-derived hash — every pushed config stays "REMOTE MODIFIED" forever (mirror of #466) #686

Description

@DasaDama

Summary

sync push stamps pull_config_hash with a hash computed from the files on disk, while sync pull and sync diff compute the remote-side hash from the API response. For any config where the local↔API round-trip is not hash-stable, those two producers disagree — so after a successful sync push the config is reported ~ REMOTE MODIFIED by every subsequent sync diff, forever, even though the remote and the working tree are byte-identical. Only a real sync pull clears it.

The stored hash exists precisely to avoid that round-trip — the diff path says so itself:

# For files unchanged since pull, use the stored pull_config_hash
# directly (avoids lossy code extraction roundtrip).

…but push fills that field with a value produced by the lossy round-trip.

Relation to #466 (please read together)

This is the mirror image of #466 and it survived the v0.72.0 fixes. #466 was sync diff/status falsely reporting "in sync" (false negative) plus reconcile ergonomics; this is sync diff falsely reporting drift (false positive), and the root cause is on the push side — nothing in the #466 / #467 / #482 / #497 fixes touches it. The --theirs reconcile path shipped for #466 does clear the symptom (any real pull does), but it cannot prevent it: the next deploy re-creates it.

Net effect for us is the same conclusion as #466's impact section, from the opposite direction: sync diff still cannot be trusted as a local-vs-production audit — now because it reports drift that does not exist.

Impact — why this is urgent for us

We keep a git repo in lock-step with production: each project is a sync working tree, and a production deploy is sync push after a merged PR (~20 projects on one stack).

  1. Every deploy now needs a second, content-free PR. The documented "refresh the manifest baseline after deploy" chore cannot work, because committing the manifest that push just wrote stores the wrong hash form. To get a truthful baseline into git we have to open a separate PR whose entire diff is manifest.json — after every single deploy. That is what pushed us to file this.
  2. Real drift hides in the noise. Our repo currently carries 18 phantom REMOTE MODIFIED configs against 2 real UI changes. We could only separate them by pulling all of production into a scratch tree and comparing file by file — exactly the kind of manual audit kbagent sync: cannot reliably reconcile a drifted tree with production; sync diff/status report false "in sync" #466 was supposed to make unnecessary.
  3. It is self-inflicted by the tool's own happy path: the more disciplined you are about deploying through sync push, the more phantom drift you accumulate.

Root cause (paths on main, v0.90.1)

step file:line pull_config_hash computed from
sync pull src/keboola_agent_cli/services/sync_service.py:787-793 config_hash(api_config_to_local(component_id, cfg, config_id)) — the API response
sync push (modified) sync_service.py:1710_compute_config_hashes (:1908-1930) self._read_config_file(config_dir) + merge_code_files(...) — the files on disk
sync diff sync_service.py:1251 builds the remote side with api_config_to_local(...), then compares it against the stored pull_config_hash mixed producers ⇒ mismatch

The same asymmetry is in the create path (sync_service.py:1652) and in the row push path (services/_sync_push_ops.py:239).

Every affected config in our data has at least one code whose API body holds parameters.blocks[].codes[].script as an array of several statements, which transform.sql cannot represent (there are no statement boundaries in the file, and _config.yml carries no blocks/codes/script at all), so merge_code_files cannot reconstruct a hash-identical body. We did not isolate the trigger further than that.

Evidence

Observed on kbagent v0.59.0, one stack, 21 projects, 244 configs. (Still on 0.59.0 because auto-update cannot cross the #424 package rename, as you noted on #466 — we will reinstall and re-verify. All three code paths above are unchanged on main @ v0.90.1, which is why we are filing now rather than waiting.)

Committed baseline vs a fresh sync pull --all-projects of the same production state, compared field by field:

  • 224 configs — baseline identical.
  • 18 configs — pull_config_hash differs and nothing else: pull_hash and pull_extra_hashes (transform.sql, _description.md) match exactly, and the pulled files are byte-identical to the tracked ones (0 differing files across 17 project trees, 11–139 files each).
  • 2 configs — genuinely changed in the UI (file hashes differ too). These are the true positives the noise was hiding.

The discriminator is who wrote the remote's current version (config detailcurrentVersion.creatorToken.description), and it was exceptionless:

remote version last written by stored pull_config_hash
kbagent-cli [...] — i.e. a sync push mismatches (18/18)
a plain user token — i.e. the UI matches

Ruled out as the discriminator: multi-statement codes per se. Several configs whose codes hold up to 39 statements each compare clean — because their baseline was written by a pull, not a push.

Finally, sync diff run against the freshly pulled tree reports all 21 projects in sync, confirming the API-derived form is the one diff accepts.

Repro

  1. In a sync working tree, pick a keboola.snowflake-transformation whose API body has a code with more than one statement in script.
  2. Change anything in it locally and kbagent sync push --project <alias>. Push succeeds.
  3. kbagent sync diff --project <alias>~ REMOTE MODIFIED … parameters.blocks[0].codes changed, with no local edits and nothing changed on the remote since. It never goes away.
  4. kbagent sync pull --project <alias> --no-jobs --no-storage → now in sync, and the only thing that changed on disk is manifest.json (pull_config_hash). No config file is touched, which is the proof that step 3 was phantom.

Suggested fix

Make push stamp the baseline with the same producer pull/diff use: hash the config as the API returns it after the write (the update response body, or a re-fetch), via config_hash(api_config_to_local(...)), instead of _compute_config_hashes(config_dir, ...). Applies to the config update path, the create path and the row path.

If that is not desirable, then a push should not stamp pull_config_hash at all — leaving it stale is at least visibly stale, whereas today it is written with a value that can never match.

A regression test would be: push a config with a multi-statement code, then assert sync diff reports it in sync.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions