deepline 0.3.90 → 0.3.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundling-sources/sdk/src/client.ts +101 -6
- package/dist/bundling-sources/sdk/src/monitor-fleet-contract.ts +2 -556
- package/dist/bundling-sources/sdk/src/monitor-fleets.ts +2 -2
- package/dist/bundling-sources/sdk/src/release.ts +7 -12
- package/dist/bundling-sources/sdk/src/types.ts +6 -1
- package/dist/bundling-sources/shared_libs/integration-contracts/monitor-fleet-contract.ts +549 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/errors.ts +96 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/limits.ts +8 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/plan.ts +320 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/verdict.ts +106 -0
- package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +5 -1
- package/dist/bundling-sources/shared_libs/monitors/monitor-fleet-limits.ts +2 -12
- package/dist/bundling-sources/shared_libs/monitors/package.json +107 -5
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +12 -7
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +48 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/db-session-lease-store.ts +578 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +521 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-receipt-gateway.ts +1378 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/runtime-topology.ts +1024 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-gateway-route-probe.ts +193 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machine-lifecycle.ts +104 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +1844 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/process-tree.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +262 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-surface.ts +98 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +303 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-publication-guardian.ts +70 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-recovery.ts +519 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-fly-rollback-alignment.ts +280 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-production-gate.ts +159 -0
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/memory-store.ts +114 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +44 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/child-play-resolver.ts +26 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +3662 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/progress-sidecar.ts +47 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +84 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-gateway-transport.ts +91 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-heartbeat.ts +424 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +321 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-sheet-persistence-retry.ts +110 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +625 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +46 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/webhook-receipt-retry.ts +50 -0
- package/dist/bundling-sources/shared_libs/plays/bundling/repository-adapter.ts +24 -0
- package/dist/bundling-sources/shared_libs/plays/child-run-projection.ts +174 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/api-client.ts +267 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/control-plane-types.ts +73 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/manifest.ts +162 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/project-serializer.ts +87 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/source-file-path.ts +26 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/target-paths.ts +36 -0
- package/dist/bundling-sources/shared_libs/plays/launch/input-contract.ts +400 -0
- package/dist/bundling-sources/shared_libs/plays/launch/prepare.ts +96 -0
- package/dist/bundling-sources/shared_libs/plays/local-file-discovery.ts +385 -0
- package/dist/bundling-sources/shared_libs/plays/play-grep-search.ts +79 -6
- package/dist/bundling-sources/shared_libs/plays/play-reference.ts +117 -0
- package/dist/bundling-sources/shared_libs/plays/run-input-summary.ts +260 -0
- package/dist/bundling-sources/shared_libs/plays/run-summary-types.ts +49 -0
- package/dist/bundling-sources/shared_libs/plays/secrets/errors.ts +19 -0
- package/dist/bundling-sources/shared_libs/plays/sheet-filter.ts +347 -0
- package/dist/bundling-sources/shared_libs/plays/standard-webhooks.ts +117 -0
- package/dist/bundling-sources/shared_libs/plays/status-result.ts +388 -0
- package/dist/bundling-sources/shared_libs/plays/transform-factor-structure.ts +377 -0
- package/dist/bundling-sources/shared_libs/plays/webhook-payload.ts +113 -0
- package/dist/cli/index.js +369 -162
- package/dist/cli/index.mjs +369 -162
- package/dist/{compiler-manifest-BaJfE-5H.d.mts → compiler-manifest-x14KL83E.d.mts} +1 -1
- package/dist/{compiler-manifest-BaJfE-5H.d.ts → compiler-manifest-x14KL83E.d.ts} +1 -1
- package/dist/index.d.mts +45 -8
- package/dist/index.d.ts +45 -8
- package/dist/index.js +61 -14
- package/dist/index.mjs +61 -14
- package/dist/install-integrity.json +54 -2
- package/dist/monitor-fleet-contract.d.mts +3 -10
- package/dist/monitor-fleet-contract.d.ts +3 -10
- package/dist/monitor-fleet-contract.js +2 -0
- package/dist/monitor-fleet-contract.mjs +1 -1
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/release.d.mts +3 -3
- package/dist/release.d.ts +3 -3
- package/dist/release.js +7 -12
- package/dist/release.mjs +7 -12
- package/package.json +2 -2
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one recovery implementation for a failed Runtime Release Bundle.
|
|
3
|
+
*
|
|
4
|
+
* Both the publishing command and the independent scaler call this Module.
|
|
5
|
+
* The publication row is the canonical desired state; a caller may only make
|
|
6
|
+
* one external recovery step while it still holds that row's lease.
|
|
7
|
+
*/
|
|
8
|
+
import { promoteVercelProductionDeployment } from './vercel-production-gate';
|
|
9
|
+
|
|
10
|
+
export type RuntimeTupleMember = Readonly<{
|
|
11
|
+
releaseId: string;
|
|
12
|
+
sha: string;
|
|
13
|
+
deploymentId: string;
|
|
14
|
+
deploymentUrl: string;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
export type RuntimeTuplePanicPublication = Readonly<{
|
|
18
|
+
operationId: string;
|
|
19
|
+
state: 'panic_reverting';
|
|
20
|
+
candidate: RuntimeTupleMember;
|
|
21
|
+
baseline: RuntimeTupleMember;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
export type RuntimeTupleActivePublication = Readonly<{
|
|
25
|
+
operationId: string;
|
|
26
|
+
state: 'publishing' | 'panic_reverting';
|
|
27
|
+
candidate: RuntimeTupleMember;
|
|
28
|
+
baseline: RuntimeTupleMember;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
export type RuntimeTupleRecoveryCommand = (input: {
|
|
32
|
+
executable: string;
|
|
33
|
+
args: readonly string[];
|
|
34
|
+
environment: Record<string, string>;
|
|
35
|
+
description: string;
|
|
36
|
+
}) => Promise<void>;
|
|
37
|
+
|
|
38
|
+
export type RuntimeTuplePanicRecovery = Readonly<{
|
|
39
|
+
reconcile: (input: { owner: string }) => Promise<boolean>;
|
|
40
|
+
activePublication: () => Promise<RuntimeTupleActivePublication | null>;
|
|
41
|
+
}>;
|
|
42
|
+
|
|
43
|
+
export class RuntimeTupleRecoveryLeaseLostError extends Error {
|
|
44
|
+
constructor(operationId: string) {
|
|
45
|
+
super(
|
|
46
|
+
`Runtime tuple publication ${operationId} recovery lease is no longer held; stopping before another external mutation.`,
|
|
47
|
+
);
|
|
48
|
+
this.name = 'RuntimeTupleRecoveryLeaseLostError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const RECOVERY_LEASE_RENEW_INTERVAL_MS = 15_000;
|
|
53
|
+
const DEFAULT_ALIAS_WAIT_MS = 120_000;
|
|
54
|
+
const ALIAS_POLL_INTERVAL_MS = 2_000;
|
|
55
|
+
// This is deliberately an optional capability hint, not a new control
|
|
56
|
+
// operation. Older control applications ignore it and still understand the
|
|
57
|
+
// idempotent `claim_panic` handshake used below.
|
|
58
|
+
const RENEWING_RECOVERY_PROTOCOL = 'renewing-v1';
|
|
59
|
+
|
|
60
|
+
function required(value: string | undefined, label: string): string {
|
|
61
|
+
const normalized = value?.trim();
|
|
62
|
+
if (!normalized) {
|
|
63
|
+
throw new Error(`${label} is required for runtime tuple recovery.`);
|
|
64
|
+
}
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function tupleMember(value: unknown, label: string): RuntimeTupleMember {
|
|
69
|
+
if (!value || typeof value !== 'object') {
|
|
70
|
+
throw new Error(`Runtime tuple publication ${label} is missing.`);
|
|
71
|
+
}
|
|
72
|
+
const source = value as Record<string, unknown>;
|
|
73
|
+
const field = (name: string) =>
|
|
74
|
+
required(
|
|
75
|
+
typeof source[name] === 'string' ? source[name] : undefined,
|
|
76
|
+
`runtime tuple publication ${label}.${name}`,
|
|
77
|
+
);
|
|
78
|
+
return {
|
|
79
|
+
releaseId: field('releaseId'),
|
|
80
|
+
sha: field('sha').toLowerCase(),
|
|
81
|
+
deploymentId: field('deploymentId'),
|
|
82
|
+
deploymentUrl: field('deploymentUrl'),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function panicPublication(value: unknown): RuntimeTuplePanicPublication {
|
|
87
|
+
if (!value || typeof value !== 'object') {
|
|
88
|
+
throw new Error('Runtime tuple panic claim returned no publication.');
|
|
89
|
+
}
|
|
90
|
+
const source = value as Record<string, unknown>;
|
|
91
|
+
const state = source.state;
|
|
92
|
+
if (state !== 'panic_reverting') {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Runtime tuple panic claim returned ${typeof state === 'string' ? state : 'an invalid'} publication state.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
operationId: required(
|
|
99
|
+
typeof source.operationId === 'string' ? source.operationId : undefined,
|
|
100
|
+
'runtime tuple publication operationId',
|
|
101
|
+
),
|
|
102
|
+
state,
|
|
103
|
+
candidate: tupleMember(source.candidate, 'candidate'),
|
|
104
|
+
baseline: tupleMember(source.baseline, 'baseline'),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function activePublication(value: unknown): RuntimeTupleActivePublication {
|
|
109
|
+
if (!value || typeof value !== 'object') {
|
|
110
|
+
throw new Error(
|
|
111
|
+
'Runtime tuple control read returned an invalid publication.',
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const source = value as Record<string, unknown>;
|
|
115
|
+
const state = source.state;
|
|
116
|
+
if (state !== 'publishing' && state !== 'panic_reverting') {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Runtime tuple control read returned ${typeof state === 'string' ? state : 'an invalid'} active publication state.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
operationId: required(
|
|
123
|
+
typeof source.operationId === 'string' ? source.operationId : undefined,
|
|
124
|
+
'runtime tuple publication operationId',
|
|
125
|
+
),
|
|
126
|
+
state,
|
|
127
|
+
candidate: tupleMember(source.candidate, 'candidate'),
|
|
128
|
+
baseline: tupleMember(source.baseline, 'baseline'),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function errorWithResponse(
|
|
133
|
+
description: string,
|
|
134
|
+
response: Response,
|
|
135
|
+
): Promise<never> {
|
|
136
|
+
const body = await response.text();
|
|
137
|
+
throw new Error(
|
|
138
|
+
`${description} failed with HTTP ${response.status}: ${body.slice(0, 500)}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isAlreadyCurrentProductionDeploymentConflict(
|
|
143
|
+
response: Response,
|
|
144
|
+
body: string,
|
|
145
|
+
): boolean {
|
|
146
|
+
if (response.status !== 409) return false;
|
|
147
|
+
try {
|
|
148
|
+
const payload = JSON.parse(body) as {
|
|
149
|
+
error?: { code?: unknown; message?: unknown };
|
|
150
|
+
};
|
|
151
|
+
return (
|
|
152
|
+
payload.error?.code === 'conflict' &&
|
|
153
|
+
typeof payload.error.message === 'string' &&
|
|
154
|
+
payload.error.message.includes(
|
|
155
|
+
'is already the current production deployment.',
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function delay(milliseconds: number): Promise<void> {
|
|
164
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Build the concrete recovery Adapter once. There is deliberately no separate
|
|
169
|
+
* guardian version: callers supply logging and command presentation only.
|
|
170
|
+
*/
|
|
171
|
+
export function createRuntimeTuplePanicRecovery(input: {
|
|
172
|
+
controlUrl: string;
|
|
173
|
+
internalToken: string;
|
|
174
|
+
vercelProtectionBypassToken?: string;
|
|
175
|
+
vercel: Readonly<{
|
|
176
|
+
token: string;
|
|
177
|
+
teamId: string;
|
|
178
|
+
projectId: string;
|
|
179
|
+
productionDomain?: string;
|
|
180
|
+
}>;
|
|
181
|
+
gatewayFlyToken: string;
|
|
182
|
+
command: RuntimeTupleRecoveryCommand;
|
|
183
|
+
fetchImpl?: typeof fetch;
|
|
184
|
+
now?: () => number;
|
|
185
|
+
delay?: (milliseconds: number) => Promise<void>;
|
|
186
|
+
aliasWaitMs?: number;
|
|
187
|
+
leaseRenewIntervalMs?: number;
|
|
188
|
+
log?: (event: string, fields: Record<string, unknown>) => void;
|
|
189
|
+
}): RuntimeTuplePanicRecovery {
|
|
190
|
+
const controlUrl = required(
|
|
191
|
+
input.controlUrl,
|
|
192
|
+
'RUNTIME_TUPLE_CONTROL_URL',
|
|
193
|
+
).replace(/\/$/, '');
|
|
194
|
+
const internalToken = required(
|
|
195
|
+
input.internalToken,
|
|
196
|
+
'DEEPLINE_INTERNAL_TOKEN',
|
|
197
|
+
);
|
|
198
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
199
|
+
const now = input.now ?? Date.now;
|
|
200
|
+
const wait = input.delay ?? delay;
|
|
201
|
+
const aliasWaitMs = input.aliasWaitMs ?? DEFAULT_ALIAS_WAIT_MS;
|
|
202
|
+
const leaseRenewIntervalMs =
|
|
203
|
+
input.leaseRenewIntervalMs ?? RECOVERY_LEASE_RENEW_INTERVAL_MS;
|
|
204
|
+
const log = input.log ?? (() => {});
|
|
205
|
+
const productionDomain =
|
|
206
|
+
input.vercel.productionDomain?.trim() || 'code.deepline.com';
|
|
207
|
+
|
|
208
|
+
const internalHeaders = (): Record<string, string> => ({
|
|
209
|
+
'content-type': 'application/json',
|
|
210
|
+
'x-deepline-internal-token': internalToken,
|
|
211
|
+
...(input.vercelProtectionBypassToken?.trim()
|
|
212
|
+
? {
|
|
213
|
+
'x-vercel-protection-bypass':
|
|
214
|
+
input.vercelProtectionBypassToken.trim(),
|
|
215
|
+
}
|
|
216
|
+
: {}),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const publicationRequest = async <T>(
|
|
220
|
+
body: Record<string, unknown>,
|
|
221
|
+
): Promise<T> => {
|
|
222
|
+
const response = await fetchImpl(
|
|
223
|
+
`${controlUrl}/api/v2/internal/runtime/tuple-publications`,
|
|
224
|
+
{
|
|
225
|
+
method: 'POST',
|
|
226
|
+
headers: internalHeaders(),
|
|
227
|
+
body: JSON.stringify(body),
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
return await errorWithResponse('Runtime tuple control request', response);
|
|
232
|
+
}
|
|
233
|
+
return (await response.json()) as T;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const readActivePublication =
|
|
237
|
+
async (): Promise<RuntimeTupleActivePublication | null> => {
|
|
238
|
+
const response = await fetchImpl(
|
|
239
|
+
`${controlUrl}/api/v2/internal/runtime/tuple-publications`,
|
|
240
|
+
{ headers: internalHeaders() },
|
|
241
|
+
);
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
return await errorWithResponse(
|
|
244
|
+
'Read runtime tuple control state',
|
|
245
|
+
response,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const body = (await response.json()) as { publication?: unknown };
|
|
249
|
+
if (body.publication === null || body.publication === undefined)
|
|
250
|
+
return null;
|
|
251
|
+
return activePublication(body.publication);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const renew = async (
|
|
255
|
+
publication: RuntimeTuplePanicPublication,
|
|
256
|
+
owner: string,
|
|
257
|
+
): Promise<boolean> => {
|
|
258
|
+
// Claiming again with the same owner has always extended the reconciliation
|
|
259
|
+
// lease. Reuse that operation instead of introducing a new required
|
|
260
|
+
// action: a newly deployed guardian must be able to recover through the
|
|
261
|
+
// previously serving control application.
|
|
262
|
+
const body = await publicationRequest<{ publication?: unknown }>({
|
|
263
|
+
action: 'claim_panic',
|
|
264
|
+
operationId: publication.operationId,
|
|
265
|
+
owner,
|
|
266
|
+
recoveryProtocol: RENEWING_RECOVERY_PROTOCOL,
|
|
267
|
+
});
|
|
268
|
+
if (body.publication === null || body.publication === undefined) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
const renewed = panicPublication(body.publication);
|
|
272
|
+
return (
|
|
273
|
+
renewed.operationId === publication.operationId &&
|
|
274
|
+
renewed.candidate.releaseId === publication.candidate.releaseId &&
|
|
275
|
+
renewed.candidate.sha === publication.candidate.sha &&
|
|
276
|
+
renewed.candidate.deploymentId === publication.candidate.deploymentId &&
|
|
277
|
+
renewed.baseline.releaseId === publication.baseline.releaseId &&
|
|
278
|
+
renewed.baseline.sha === publication.baseline.sha &&
|
|
279
|
+
renewed.baseline.deploymentId === publication.baseline.deploymentId
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const restoreAdmissionPointer = async (
|
|
284
|
+
publication: RuntimeTuplePanicPublication,
|
|
285
|
+
): Promise<void> => {
|
|
286
|
+
const response = await fetchImpl(
|
|
287
|
+
`${publication.baseline.deploymentUrl}/api/v2/internal/runtime/releases`,
|
|
288
|
+
{
|
|
289
|
+
method: 'POST',
|
|
290
|
+
headers: internalHeaders(),
|
|
291
|
+
body: JSON.stringify({
|
|
292
|
+
action: 'rollback',
|
|
293
|
+
environment: 'production',
|
|
294
|
+
schedulerBackend: 'absurd',
|
|
295
|
+
releaseId: publication.baseline.releaseId,
|
|
296
|
+
}),
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
return await errorWithResponse(
|
|
301
|
+
'Restore runtime admission pointer',
|
|
302
|
+
response,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const restoreAndVerifyPublicApplication = async (
|
|
308
|
+
publication: RuntimeTuplePanicPublication,
|
|
309
|
+
): Promise<void> => {
|
|
310
|
+
const activeDeploymentId = async (): Promise<string | null> => {
|
|
311
|
+
const alias = await fetchImpl(
|
|
312
|
+
`https://api.vercel.com/v4/aliases/${encodeURIComponent(productionDomain)}?teamId=${encodeURIComponent(input.vercel.teamId)}`,
|
|
313
|
+
{ headers: { authorization: `Bearer ${input.vercel.token}` } },
|
|
314
|
+
);
|
|
315
|
+
if (!alias.ok) {
|
|
316
|
+
await errorWithResponse('Read Vercel Production alias', alias);
|
|
317
|
+
}
|
|
318
|
+
const body = (await alias.json()) as {
|
|
319
|
+
deploymentId?: unknown;
|
|
320
|
+
deployment?: { id?: unknown };
|
|
321
|
+
};
|
|
322
|
+
return typeof body.deploymentId === 'string'
|
|
323
|
+
? body.deploymentId
|
|
324
|
+
: typeof body.deployment?.id === 'string'
|
|
325
|
+
? body.deployment.id
|
|
326
|
+
: null;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const activeBeforeRestore = await activeDeploymentId();
|
|
330
|
+
if (activeBeforeRestore === publication.baseline.deploymentId) return;
|
|
331
|
+
if (activeBeforeRestore !== publication.candidate.deploymentId) {
|
|
332
|
+
// Recovery owns only the failed candidate. A normal Vercel Git deployment
|
|
333
|
+
// may advance the alias while the candidate is canarying; restoring a
|
|
334
|
+
// captured baseline here would roll that newer deployment back.
|
|
335
|
+
log('preserving Vercel Production deployment outside the failed tuple', {
|
|
336
|
+
operationId: publication.operationId,
|
|
337
|
+
activeDeploymentId: activeBeforeRestore,
|
|
338
|
+
candidateDeploymentId: publication.candidate.deploymentId,
|
|
339
|
+
baselineDeploymentId: publication.baseline.deploymentId,
|
|
340
|
+
});
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const response = await promoteVercelProductionDeployment({
|
|
345
|
+
deploymentId: publication.baseline.deploymentId,
|
|
346
|
+
fetchImpl,
|
|
347
|
+
projectId: input.vercel.projectId,
|
|
348
|
+
teamId: input.vercel.teamId,
|
|
349
|
+
token: input.vercel.token,
|
|
350
|
+
});
|
|
351
|
+
if (!response.ok && response.status !== 202) {
|
|
352
|
+
const body = await response.text();
|
|
353
|
+
if (!isAlreadyCurrentProductionDeploymentConflict(response, body)) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`Restore Vercel Production deployment failed with HTTP ${response.status}: ${body.slice(0, 500)}`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const deadline = now() + aliasWaitMs;
|
|
361
|
+
while (now() < deadline) {
|
|
362
|
+
if ((await activeDeploymentId()) === publication.baseline.deploymentId) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
await wait(ALIAS_POLL_INTERVAL_MS);
|
|
366
|
+
}
|
|
367
|
+
throw new Error(
|
|
368
|
+
`Timed out waiting for Vercel Production alias ${productionDomain} to restore deployment ${publication.baseline.deploymentId}.`,
|
|
369
|
+
);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const restoreGatewayTuple = async (
|
|
373
|
+
publication: RuntimeTuplePanicPublication,
|
|
374
|
+
): Promise<void> => {
|
|
375
|
+
await input.command({
|
|
376
|
+
executable: 'bun',
|
|
377
|
+
args: [
|
|
378
|
+
'scripts/runtime-topology.ts',
|
|
379
|
+
'rollback',
|
|
380
|
+
'--environment',
|
|
381
|
+
'production',
|
|
382
|
+
'--sha',
|
|
383
|
+
publication.candidate.sha,
|
|
384
|
+
'--apply',
|
|
385
|
+
],
|
|
386
|
+
environment: { FLY_API_TOKEN: input.gatewayFlyToken },
|
|
387
|
+
description: 'Restore retained runtime gateway tuple',
|
|
388
|
+
});
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* A reverted candidate's gateway Machines are cordoned again by the tuple
|
|
393
|
+
* rollback but were never anyone's "source", so the ordinary generation
|
|
394
|
+
* drain would keep them forever. Remove them through the exact-SHA cleanup
|
|
395
|
+
* the release controller uses for a pre-publication failure.
|
|
396
|
+
*/
|
|
397
|
+
const discardCandidateGateways = async (
|
|
398
|
+
publication: RuntimeTuplePanicPublication,
|
|
399
|
+
): Promise<void> => {
|
|
400
|
+
await input.command({
|
|
401
|
+
executable: 'bun',
|
|
402
|
+
args: [
|
|
403
|
+
'scripts/runtime-topology.ts',
|
|
404
|
+
'cleanup',
|
|
405
|
+
'--environment',
|
|
406
|
+
'production',
|
|
407
|
+
'--sha',
|
|
408
|
+
publication.candidate.sha,
|
|
409
|
+
'--apply',
|
|
410
|
+
],
|
|
411
|
+
environment: { FLY_API_TOKEN: input.gatewayFlyToken },
|
|
412
|
+
description: 'Discard reverted runtime gateway candidates',
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const reconcile = async ({ owner }: { owner: string }): Promise<boolean> => {
|
|
417
|
+
const claimed = await publicationRequest<{ publication?: unknown }>({
|
|
418
|
+
action: 'claim_panic',
|
|
419
|
+
operationId: 'scaler',
|
|
420
|
+
owner,
|
|
421
|
+
recoveryProtocol: RENEWING_RECOVERY_PROTOCOL,
|
|
422
|
+
});
|
|
423
|
+
if (claimed.publication === null || claimed.publication === undefined) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
const publication = panicPublication(claimed.publication);
|
|
427
|
+
log('PANIC: restoring baseline tuple', {
|
|
428
|
+
operationId: publication.operationId,
|
|
429
|
+
candidateReleaseId: publication.candidate.releaseId,
|
|
430
|
+
baselineReleaseId: publication.baseline.releaseId,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
let leaseFailure: unknown = null;
|
|
434
|
+
let closed = false;
|
|
435
|
+
const renewLease = async (): Promise<void> => {
|
|
436
|
+
if (closed || leaseFailure) return;
|
|
437
|
+
if (!(await renew(publication, owner))) {
|
|
438
|
+
leaseFailure = new RuntimeTupleRecoveryLeaseLostError(
|
|
439
|
+
publication.operationId,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
const assertLease = async (): Promise<void> => {
|
|
444
|
+
if (leaseFailure) throw leaseFailure;
|
|
445
|
+
try {
|
|
446
|
+
await renewLease();
|
|
447
|
+
} catch (error) {
|
|
448
|
+
leaseFailure = error;
|
|
449
|
+
}
|
|
450
|
+
if (leaseFailure) throw leaseFailure;
|
|
451
|
+
};
|
|
452
|
+
const heartbeat = setInterval(() => {
|
|
453
|
+
void renewLease().catch((error) => {
|
|
454
|
+
leaseFailure = error;
|
|
455
|
+
});
|
|
456
|
+
}, leaseRenewIntervalMs);
|
|
457
|
+
|
|
458
|
+
const recoverStep = async (
|
|
459
|
+
name: string,
|
|
460
|
+
effect: () => Promise<void>,
|
|
461
|
+
): Promise<void> => {
|
|
462
|
+
await assertLease();
|
|
463
|
+
log('recovery step started', {
|
|
464
|
+
operationId: publication.operationId,
|
|
465
|
+
step: name,
|
|
466
|
+
});
|
|
467
|
+
await effect();
|
|
468
|
+
await assertLease();
|
|
469
|
+
log('recovery step verified', {
|
|
470
|
+
operationId: publication.operationId,
|
|
471
|
+
step: name,
|
|
472
|
+
});
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
await recoverStep(
|
|
477
|
+
'restore_admission',
|
|
478
|
+
async () => await restoreAdmissionPointer(publication),
|
|
479
|
+
);
|
|
480
|
+
await recoverStep(
|
|
481
|
+
'restore_public_application',
|
|
482
|
+
async () => await restoreAndVerifyPublicApplication(publication),
|
|
483
|
+
);
|
|
484
|
+
await recoverStep(
|
|
485
|
+
'restore_gateway_tuple',
|
|
486
|
+
async () => await restoreGatewayTuple(publication),
|
|
487
|
+
);
|
|
488
|
+
await assertLease();
|
|
489
|
+
await publicationRequest({
|
|
490
|
+
action: 'reverted',
|
|
491
|
+
operationId: publication.operationId,
|
|
492
|
+
owner,
|
|
493
|
+
});
|
|
494
|
+
log('baseline tuple restored', { operationId: publication.operationId });
|
|
495
|
+
// The baseline is durably restored above; candidate removal must never
|
|
496
|
+
// hold the publication in panic_reverting and block the next release.
|
|
497
|
+
// A failure here is logged and the scaler's orphan sweep finishes it.
|
|
498
|
+
try {
|
|
499
|
+
await discardCandidateGateways(publication);
|
|
500
|
+
log('candidate gateways discarded', {
|
|
501
|
+
operationId: publication.operationId,
|
|
502
|
+
candidateSha: publication.candidate.sha,
|
|
503
|
+
});
|
|
504
|
+
} catch (error) {
|
|
505
|
+
log('candidate gateway cleanup deferred to the scaler orphan sweep', {
|
|
506
|
+
operationId: publication.operationId,
|
|
507
|
+
candidateSha: publication.candidate.sha,
|
|
508
|
+
error: error instanceof Error ? error.message : String(error),
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
return true;
|
|
512
|
+
} finally {
|
|
513
|
+
closed = true;
|
|
514
|
+
clearInterval(heartbeat);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
return { reconcile, activePublication: readActivePublication };
|
|
519
|
+
}
|