Skip to content

feat(sync): stop before the merge when upstream changed its sync config, add migrate --run - #39

Merged
flipvh merged 2 commits into
mainfrom
fix/sync-feedback-2026-10-04
Oct 4, 2026
Merged

flipvh merged 2 commits into
mainfrom
fix/sync-feedback-2026-10-04

Conversation

@flipvh

@flipvh flipvh commented Oct 4, 2026

Copy link
Copy Markdown
Member

The CLI side of raak's and projectcampus's feedback on their syncs to cella 0.16.0. The template side is cellajs/cella#1312.

Sync config is checked before the merge

cella/cella.config.ts never syncs, and the merge runs on the app's lists as they stand. Both apps received paths upstream had newly ignored (upstream renamed an ignored folder, the apps' lists still named the old one), and the report that names such entries came after the merge.

  • A new merge now compares upstream's sync config from the last sync point first. With entries the app's config does not follow, the run prints them and stops before anything merges. --keep-config merges with the config as it stands (also on forks). analyze still only reports, and the rerun that commits a staged merge is not checked.
  • settings.packageJsonSync is part of the comparison. Replayed on projectcampus's config from before its sync, the report now holds all six entries it had to find by hand, exports included.

This changes what an unattended cella sync does: it exits 1 where it used to merge.

A stop before the merge leaves nothing behind

  • Every stop before the merge (this check, the CLI version check, a --ref that does not resolve, an unreachable upstream) removes the temporary cella/sync/* branch and returns to the starting branch. Before, the empty branch stayed checked out.
  • An upstream release or branch behind the last sync point ends as a no-op with exit 0. A --ref behind it is still refused. analyze still reports it as an error.

Reports

  • The protected-but-behind lists of analyze and sync end with one git diff <last-sync>..<upstream> -- <entry> line per pinned or ignored entry. An e2e test runs the printed command and checks it shows upstream's change without the app's.
  • ignored entry not found only warns when the path is in neither the app nor upstream (read at the upstream branch as last fetched). An app ignores some paths to keep them from arriving, and every run warned about those.

migrate --run <id> [-- <args>]

A codemod's roots cover template-owned files, and one rewrote a file upstream had changed after writing the codemod. --run extracts the note, runs its codemod with tsx from the app root, and gives every file the codemod changed that was byte-identical to the last synced upstream commit its upstream content back. Without arguments the codemod gets inventory and the note's roots, the report-only run: all eight codemods in cella take that subcommand first. --script <file> names the script when a folder holds several.

The legacy notes folder

projectcampus reported that a conflicted first run kept the old { "applied": [...] } record and its cella/migrations/ copy. Its lockfile shows that merge ran on CLI 0.2.3, which predates this code. The new e2e test covers that path on the current code: conflicted run from the legacy state, upstream changing and deleting existing note files in range. It passes on main unchanged, so no fix was needed.

Verification

  • pnpm lint, pnpm ts: clean. pnpm test: 26 files, 271 tests pass (24 files, 236 tests before). pnpm test:release: 1 passes.
  • The --run example from the README was run in a sandbox app with a copy of the real tailwind note: it rewrote the app's file and restored the template's.
  • Not run against a real raak or projectcampus checkout. forks --keep-config has a parse test only.

After the release

cella can then require this version in its root package.json (the version check makes apps install it before their next sync) and point its migrate skill at --run.

🤖 Generated with Claude Code

flipvh and others added 2 commits October 4, 2026 14:32
… state

The app has the old `{ "applied": [...] }` record and a full copy of `cella/migrations/`.
Within the sync range upstream changes and deletes existing note files and adds a note,
and an unrelated file conflicts. The first run stops at the conflict with the folder gone
from the working tree and the index and the record converted to `{ "pending": [...] }`.
The finishing rerun commits, and the folder stays gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ig, add migrate --run

Sync
- A new merge checks upstream's sync config first. When upstream changed `overrides.pinned`,
  `overrides.ignored` or `settings.packageJsonSync` since the last sync point in ways the app's
  config does not follow, the run prints the report and stops before anything is merged: the merge
  would run on the app's old lists, so a path upstream newly ignores or pins would arrive as an
  ordinary synced file. `--keep-config` merges with the config as it stands (also on `forks`).
  `analyze` keeps reporting without stopping, and the rerun that commits a staged merge is not gated.
- `settings.packageJsonSync` is part of that comparison: keys upstream added that the app lacks and
  keys upstream dropped that the app still lists appear in the same report (`analyze --json` too).
- A run that stops before the merge drops its temporary `cella/sync/*` branch and returns to the
  branch it started from: the sync config gate, the CLI version gate, a `--ref` that does not
  resolve, an unreachable upstream. Before, the empty branch stayed checked out.
- An upstream release or branch behind the last sync point ends the run as a no-op with a message
  instead of an error. A pinned `--ref` behind it is still refused.
- The protected-but-behind lists of `analyze` and `sync` end with one
  `git diff <last-sync>..<upstream> -- <entry>` line per pinned or ignored entry.
- `ignored entry not found` only warns when the path is in neither the app nor upstream, read at the
  upstream branch as last fetched. Nothing changes while that ref does not resolve.

Migrate
- `pnpm cella migrate --run <id> [-- <args>]` extracts a note, runs its codemod with tsx from the
  app root and gives the files the codemod changed that were byte-identical to the last synced
  upstream commit their upstream content back. Without arguments the codemod gets `inventory` and
  the note's roots. `--script <file>` names the script when the folder holds several.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@flipvh
flipvh merged commit 03ad3eb into main Oct 4, 2026
7 checks passed
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