Skip to content
48 changes: 33 additions & 15 deletions backend/src/services/pullRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,21 @@ function mapDeploymentJob(row: DeploymentJobRow): DeploymentJob {
};
}

interface PublicDeploymentJob extends Omit<DeploymentJob, "status"> {
status: "building" | "verifying" | "isOk" | "failed";
}

/**
* Keeps the persisted cutover protocol compatible with the previous release
* while exposing the work actually in progress to operators.
*/
function publicDeploymentJob(job: DeploymentJob): PublicDeploymentJob {
return {
...job,
status: job.status === "restart-scheduled" ? "verifying" : job.status,
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

/** Reads one deployment job. */
function readDeploymentJob(jobId: string): DeploymentJob | undefined {
const row = database
Expand Down Expand Up @@ -734,7 +749,7 @@ function refreshDeploymentHeartbeat(job: DeploymentJob): DeploymentJob {
}

/** Performs read deployment jobs. */
export function readDeploymentJobs(): DeploymentJob[] {
export function readDeploymentJobs(): PublicDeploymentJob[] {
return (
database
.prepare(
Expand All @@ -755,7 +770,7 @@ export function readDeploymentJobs(): DeploymentJob[] {
`
)
.all(RECENT_DEPLOYMENTS_LIMIT) as unknown as DeploymentJobRow[]
).map((row) => mapDeploymentJob(row));
).map((row) => publicDeploymentJob(mapDeploymentJob(row)));
}

interface DeploymentRuntimeResultRow {
Expand Down Expand Up @@ -1861,7 +1876,10 @@ function shellQuote(value: string): string {
function deploymentJobUpdateCommand(job: DeploymentJob): string {
const script = `
import { Database } from "bun:sqlite";
const job = JSON.parse(process.env.MIRA_DEPLOYMENT_JOB || "{}");
const job = {
...JSON.parse(process.env.MIRA_DEPLOYMENT_JOB || "{}"),
updatedAt: new Date().toISOString(),
};
const database = new Database(process.env.MIRA_DEPLOYMENT_DB);
const sqlNull = JSON.parse("null");
function sqlNullable(value) {
Expand Down Expand Up @@ -2105,11 +2123,11 @@ async function scheduleReleaseCutover(
...job,
status: "isOk",
updatedAt: dateToISOString(new Date()),
note: "Atomic release activated. Web, worker, and commit readiness passed",
note: `Atomic release activated. Web, worker, commit, and ${DEPLOYMENT_WORKER_STABILITY_SECONDS}-second worker stability checks passed`,
};
const okWithRetentionWarningJob: DeploymentJob = {
...okJob,
note: "Atomic release activated and ready; release retention cleanup failed",
note: `Atomic release activated and verified, including ${DEPLOYMENT_WORKER_STABILITY_SECONDS}-second worker stability; release retention cleanup failed`,
};
const rolledBackJob: DeploymentJob = {
...job,
Expand Down Expand Up @@ -2212,7 +2230,7 @@ async function scheduleReleaseRollback(
...job,
status: "isOk",
updatedAt: dateToISOString(new Date()),
note: `Atomic rollback activated ${targetShort}. Web, worker, and commit readiness passed`,
note: `Atomic rollback activated ${targetShort}. Web, worker, commit, and ${DEPLOYMENT_WORKER_STABILITY_SECONDS}-second worker stability checks passed`,
};
const restoredJob: DeploymentJob = {
...job,
Expand Down Expand Up @@ -2322,7 +2340,7 @@ function didScheduleOrphanedReleaseCutoverRecovery(
...job,
status: "isOk",
updatedAt: dateToISOString(new Date()),
note: "Interrupted release cutover recovered; active candidate passed restart and commit-bound readiness",
note: `Interrupted release cutover recovered; active candidate passed restart, commit-bound readiness, and ${DEPLOYMENT_WORKER_STABILITY_SECONDS}-second worker stability`,
};
const script = [
"sleep 1",
Expand Down Expand Up @@ -2527,16 +2545,16 @@ async function runDeploymentJob(
);
persistCutover(releaseCutover);

const restartScheduled: DeploymentJob = {
const cutoverJob: DeploymentJob = {
...currentJob,
status: "restart-scheduled",
updatedAt: dateToISOString(new Date()),
commit: candidate.manifest.commitSha,
commitTitle: candidate.manifest.commitTitle,
note: "Immutable release published. Detached activation and rollback check scheduled",
note: `Release published. Activating it, restarting services, then verifying web, worker, deployed commit, and ${DEPLOYMENT_WORKER_STABILITY_SECONDS} seconds of worker stability; automatic rollback is armed`,
};
writeDeploymentJob(restartScheduled);
await scheduleReleaseCutover(restartScheduled, releaseCutover, signal);
writeDeploymentJob(cutoverJob);
await scheduleReleaseCutover(cutoverJob, releaseCutover, signal);
return releaseCutover;
} catch (error) {
const failed: DeploymentJob = {
Expand Down Expand Up @@ -2590,17 +2608,17 @@ async function runRollbackJob(
}
assertDashboardReleaseHostRuntimeCompatible(state.previous);

const restartScheduled: DeploymentJob = {
const cutoverJob: DeploymentJob = {
...currentJob,
status: "restart-scheduled",
updatedAt: dateToISOString(new Date()),
commit: state.previous.commitSha,
commitTitle: state.previous.manifest.commitTitle,
note: "Verified rollback target. Detached atomic rollback and readiness check scheduled",
note: `Rollback target verified. Activating it, restarting services, then verifying web, worker, deployed commit, and ${DEPLOYMENT_WORKER_STABILITY_SECONDS} seconds of worker stability; automatic restoration is armed`,
};
writeDeploymentJob(restartScheduled);
writeDeploymentJob(cutoverJob);
await scheduleReleaseRollback(
restartScheduled,
cutoverJob,
state.previous.commitSha,
state.current.commitSha,
signal
Expand Down
21 changes: 16 additions & 5 deletions backend/test/serviceBehavior.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1571,7 +1571,7 @@ describe("backend service behavior", () => {
)
.run(
newerId,
"done",
"restart-scheduled",
"2026-06-24T11:00:00.000Z",
"2026-06-24T11:01:00.000Z",
"def456",
Expand All @@ -1591,7 +1591,7 @@ describe("backend service behavior", () => {
);
expect(jobs.find((job) => job.id === newerId)).toMatchObject({
id: newerId,
status: "done",
status: "verifying",
commit: "def456",
commitTitle: "Newer deploy",
note: "newer note",
Expand Down Expand Up @@ -1971,7 +1971,10 @@ printf 'scheduled\n'
.get(rollback.id) as
{ note: string | null; status: string } | undefined;
return (
row?.status === "restart-scheduled" && existsSync(systemdScriptLog)
row?.status === "restart-scheduled" &&
row.note ===
"Rollback target verified. Activating it, restarting services, then verifying web, worker, deployed commit, and 31 seconds of worker stability; automatic restoration is armed" &&
existsSync(systemdScriptLog)
);
}, 5000);
await waitFor(
Expand All @@ -1992,6 +1995,10 @@ printf 'scheduled\n'
expect(guardian).toContain(
"Original release cccccccc was restored automatically"
);
expect(guardian).toContain(
"Atomic rollback activated dddddddd. Web, worker, commit, and 31-second worker stability checks passed"
);
expect(guardian).toContain("updatedAt: new Date().toISOString()");
expect(readFileSync(systemdArgumentsLog, "utf8")).toContain(
`--unit=mira-dashboard-deploy-${rollback.id}\n`
);
Expand Down Expand Up @@ -2531,7 +2538,7 @@ printf 'scheduled\n'
expect(row).toEqual({
commit_sha: candidateCommit,
commit_title: "Deployable dashboard commit",
note: "Immutable release published. Detached activation and rollback check scheduled",
note: "Release published. Activating it, restarting services, then verifying web, worker, deployed commit, and 31 seconds of worker stability; automatic rollback is armed",
status: "restart-scheduled",
});
await expect(Bun.file(gitLog).text()).resolves.toContain(
Expand Down Expand Up @@ -2571,6 +2578,10 @@ printf 'scheduled\n'
expect(restartCommand).toContain("worker_identity");
expect(restartCommand).toContain("ExecMainStartTimestampMonotonic");
expect(restartCommand).toContain("sleep 31");
expect(restartCommand).toContain(
"Atomic release activated. Web, worker, commit, and 31-second worker stability checks passed"
);
expect(restartCommand).toContain("updatedAt: new Date().toISOString()");
expect(restartCommand).toContain(".checks.release.backendCommit");
expect(restartCommand).toContain("releaseLifecycle.js");
expect(restartCommand).toContain(
Expand Down Expand Up @@ -2666,7 +2677,7 @@ printf 'scheduled\n'
'if restart_services && ready_for_commit "${candidate_commit:0:8}"; then'
);
expect(recoveryCommand).toContain(
"Interrupted release cutover recovered; active candidate passed restart and commit-bound readiness"
"Interrupted release cutover recovered; active candidate passed restart, commit-bound readiness, and 31-second worker stability"
);
expect(
recoveryCommand.indexOf(
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useDelivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export interface PullRequestSummary {
/** Represents deployment job. */
export interface DeploymentJob {
id: string;
status: "building" | "restart-scheduled" | "isOk" | "failed";
status: "building" | "verifying" | "isOk" | "failed";
startedAt: string;
updatedAt: string;
commit?: string;
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Delivery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ function normalizedCheckValue(value: unknown): string {
function deploymentVariant(status: DeploymentJob["status"]) {
if (status === "isOk") return "success" as const;
if (status === "failed") return "error" as const;
if (status === "restart-scheduled") return "warning" as const;
if (status === "verifying") return "warning" as const;
return "info" as const;
}

Expand Down
40 changes: 39 additions & 1 deletion src/test/pageBehavior.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1802,7 +1802,7 @@ function apiResponse(url: string, method: string, init?: RequestInit) {
deployment: {
id: "deploy-2",
commit: "def456",
status: "restart-scheduled",
status: "verifying",
updatedAt: "2026-06-24T08:15:00.000Z",
note: "Deploy scheduled",
},
Expand Down Expand Up @@ -3299,6 +3299,44 @@ describe("Mira Dashboard pages", () => {
view.queryClient.clear();
});

it("labels post-restart deployment checks as verifying", async () => {
Object.defineProperty(globalThis, "fetch", {
configurable: true,
value: jest.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
if (url === "/api/pull-requests/deployments") {
return Response.json({
deployments: [
{
id: "deploy-verifying",
commit: "abc123",
commitTitle: "Verify deployment status",
status: "verifying",
updatedAt: "2026-06-24T08:10:00.000Z",
note: "Restarting services and verifying readiness",
},
],
});
}

const method = init?.method ?? "GET";
return apiResponse(url, method, init);
}),
writable: true,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const view = renderPage(createElement(Delivery));

await waitFor(() => {
expect(
screen.getByText("Restarting services and verifying readiness")
).toBeInTheDocument();
expect(screen.getByText("verifying")).toBeInTheDocument();
});
view.unmount();
view.queryClient.clear();
});

it("summarizes pull request checks from the latest record per check", async () => {
Object.defineProperty(globalThis, "fetch", {
configurable: true,
Expand Down