/*
RFC-1297: record the outcome of a site rollback. After a channel
rollback succeeds, lastPropagated[<channel>] must name the release now live
(state "rolled-back", healthy null until re-verified) and the Bordbuch must
carry a deployment-rolled-back event — before this RFC, leitstand.rollback left
both untouched, letting live and recorded state diverge silently.
- Not a contract source of truth — callers depend on the module's exported API, not this file's internals.
- RFC-1297: initial — recordSiteRollback writes lastPropagated + appends deployment-rolled-back; reconciles live releaseId via readLiveChannelStates when the rollback target is unknown.
- RFC-1297
*/
import type { WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
import { readSystemStateSmart, writeSystemStateSmart } from "../sternsystem/registry-io.ts";
import { readLiveChannelStates, type LiveChannelName } from "../sternsystem/live-drift.ts";
import { appendAndCommitBordbuch } from "../bordbuch/bordbuch-commit-helper.ts";
export interface RecordSiteRollbackResult {
/** The release now believed live on the channel (null when undeterminable). */
releaseId: string | null;
/** True when system-state.yaml lastPropagated was updated. */
stateWritten: boolean;
}
/**
* RFC-1297 §4 steps 2–4: after `leitstand.rollback` succeeds on a site channel,
* record reality — write lastPropagated[channel] with state "rolled-back" and
* append a `deployment-rolled-back` Bordbuch event.
*
* The live releaseId is `rollbackTarget.releaseId` when the rollback resolved a
* known Worker version; otherwise the channel's build-identity.json is probed
* after the CDN purge. When neither yields a release, the state write is
* skipped (lastPropagated.releaseId is a required string — recording a null
* release would corrupt the record) and only the Bordbuch event remains.
*/
export async function recordSiteRollback(ctx: {
workspaceRoot: string;
systemId: string;
channel: LiveChannelName;
operationId: string;
rollbackTarget: { versionId?: string; releaseId?: string };
io: WorkspaceIO;
fetchImpl?: typeof fetch;
}): Promise {
const { workspaceRoot, systemId, channel, operationId, rollbackTarget, io } = ctx;
let releaseId = rollbackTarget.releaseId ?? null;
if (releaseId === null) {
try {
const states = await readLiveChannelStates({
workspaceRoot,
systemId,
io,
fetchImpl: ctx.fetchImpl,
});
releaseId = states.find((s) => s.channel === channel)?.liveReleaseId ?? null;
} catch {
// Non-fatal — live probe failed; releaseId stays null.
}
}
let stateWritten = false;
if (releaseId !== null) {
try {
const state = await readSystemStateSmart(workspaceRoot, systemId, io);
state.lastPropagated[channel] = {
releaseId,
at: new Date().toISOString(),
healthy: null,
state: "rolled-back",
operationId,
leaseExpiresAt: null,
...(rollbackTarget.versionId ? { workerVersionId: rollbackTarget.versionId } : {}),
};
await writeSystemStateSmart(workspaceRoot, systemId, state, io);
stateWritten = true;
} catch {
// Non-fatal — Bordbuch event still records the rollback below.
}
}
try {
await appendAndCommitBordbuch(
workspaceRoot,
systemId,
"deployment-rolled-back",
`${channel} rolled back to ${releaseId ?? "unknown"}`,
"leitstand",
{ releaseId, metadata: { channel, operationId } },
);
} catch {
// Non-fatal — the rollback itself already succeeded.
}
return { releaseId, stateWritten };
}