Skip to content
Merged
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
15 changes: 15 additions & 0 deletions src/core/json-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ export interface JsonSummaryDetail {
model?: string;
/** PROD-2542: the required fields it was missing, as "name (Type)". */
fields?: string[];
/**
* PROD-2773 (additive): set on warnings that are not item failures, e.g. "foreignCdnReference"
* — one entry per asset URL into a CDN container owned by neither instance, with the URL, its
* container and every item/field/locale that references it.
*/
code?: string;
url?: string;
container?: string;
references?: Array<{
contentID: number;
referenceName: string;
locale: string;
field: string;
kind: "attachment" | "text";
}>;
}

/**
Expand Down
19 changes: 18 additions & 1 deletion src/core/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { Pull } from "./pull";
import { preflightReport } from "../lib/preflight/preflight-report";
import { buildJsonSummary, writeJsonSummary, JsonSummaryDetail, JsonSummaryPhase } from "./json-summary";
import type { FailureDetail } from "../types/sourceData";
import { foreignCdnReport } from "../lib/content/foreign-cdn-report";
import { resetForeignCdnDetectorCache } from "../lib/pushers/content-pusher/util/report-foreign-cdn-references";

/**
* PROD-2310: Decide whether a set of auto-publish errors should fail the sync exit code.
Expand Down Expand Up @@ -62,6 +64,9 @@ export class Push {

// Preflight (PROD-2203): start each run with a clean report of planned actions
preflightReport.reset();
// PROD-2773: foreign CDN references are reported once per URL per run
foreignCdnReport.reset();
resetForeignCdnDetectorCache();

// Initialize logger for push operation
// Determine if this is a sync operation by checking if both source and target GUIDs exist
Expand Down Expand Up @@ -299,6 +304,14 @@ export class Push {
});
}

// PROD-2773: asset URLs into a CDN container owned by neither instance — passed through
// unchanged, so non-blocking, but an image/file field holding one may arrive empty.
if (foreignCdnReport.hasEntries()) {
const [headline, ...rest] = foreignCdnReport.renderSummaryLines();
console.log(ansiColors.yellow(`\n ${headline}`));
rest.forEach((line) => console.log(ansiColors.yellow(` ${line}`)));
}

// PROD-2311: post-publish mapping/version bookkeeping notices are NOT publish
// failures — show them under a non-blocking header so they don't read as broken.
if (mappingWarnings.length > 0) {
Expand All @@ -320,6 +333,9 @@ export class Push {
// Preflight (PROD-2203): print the report of actions that WOULD have been
// taken, and signal conflicts via a non-zero exit code so automation can gate on it.
if (state.preflight) {
if (foreignCdnReport.hasEntries()) {
preflightReport.recordWarning(`${foreignCdnReport.headline()} — see the list above`);
}
preflightReport.print();
if (preflightReport.hasConflicts()) {
process.exitCode = 1;
Expand All @@ -334,7 +350,8 @@ export class Push {
exitCode: typeof process.exitCode === "number" ? process.exitCode : success ? 0 : 1,
phases: results.flatMap((r) => r.phases ?? []),
failures: syncFailureDetails,
warnings: syncWarningDetails,
// PROD-2773: one additive entry per foreign CDN URL (code "foreignCdnReference")
warnings: [...syncWarningDetails, ...foreignCdnReport.toWarningDetails(sourceGuid)],
operationErrors: syncErrors,
autoPublishErrors,
startedAt,
Expand Down
82 changes: 82 additions & 0 deletions src/core/tests/push.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Push, hasBlockingAutoPublishErrors, formatFailureName } from "../push";
import { resetState, setState } from "../state";
import { Pull } from "../pull";
import { Pushers } from "../../lib/pushers/orchestrate-pushers";
import { foreignCdnReport } from "../../lib/content/foreign-cdn-report";

beforeEach(() => {
resetState();
Expand Down Expand Up @@ -286,6 +289,85 @@ describe("Push.pushInstances — --jsonSummary", () => {
expect(parsed.operationErrors).toEqual([{ type: "fatal", error: "Model validation failed" }]);
});

describe("foreign CDN references (PROD-2773)", () => {
const FOREIGN = "https://cdn-usa2.aglty.io/upstream-qa/hero-background-image.png";

/** Have the mocked orchestrator record what the content pusher would have found. */
async function runWithForeignReference(extraState: Record<string, any>) {
jest.spyOn(Pull.prototype, "pullInstances").mockResolvedValue(undefined as any);
jest.spyOn(Pushers.prototype, "instanceOrchestrator").mockImplementation(async () => {
foreignCdnReport.setOwnContainers(["site-qa"], ["site-cat"]);
foreignCdnReport.record(
{ url: FOREIGN, container: "upstream-qa", field: "mobileBackgroundImage", kind: "attachment" },
{ contentID: 474, referenceName: "home_homebanner_homebanner", locale: "en-us" }
);
return [pushResult({ warningDetails: [{ name: "Home", error: "Dropped Component Hero — unresolved" }] })] as any;
});
setState({ sourceGuid: "src-u", targetGuid: "tgt-u", ...extraState });
return new Push().pushInstances();
}

it("adds one warning per URL to --jsonSummary without affecting success", async () => {
const out = path.join(tmp, "summary.json");
const result = await runWithForeignReference({ jsonSummary: out });

const parsed = JSON.parse(fs.readFileSync(out, "utf8"));
expect(result.success).toBe(true);
expect(parsed.success).toBe(true);
expect(parsed.exitCode).toBe(0);
expect(parsed.schemaVersion).toBe(1);
expect(parsed.failures).toEqual([]);
expect(parsed.warnings).toHaveLength(2);
expect(parsed.warnings[0]).toEqual({ name: "Home", error: "Dropped Component Hero — unresolved" });
expect(parsed.warnings[1]).toMatchObject({
name: FOREIGN,
type: "content",
contentID: 474,
guid: "src-u",
locale: "en-us",
code: "foreignCdnReference",
url: FOREIGN,
container: "upstream-qa",
references: [
{
contentID: 474,
referenceName: "home_homebanner_homebanner",
locale: "en-us",
field: "mobileBackgroundImage",
kind: "attachment",
},
],
});
expect(parsed.warnings[1].error).toContain("belongs to neither the source (site-qa)");

const printed = (console.log as jest.Mock).mock.calls.map((c) => String(c[0])).join("\n");
expect(printed).toContain("Foreign CDN references: 1 URL in 1 item (non-blocking):");
expect(printed).toContain(FOREIGN);
});

it("lists the count in the preflight report without changing its totals or exit code", async () => {
const out = path.join(tmp, "summary.json");
await runWithForeignReference({ jsonSummary: out, preflight: true });

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.hasConflicts).toBe(false);
expect(parsed.warnings.some((w: any) => w.code === "foreignCdnReference")).toBe(true);

const printed = (console.log as jest.Mock).mock.calls.map((c) => String(c[0])).join("\n");
expect(printed).toContain("Warnings (1, non-blocking — not counted above):");
});

it("starts each run with an empty report", async () => {
const out = path.join(tmp, "summary.json");
await runWithForeignReference({});
await runPush([pushResult()], { jsonSummary: out });
expect(JSON.parse(fs.readFileSync(out, "utf8")).warnings).toEqual([]);
});
});

it("does not fail the run when the summary path cannot be written", async () => {
// A reporting artifact must never turn a good sync into a failure.
const blocker = path.join(tmp, "not-a-dir");
Expand Down
Loading
Loading