Skip to content

fix(cli): make failed meta deps sync atomic and fail check on unloadable snapshot - #474

Merged
dmealing merged 3 commits into
mainfrom
fm/mo-deps-sync-rollback
Oct 28, 2026
Merged

dmealing merged 3 commits into
mainfrom
fm/mo-deps-sync-rollback

Conversation

@dmealing

Copy link
Copy Markdown
Member

What Changed

  • meta deps sync backs up deps.lock.json and the .metaobjects/deps/ snapshot before writing. On any failure, including the full-collection load, it restores both, prints a rollback notice, and exits 1 with the load error. The per-dependency report prints only after the sync succeeds.
  • meta deps check loads the committed model once its hashes match. It exits 1 and reports the load error (text, or loadError in structured output) when the model does not load. It still never writes the lock or snapshot.
  • docs/features/metadata-dependencies.md describes the atomic sync and the load step in check. CHANGELOG.md gets an [Unreleased] Fixed entry.
Validation

Risk Assessment

✅ Low: The change is small and matches the intent: a failed sync restores the previous lock and snapshot and exits 1, and meta deps check now exits 1 when the model won't load. The backup covers everything applySync writes, and the two regression tests exercise the real command.

Testing

I ran the real meta CLI against a throwaway publisher and consumer project. A consumer file that clashes with the dependency makes sync fail, and every failure path left the previous state intact. The full packages/cli suite passes (1746 pass, 0 fail). An unscoped bun test over server/typescript timed out at the 10-minute background limit, so I scoped the regression run to the package that changed. The baseline ts-fast and ts-unit run was already green.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Clean first meta deps sync then meta deps check both exit 0 and write the lock and snapshot ✅ pass live live-transcript.txt S1: exit=0, exit=0; lock and snapshot present
Re-sync to a new publisher version while the consumer model clashes: exit 1 with the load error, rollback message, no 'synced' line, lock and snapshot byte-identical ✅ pass live live-transcript.txt S2: ERR_SOURCE_MULTIPLE_PRIMARY, 'rolled back', exit=1, md5 diff reports STATE IDENTICAL, no 'synced' line
meta deps check on a loadable-hash but unloadable snapshot is not green (text and json) ✅ pass live live-transcript.txt S3: exit=1 with 'the locked dependency set does not load'; json carries loadError
First sync that fails to load leaves no lock and no snapshot ✅ pass live live-transcript.txt S4: exit=1, only config.json remains
Dry-run with a clash writes nothing ✅ pass live live-transcript.txt S5: exit=0, only config.json remains
After the clash is removed, sync and check succeed again, and no backup temp dirs are left behind ✅ pass live live-transcript.txt S6: exit=0, exit=0, no leftover meta-deps-backup dirs
Evidence: Live CLI transcript (all six scenarios)
\### S1: clean first sync + check
synced acme-common (new)→1.0.0 ((new)→10fbf886)
exit=0
acme-common: current — 1.0.0 (10fbf886)
exit=0
.metaobjects/config.json
.metaobjects/deps/acme-common/acme-common.metaobjects.json
.metaobjects/deps.lock.json
c6ef44115291b11635160a4e770672d8  .metaobjects/deps.lock.json
807a2c05c3fbade83ff6dda60df96516  .metaobjects/deps/acme-common/acme-common.metaobjects.json
\### S2: clash + new publisher version, re-sync must fail & roll back
meta: meta deps sync: the updated dependency set does not load: ERR_SOURCE_MULTIPLE_PRIMARY: object "Customer" declares 2 sources with role "primary"; exactly one is required
  in dep:acme-common/acme-common.metaobjects.json, meta.clash.json at $['metadata.root'].children[2]['object.entity']
meta: meta deps sync: rolled back — the previous lock and snapshot are restored.
exit=1
STATE IDENTICAL
\### S3: check when snapshot cannot load (restore publisher v1 so hashes match)
acme-common: current — 1.0.0 (10fbf886)
meta: meta deps check: the locked dependency set does not load: object "Customer" declares 2 sources with role "primary"; exactly one is required
exit=1
{
  "dependencies": [
    {
      "name": "acme-common",
      "status": "current",
      "lockVersion": "1.0.0",
      "lockIntegrity": "sha256-10fbf886e22faceca32c56e5e647c3ff1c82f503e638cba3bd3aa9390f7c409d",
      "installedVersion": "1.0.0",
      "installedIntegrity": "sha256-10fbf886e22faceca32c56e5e647c3ff1c82f503e638cba3bd3aa9390f7c409d",
      "line": "acme-common: current — 1.0.0 (10fbf886)"
    }
  ],
  "loadError": "object \"Customer\" declares 2 sources with role \"primary\"; exactly one is required"
}
exit=1
\### S4: first sync clash -> no lock/snapshot left
meta: meta deps sync: the updated dependency set does not load: ERR_SOURCE_MULTIPLE_PRIMARY: object "Customer" declares 2 sources with role "primary"; exactly one is required
  in dep:acme-common/acme-common.metaobjects.json, meta.clash.json at $['metadata.root'].children[2]['object.entity']
meta: meta deps sync: rolled back — the previous lock and snapshot are restored.
exit=1
.metaobjects/config.json
\### S5: dry-run with clash writes nothing
would sync acme-common (new)→1.0.0 ((new)→10fbf886)
exit=0
.metaobjects/config.json
\### S6: remove clash, sync succeeds again
synced acme-common (new)→1.0.0 ((new)→10fbf886)
exit=0
acme-common: current — 1.0.0 (10fbf886)
exit=0
no leftover backup tmp dirs
  • Outcome: 🔧 1 issue found → auto-fixed ✅ across 2 runs (1h40m18s)

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

🔧 **Test** - 1 issue found → auto-fixed ✅
  • ⚠️ server/typescript/packages/cli/src/commands/deps.ts:75 - Failed re-sync prints 'synced acme-common 1.0.0→1.1.0' before rolling back and exiting 1, so the output reports success for a sync that was reverted.
  • Live validation: ✅ go - 6 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Happy-path sync writes lock and snapshot; check is green ✅ pass live bun bin/meta.ts deps sync / deps check in disposable consumer: exit 0, 'acme-common: current — 1.0.0 (10fbf886)'
Failed re-sync (load clash) exits 1 with load error and leaves previous lock and snapshot byte-identical ✅ pass live md5sum of deps.lock.json and deps/acme-common/*.json unchanged after exit 1 'does not load: ERR_SOURCE_MULTIPLE_PRIMARY'
Failed first sync leaves no deps.lock.json and no .metaobjects/deps/ ✅ pass live exit 1 with load error; ls .metaobjects shows only config.json
Previous state still loads after failed sync (every command works) ✅ pass live meta verify --templates exit 0 after removing clash file; no load error
Adversarial: deps check with matching hashes but unloadable snapshot is not green ✅ pass live text: exit 1 'meta deps check: the locked dependency set does not load'; json: exit 1 with loadError field
deps check returns green again once the model loads ✅ pass live exit 0 after clash file removed
Targeted regression test file ⏸️ untested no Only an automated test was run (bun test packages/cli/test/deps-sync.test.ts: 13 pass). It was not driven through the product CLI in this run, so it does not count as a live result.
Dry-run sync leaves state untouched (backup skipped on dryRun) ⏸️ untested no Not driven live in this step; the backup is skipped on dry-run per code reading, and the existing test suite does not assert it
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains (baseline, passed)
  • bun bin/meta.ts deps sync --format text --cwd <consumer> (happy path, exit 0)
  • bun bin/meta.ts deps check --format text --cwd <consumer> (exit 0 when current)
  • bun bin/meta.ts deps sync with clashing meta.clash.json on re-sync to v1.1.0 (exit 1, load error, lock+snapshot md5 unchanged)
  • bun bin/meta.ts deps sync with clashing model on first sync (exit 1, no deps.lock.json, no .metaobjects/deps/)
  • bun bin/meta.ts deps check with unloadable snapshot, hashes matching (text exit 1 'does not load'; json exit 1 with loadError)
  • bun bin/meta.ts deps check after clash removed (exit 0)
  • bun bin/meta.ts verify --templates load probe after restore (exit 0)
  • bun test packages/cli/test/deps-sync.test.ts (13 pass, automated)
  • bun test packages/cli (1746 pass, 0 fail, 3 skip, automated)

🔧 Fix applied.
✅ Re-checked - no issues remain.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Clean first meta deps sync then meta deps check both exit 0 and write the lock and snapshot ✅ pass live live-transcript.txt S1: exit=0, exit=0; lock and snapshot present
Re-sync to a new publisher version while the consumer model clashes: exit 1 with the load error, rollback message, no 'synced' line, lock and snapshot byte-identical ✅ pass live live-transcript.txt S2: ERR_SOURCE_MULTIPLE_PRIMARY, 'rolled back', exit=1, md5 diff reports STATE IDENTICAL, no 'synced' line
meta deps check on a loadable-hash but unloadable snapshot is not green (text and json) ✅ pass live live-transcript.txt S3: exit=1 with 'the locked dependency set does not load'; json carries loadError
First sync that fails to load leaves no lock and no snapshot ✅ pass live live-transcript.txt S4: exit=1, only config.json remains
Dry-run with a clash writes nothing ✅ pass live live-transcript.txt S5: exit=0, only config.json remains
After the clash is removed, sync and check succeed again, and no backup temp dirs are left behind ✅ pass live live-transcript.txt S6: exit=0, exit=0, no leftover meta-deps-backup dirs
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • Real meta CLI (bun bin/meta.ts) against a disposable publisher/consumer project: clean sync + check, clashing re-sync, check on unloadable snapshot (text and json), clashing first sync, dry-run with clash, recovery after removing the clash
  • bun test test/deps-sync.test.ts in packages/cli (13 pass)
  • bun test for the whole packages/cli suite (1746 pass, 3 skip, 0 fail)
  • Baseline already green: scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@dmealing
dmealing merged commit be3afd9 into main Oct 28, 2026
1 check passed
@dmealing
dmealing deleted the fm/mo-deps-sync-rollback branch October 28, 2026 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant