Skip to content
Draft
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
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,48 @@ agility reverse-sync --sourceGuid A --targetGuid B --locales en-us --preflight
agility reverse-sync --sourceGuid A --targetGuid B --locales en-us
```

### Verify

Compare a synced target instance against its source and report every difference, independent of the sync's own logs and change tracking.

```bash
agility verify --sourceGuid <source> --targetGuid <target> [--locales en-us,fr-ca] [--channel website] [--models ...] [--pages ...] [--containers ...] [--jsonSummary <path>]
```

Use the same `--sourceGuid` / `--targetGuid` as the forward `sync`. Verify never writes to either instance or to the mapping files.

#### What it does

1. Waits until neither instance has a content update in progress (preview and published), up to 10 minutes.
2. Runs a full pull of both instances, then checks again. If an update ran during the pull, it pulls once more.
3. Compares the two instances through the mapping files:
- mapping integrity (unmapped source items, mappings to missing targets, two sources mapped to one target)
- models, containers and templates
- content counts per model and container, list order, and field values (linked content, assets and companion fields resolved through the mappings)
- component order within each page zone, and which zone each component is in
- sitemap parent and sibling order per locale and channel

#### Results

- **Failures**: field mismatches, missing items, components in the wrong order or zone, sitemap order errors.
- **Warnings**: content that exists only on the target, publish-state differences. These don't fail the run.
- **Source defects**: differences explained by the source data, such as an item missing a required field or an item in a deleted container. These are listed but don't fail the run.

The console prints a summary table per locale and the first 20 findings per category. The full report is written to `--jsonSummary`, or to `agility-files/logs/verify-{source}-{target}-{timestamp}.json`.

| Exit code | Meaning |
| --------- | ----------------------------------------------------------------------------- |
| `0` | Target matches source |
| `1` | Differences found |
| `2` | Could not verify (auth, status check timeout, pull failure, missing mappings) |

```bash
# Blue/green: sync into the new instance, then prove it before switching traffic
agility sync --sourceGuid A --targetGuid B --locales en-us --preflight
agility sync --sourceGuid A --targetGuid B --locales en-us
agility verify --sourceGuid A --targetGuid B --locales en-us
```

## File Structure

The CLI organizes downloaded content in a structured format:
Expand Down
21 changes: 20 additions & 1 deletion src/core/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { serverUser } from "../types/serverUser";
import { state } from "./state";
import * as mgmtApi from "@agility/management-sdk";
const open = require("open");

Check warning on line 4 in src/core/auth.ts

View workflow job for this annotation

GitHub Actions / ESLint Check

A `require()` style import is forbidden
const FormData = require("form-data");

Check warning on line 5 in src/core/auth.ts

View workflow job for this annotation

GitHub Actions / ESLint Check

A `require()` style import is forbidden
import fs from "fs";
import path from "path";

Expand All @@ -10,7 +10,7 @@
function getKeytar() {
if (_keytar === undefined) {
try {
_keytar = require("keytar");

Check warning on line 13 in src/core/auth.ts

View workflow job for this annotation

GitHub Actions / ESLint Check

A `require()` style import is forbidden
} catch {
_keytar = null;
}
Expand Down Expand Up @@ -225,7 +225,7 @@
return "https://mgmt.aglty.io";
}

getBaseUrl(guid: string, userBaseUrl: string = null): string {

Check warning on line 228 in src/core/auth.ts

View workflow job for this annotation

GitHub Actions / ESLint Check

'userBaseUrl' is assigned a value but never used
let baseUrl = this.determineBaseUrl(guid);
return `${baseUrl}/oauth`;
}
Expand Down Expand Up @@ -993,7 +993,7 @@
* Validate command-specific requirements and set up instance access
* This should be called by each command after auth.init()
*/
async validateCommand(commandType: "pull" | "sync" | "clean" | "interactive" | "push"): Promise<boolean> {
async validateCommand(commandType: "pull" | "sync" | "clean" | "interactive" | "push" | "verify"): Promise<boolean> {
const missingFields: string[] = [];

// Check command-specific requirements
Expand Down Expand Up @@ -1030,6 +1030,20 @@
if (!state.channel) missingFields.push("channel (use --channel or AGILITY_WEBSITE in .env)");
break;

case "verify":
// Verify (PROD-2552) reads both instances and writes to neither
if (!state.sourceGuid)
missingFields.push("sourceGuid (use --sourceGuid or AGILITY_GUID in .env)");
if (!state.targetGuid)
missingFields.push("targetGuid (use --targetGuid or AGILITY_TARGET_GUID in .env)");

const hasVerifyUserLocales = state.locale && state.locale.length > 0;
const hasVerifyAutoDetectedLocales = state.guidLocaleMap && state.guidLocaleMap.size > 0;
if (!hasVerifyUserLocales && !hasVerifyAutoDetectedLocales) {
missingFields.push("locale (use --locales or AGILITY_LOCALES in .env, or locales will be auto-detected)");
}
break;

case "clean":
// Clean needs minimal validation since it prompts for instance selection
break;
Expand Down Expand Up @@ -1078,6 +1092,11 @@
);
return false;
}
} else if (commandType === "verify") {
// Verify only needs read access to both instances; the pull uses the API keys auth.init()
// already resolved for each guid.
await this.validateInstanceAccess(state.sourceGuid, "source");
await this.validateInstanceAccess(state.targetGuid, "target");
} else if (commandType === "pull" && state.sourceGuid) {
// Pull operation - validate source access and get API keys
await this.validateInstanceAccess(state.sourceGuid, "instance");
Expand Down
2 changes: 1 addition & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// Core authentication and state management
export { Auth } from "./auth";
export { state, setState, resetState, primeFromEnv, getState, getUIMode, enableReverseSync } from "./state";
export { systemArgs, workflowArgs, type SystemArgsType } from "./system-args";
export { systemArgs, workflowArgs, verifyArgs, type SystemArgsType } from "./system-args";
export { normalizeProcessArgs, normalizeArgv } from "./arg-normalizer";

// Main operation services
Expand Down
5 changes: 3 additions & 2 deletions src/core/json-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,13 +310,14 @@ export function buildJsonSummary(input: BuildJsonSummaryInput): JsonSummary {
}

/**
* Write the summary to disk, creating parent directories as needed.
* Write the summary to disk, creating parent directories as needed. Also used for the
* `agility verify` report (PROD-2552), which has its own schema but the same write contract.
*
* Never throws. A reporting artifact must not be able to fail a sync that otherwise
* succeeded — an unwritable path is worth a warning, not a non-zero exit. Returns whether
* the write landed so the caller can say so.
*/
export function writeJsonSummary(filePath: string, summary: JsonSummary): { written: boolean; error?: string } {
export function writeJsonSummary(filePath: string, summary: JsonSummary | object): { written: boolean; error?: string } {
try {
const resolved = path.resolve(filePath);
const dir = path.dirname(resolved);
Expand Down
9 changes: 7 additions & 2 deletions src/core/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function safeStringify(obj: any, indent?: number): string {
}
}

export type OperationType = "pull" | "push" | "sync" | "reverse-sync";
export type OperationType = "pull" | "push" | "sync" | "reverse-sync" | "verify";

export type EntityType =
| "model"
Expand Down Expand Up @@ -524,7 +524,12 @@ export class Logs {
}

// Build filename with GUID
if (this.operationType === "push" || this.operationType === "sync" || this.operationType === "reverse-sync") {
if (
this.operationType === "push" ||
this.operationType === "sync" ||
this.operationType === "reverse-sync" ||
this.operationType === "verify"
) {
const sourceGuid = state.sourceGuid || "unknown";
const targetGuid = state.targetGuid || "unknown";
filename = `${sourceGuid}-${targetGuid}-${this.operationType}-${timestamp}.txt`;
Expand Down
50 changes: 50 additions & 0 deletions src/core/system-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,56 @@ export const workflowArgs = {
},
};

/**
* Args accepted by the `verify` command (PROD-2552).
*
* A deliberate subset of systemArgs, reusing their parsing and aliases — NOT a spread of the whole
* set: verify never writes to an instance, so the sync-only options (overwrite, autoPublish,
* preflight, fullPull, elements, modelsWithDeps) would advertise behavior it does not have. Verify
* always does a full pull of both instances.
*/
export const verifyArgs = {
token: systemArgs.token,
dev: systemArgs.dev,
headless: systemArgs.headless,
verbose: systemArgs.verbose,
sourceGuid: {
...systemArgs.sourceGuid,
describe:
"The source instance GUID of the sync being verified. Required; falls back to AGILITY_GUID from .env.",
},
targetGuid: {
...systemArgs.targetGuid,
describe:
"The target instance GUID of the sync being verified. Required; falls back to AGILITY_TARGET_GUID from .env.",
},
locales: systemArgs.locales,
channel: {
...systemArgs.channel,
describe: "Verify only this channel's sitemap. If not provided, every channel is verified.",
default: undefined,
},
models: {
...systemArgs.models,
describe: "Verify only these models (comma-separated reference names), as `sync --models` scopes a sync.",
},
pages: {
...systemArgs.pages,
describe:
"Verify only these pages and what they need (comma-separated paths, names or IDs), as `sync --pages` scopes a sync.",
},
containers: {
...systemArgs.containers,
describe:
"Verify only these containers and what they depend on (comma-separated reference names, titles or IDs), as `sync --containers` scopes a sync.",
},
jsonSummary: {
...systemArgs.jsonSummary,
describe:
"Write the full verify report (every finding, per-locale counts, timings) as JSON to this path. Defaults to agility-files/logs/verify-<source>-<target>-<timestamp>.json.",
},
};

/**
* Type helper for command arguments that include system args
*/
Expand Down
Loading
Loading