mirror of
https://github.com/we-promise/sure.git
synced 2026-06-04 18:29:02 +00:00
ci(preview): stabilize image push and readiness diagnostics (#2084)
* ci(preview): rewrite image config before registry push Point the trusted preview deploy config at the loaded CI image before Wrangler validates the worker config for the Cloudflare registry push. This keeps the existing trusted deploy boundary intact while fixing the post-2062 image-push ordering regression. * ci(preview): require trusted readiness diagnostics * ci(preview): use nonce for diagnostics events * ci(preview): retain diagnostics timing anchors
This commit is contained in:
@@ -24,6 +24,22 @@ interface PreviewProgress {
|
||||
detail: string;
|
||||
}
|
||||
|
||||
interface PreviewTimings {
|
||||
containerStartedAt: string | null;
|
||||
bootStartedAt: string | null;
|
||||
railsStartedAt: string | null;
|
||||
railsReadyAt: string | null;
|
||||
demoDataStartedAt: string | null;
|
||||
demoDataReadyAt: string | null;
|
||||
demoDataReadyStage: string | null;
|
||||
demoDataFailedAt: string | null;
|
||||
previewReadyAt: string | null;
|
||||
secondsToRailsReady: number | null;
|
||||
secondsToDemoDataReady: number | null;
|
||||
secondsFromRailsReadyToDemoDataReady: number | null;
|
||||
secondsToPreviewReady: number | null;
|
||||
}
|
||||
|
||||
interface PreviewStatusPayload {
|
||||
state: unknown;
|
||||
containerRunning: boolean;
|
||||
@@ -31,13 +47,28 @@ interface PreviewStatusPayload {
|
||||
diagnosticsHistory: DiagnosticRecord[];
|
||||
previewReady: boolean;
|
||||
previewFailed: boolean;
|
||||
timings: PreviewTimings;
|
||||
progress: PreviewProgress;
|
||||
}
|
||||
|
||||
const DIAGNOSTICS_KEY = "preview-diagnostics";
|
||||
const DIAGNOSTICS_HISTORY_KEY = "preview-diagnostics-history";
|
||||
const DIAGNOSTICS_HISTORY_LIMIT = 50;
|
||||
const PREVIEW_DIAGNOSTICS_NONCE = "${PREVIEW_DIAGNOSTICS_NONCE}";
|
||||
const READY_STAGES = new Set(["demo-data-ready", "demo-data-skip"]);
|
||||
const FAILED_STAGES = new Set(["demo-data-failed", "failed"]);
|
||||
const TIMING_ANCHOR_STAGES = new Set([
|
||||
"boot",
|
||||
"rails-start",
|
||||
"rails-up-ready",
|
||||
"demo-data-check",
|
||||
"demo-data-deferred",
|
||||
"demo-data-load",
|
||||
"demo-data-ready",
|
||||
"demo-data-skip",
|
||||
"demo-data-failed",
|
||||
"failed",
|
||||
]);
|
||||
const WAITING_MESSAGES: Record<string, string> = {
|
||||
boot: "Waking preview…",
|
||||
"redis-start": "Starting Redis…",
|
||||
@@ -78,6 +109,7 @@ export class RailsContainer extends Container {
|
||||
BINDING: "::",
|
||||
DEMO_DATA_SEED: "${PR_NUMBER}",
|
||||
PREVIEW_ORIGIN: "https://sure-preview-${PR_NUMBER}.sure-finances.workers.dev",
|
||||
PREVIEW_DIAGNOSTICS_NONCE,
|
||||
};
|
||||
sleepAfter = "30m";
|
||||
enableInternet = true;
|
||||
@@ -90,20 +122,37 @@ export class RailsContainer extends Container {
|
||||
const diagnostic = {
|
||||
...payload,
|
||||
state: await this.getState(),
|
||||
};
|
||||
} as DiagnosticRecord;
|
||||
|
||||
await this.ctx.storage.put(DIAGNOSTICS_KEY, diagnostic);
|
||||
|
||||
const history =
|
||||
((await this.ctx.storage.get(DIAGNOSTICS_HISTORY_KEY)) as Record<string, unknown>[] | undefined) ?? [];
|
||||
((await this.ctx.storage.get(DIAGNOSTICS_HISTORY_KEY)) as DiagnosticRecord[] | undefined) ?? [];
|
||||
|
||||
history.push(diagnostic);
|
||||
await this.ctx.storage.put(DIAGNOSTICS_HISTORY_KEY, this.trimDiagnosticsHistory(history));
|
||||
}
|
||||
|
||||
if (history.length > 20) {
|
||||
history.splice(0, history.length - 20);
|
||||
}
|
||||
private isTimingAnchor(record: DiagnosticRecord): boolean {
|
||||
return (
|
||||
record.event === "start" ||
|
||||
(record.event === "entrypoint" &&
|
||||
typeof record.payload?.stage === "string" &&
|
||||
TIMING_ANCHOR_STAGES.has(record.payload.stage))
|
||||
);
|
||||
}
|
||||
|
||||
await this.ctx.storage.put(DIAGNOSTICS_HISTORY_KEY, history);
|
||||
private trimDiagnosticsHistory(history: DiagnosticRecord[]): DiagnosticRecord[] {
|
||||
if (history.length <= DIAGNOSTICS_HISTORY_LIMIT) return history;
|
||||
|
||||
const anchors = history.filter((record) => this.isTimingAnchor(record)).slice(-DIAGNOSTICS_HISTORY_LIMIT);
|
||||
const anchored = new Set(anchors);
|
||||
const remainingSlots = Math.max(DIAGNOSTICS_HISTORY_LIMIT - anchors.length, 0);
|
||||
const recentNonAnchors =
|
||||
remainingSlots > 0 ? history.filter((record) => !anchored.has(record)).slice(-remainingSlots) : [];
|
||||
const kept = new Set([...anchors, ...recentNonAnchors]);
|
||||
|
||||
return history.filter((record) => kept.has(record));
|
||||
}
|
||||
|
||||
private async getDiagnostics(): Promise<{
|
||||
@@ -130,6 +179,60 @@ export class RailsContainer extends Container {
|
||||
}
|
||||
}
|
||||
|
||||
private validTimestamp(value: string | undefined): string | null {
|
||||
if (!value) return null;
|
||||
|
||||
const timestamp = Date.parse(value);
|
||||
return Number.isNaN(timestamp) ? null : value;
|
||||
}
|
||||
|
||||
private secondsBetween(startAt: string | null, endAt: string | null): number | null {
|
||||
if (!startAt || !endAt) return null;
|
||||
|
||||
const start = Date.parse(startAt);
|
||||
const end = Date.parse(endAt);
|
||||
if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;
|
||||
|
||||
return Math.round(((end - start) / 1000) * 100) / 100;
|
||||
}
|
||||
|
||||
private buildPreviewTimings(allDiagnostics: DiagnosticRecord[], previewReady: boolean): PreviewTimings {
|
||||
const entrypointDiagnostics = allDiagnostics.filter(
|
||||
(item) => item.event === "entrypoint" && typeof item.payload?.stage === "string"
|
||||
);
|
||||
const firstEventAt = (event: string) => this.validTimestamp(allDiagnostics.find((item) => item.event === event)?.at);
|
||||
const firstStageAt = (...stages: string[]) =>
|
||||
this.validTimestamp(entrypointDiagnostics.find((item) => stages.includes(item.payload?.stage ?? ""))?.at);
|
||||
|
||||
const containerStartedAt = firstEventAt("start");
|
||||
const bootStartedAt = firstStageAt("boot") ?? containerStartedAt;
|
||||
const railsStartedAt = firstStageAt("rails-start");
|
||||
const railsReadyAt = firstStageAt("rails-up-ready");
|
||||
const demoDataStartedAt =
|
||||
firstStageAt("demo-data-load") ?? firstStageAt("demo-data-deferred") ?? firstStageAt("demo-data-check");
|
||||
const demoDataReady = entrypointDiagnostics.find((item) => READY_STAGES.has(item.payload?.stage ?? ""));
|
||||
const demoDataReadyAt = this.validTimestamp(demoDataReady?.at);
|
||||
const demoDataReadyStage = demoDataReady?.payload?.stage ?? null;
|
||||
const demoDataFailedAt = firstStageAt("demo-data-failed");
|
||||
const previewReadyAt = previewReady ? (demoDataReadyAt ?? railsReadyAt) : null;
|
||||
|
||||
return {
|
||||
containerStartedAt,
|
||||
bootStartedAt,
|
||||
railsStartedAt,
|
||||
railsReadyAt,
|
||||
demoDataStartedAt,
|
||||
demoDataReadyAt,
|
||||
demoDataReadyStage,
|
||||
demoDataFailedAt,
|
||||
previewReadyAt,
|
||||
secondsToRailsReady: this.secondsBetween(bootStartedAt, railsReadyAt),
|
||||
secondsToDemoDataReady: this.secondsBetween(demoDataStartedAt, demoDataReadyAt),
|
||||
secondsFromRailsReadyToDemoDataReady: this.secondsBetween(railsReadyAt, demoDataReadyAt),
|
||||
secondsToPreviewReady: this.secondsBetween(bootStartedAt, previewReadyAt),
|
||||
};
|
||||
}
|
||||
|
||||
private async buildPreviewStatus(base: {
|
||||
state: unknown;
|
||||
containerRunning: boolean;
|
||||
@@ -151,10 +254,11 @@ export class RailsContainer extends Container {
|
||||
? (base.state as { status?: string }).status === "healthy"
|
||||
: false) ||
|
||||
entrypointDiagnostics.some((item) => item.payload?.stage === "rails-up-ready");
|
||||
const previewReady = liveProbeReady || (sampleDataReady && railsResponding);
|
||||
const previewReady = sampleDataReady && railsResponding;
|
||||
const previewFailed =
|
||||
entrypointDiagnostics.some((item) => FAILED_STAGES.has(item.payload?.stage ?? "")) ||
|
||||
base.diagnostics?.event === "error";
|
||||
const timings = this.buildPreviewTimings(allDiagnostics, previewReady);
|
||||
|
||||
let phase: PreviewProgress["phase"] = "cold";
|
||||
if (previewFailed) {
|
||||
@@ -188,6 +292,7 @@ export class RailsContainer extends Container {
|
||||
...base,
|
||||
previewReady,
|
||||
previewFailed,
|
||||
timings,
|
||||
progress: {
|
||||
phase,
|
||||
stage: latestStage,
|
||||
@@ -294,6 +399,10 @@ export class RailsContainer extends Container {
|
||||
}
|
||||
|
||||
if (url.pathname === "/_container_event" && request.method === "POST") {
|
||||
if (request.headers.get("x-preview-diagnostics-nonce") !== PREVIEW_DIAGNOSTICS_NONCE) {
|
||||
return new Response("not found", { status: 404 });
|
||||
}
|
||||
|
||||
const payload = await request.json();
|
||||
await this.recordDiagnostic({
|
||||
event: "entrypoint",
|
||||
|
||||
Reference in New Issue
Block a user