Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ agility pull [options]
| `--elements` | string | `Models,Galleries,Assets,Containers,Content,Templates,Pages,Sitemaps,UrlRedirections` | Comma-separated list of elements to process |
| `--models` | string | _(empty)_ | Comma-separated list of model reference names to sync (only syncs the specified models) |

#### Syncing deletes

`--syncDeletes` adds a final phase to a sync. For every mapping whose source entity is gone from the source pull it decides:

| Outcome | When |
| --- | --- |
| **deleted** | The source entity is deleted and the target is unchanged since the last sync (or `--overwrite` is set). The mapping is removed. |
| **conflict** (kept) | The target was edited since the last sync. Re-run with `--overwrite` to delete it anyway. |
| **blocked** (kept) | Something that stays on the target still uses it — e.g. a target-only child page, a page or item that links to the content item, items in the container, a container or item built on the model, content that embeds the asset. |
| **mapping removed** | The target is already gone, or another live source entity maps to the same target. |
| **kept** | The source item is unpublished rather than deleted, or the delete could not be confirmed. |

Run it with `--preflight` first: the report lists every planned delete under `Delete …` phases. Deleted pages and content items go to the instance's normal delete flow (the batch is labelled "Deleted by agility-cli --syncDeletes").

**Authentication & Environment Options:**

| Option | Type | Default | Description |
Expand Down Expand Up @@ -105,6 +119,8 @@ agility sync [options]
| Option | Type | Default | Description |
| --------------- | ------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--overwrite` | boolean | `false` | Conflict-scoped override. By default, when a target item has its own changes that conflict with the source, the CLI skips it to avoid data loss. With `--overwrite`, those conflicting target items are overwritten with the source version. Non-conflicting updates are applied either way. Also covers a mapped target item that no longer exists (deleted or unpublished on the target): by default that is reported as a conflict and skipped, with `--overwrite` the item is recreated and the mapping repointed. |
| `--syncDeletes` | boolean | `false` | After syncing, delete from the target the entities that were deleted in the source since the last sync (URL redirections, pages, content, containers, templates, models, assets, galleries — dependents first). Only entities the CLI previously synced (that have a mapping) are considered; nothing created directly on the target is touched. A target entity edited since the last sync is reported as a conflict and kept unless `--overwrite` is also set. An entity that something kept on the target still uses (a child page, a page or item that links to the content, a container's items, ...) is kept and reported. Content and page deletes are confirmed against the source first, so an item that was only unpublished is never deleted. Cannot be combined with `--pages`, `--containers`, `--models` or `--models-with-deps`; use `--elements` to limit the types. Works with `--preflight`. Not read from `.env`. See [Syncing deletes](#syncing-deletes). |
| `--allowMassDeletes` | boolean | `false` | With `--syncDeletes`: allow a run to delete more than half (and more than 10) of the mapped entities of one type. Without it such a run deletes nothing and fails, because a stale or partial local cache looks the same as a mass delete. |
| `--autoPublish` | string | _(disabled)_ | Automatically publish synced items that were published in the source instance. Values: `content`, `pages`, `both`. If flag is provided without a value, defaults to `both`. Items that are only in staging (not published) in the source are skipped. |
| `--fullPull` | boolean | `false` | Full content re-pull. Discards the stored content sync token for every locale, re-pulls all content items and pages, and removes local content/page files that no longer exist on the instance. Use when a local cache is suspected stale. Models, containers, templates, galleries and assets reconcile against the instance on every pull regardless. |
| `--preflight` | boolean | `false` | Dry-run preview. Runs the full source-pull, target-pull, dependency analysis and change detection, then reports the creates/updates/skips/conflicts a real sync would produce — **without writing anything** to the target instance or mapping files. Exits non-zero when conflicts are detected, so CI can gate a real sync on a clean preflight. |
Expand Down Expand Up @@ -141,6 +157,10 @@ agility sync --sourceGuid="abc123" --targetGuid="def456" --overwrite
# Preview what a sync would do without writing anything (exits non-zero if conflicts are found)
agility sync --sourceGuid="abc123" --targetGuid="def456" --preflight

# Also delete from the target what was deleted in the source since the last sync (preview first)
agility sync --sourceGuid="abc123" --targetGuid="def456" --syncDeletes --preflight
agility sync --sourceGuid="abc123" --targetGuid="def456" --syncDeletes

# Sync only specified models (models only, no dependencies)
agility sync --sourceGuid="abc123" --targetGuid="def456" --models="BlogPost,BlogCategory"

Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
},
"dependencies": {
"@agility/content-sync": "^1.2.0",
"@agility/management-sdk": "^0.1.38",
"@agility/management-sdk": "^0.2.0-beta.3",
"ansi-colors": "^4.1.3",
"axios": "^0.27.2",
"cli-progress": "^3.11.2",
Expand Down
10 changes: 8 additions & 2 deletions src/core/json-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,22 @@ export interface JsonSummaryPreflightPhase {
phase: string;
create: number;
update: number;
/** --syncDeletes: target entities a real run would delete. */
delete: number;
skip: number;
conflict: number;
/** PROD-2542: would-be writes that fail validation against the target model. */
invalid: number;
}

export interface JsonSummaryPreflight {
totals: { create: number; update: number; skip: number; conflict: number; invalid: number };
totals: { create: number; update: number; delete: number; skip: number; conflict: number; invalid: number };
hasConflicts: boolean;
phases: JsonSummaryPreflightPhase[];
/** Every recorded action, in the order the pushers recorded them. */
entries: Array<{
phase: string;
action: "create" | "update" | "skip" | "conflict" | "invalid";
action: "create" | "update" | "delete" | "skip" | "conflict" | "invalid";
name: string;
locale?: string;
detail?: string;
Expand All @@ -123,6 +125,8 @@ export interface JsonSummary {
options: {
preflight: boolean;
overwrite: boolean;
/** --syncDeletes. Optional so summaries written before the flag existed still parse. */
syncDeletes?: boolean;
autoPublish: string;
elements: string[];
locales: string[];
Expand Down Expand Up @@ -235,6 +239,7 @@ export function emitAbortSummary(
targetGuid?: string;
preflight?: boolean;
overwrite?: boolean;
syncDeletes?: boolean;
autoPublish?: string;
elements?: string;
locale?: string[];
Expand All @@ -253,6 +258,7 @@ export function emitAbortSummary(
options: {
preflight: runState.preflight === true,
overwrite: runState.overwrite === true,
syncDeletes: runState.syncDeletes === true,
autoPublish: runState.autoPublish || "",
elements: (runState.elements || "").split(",").filter(Boolean),
locales: Array.isArray(runState.locale) ? runState.locale : [],
Expand Down
1 change: 1 addition & 0 deletions src/core/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ export class Push {
options: {
preflight: state.preflight === true,
overwrite: state.overwrite === true,
syncDeletes: state.syncDeletes === true,
autoPublish: state.autoPublish || "",
elements: (state.elements || "").split(",").filter(Boolean),
locales: Array.isArray(state.locale) ? state.locale : [],
Expand Down
8 changes: 8 additions & 0 deletions src/core/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface State {

// Operation control
overwrite: boolean;
syncDeletes: boolean; // Delete from the target what was deleted in the source since the last sync
allowMassDeletes: boolean; // Lift the --syncDeletes mass-delete guard
preflight: boolean; // Preflight mode - report planned sync actions without writing to target/mappings
fullPull: boolean; // PROD-2614: discard content sync tokens and do a full content re-pull that removes stale local files
jsonSummary: string; // Path to write the machine-readable run summary to; empty = disabled
Expand Down Expand Up @@ -166,6 +168,8 @@ export const state: State = {

// Operation control
overwrite: false,
syncDeletes: false,
allowMassDeletes: false,
preflight: false,
fullPull: false,
jsonSummary: "",
Expand Down Expand Up @@ -276,6 +280,8 @@ export function setState(argv: any) {

// Operation control
if (argv.overwrite !== undefined) state.overwrite = argv.overwrite;
if (argv.syncDeletes !== undefined) state.syncDeletes = argv.syncDeletes;
if (argv.allowMassDeletes !== undefined) state.allowMassDeletes = argv.allowMassDeletes;

// Operation control (preflight)
if (argv.preflight !== undefined) state.preflight = argv.preflight;
Expand Down Expand Up @@ -473,6 +479,8 @@ export function resetState() {

// Operation control
state.overwrite = false;
state.syncDeletes = false;
state.allowMassDeletes = false;
state.preflight = false;
state.fullPull = false;
state.jsonSummary = "";
Expand Down
18 changes: 18 additions & 0 deletions src/core/system-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,22 @@ export const systemArgs = {
default: false,
},

// Sync deletes. Deliberately not primed from .env: a destructive mode is opted into per run.
syncDeletes: {
describe:
"(sync/push only) After syncing, delete from the target the entities that were deleted in the source since the last sync. Only entities the CLI previously synced (that have a mapping) are considered — nothing created directly on the target is touched. A target entity that was changed since the last sync is a conflict and is kept unless --overwrite is also set; one that something else on the target still uses is kept and reported. Content and page deletes are confirmed against the source first (an unpublished item is not deleted). Default: false.",
type: "boolean" as const,
alias: ["sync-deletes", "syncdeletes", "SyncDeletes", "SYNC_DELETES", "SYNCDELETES"],
default: false,
},
allowMassDeletes: {
describe:
"(with --syncDeletes) Allow a sync to delete more than half of the mapped entities of a kind. Without it such a run refuses to delete anything, since a stale or partial local cache looks the same as a mass delete. Default: false.",
type: "boolean" as const,
alias: ["allow-mass-deletes", "allowmassdeletes", "AllowMassDeletes", "ALLOW_MASS_DELETES"],
default: false,
},

// Auto-publish after sync
autoPublish: {
describe:
Expand Down Expand Up @@ -267,6 +283,8 @@ export interface SystemArgs {
preflight?: boolean; // Preflight mode - report planned sync actions without writing to target/mappings
verbose?: boolean;
overwrite?: boolean;
syncDeletes?: boolean;
allowMassDeletes?: boolean;
elements?: string;
guid?: string;
sourceGuid?: string;
Expand Down
8 changes: 4 additions & 4 deletions src/core/tests/json-summary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,9 @@ describe("buildJsonSummary — shape", () => {
const summary = buildJsonSummary(
makeInput({
preflight: {
totals: { create: 2, update: 1, skip: 0, conflict: 1, invalid: 0 },
totals: { create: 2, update: 1, delete: 0, skip: 0, conflict: 1, invalid: 0 },
hasConflicts: true,
phases: [{ phase: "Models", create: 2, update: 1, skip: 0, conflict: 1, invalid: 0 }],
phases: [{ phase: "Models", create: 2, update: 1, delete: 0, skip: 0, conflict: 1, invalid: 0 }],
entries: [{ phase: "Models", action: "conflict", name: "LinkCard" }],
},
})
Expand Down Expand Up @@ -395,9 +395,9 @@ describe("buildJsonSummary — required-field validation failures (PROD-2542)",

it("carries the preflight invalid count additively", () => {
const preflight = {
totals: { create: 1, update: 0, skip: 0, conflict: 0, invalid: 1 },
totals: { create: 1, update: 0, delete: 0, skip: 0, conflict: 0, invalid: 1 },
hasConflicts: false,
phases: [{ phase: "Content", create: 1, update: 0, skip: 0, conflict: 0, invalid: 1 }],
phases: [{ phase: "Content", create: 1, update: 0, delete: 0, skip: 0, conflict: 0, invalid: 1 }],
entries: [
{ phase: "Content", action: "invalid" as const, name: "home_promobanner", locale: "en-us", detail: "x" },
{ phase: "Content", action: "create" as const, name: "ok", locale: "en-us" },
Expand Down
2 changes: 1 addition & 1 deletion src/core/tests/push.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ describe("Push.pushInstances — --jsonSummary", () => {
const parsed = JSON.parse(fs.readFileSync(out, "utf8"));
expect(parsed.exitCode).toBe(0);
expect(process.exitCode).toBeUndefined();
expect(parsed.preflight.totals).toEqual({ create: 0, update: 0, skip: 0, conflict: 0, invalid: 0 });
expect(parsed.preflight.totals).toEqual({ create: 0, update: 0, delete: 0, skip: 0, conflict: 0, invalid: 0 });
expect(parsed.preflight.hasConflicts).toBe(false);
expect(parsed.warnings.some((w: any) => w.code === "foreignCdnReference")).toBe(true);

Expand Down
Loading
Loading