Skip to content

Add --syncDeletes: delete on the target what was deleted in the source - #252

Closed
5PK wants to merge 3 commits into
mainfrom
feature/sync-deletes
Closed

5PK wants to merge 3 commits into
mainfrom
feature/sync-deletes

Conversation

@5PK

@5PK 5PK commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds --syncDeletes, an opt-in final phase for sync / push / reverse-sync. It deletes on the target what was deleted in the source since the last sync. All eight entity types are covered and deleted in dependency order: URL redirections, pages (children first), content, containers, templates, models, assets, galleries.

Only entities reached through a mapping record are considered. Anything created directly on the target is never touched.

How each candidate is decided

A candidate is a mapping whose source entity is missing from the source pull.

Outcome When
delete Source confirmed deleted and target unchanged since the last sync (or --overwrite). The mapping is removed.
conflict (kept) Target edited since the last sync. Content and pages compare the live version ID; the other types compare last-modified dates. Needs --overwrite.
blocked (kept) Something that stays on the target still uses it: child pages (from the live, post-push sitemap), content or pages linking to the item, a container's items or linked-list fields, dynamic pages, a model's containers, content, page modules or model fields, an asset URL embedded in content, a gallery's assets.
unmap Target already gone, or another live source maps to the same target. Only the mapping is removed.
keep Source not confirmed deleted.

Safety rules

  • Confirming content and page deletes: a full pull also leaves out unpublished items, so each content or page candidate is checked against the source API. Only a 404 or state 3 counts as deleted; an unpublished item is kept.
  • Empty source lists: an empty source list for a type is never treated as "everything deleted".
  • Mass-delete guard: if one type (per locale) would lose more than 50% and more than 10 of its mapped entities, the phase deletes nothing and fails. --allowMassDeletes lifts this.
  • Scoped runs: --syncDeletes is refused with --pages, --containers, --models and --models-with-deps. --elements limits which types are considered.
  • No .env default: the flag is not read from .env, so a destructive mode is chosen per run.
  • Re-check before deleting: each content or page delete re-reads the target first. A target already removed, for example along with its page, counts as done.

Reporting

  • --preflight lists the planned deletes under Delete … phases, using a new delete preflight action.
  • --jsonSummary gets a syncDeletes phase and a syncDeletes option.
  • Conflicts and blocked deletes appear as warnings; failed deletes appear as failures, and their mappings are kept.

URL redirections

Redirections are deleted with apiClient.urlRedirectionMethods.deleteUrlRedirections, in chunks of the SDK's MAX_URL_REDIRECTIONS_PER_REQUEST (250). The method comes from agility/agility-cms-management-sdk-typescript#62, and this PR bumps @agility/management-sdk to ^0.2.0-beta.3 for it.

@agility/management-sdk 0.2.0-beta.3 is published (npm beta tag) and locked in package-lock.json. The CLI type-checks cleanly against the 0.2 line with no other changes; 0.1.40 to 0.2.0 only adds GEO/SEO page properties, geo on content items, search, and content list item order.

Draft: still to verify against a clone pair

Run --preflight first.

  1. Reading a deleted item: does a GET of a deleted content item or page return 404 or state 3? If it returns something else, those candidates show up as kept with "could not confirm", so nothing gets deleted.
  2. Published items: can a published content item or page be deleted directly?
  3. Locales: does a content or page delete in one locale affect other locales? Both outcomes are handled by the pre-delete re-check.
  4. Redirections endpoint: is the URL-redirection DELETE endpoint (used by the SDK method) deployed in the test environment?
  5. Server refusals: when the server refuses to delete a model or template that's still in use, it should be reported as a failure with the mapping kept.

Known limits

  • The "still in use" checks only look at the locales in the run.
  • Asset references are only searched in content fields; page SEO and other places aren't checked.
  • Number-only text fields count as content references, which can keep an item that isn't really in use. That errs on the side of keeping.
  • --elements dependency auto-inclusion applies to the deletes too: --elements=Content also brings in models, containers and so on.

Testing

  • src/lib/deleters/tests/delete-plan.test.ts: decision rules, mapping parsing, the mass-delete guard and reference scanning.
  • src/lib/deleters/tests/sync-deletes.test.ts: a fixture with a mapped entity of every type. Covers conflict, blocked, unpublished, --overwrite, preflight, a failed delete, a target that disappears before its delete, an empty source list, --elements, and the mass-delete guard.
  • src/tests/sync/sync-deletes.test.ts: hermetic end-to-end runs through the real orchestrator. Sync, delete in the source, re-sync with the flag. Also checks that nothing is deleted without the flag, a blocked model, preflight writing nothing, redirection deletes going through urlRedirectionMethods, and the scoped-run refusal.
  • The existing preflight and JSON summary tests now include delete: 0 in their totals.
  • Full suite: 137 suites, 2590 tests passing against the published 0.2.0-beta.3 (clean npm ci). type-check and type-check:tests are clean.

🤖 Generated with Claude Code

5PK and others added 3 commits October 30, 2026 12:07
A final sync phase, opt-in per run, that deletes the target counterpart of
every mapped entity whose source is gone: URL redirections, pages (children
first), content, containers, templates, models, assets and galleries, in
dependency order. Only entities reached through a mapping are considered.

Safety rules:
- Content and pages are confirmed deleted against the source API (404 or
  state 3); a full pull also omits unpublished items, which are kept.
- An empty source list for a type is never read as "everything deleted".
- A target edited since the last sync is a conflict unless --overwrite.
- A target that something staying on the target still uses (child pages,
  linking content or pages, container items, model usage, embedded asset
  URLs, gallery assets) is kept and reported.
- Mass-delete guard: more than 50% and more than 10 of one type deletes
  nothing and fails unless --allowMassDeletes.
- Refused with --pages, --containers, --models, --models-with-deps; not
  primed from .env.

Deleted and already-gone targets drop their mapping records. --preflight
reports planned deletes with a new "delete" action; --jsonSummary gets a
syncDeletes phase and option. URL redirections are deleted through the
Management API's bulk DELETE endpoint, called directly until the CLI moves
to an SDK release with urlRedirectionMethods.deleteUrlRedirections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--syncDeletes now deletes redirections with
apiClient.urlRedirectionMethods.deleteUrlRedirections (management SDK
0.2.0-beta.3) instead of its own fetch call, and chunks by the SDK's
MAX_URL_REDIRECTIONS_PER_REQUEST. The CLI's hand-rolled delete helper and
its result type are removed.

Bumps @agility/management-sdk to ^0.2.0-beta.3. That version is not on npm
yet, so the lockfile is regenerated once it is published.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@5PK

5PK commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: everything here is in #254 (release/1.3.0-beta.0), which takes these changes to main for the 1.3.0 beta. This branch has no commits that #254 doesn't. The description and review threads here still apply.

🤖 Generated with Claude Code

@5PK 5PK closed this Nov 2, 2026
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