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
package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-fly-rollback-alignment.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const ROLLBACK_EVENT_TYPES = new Set([
|
|
4
|
+
'deployment.rollback',
|
|
5
|
+
'deployment-rollback',
|
|
6
|
+
]);
|
|
7
|
+
const TRACKED_EVENT_TYPES = new Set([
|
|
8
|
+
'deployment.succeeded',
|
|
9
|
+
'deployment.promoted',
|
|
10
|
+
...ROLLBACK_EVENT_TYPES,
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export type AlignmentStatus =
|
|
14
|
+
| 'ignored'
|
|
15
|
+
| 'tracked'
|
|
16
|
+
| 'rollback_tracked'
|
|
17
|
+
| 'rollback_needs_manual_mapping';
|
|
18
|
+
|
|
19
|
+
export interface VercelWebhookEvent {
|
|
20
|
+
type?: string;
|
|
21
|
+
id?: string;
|
|
22
|
+
createdAt?: number;
|
|
23
|
+
payload?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface VercelDeploymentSummary {
|
|
27
|
+
id: string;
|
|
28
|
+
url?: string;
|
|
29
|
+
meta?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface VercelRollbackNotice {
|
|
33
|
+
status: AlignmentStatus;
|
|
34
|
+
eventType: string;
|
|
35
|
+
deliveryId: string | null;
|
|
36
|
+
vercelProject: string | null;
|
|
37
|
+
fromDeploymentId: string | null;
|
|
38
|
+
toDeploymentId: string | null;
|
|
39
|
+
toDeploymentUrl: string | null;
|
|
40
|
+
gitSha: string | null;
|
|
41
|
+
workerBuildId: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface VercelDeploymentEventSummary {
|
|
45
|
+
status: 'ignored' | 'tracked';
|
|
46
|
+
eventType: string;
|
|
47
|
+
deliveryId: string | null;
|
|
48
|
+
vercelProject: string | null;
|
|
49
|
+
deploymentId: string | null;
|
|
50
|
+
deploymentUrl: string | null;
|
|
51
|
+
target: string | null;
|
|
52
|
+
gitSha: string | null;
|
|
53
|
+
workerBuildId: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readRecord(value: unknown): Record<string, unknown> | null {
|
|
57
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
58
|
+
return value as Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readString(value: unknown): string | null {
|
|
62
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readNestedString(
|
|
66
|
+
record: Record<string, unknown> | null,
|
|
67
|
+
path: string[],
|
|
68
|
+
): string | null {
|
|
69
|
+
let current: unknown = record;
|
|
70
|
+
for (const key of path) {
|
|
71
|
+
const next = readRecord(current);
|
|
72
|
+
if (!next) return null;
|
|
73
|
+
current = next[key];
|
|
74
|
+
}
|
|
75
|
+
return readString(current);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeGitSha(value: string | null): string | null {
|
|
79
|
+
if (!value) return null;
|
|
80
|
+
const trimmed = value.trim();
|
|
81
|
+
return /^[a-f0-9]{7,40}$/i.test(trimmed) ? trimmed : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function verifyVercelSignature(input: {
|
|
85
|
+
rawBody: string;
|
|
86
|
+
signature: string | null;
|
|
87
|
+
secret: string;
|
|
88
|
+
}): boolean {
|
|
89
|
+
const signature = input.signature?.trim();
|
|
90
|
+
if (!signature) return false;
|
|
91
|
+
const expected = createHmac('sha1', input.secret)
|
|
92
|
+
.update(input.rawBody)
|
|
93
|
+
.digest('hex');
|
|
94
|
+
const expectedBuffer = Buffer.from(expected, 'hex');
|
|
95
|
+
const actualBuffer = Buffer.from(signature, 'hex');
|
|
96
|
+
if (expectedBuffer.byteLength !== actualBuffer.byteLength) return false;
|
|
97
|
+
return timingSafeEqual(expectedBuffer, actualBuffer);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function extractVercelDeploymentEvent(event: VercelWebhookEvent): {
|
|
101
|
+
eventType: string;
|
|
102
|
+
deliveryId: string | null;
|
|
103
|
+
deploymentId: string | null;
|
|
104
|
+
fromDeploymentId: string | null;
|
|
105
|
+
toDeploymentId: string | null;
|
|
106
|
+
vercelProject: string | null;
|
|
107
|
+
deploymentUrl: string | null;
|
|
108
|
+
target: string | null;
|
|
109
|
+
} {
|
|
110
|
+
const payload = readRecord(event.payload);
|
|
111
|
+
const eventType = readString(event.type) ?? 'unknown';
|
|
112
|
+
const deployment = readRecord(payload?.deployment);
|
|
113
|
+
const previousDeployment = readRecord(payload?.previousDeployment);
|
|
114
|
+
const targetDeployment = readRecord(payload?.targetDeployment);
|
|
115
|
+
const rollback = readRecord(payload?.rollback);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
eventType,
|
|
119
|
+
deliveryId: readString(event.id),
|
|
120
|
+
deploymentId:
|
|
121
|
+
readNestedString(deployment, ['id']) ?? readString(payload?.deploymentId),
|
|
122
|
+
fromDeploymentId:
|
|
123
|
+
readString(payload?.fromDeploymentId) ??
|
|
124
|
+
readNestedString(rollback, ['fromDeploymentId']) ??
|
|
125
|
+
readNestedString(previousDeployment, ['id']) ??
|
|
126
|
+
readNestedString(deployment, ['id']),
|
|
127
|
+
toDeploymentId:
|
|
128
|
+
readString(payload?.toDeploymentId) ??
|
|
129
|
+
readString(payload?.targetDeploymentId) ??
|
|
130
|
+
readNestedString(rollback, ['toDeploymentId']) ??
|
|
131
|
+
readNestedString(targetDeployment, ['id']),
|
|
132
|
+
vercelProject:
|
|
133
|
+
readNestedString(payload, ['project', 'name']) ??
|
|
134
|
+
readString(payload?.projectName) ??
|
|
135
|
+
readString(payload?.projectId) ??
|
|
136
|
+
readNestedString(deployment, ['name']),
|
|
137
|
+
deploymentUrl:
|
|
138
|
+
readNestedString(deployment, ['url']) ??
|
|
139
|
+
readNestedString(payload, ['links', 'deployment']),
|
|
140
|
+
target: readString(payload?.target),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const extractVercelRollbackIds = extractVercelDeploymentEvent;
|
|
145
|
+
|
|
146
|
+
export function extractGitShaFromVercelDeployment(
|
|
147
|
+
deployment: VercelDeploymentSummary | null,
|
|
148
|
+
): string | null {
|
|
149
|
+
const meta = deployment?.meta ?? {};
|
|
150
|
+
return normalizeGitSha(
|
|
151
|
+
readString(meta.githubCommitSha) ??
|
|
152
|
+
readString(meta.githubCommitSHA) ??
|
|
153
|
+
readString(meta.gitCommitSha) ??
|
|
154
|
+
readString(meta.commitSha) ??
|
|
155
|
+
readString(meta.VERCEL_GIT_COMMIT_SHA),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function extractDeploymentSummaryFromVercelEvent(
|
|
160
|
+
event: VercelWebhookEvent,
|
|
161
|
+
): VercelDeploymentSummary | null {
|
|
162
|
+
const payload = readRecord(event.payload);
|
|
163
|
+
const ids = extractVercelDeploymentEvent(event);
|
|
164
|
+
const deployment =
|
|
165
|
+
readRecord(payload?.targetDeployment) ?? readRecord(payload?.deployment);
|
|
166
|
+
if (!deployment) return null;
|
|
167
|
+
|
|
168
|
+
const id =
|
|
169
|
+
readString(deployment.id) ?? ids.toDeploymentId ?? ids.deploymentId;
|
|
170
|
+
if (!id) return null;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
id,
|
|
174
|
+
url: readString(deployment.url) ?? ids.deploymentUrl ?? undefined,
|
|
175
|
+
meta: readRecord(deployment.meta) ?? undefined,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function buildVercelRollbackNotice(input: {
|
|
180
|
+
event: VercelWebhookEvent;
|
|
181
|
+
targetDeployment: VercelDeploymentSummary | null;
|
|
182
|
+
}): VercelRollbackNotice {
|
|
183
|
+
const ids = extractVercelDeploymentEvent(input.event);
|
|
184
|
+
if (!ROLLBACK_EVENT_TYPES.has(ids.eventType)) {
|
|
185
|
+
return {
|
|
186
|
+
status: 'ignored',
|
|
187
|
+
eventType: ids.eventType,
|
|
188
|
+
deliveryId: ids.deliveryId,
|
|
189
|
+
vercelProject: ids.vercelProject,
|
|
190
|
+
fromDeploymentId: ids.fromDeploymentId,
|
|
191
|
+
toDeploymentId: ids.toDeploymentId,
|
|
192
|
+
toDeploymentUrl: input.targetDeployment?.url ?? null,
|
|
193
|
+
gitSha: null,
|
|
194
|
+
workerBuildId: null,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const gitSha = extractGitShaFromVercelDeployment(input.targetDeployment);
|
|
199
|
+
if (!gitSha) {
|
|
200
|
+
return {
|
|
201
|
+
status: 'rollback_needs_manual_mapping',
|
|
202
|
+
eventType: ids.eventType,
|
|
203
|
+
deliveryId: ids.deliveryId,
|
|
204
|
+
vercelProject: ids.vercelProject,
|
|
205
|
+
fromDeploymentId: ids.fromDeploymentId,
|
|
206
|
+
toDeploymentId: ids.toDeploymentId,
|
|
207
|
+
toDeploymentUrl: input.targetDeployment?.url ?? null,
|
|
208
|
+
gitSha: null,
|
|
209
|
+
workerBuildId: null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const workerBuildId = `git-${gitSha}`;
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
status: 'rollback_tracked',
|
|
217
|
+
eventType: ids.eventType,
|
|
218
|
+
deliveryId: ids.deliveryId,
|
|
219
|
+
vercelProject: ids.vercelProject,
|
|
220
|
+
fromDeploymentId: ids.fromDeploymentId,
|
|
221
|
+
toDeploymentId: ids.toDeploymentId,
|
|
222
|
+
toDeploymentUrl: input.targetDeployment?.url ?? null,
|
|
223
|
+
gitSha,
|
|
224
|
+
workerBuildId,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function buildVercelDeploymentEventSummary(input: {
|
|
229
|
+
event: VercelWebhookEvent;
|
|
230
|
+
deployment: VercelDeploymentSummary | null;
|
|
231
|
+
}): VercelDeploymentEventSummary {
|
|
232
|
+
const ids = extractVercelDeploymentEvent(input.event);
|
|
233
|
+
const gitSha = extractGitShaFromVercelDeployment(input.deployment);
|
|
234
|
+
return {
|
|
235
|
+
status: TRACKED_EVENT_TYPES.has(ids.eventType) ? 'tracked' : 'ignored',
|
|
236
|
+
eventType: ids.eventType,
|
|
237
|
+
deliveryId: ids.deliveryId,
|
|
238
|
+
vercelProject: ids.vercelProject,
|
|
239
|
+
deploymentId: ids.deploymentId,
|
|
240
|
+
deploymentUrl: input.deployment?.url ?? ids.deploymentUrl,
|
|
241
|
+
target: ids.target,
|
|
242
|
+
gitSha,
|
|
243
|
+
workerBuildId: gitSha ? `git-${gitSha}` : null,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function buildVercelRollbackSlackText(
|
|
248
|
+
plan: VercelRollbackNotice,
|
|
249
|
+
): string {
|
|
250
|
+
const lines = [
|
|
251
|
+
plan.status === 'rollback_tracked'
|
|
252
|
+
? ':warning: Vercel rollback detected; runtime rollback is manual.'
|
|
253
|
+
: ':warning: Vercel rollback detected; runtime build mapping needs manual lookup.',
|
|
254
|
+
plan.vercelProject ? `Project: ${plan.vercelProject}` : null,
|
|
255
|
+
plan.fromDeploymentId ? `From: ${plan.fromDeploymentId}` : null,
|
|
256
|
+
plan.toDeploymentId ? `To: ${plan.toDeploymentId}` : null,
|
|
257
|
+
plan.workerBuildId ? `Worker build: ${plan.workerBuildId}` : null,
|
|
258
|
+
];
|
|
259
|
+
return lines.filter(Boolean).join('\n');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function buildVercelDeploymentSlackText(
|
|
263
|
+
summary: VercelDeploymentEventSummary,
|
|
264
|
+
): string {
|
|
265
|
+
const title =
|
|
266
|
+
summary.eventType === 'deployment.succeeded'
|
|
267
|
+
? ':white_check_mark: Vercel deployment succeeded.'
|
|
268
|
+
: ':large_green_circle: Vercel deployment promoted.';
|
|
269
|
+
const lines = [
|
|
270
|
+
title,
|
|
271
|
+
summary.vercelProject ? `Project: ${summary.vercelProject}` : null,
|
|
272
|
+
summary.deploymentId ? `Deployment: ${summary.deploymentId}` : null,
|
|
273
|
+
summary.deploymentUrl ? `URL: ${summary.deploymentUrl}` : null,
|
|
274
|
+
summary.target ? `Target: ${summary.target}` : null,
|
|
275
|
+
summary.workerBuildId
|
|
276
|
+
? `Aligned worker build: ${summary.workerBuildId}`
|
|
277
|
+
: null,
|
|
278
|
+
];
|
|
279
|
+
return lines.filter(Boolean).join('\n');
|
|
280
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export type VercelDeployment = {
|
|
2
|
+
id?: string;
|
|
3
|
+
/** Vercel's deployment-list endpoint names this same identifier `uid`. */
|
|
4
|
+
uid?: string;
|
|
5
|
+
url?: string;
|
|
6
|
+
gitSource?: { sha?: string };
|
|
7
|
+
meta?: { githubCommitSha?: string };
|
|
8
|
+
readyState?: string;
|
|
9
|
+
target?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type VercelGateDecision =
|
|
13
|
+
| { kind: 'ready' }
|
|
14
|
+
| { kind: 'superseded'; activeSha: string }
|
|
15
|
+
| { kind: 'assert-mismatch'; activeSha?: string }
|
|
16
|
+
| { kind: 'wait' };
|
|
17
|
+
|
|
18
|
+
export type VercelStagedProductionDecision =
|
|
19
|
+
| { kind: 'ready'; deployment: VercelDeployment }
|
|
20
|
+
| { kind: 'auto-assigned' }
|
|
21
|
+
| { kind: 'vercel-failed'; description?: string; targetUrl?: string }
|
|
22
|
+
| { kind: 'wait' };
|
|
23
|
+
|
|
24
|
+
export type VercelStagedProductionPolicyDecision =
|
|
25
|
+
| { kind: 'ready' }
|
|
26
|
+
| { kind: 'auto-assign-enabled' };
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The release controller only owns the Vercel alias while Vercel is configured
|
|
30
|
+
* to leave Production builds staged. Treat an absent or legacy project setting
|
|
31
|
+
* as unsafe rather than hoping the candidate gate catches an alias move later.
|
|
32
|
+
*/
|
|
33
|
+
export function decideVercelStagedProductionPolicy(input: {
|
|
34
|
+
autoAssignCustomDomains?: boolean;
|
|
35
|
+
}): VercelStagedProductionPolicyDecision {
|
|
36
|
+
return input.autoAssignCustomDomains === false
|
|
37
|
+
? { kind: 'ready' }
|
|
38
|
+
: { kind: 'auto-assign-enabled' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function deploymentSha(deployment: VercelDeployment): string | undefined {
|
|
42
|
+
return deployment.meta?.githubCommitSha ?? deployment.gitSource?.sha;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function deploymentId(deployment: VercelDeployment): string | undefined {
|
|
46
|
+
return deployment.id ?? deployment.uid;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Git creates the production build; the release controller only promotes the
|
|
51
|
+
* already-staged deployment after its runtime proof has passed. Keep that one
|
|
52
|
+
* provider mutation shared by normal release and panic recovery.
|
|
53
|
+
*/
|
|
54
|
+
export async function promoteVercelProductionDeployment(input: {
|
|
55
|
+
deploymentId: string;
|
|
56
|
+
fetchImpl?: typeof fetch;
|
|
57
|
+
projectId: string;
|
|
58
|
+
teamId: string;
|
|
59
|
+
token: string;
|
|
60
|
+
}): Promise<Response> {
|
|
61
|
+
return await (input.fetchImpl ?? fetch)(
|
|
62
|
+
`https://api.vercel.com/v10/projects/${encodeURIComponent(input.projectId)}/promote/${encodeURIComponent(input.deploymentId)}?teamId=${encodeURIComponent(input.teamId)}`,
|
|
63
|
+
{
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
authorization: `Bearer ${input.token}`,
|
|
67
|
+
'content-type': 'application/json',
|
|
68
|
+
},
|
|
69
|
+
body: '{}',
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A staged production deployment is the exact production build we will
|
|
76
|
+
* promote later, but it must not yet own the public production alias. This is
|
|
77
|
+
* deliberately separate from decideVercelProductionGate: accepting an active
|
|
78
|
+
* alias here would make the release controller silently lose traffic control.
|
|
79
|
+
*/
|
|
80
|
+
export function decideVercelStagedProductionGate(input: {
|
|
81
|
+
expectedSha: string;
|
|
82
|
+
activeDeployment: VercelDeployment;
|
|
83
|
+
deployments: VercelDeployment[];
|
|
84
|
+
vercelStatus?: {
|
|
85
|
+
state?: string;
|
|
86
|
+
description?: string;
|
|
87
|
+
target_url?: string;
|
|
88
|
+
};
|
|
89
|
+
}): VercelStagedProductionDecision {
|
|
90
|
+
if (
|
|
91
|
+
input.vercelStatus?.state === 'failure' ||
|
|
92
|
+
input.vercelStatus?.state === 'error'
|
|
93
|
+
) {
|
|
94
|
+
return {
|
|
95
|
+
kind: 'vercel-failed',
|
|
96
|
+
description: input.vercelStatus.description,
|
|
97
|
+
targetUrl: input.vercelStatus.target_url,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const activeServesExpectedSha =
|
|
101
|
+
deploymentSha(input.activeDeployment) === input.expectedSha;
|
|
102
|
+
if (activeServesExpectedSha) {
|
|
103
|
+
return { kind: 'auto-assigned' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const candidates = input.deployments.filter(
|
|
107
|
+
(deployment) =>
|
|
108
|
+
deploymentSha(deployment) === input.expectedSha &&
|
|
109
|
+
deployment.target === 'production' &&
|
|
110
|
+
deploymentId(deployment) !== deploymentId(input.activeDeployment),
|
|
111
|
+
);
|
|
112
|
+
const staged = candidates.find(
|
|
113
|
+
(deployment) =>
|
|
114
|
+
deployment.readyState === 'READY' &&
|
|
115
|
+
Boolean(deploymentId(deployment)) &&
|
|
116
|
+
Boolean(deployment.url),
|
|
117
|
+
);
|
|
118
|
+
if (staged) return { kind: 'ready', deployment: staged };
|
|
119
|
+
|
|
120
|
+
if (candidates.some((deployment) => deployment.readyState === 'ERROR')) {
|
|
121
|
+
return {
|
|
122
|
+
kind: 'vercel-failed',
|
|
123
|
+
description: input.vercelStatus?.description,
|
|
124
|
+
targetUrl: input.vercelStatus?.target_url,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return { kind: 'wait' };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Decide whether a shared-plane mutation may proceed from the deployment that
|
|
132
|
+
* is actually behind the production alias. Kept pure so the race cases are
|
|
133
|
+
* tested without making Vercel or GitHub API calls.
|
|
134
|
+
*/
|
|
135
|
+
export function decideVercelProductionGate(input: {
|
|
136
|
+
expectedSha: string;
|
|
137
|
+
activeDeployment: VercelDeployment;
|
|
138
|
+
activeRevisionIsNewer: boolean;
|
|
139
|
+
mode: 'wait' | 'assert';
|
|
140
|
+
}): VercelGateDecision {
|
|
141
|
+
const activeSha =
|
|
142
|
+
input.activeDeployment.meta?.githubCommitSha ??
|
|
143
|
+
input.activeDeployment.gitSource?.sha;
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
activeSha === input.expectedSha &&
|
|
147
|
+
input.activeDeployment.readyState === 'READY' &&
|
|
148
|
+
input.activeDeployment.target === 'production'
|
|
149
|
+
) {
|
|
150
|
+
return { kind: 'ready' };
|
|
151
|
+
}
|
|
152
|
+
if (input.activeRevisionIsNewer && activeSha) {
|
|
153
|
+
return { kind: 'superseded', activeSha };
|
|
154
|
+
}
|
|
155
|
+
if (input.mode === 'assert') {
|
|
156
|
+
return { kind: 'assert-mismatch', activeSha };
|
|
157
|
+
}
|
|
158
|
+
return { kind: 'wait' };
|
|
159
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ReaderFenceApplyOutcome,
|
|
3
|
+
RuntimeGatewayReaderFence,
|
|
4
|
+
RuntimeGatewayReaderFenceCandidate,
|
|
5
|
+
} from '../integration-operation-reader-fence/contracts';
|
|
6
|
+
import type { RuntimeGatewayReaderFenceStore } from '../integration-operation-reader-fence/ports';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Recording adapter for contract tests and local assembly. It implements the
|
|
10
|
+
* same atomic compare-and-advance law required of the Runtime Postgres mirror.
|
|
11
|
+
*/
|
|
12
|
+
export function createMemoryRuntimeGatewayReaderFenceStore(
|
|
13
|
+
initial: readonly RuntimeGatewayReaderFence[] = [],
|
|
14
|
+
): RuntimeGatewayReaderFenceStore {
|
|
15
|
+
const fences = new Map(
|
|
16
|
+
initial.map((fence) => [fence.schema, copyFence(fence)]),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
async read({ schema }) {
|
|
21
|
+
const fence = fences.get(schema);
|
|
22
|
+
return fence ? copyFence(fence) : null;
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async apply(candidate): Promise<ReaderFenceApplyOutcome> {
|
|
26
|
+
const next = copyFence(candidate);
|
|
27
|
+
const current = fences.get(next.schema);
|
|
28
|
+
if (!current) {
|
|
29
|
+
fences.set(next.schema, next);
|
|
30
|
+
return { kind: 'advanced', fence: copyFence(next) };
|
|
31
|
+
}
|
|
32
|
+
if (next.epoch < current.epoch) {
|
|
33
|
+
return {
|
|
34
|
+
kind: 'rejected',
|
|
35
|
+
reason: 'stale_epoch',
|
|
36
|
+
current: copyFence(current),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (next.epoch === current.epoch) {
|
|
40
|
+
if (sameFence(next, current)) {
|
|
41
|
+
return { kind: 'already_applied', fence: copyFence(current) };
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
kind: 'rejected',
|
|
45
|
+
reason: 'epoch_conflict',
|
|
46
|
+
current: copyFence(current),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (current.mode === 'canonical_v1' && next.mode !== 'canonical_v1') {
|
|
50
|
+
return {
|
|
51
|
+
kind: 'rejected',
|
|
52
|
+
reason: 'unsafe_mode_downgrade',
|
|
53
|
+
current: copyFence(current),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
fences.set(next.schema, next);
|
|
57
|
+
return { kind: 'advanced', fence: copyFence(next) };
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sameFence(
|
|
63
|
+
left: RuntimeGatewayReaderFence,
|
|
64
|
+
right: RuntimeGatewayReaderFence,
|
|
65
|
+
): boolean {
|
|
66
|
+
if (
|
|
67
|
+
left.schema !== right.schema ||
|
|
68
|
+
left.epoch !== right.epoch ||
|
|
69
|
+
left.mode !== right.mode
|
|
70
|
+
) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (!left.canonicalReaderProof || !right.canonicalReaderProof) {
|
|
74
|
+
return left.canonicalReaderProof === right.canonicalReaderProof;
|
|
75
|
+
}
|
|
76
|
+
return (
|
|
77
|
+
left.canonicalReaderProof.capability ===
|
|
78
|
+
right.canonicalReaderProof.capability &&
|
|
79
|
+
left.canonicalReaderProof.sourceSha ===
|
|
80
|
+
right.canonicalReaderProof.sourceSha &&
|
|
81
|
+
sameMachineSet(
|
|
82
|
+
left.canonicalReaderProof.machineIds.receipt,
|
|
83
|
+
right.canonicalReaderProof.machineIds.receipt,
|
|
84
|
+
) &&
|
|
85
|
+
sameMachineSet(
|
|
86
|
+
left.canonicalReaderProof.machineIds.execution,
|
|
87
|
+
right.canonicalReaderProof.machineIds.execution,
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sameMachineSet(
|
|
93
|
+
left: readonly string[],
|
|
94
|
+
right: readonly string[],
|
|
95
|
+
): boolean {
|
|
96
|
+
return left.length === right.length && left.every((id) => right.includes(id));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function copyFence(
|
|
100
|
+
fence: RuntimeGatewayReaderFenceCandidate,
|
|
101
|
+
): RuntimeGatewayReaderFence {
|
|
102
|
+
return {
|
|
103
|
+
...fence,
|
|
104
|
+
canonicalReaderProof: fence.canonicalReaderProof
|
|
105
|
+
? {
|
|
106
|
+
...fence.canonicalReaderProof,
|
|
107
|
+
machineIds: {
|
|
108
|
+
receipt: [...fence.canonicalReaderProof.machineIds.receipt],
|
|
109
|
+
execution: [...fence.canonicalReaderProof.machineIds.execution],
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
: null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
normalizeDatasetBornFrom,
|
|
21
21
|
type PlayDatasetBornFrom,
|
|
22
22
|
} from './cell-provenance';
|
|
23
|
+
import { isLedgerTerminalResultRef } from './output-size-limits';
|
|
23
24
|
|
|
24
25
|
export type PlayRunLedgerStatus =
|
|
25
26
|
| 'queued'
|
|
@@ -1019,6 +1020,32 @@ function conflictingTerminalSnapshot(
|
|
|
1019
1020
|
);
|
|
1020
1021
|
}
|
|
1021
1022
|
|
|
1023
|
+
/**
|
|
1024
|
+
* A legacy out-of-line terminal reference did not prove that its Postgres
|
|
1025
|
+
* value was written in full. A later ref bearing that proof is an enrichment
|
|
1026
|
+
* of the same terminal result, not a competing outcome or payload.
|
|
1027
|
+
*/
|
|
1028
|
+
function canPromoteTerminalResultReference(
|
|
1029
|
+
current: unknown,
|
|
1030
|
+
next: unknown,
|
|
1031
|
+
): boolean {
|
|
1032
|
+
return (
|
|
1033
|
+
isLedgerTerminalResultRef(current) &&
|
|
1034
|
+
current.content !== 'full' &&
|
|
1035
|
+
isLedgerTerminalResultRef(next) &&
|
|
1036
|
+
next.content === 'full'
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function mergeTerminalResult(input: {
|
|
1041
|
+
current: unknown;
|
|
1042
|
+
next: unknown;
|
|
1043
|
+
}): unknown {
|
|
1044
|
+
return canPromoteTerminalResultReference(input.current, input.next)
|
|
1045
|
+
? input.next
|
|
1046
|
+
: (input.next ?? input.current);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1022
1049
|
function retryablePlatformDeployFailureSnapshot(
|
|
1023
1050
|
base: PlayRunLedgerSnapshot,
|
|
1024
1051
|
eventError?: string | null,
|
|
@@ -1249,6 +1276,7 @@ export function reducePlayRunLedgerEvent(
|
|
|
1249
1276
|
conflictingTerminalSnapshot(base, event.type, occurredAt, null, {
|
|
1250
1277
|
allowStaleSameStatusPayloadMerge:
|
|
1251
1278
|
(base.result === undefined && event.result !== undefined) ||
|
|
1279
|
+
canPromoteTerminalResultReference(base.result, event.result) ||
|
|
1252
1280
|
(base.resultSummary === undefined &&
|
|
1253
1281
|
event.resultSummary !== undefined),
|
|
1254
1282
|
}) ??
|
|
@@ -1261,7 +1289,10 @@ export function reducePlayRunLedgerEvent(
|
|
|
1261
1289
|
activeStepId: null,
|
|
1262
1290
|
activeActivityId: null,
|
|
1263
1291
|
activeArtifactTableNamespace: null,
|
|
1264
|
-
result:
|
|
1292
|
+
result: mergeTerminalResult({
|
|
1293
|
+
current: base.result,
|
|
1294
|
+
next: event.result,
|
|
1295
|
+
}),
|
|
1265
1296
|
resultSummary: event.resultSummary ?? base.resultSummary,
|
|
1266
1297
|
terminalSource: nextTerminalSource(base, event.source),
|
|
1267
1298
|
})
|
|
@@ -1272,7 +1303,8 @@ export function reducePlayRunLedgerEvent(
|
|
|
1272
1303
|
conflictingTerminalSnapshot(base, event.type, occurredAt, event.error, {
|
|
1273
1304
|
allowStaleSameStatusPayloadMerge:
|
|
1274
1305
|
(base.error == null && event.error != null) ||
|
|
1275
|
-
(base.result === undefined && event.result !== undefined)
|
|
1306
|
+
(base.result === undefined && event.result !== undefined) ||
|
|
1307
|
+
canPromoteTerminalResultReference(base.result, event.result),
|
|
1276
1308
|
}) ??
|
|
1277
1309
|
withTiming({
|
|
1278
1310
|
...settleRunningStepsOnTerminal(base, 'failed', occurredAt),
|
|
@@ -1286,7 +1318,10 @@ export function reducePlayRunLedgerEvent(
|
|
|
1286
1318
|
activeStepId: null,
|
|
1287
1319
|
activeActivityId: null,
|
|
1288
1320
|
activeArtifactTableNamespace: null,
|
|
1289
|
-
result:
|
|
1321
|
+
result: mergeTerminalResult({
|
|
1322
|
+
current: base.result,
|
|
1323
|
+
next: event.result,
|
|
1324
|
+
}),
|
|
1290
1325
|
terminalSource: nextTerminalSource(base, event.source),
|
|
1291
1326
|
})
|
|
1292
1327
|
);
|
|
@@ -1295,7 +1330,8 @@ export function reducePlayRunLedgerEvent(
|
|
|
1295
1330
|
conflictingTerminalSnapshot(base, event.type, occurredAt, event.error, {
|
|
1296
1331
|
allowStaleSameStatusPayloadMerge:
|
|
1297
1332
|
(base.error == null && event.error != null) ||
|
|
1298
|
-
(base.result === undefined && event.result !== undefined)
|
|
1333
|
+
(base.result === undefined && event.result !== undefined) ||
|
|
1334
|
+
canPromoteTerminalResultReference(base.result, event.result),
|
|
1299
1335
|
}) ??
|
|
1300
1336
|
withTiming({
|
|
1301
1337
|
...settleRunningStepsOnTerminal(base, 'failed', occurredAt),
|
|
@@ -1309,7 +1345,10 @@ export function reducePlayRunLedgerEvent(
|
|
|
1309
1345
|
activeStepId: null,
|
|
1310
1346
|
activeActivityId: null,
|
|
1311
1347
|
activeArtifactTableNamespace: null,
|
|
1312
|
-
result:
|
|
1348
|
+
result: mergeTerminalResult({
|
|
1349
|
+
current: base.result,
|
|
1350
|
+
next: event.result,
|
|
1351
|
+
}),
|
|
1313
1352
|
terminalSource: nextTerminalSource(base, event.source),
|
|
1314
1353
|
})
|
|
1315
1354
|
);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createSingleFlight } from '../single-flight';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cache authorized child metadata for one Node runner execution. The caller
|
|
5
|
+
* owns this resolver beside the root PlayContext, so all map rows share it and
|
|
6
|
+
* the cache becomes unreachable as soon as the run settles.
|
|
7
|
+
*/
|
|
8
|
+
export function createRunScopedChildPlayResolver<T>(input: {
|
|
9
|
+
resolvePlay: (playRef: string) => Promise<T | null>;
|
|
10
|
+
}): (playRef: string) => Promise<T | null> {
|
|
11
|
+
const resolved = new Map<string, T>();
|
|
12
|
+
const flights = createSingleFlight<string, T | null>();
|
|
13
|
+
|
|
14
|
+
return async (playRef) => {
|
|
15
|
+
const cached = resolved.get(playRef);
|
|
16
|
+
if (cached) return cached;
|
|
17
|
+
|
|
18
|
+
return flights.run(playRef, async () => {
|
|
19
|
+
const cachedAfterAdmission = resolved.get(playRef);
|
|
20
|
+
if (cachedAfterAdmission) return cachedAfterAdmission;
|
|
21
|
+
const play = await input.resolvePlay(playRef);
|
|
22
|
+
if (play) resolved.set(playRef, play);
|
|
23
|
+
return play;
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
}
|