@selleragent/sa-admin 0.1.0
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/.tsbuildinfo +1 -0
- package/dist/auth.d.ts +73 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +313 -0
- package/dist/auth.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1199 -0
- package/dist/cli.js.map +1 -0
- package/dist/context.d.ts +19 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +46 -0
- package/dist/context.js.map +1 -0
- package/dist/help.d.ts +2 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +469 -0
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/project.d.ts +120 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +241 -0
- package/dist/project.js.map +1 -0
- package/dist/provisioning.d.ts +27 -0
- package/dist/provisioning.d.ts.map +1 -0
- package/dist/provisioning.js +55 -0
- package/dist/provisioning.js.map +1 -0
- package/dist/render.d.ts +3 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +14 -0
- package/dist/render.js.map +1 -0
- package/dist/rollout.d.ts +231 -0
- package/dist/rollout.d.ts.map +1 -0
- package/dist/rollout.js +971 -0
- package/dist/rollout.js.map +1 -0
- package/package.json +40 -0
package/dist/rollout.js
ADDED
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inspectRolloutPreflight = inspectRolloutPreflight;
|
|
4
|
+
exports.runRolloutMigrate = runRolloutMigrate;
|
|
5
|
+
exports.runRolloutPromoteProfile = runRolloutPromoteProfile;
|
|
6
|
+
exports.runRolloutFinalizeTelegram = runRolloutFinalizeTelegram;
|
|
7
|
+
exports.listRecordedRollouts = listRecordedRollouts;
|
|
8
|
+
exports.inspectRecordedRollout = inspectRecordedRollout;
|
|
9
|
+
exports.runRolloutVerify = runRolloutVerify;
|
|
10
|
+
exports.inspectReleaseVerdict = inspectReleaseVerdict;
|
|
11
|
+
exports.inspectReleaseEvidence = inspectReleaseEvidence;
|
|
12
|
+
exports.runRolloutPromote = runRolloutPromote;
|
|
13
|
+
const node_crypto_1 = require("node:crypto");
|
|
14
|
+
const shared_1 = require("@selleragent/shared");
|
|
15
|
+
const auth_1 = require("./auth");
|
|
16
|
+
const project_1 = require("./project");
|
|
17
|
+
function normalizeEnvironment(environment) {
|
|
18
|
+
return environment.trim().toLowerCase() === 'prod' ? 'production' : environment.trim().toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
function schemaSourceKindForEnvironment(environment) {
|
|
21
|
+
const normalized = normalizeEnvironment(environment);
|
|
22
|
+
if (normalized === 'beta') {
|
|
23
|
+
return 'beta_rollout';
|
|
24
|
+
}
|
|
25
|
+
if (normalized === 'production') {
|
|
26
|
+
return 'prod_rollout';
|
|
27
|
+
}
|
|
28
|
+
return 'local_bootstrap';
|
|
29
|
+
}
|
|
30
|
+
function backupSourceKindForEnvironment(environment) {
|
|
31
|
+
const normalized = normalizeEnvironment(environment);
|
|
32
|
+
if (normalized === 'beta') {
|
|
33
|
+
return 'beta_rollout';
|
|
34
|
+
}
|
|
35
|
+
if (normalized === 'production') {
|
|
36
|
+
return 'prod_rollout';
|
|
37
|
+
}
|
|
38
|
+
return 'local_drill';
|
|
39
|
+
}
|
|
40
|
+
function nowIso() {
|
|
41
|
+
return new Date().toISOString();
|
|
42
|
+
}
|
|
43
|
+
async function probeUrl(url) {
|
|
44
|
+
if (!url) {
|
|
45
|
+
return {
|
|
46
|
+
url: null,
|
|
47
|
+
ok: false,
|
|
48
|
+
status: null,
|
|
49
|
+
error: 'URL is not configured.'
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch(url, {
|
|
54
|
+
method: 'GET',
|
|
55
|
+
headers: {
|
|
56
|
+
accept: 'application/json, text/html;q=0.9, */*;q=0.1'
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
url,
|
|
61
|
+
ok: response.ok,
|
|
62
|
+
status: response.status,
|
|
63
|
+
error: response.ok ? null : `HTTP ${response.status}`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
url,
|
|
69
|
+
ok: false,
|
|
70
|
+
status: null,
|
|
71
|
+
error: error instanceof Error ? error.message : String(error)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function buildPhase(input) {
|
|
76
|
+
return {
|
|
77
|
+
phaseId: input.phaseId,
|
|
78
|
+
label: input.label,
|
|
79
|
+
status: input.status,
|
|
80
|
+
summary: input.summary,
|
|
81
|
+
details: input.details ?? {},
|
|
82
|
+
startedAt: input.startedAt ?? null,
|
|
83
|
+
completedAt: input.completedAt ?? null
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function isBlockerStatus(status) {
|
|
87
|
+
return status === 'fail';
|
|
88
|
+
}
|
|
89
|
+
function authBlockerFromState(input) {
|
|
90
|
+
if (!input.session) {
|
|
91
|
+
return ['No sa-admin session is available. Run `sa-admin auth login` before rollout.'];
|
|
92
|
+
}
|
|
93
|
+
if (input.whoami.stale) {
|
|
94
|
+
return ['The current sa-admin session is stale. Refresh it with `sa-admin auth login`.'];
|
|
95
|
+
}
|
|
96
|
+
if (!input.whoami.accessContext?.authenticated) {
|
|
97
|
+
return ['The current sa-admin session is not authenticated.'];
|
|
98
|
+
}
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
function readinessToRolloutStatus(status) {
|
|
102
|
+
switch (status) {
|
|
103
|
+
case 'pass':
|
|
104
|
+
return 'pass';
|
|
105
|
+
case 'warn':
|
|
106
|
+
return 'warn';
|
|
107
|
+
case 'fail':
|
|
108
|
+
return 'fail';
|
|
109
|
+
default:
|
|
110
|
+
return 'warn';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function completeRolloutRecord(input) {
|
|
114
|
+
return {
|
|
115
|
+
rolloutId: input.rolloutId,
|
|
116
|
+
environment: input.environment,
|
|
117
|
+
businessProfileSlug: input.businessProfileSlug,
|
|
118
|
+
startedBy: input.startedBy,
|
|
119
|
+
startedAt: input.startedAt,
|
|
120
|
+
completedAt: nowIso(),
|
|
121
|
+
status: input.status,
|
|
122
|
+
summary: input.summary,
|
|
123
|
+
stopReason: input.stopReason ?? null,
|
|
124
|
+
backupId: input.backupId ?? null,
|
|
125
|
+
profileVersionId: input.profileVersionId ?? null,
|
|
126
|
+
readinessStatus: input.readinessStatus ?? null,
|
|
127
|
+
codeRef: input.codeRef,
|
|
128
|
+
phases: input.phases,
|
|
129
|
+
details: input.details ?? {}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function completeReleaseVerificationRecord(input) {
|
|
133
|
+
return {
|
|
134
|
+
verificationId: input.verificationId,
|
|
135
|
+
rolloutId: input.rolloutId ?? null,
|
|
136
|
+
environment: input.environment,
|
|
137
|
+
businessProfileSlug: input.businessProfileSlug,
|
|
138
|
+
verifiedBy: input.verifiedBy,
|
|
139
|
+
verifiedAt: input.verifiedAt,
|
|
140
|
+
summary: input.summary,
|
|
141
|
+
verdict: input.verdict,
|
|
142
|
+
rollbackRecommended: input.rollbackRecommended ?? input.verdict === 'rollback_recommended',
|
|
143
|
+
backupId: input.backupId ?? null,
|
|
144
|
+
profileVersionId: input.profileVersionId ?? null,
|
|
145
|
+
readinessStatus: input.readinessStatus ?? null,
|
|
146
|
+
codeRef: input.codeRef,
|
|
147
|
+
warnings: input.warnings ?? [],
|
|
148
|
+
phases: input.phases,
|
|
149
|
+
details: input.details ?? {}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function probeSystemHealth(baseUrl) {
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch(`${baseUrl.replace(/\/+$/, '')}/health`, {
|
|
155
|
+
method: 'GET',
|
|
156
|
+
headers: {
|
|
157
|
+
accept: 'application/json'
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
const body = (await response.json().catch(() => null));
|
|
161
|
+
return {
|
|
162
|
+
ok: response.ok,
|
|
163
|
+
status: response.status,
|
|
164
|
+
requestId: response.headers.get('x-request-id') ??
|
|
165
|
+
(typeof body?.requestId === 'string' ? body.requestId : null),
|
|
166
|
+
body,
|
|
167
|
+
error: response.ok ? null : `HTTP ${response.status}`
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
status: null,
|
|
174
|
+
requestId: null,
|
|
175
|
+
body: null,
|
|
176
|
+
error: error instanceof Error ? error.message : String(error)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function releaseVerdictFromPhases(phases) {
|
|
181
|
+
if (phases.some((phase) => phase.status === 'fail')) {
|
|
182
|
+
return 'rollback_recommended';
|
|
183
|
+
}
|
|
184
|
+
if (phases.some((phase) => phase.status === 'warn')) {
|
|
185
|
+
return 'accepted_with_warnings';
|
|
186
|
+
}
|
|
187
|
+
return 'accepted';
|
|
188
|
+
}
|
|
189
|
+
function releaseSummaryFromVerdict(verdict, warnings) {
|
|
190
|
+
switch (verdict) {
|
|
191
|
+
case 'rollback_recommended':
|
|
192
|
+
return 'Post-release verification found blocking failures; rollback is recommended.';
|
|
193
|
+
case 'accepted_with_warnings':
|
|
194
|
+
return warnings.length > 0
|
|
195
|
+
? `Release accepted with warnings (${warnings.length} warning${warnings.length === 1 ? '' : 's'}).`
|
|
196
|
+
: 'Release accepted with warnings.';
|
|
197
|
+
default:
|
|
198
|
+
return 'Release accepted after post-release verification.';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function resolveRolloutForVerification(input) {
|
|
202
|
+
if (input.rolloutId) {
|
|
203
|
+
return (await input.auth.client.ops.getRolloutExecution({
|
|
204
|
+
rolloutId: input.rolloutId
|
|
205
|
+
})).rollout;
|
|
206
|
+
}
|
|
207
|
+
const listed = await input.auth.client.ops.listRolloutExecutions({
|
|
208
|
+
environment: input.context.environment,
|
|
209
|
+
businessProfileSlug: input.context.manifest.business_slug
|
|
210
|
+
});
|
|
211
|
+
return listed.rollouts[0] ?? null;
|
|
212
|
+
}
|
|
213
|
+
async function resolveReleaseVerificationForInspection(input) {
|
|
214
|
+
if (input.verificationId) {
|
|
215
|
+
return (await input.auth.client.ops.getReleaseVerification({
|
|
216
|
+
verificationId: input.verificationId
|
|
217
|
+
})).verification;
|
|
218
|
+
}
|
|
219
|
+
const listed = await input.auth.client.ops.listReleaseVerifications({
|
|
220
|
+
environment: input.context.environment,
|
|
221
|
+
businessProfileSlug: input.context.manifest.business_slug,
|
|
222
|
+
rolloutId: input.rolloutId ?? null
|
|
223
|
+
});
|
|
224
|
+
return listed.verifications[0] ?? null;
|
|
225
|
+
}
|
|
226
|
+
async function inspectRolloutPreflight(input) {
|
|
227
|
+
const codeRefMetadata = (0, shared_1.resolveBusinessProfileGitMetadata)(input.context.root.rootDir);
|
|
228
|
+
const codeRef = {
|
|
229
|
+
repositoryUrl: codeRefMetadata?.repositoryUrl ?? null,
|
|
230
|
+
commitSha: codeRefMetadata?.commitSha ?? null,
|
|
231
|
+
branchName: codeRefMetadata?.branchName ?? null
|
|
232
|
+
};
|
|
233
|
+
const auth = await (0, auth_1.resolveAuthenticatedClient)({
|
|
234
|
+
context: input.context,
|
|
235
|
+
allowBootstrap: false
|
|
236
|
+
});
|
|
237
|
+
const whoami = await (0, auth_1.resolveWhoAmI)(input.context);
|
|
238
|
+
const apiProbe = await probeUrl(`${input.context.baseUrl.replace(/\/+$/, '')}/health`);
|
|
239
|
+
const webProbe = await probeUrl(input.context.webBaseUrl);
|
|
240
|
+
let validation = null;
|
|
241
|
+
let validationError = null;
|
|
242
|
+
try {
|
|
243
|
+
validation = await (0, project_1.validateProfileProject)(input.context);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
validationError = error instanceof Error ? error.message : String(error);
|
|
247
|
+
}
|
|
248
|
+
const declarations = await (0, project_1.loadTelegramBotDeclarations)(input.context);
|
|
249
|
+
const compatibleIntegrations = declarations
|
|
250
|
+
.filter((entry) => entry.environmentMatches)
|
|
251
|
+
.map((entry) => ({
|
|
252
|
+
integrationKey: entry.integrationKey,
|
|
253
|
+
environments: [...entry.environments],
|
|
254
|
+
label: entry.label,
|
|
255
|
+
enabled: entry.enabled,
|
|
256
|
+
mode: entry.mode,
|
|
257
|
+
...(entry.webhookPath ? { webhookPath: entry.webhookPath } : {})
|
|
258
|
+
}));
|
|
259
|
+
const blockers = [
|
|
260
|
+
...authBlockerFromState({
|
|
261
|
+
session: whoami.session,
|
|
262
|
+
whoami
|
|
263
|
+
})
|
|
264
|
+
];
|
|
265
|
+
const warnings = [];
|
|
266
|
+
const phases = [];
|
|
267
|
+
phases.push(buildPhase({
|
|
268
|
+
phaseId: 'deploy_pair',
|
|
269
|
+
label: 'Deploy pair preflight',
|
|
270
|
+
status: apiProbe.ok && (webProbe.ok || !input.context.webBaseUrl)
|
|
271
|
+
? 'pass'
|
|
272
|
+
: apiProbe.ok
|
|
273
|
+
? 'warn'
|
|
274
|
+
: 'fail',
|
|
275
|
+
summary: apiProbe.ok
|
|
276
|
+
? webProbe.ok || !input.context.webBaseUrl
|
|
277
|
+
? 'API and configured web surfaces are reachable.'
|
|
278
|
+
: 'API is reachable, but the configured web surface could not be confirmed.'
|
|
279
|
+
: 'API health probe failed.',
|
|
280
|
+
details: {
|
|
281
|
+
api: apiProbe,
|
|
282
|
+
web: webProbe
|
|
283
|
+
}
|
|
284
|
+
}));
|
|
285
|
+
if (!apiProbe.ok) {
|
|
286
|
+
blockers.push(`API preflight failed: ${apiProbe.error ?? apiProbe.status ?? 'unknown error'}.`);
|
|
287
|
+
}
|
|
288
|
+
else if (!webProbe.ok && input.context.webBaseUrl) {
|
|
289
|
+
warnings.push(`Web preflight warning: ${webProbe.error ?? webProbe.status ?? 'unreachable'}.`);
|
|
290
|
+
}
|
|
291
|
+
phases.push(buildPhase({
|
|
292
|
+
phaseId: 'auth',
|
|
293
|
+
label: 'Admin auth session',
|
|
294
|
+
status: blockers.some((entry) => entry.includes('sa-admin session') || entry.includes('authenticated'))
|
|
295
|
+
? 'fail'
|
|
296
|
+
: 'pass',
|
|
297
|
+
summary: whoami.session && whoami.accessContext?.authenticated && !whoami.stale
|
|
298
|
+
? `Authenticated as ${whoami.accessContext.operator?.email ?? 'unknown operator'}.`
|
|
299
|
+
: 'No valid employee-backed sa-admin session is currently available.',
|
|
300
|
+
details: {
|
|
301
|
+
authMode: auth.authMode,
|
|
302
|
+
session: whoami.session,
|
|
303
|
+
stale: whoami.stale,
|
|
304
|
+
authenticated: whoami.accessContext?.authenticated ?? false
|
|
305
|
+
}
|
|
306
|
+
}));
|
|
307
|
+
phases.push(buildPhase({
|
|
308
|
+
phaseId: 'profile',
|
|
309
|
+
label: 'Business-profile validation',
|
|
310
|
+
status: validationError ? 'fail' : compatibleIntegrations.length === 0 ? 'warn' : 'pass',
|
|
311
|
+
summary: validationError
|
|
312
|
+
? 'The current business-profile repo failed governed validation.'
|
|
313
|
+
: compatibleIntegrations.length === 0
|
|
314
|
+
? 'The current repo validated, but no Telegram integrations match the target environment.'
|
|
315
|
+
: 'The current business-profile repo validated successfully.',
|
|
316
|
+
details: {
|
|
317
|
+
validation,
|
|
318
|
+
validationError,
|
|
319
|
+
compatibleIntegrations
|
|
320
|
+
}
|
|
321
|
+
}));
|
|
322
|
+
if (validationError) {
|
|
323
|
+
blockers.push(validationError);
|
|
324
|
+
}
|
|
325
|
+
if (compatibleIntegrations.length === 0) {
|
|
326
|
+
warnings.push('No Telegram integrations match the current target environment.');
|
|
327
|
+
}
|
|
328
|
+
let schema = null;
|
|
329
|
+
let readiness = null;
|
|
330
|
+
let preReleaseBackups = [];
|
|
331
|
+
if (auth.authMode !== 'public') {
|
|
332
|
+
schema = (await auth.client.ops.getSchemaStatus({})).schema;
|
|
333
|
+
readiness = await auth.client.ops.getReadiness({
|
|
334
|
+
businessProfileSlug: input.context.manifest.business_slug
|
|
335
|
+
});
|
|
336
|
+
preReleaseBackups = (await auth.client.ops.listBackupArtifacts({
|
|
337
|
+
environment: input.context.environment,
|
|
338
|
+
backupKind: 'pre_release'
|
|
339
|
+
})).artifacts;
|
|
340
|
+
}
|
|
341
|
+
const schemaStatus = schema ? readinessToRolloutStatus(schema.status) : 'warn';
|
|
342
|
+
phases.push(buildPhase({
|
|
343
|
+
phaseId: 'migrations',
|
|
344
|
+
label: 'Schema migration preflight',
|
|
345
|
+
status: schemaStatus,
|
|
346
|
+
summary: schema
|
|
347
|
+
? schema.pendingMigrations.length === 0
|
|
348
|
+
? 'Schema ledger is ready for rollout.'
|
|
349
|
+
: `Schema ledger has ${schema.pendingMigrations.length} pending migration(s).`
|
|
350
|
+
: 'Schema status could not be read without an authenticated session.',
|
|
351
|
+
details: {
|
|
352
|
+
schema,
|
|
353
|
+
latestPreReleaseBackupId: preReleaseBackups[0]?.backupId ?? null,
|
|
354
|
+
preReleaseBackupCount: preReleaseBackups.length
|
|
355
|
+
}
|
|
356
|
+
}));
|
|
357
|
+
if (schema?.status === 'fail') {
|
|
358
|
+
blockers.push('Schema migration ledger is in fail state.');
|
|
359
|
+
}
|
|
360
|
+
else if (schema && schema.pendingMigrations.length > 0) {
|
|
361
|
+
warnings.push(`There are ${schema.pendingMigrations.length} pending schema migrations.`);
|
|
362
|
+
}
|
|
363
|
+
phases.push(buildPhase({
|
|
364
|
+
phaseId: 'telegram',
|
|
365
|
+
label: 'Telegram rollout targets',
|
|
366
|
+
status: compatibleIntegrations.length > 0 ? 'pass' : 'warn',
|
|
367
|
+
summary: compatibleIntegrations.length > 0
|
|
368
|
+
? `Resolved ${compatibleIntegrations.length} compatible Telegram integration declaration(s).`
|
|
369
|
+
: 'No Telegram integration declarations match the target environment.',
|
|
370
|
+
details: {
|
|
371
|
+
compatibleIntegrations
|
|
372
|
+
}
|
|
373
|
+
}));
|
|
374
|
+
const ready = blockers.length === 0 && !phases.some((phase) => isBlockerStatus(phase.status));
|
|
375
|
+
return {
|
|
376
|
+
environment: input.context.environment,
|
|
377
|
+
businessProfileSlug: input.context.manifest.business_slug,
|
|
378
|
+
projectRoot: input.context.root.rootDir,
|
|
379
|
+
baseUrl: input.context.baseUrl,
|
|
380
|
+
webBaseUrl: input.context.webBaseUrl,
|
|
381
|
+
authMode: auth.authMode,
|
|
382
|
+
session: whoami.session,
|
|
383
|
+
accessContext: whoami.accessContext,
|
|
384
|
+
blockers,
|
|
385
|
+
warnings,
|
|
386
|
+
ready,
|
|
387
|
+
codeRef,
|
|
388
|
+
compatibleIntegrations,
|
|
389
|
+
phases,
|
|
390
|
+
validation,
|
|
391
|
+
schema,
|
|
392
|
+
readiness,
|
|
393
|
+
preReleaseBackups
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function requireAuthenticatedRolloutClient(auth) {
|
|
397
|
+
if (auth.authMode === 'public' || !auth.accessContext?.authenticated) {
|
|
398
|
+
throw new Error('Rollout commands require an authenticated sa-admin session. Run `sa-admin auth login` first.');
|
|
399
|
+
}
|
|
400
|
+
return auth;
|
|
401
|
+
}
|
|
402
|
+
async function runRolloutMigrate(input) {
|
|
403
|
+
const auth = requireAuthenticatedRolloutClient(await (0, auth_1.resolveAuthenticatedClient)({
|
|
404
|
+
context: input.context,
|
|
405
|
+
allowBootstrap: false
|
|
406
|
+
}));
|
|
407
|
+
const result = await auth.client.ops.applySchemaMigrations({
|
|
408
|
+
appliedBy: auth.accessContext?.operator?.email ?? null,
|
|
409
|
+
sourceKind: schemaSourceKindForEnvironment(input.context.environment)
|
|
410
|
+
});
|
|
411
|
+
return {
|
|
412
|
+
authMode: auth.authMode,
|
|
413
|
+
...result
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
async function runRolloutPromoteProfile(input) {
|
|
417
|
+
const auth = requireAuthenticatedRolloutClient(await (0, auth_1.resolveAuthenticatedClient)({
|
|
418
|
+
context: input.context,
|
|
419
|
+
allowBootstrap: false
|
|
420
|
+
}));
|
|
421
|
+
const prepared = await (0, project_1.buildProfileBundleForUpload)(input.context);
|
|
422
|
+
const result = await auth.client.businessProfiles.uploadBundle({
|
|
423
|
+
bundle: prepared.bundle,
|
|
424
|
+
activateOnUpload: true,
|
|
425
|
+
targetEnvironment: input.context.environment,
|
|
426
|
+
sourceRepositoryUrl: prepared.sourceRepositoryUrl,
|
|
427
|
+
sourceCommitSha: prepared.sourceCommitSha,
|
|
428
|
+
resolvedSecrets: prepared.resolvedSecrets
|
|
429
|
+
});
|
|
430
|
+
return {
|
|
431
|
+
authMode: auth.authMode,
|
|
432
|
+
...result,
|
|
433
|
+
sourceRepositoryUrl: prepared.sourceRepositoryUrl,
|
|
434
|
+
sourceCommitSha: prepared.sourceCommitSha
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
async function runRolloutFinalizeTelegram(input) {
|
|
438
|
+
const auth = requireAuthenticatedRolloutClient(await (0, auth_1.resolveAuthenticatedClient)({
|
|
439
|
+
context: input.context,
|
|
440
|
+
allowBootstrap: false
|
|
441
|
+
}));
|
|
442
|
+
const declarations = await (0, project_1.loadTelegramBotDeclarations)(input.context);
|
|
443
|
+
const compatible = declarations.filter((entry) => entry.environmentMatches);
|
|
444
|
+
const integrations = [];
|
|
445
|
+
for (const declaration of compatible) {
|
|
446
|
+
const integrationKey = declaration.integrationKey;
|
|
447
|
+
const verify = await auth.client.integrations.verifyTelegramIntegration({
|
|
448
|
+
integrationKey
|
|
449
|
+
});
|
|
450
|
+
const syncWebhook = await auth.client.integrations.syncTelegramWebhook({
|
|
451
|
+
integrationKey
|
|
452
|
+
});
|
|
453
|
+
const syncCommands = await auth.client.integrations.syncTelegramCommands({
|
|
454
|
+
integrationKey
|
|
455
|
+
});
|
|
456
|
+
const reconcile = await auth.client.integrations.reconcileTelegramIntegration({
|
|
457
|
+
integrationKey
|
|
458
|
+
});
|
|
459
|
+
integrations.push({
|
|
460
|
+
integrationKey,
|
|
461
|
+
verify,
|
|
462
|
+
syncWebhook,
|
|
463
|
+
syncCommands,
|
|
464
|
+
reconcile
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
authMode: auth.authMode,
|
|
469
|
+
integrations
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
async function listRecordedRollouts(input) {
|
|
473
|
+
const auth = requireAuthenticatedRolloutClient(await (0, auth_1.resolveAuthenticatedClient)({
|
|
474
|
+
context: input.context,
|
|
475
|
+
allowBootstrap: false
|
|
476
|
+
}));
|
|
477
|
+
return {
|
|
478
|
+
authMode: auth.authMode,
|
|
479
|
+
...(await auth.client.ops.listRolloutExecutions({
|
|
480
|
+
environment: input.environment ?? input.context.environment,
|
|
481
|
+
businessProfileSlug: input.businessProfileSlug ?? input.context.manifest.business_slug
|
|
482
|
+
}))
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
async function inspectRecordedRollout(input) {
|
|
486
|
+
const auth = requireAuthenticatedRolloutClient(await (0, auth_1.resolveAuthenticatedClient)({
|
|
487
|
+
context: input.context,
|
|
488
|
+
allowBootstrap: false
|
|
489
|
+
}));
|
|
490
|
+
return {
|
|
491
|
+
authMode: auth.authMode,
|
|
492
|
+
...(await auth.client.ops.getRolloutExecution({
|
|
493
|
+
rolloutId: input.rolloutId
|
|
494
|
+
}))
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
async function runRolloutVerify(input) {
|
|
498
|
+
const auth = requireAuthenticatedRolloutClient(await (0, auth_1.resolveAuthenticatedClient)({
|
|
499
|
+
context: input.context,
|
|
500
|
+
allowBootstrap: false
|
|
501
|
+
}));
|
|
502
|
+
const rollout = await resolveRolloutForVerification({
|
|
503
|
+
auth,
|
|
504
|
+
context: input.context,
|
|
505
|
+
rolloutId: input.rolloutId ?? null
|
|
506
|
+
});
|
|
507
|
+
if (!rollout) {
|
|
508
|
+
throw new Error('No rollout record is available to verify.');
|
|
509
|
+
}
|
|
510
|
+
const businessProfileSlug = rollout.businessProfileSlug ?? input.context.manifest.business_slug ?? null;
|
|
511
|
+
const verifiedAt = nowIso();
|
|
512
|
+
const verifiedBy = auth.accessContext?.operator?.email ?? null;
|
|
513
|
+
const phases = [];
|
|
514
|
+
const environmentStartedAt = nowIso();
|
|
515
|
+
const health = await probeSystemHealth(input.context.baseUrl);
|
|
516
|
+
phases.push(buildPhase({
|
|
517
|
+
phaseId: 'environment_identity',
|
|
518
|
+
label: 'Environment identity',
|
|
519
|
+
status: health.ok && typeof health.body?.environment === 'string'
|
|
520
|
+
? normalizeEnvironment(String(health.body.environment)) ===
|
|
521
|
+
normalizeEnvironment(input.context.environment)
|
|
522
|
+
? 'pass'
|
|
523
|
+
: 'fail'
|
|
524
|
+
: 'fail',
|
|
525
|
+
summary: health.ok && typeof health.body?.environment === 'string'
|
|
526
|
+
? `Health endpoint reports environment ${String(health.body.environment)}.`
|
|
527
|
+
: `Health probe failed${health.error ? `: ${health.error}` : '.'}`,
|
|
528
|
+
startedAt: environmentStartedAt,
|
|
529
|
+
completedAt: nowIso(),
|
|
530
|
+
details: {
|
|
531
|
+
health
|
|
532
|
+
}
|
|
533
|
+
}));
|
|
534
|
+
const accessStartedAt = nowIso();
|
|
535
|
+
const whoami = await (0, auth_1.resolveWhoAmI)(input.context);
|
|
536
|
+
const activeProfile = await auth.client.businessProfiles.get({
|
|
537
|
+
businessProfileSlug: businessProfileSlug ?? input.context.manifest.business_slug
|
|
538
|
+
});
|
|
539
|
+
const accessStatus = whoami.stale || !whoami.accessContext?.authenticated || !activeProfile.profile ? 'fail' : 'pass';
|
|
540
|
+
phases.push(buildPhase({
|
|
541
|
+
phaseId: 'operator_access',
|
|
542
|
+
label: 'Operator access',
|
|
543
|
+
status: accessStatus,
|
|
544
|
+
summary: accessStatus === 'pass'
|
|
545
|
+
? `Authenticated operator ${whoami.accessContext?.operator?.email ?? verifiedBy ?? 'unknown'} can access business profile state.`
|
|
546
|
+
: 'Authenticated operator session could not prove post-release access.',
|
|
547
|
+
startedAt: accessStartedAt,
|
|
548
|
+
completedAt: nowIso(),
|
|
549
|
+
details: {
|
|
550
|
+
whoami,
|
|
551
|
+
activeProfile
|
|
552
|
+
}
|
|
553
|
+
}));
|
|
554
|
+
const schemaStartedAt = nowIso();
|
|
555
|
+
const schema = await auth.client.ops.getSchemaStatus({});
|
|
556
|
+
const readiness = await auth.client.ops.getReadiness({
|
|
557
|
+
businessProfileSlug: businessProfileSlug ?? input.context.manifest.business_slug
|
|
558
|
+
});
|
|
559
|
+
const activeVersionId = activeProfile.activeVersion?.versionId ?? null;
|
|
560
|
+
const schemaProfileStatus = schema.schema.status === 'fail'
|
|
561
|
+
? 'fail'
|
|
562
|
+
: rollout.profileVersionId && activeVersionId && rollout.profileVersionId !== activeVersionId
|
|
563
|
+
? 'fail'
|
|
564
|
+
: readiness.status === 'fail'
|
|
565
|
+
? 'fail'
|
|
566
|
+
: schema.schema.status === 'warn' || readiness.status === 'warn'
|
|
567
|
+
? 'warn'
|
|
568
|
+
: 'pass';
|
|
569
|
+
phases.push(buildPhase({
|
|
570
|
+
phaseId: 'schema_profile_compatibility',
|
|
571
|
+
label: 'Schema and profile compatibility',
|
|
572
|
+
status: schemaProfileStatus,
|
|
573
|
+
summary: rollout.profileVersionId && activeVersionId && rollout.profileVersionId !== activeVersionId
|
|
574
|
+
? `Active profile version ${activeVersionId} no longer matches rollout version ${rollout.profileVersionId}.`
|
|
575
|
+
: schema.schema.status === 'fail'
|
|
576
|
+
? 'Schema status is failing after rollout.'
|
|
577
|
+
: readiness.status === 'fail'
|
|
578
|
+
? 'Readiness failed during post-release verification.'
|
|
579
|
+
: schema.schema.status === 'warn' || readiness.status === 'warn'
|
|
580
|
+
? 'Schema or readiness completed with warnings.'
|
|
581
|
+
: 'Schema ledger, readiness, and active profile version are compatible.',
|
|
582
|
+
startedAt: schemaStartedAt,
|
|
583
|
+
completedAt: nowIso(),
|
|
584
|
+
details: {
|
|
585
|
+
schema: schema.schema,
|
|
586
|
+
readiness,
|
|
587
|
+
rolloutProfileVersionId: rollout.profileVersionId ?? null,
|
|
588
|
+
activeProfileVersionId: activeVersionId
|
|
589
|
+
}
|
|
590
|
+
}));
|
|
591
|
+
const telegramStartedAt = nowIso();
|
|
592
|
+
const declarations = await (0, project_1.loadTelegramBotDeclarations)(input.context);
|
|
593
|
+
const compatible = declarations.filter((entry) => entry.environmentMatches);
|
|
594
|
+
const telegramChecks = [];
|
|
595
|
+
for (const declaration of compatible) {
|
|
596
|
+
const verification = await auth.client.integrations.verifyTelegramIntegration({
|
|
597
|
+
integrationKey: declaration.integrationKey
|
|
598
|
+
});
|
|
599
|
+
telegramChecks.push({
|
|
600
|
+
integrationKey: declaration.integrationKey,
|
|
601
|
+
label: declaration.label,
|
|
602
|
+
status: verification.snapshot.status,
|
|
603
|
+
summary: verification.snapshot.summary
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
const telegramStatus = telegramChecks.length === 0
|
|
607
|
+
? 'skipped'
|
|
608
|
+
: telegramChecks.some((entry) => entry.status === 'fail')
|
|
609
|
+
? 'fail'
|
|
610
|
+
: telegramChecks.some((entry) => entry.status === 'warn')
|
|
611
|
+
? 'warn'
|
|
612
|
+
: 'pass';
|
|
613
|
+
phases.push(buildPhase({
|
|
614
|
+
phaseId: 'telegram_integrations',
|
|
615
|
+
label: 'Telegram integrations',
|
|
616
|
+
status: telegramStatus,
|
|
617
|
+
summary: telegramChecks.length === 0
|
|
618
|
+
? 'No environment-compatible Telegram integrations required post-release verification.'
|
|
619
|
+
: telegramStatus === 'fail'
|
|
620
|
+
? 'At least one Telegram integration failed post-release verification.'
|
|
621
|
+
: telegramStatus === 'warn'
|
|
622
|
+
? 'Telegram integrations verified with warnings.'
|
|
623
|
+
: `Verified ${telegramChecks.length} Telegram integration(s).`,
|
|
624
|
+
startedAt: telegramStartedAt,
|
|
625
|
+
completedAt: nowIso(),
|
|
626
|
+
details: {
|
|
627
|
+
integrations: telegramChecks
|
|
628
|
+
}
|
|
629
|
+
}));
|
|
630
|
+
const warnings = phases
|
|
631
|
+
.filter((phase) => phase.status === 'warn')
|
|
632
|
+
.map((phase) => `${phase.label}: ${phase.summary}`);
|
|
633
|
+
const verdict = releaseVerdictFromPhases(phases);
|
|
634
|
+
const verification = completeReleaseVerificationRecord({
|
|
635
|
+
verificationId: (0, node_crypto_1.randomUUID)(),
|
|
636
|
+
rolloutId: rollout.rolloutId,
|
|
637
|
+
environment: rollout.environment,
|
|
638
|
+
businessProfileSlug,
|
|
639
|
+
verifiedBy,
|
|
640
|
+
verifiedAt,
|
|
641
|
+
verdict,
|
|
642
|
+
summary: releaseSummaryFromVerdict(verdict, warnings),
|
|
643
|
+
backupId: rollout.backupId ?? null,
|
|
644
|
+
profileVersionId: rollout.profileVersionId ?? null,
|
|
645
|
+
readinessStatus: readiness.status,
|
|
646
|
+
codeRef: rollout.codeRef,
|
|
647
|
+
warnings,
|
|
648
|
+
phases,
|
|
649
|
+
details: {
|
|
650
|
+
health,
|
|
651
|
+
whoami,
|
|
652
|
+
rollout,
|
|
653
|
+
schema: schema.schema,
|
|
654
|
+
readiness,
|
|
655
|
+
activeProfileVersionId: activeVersionId,
|
|
656
|
+
telegramIntegrations: telegramChecks
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
const recorded = await auth.client.ops.recordReleaseVerification({
|
|
660
|
+
verification
|
|
661
|
+
});
|
|
662
|
+
return {
|
|
663
|
+
authMode: auth.authMode,
|
|
664
|
+
verification: recorded.verification
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
async function inspectReleaseVerdict(input) {
|
|
668
|
+
const auth = requireAuthenticatedRolloutClient(await (0, auth_1.resolveAuthenticatedClient)({
|
|
669
|
+
context: input.context,
|
|
670
|
+
allowBootstrap: false
|
|
671
|
+
}));
|
|
672
|
+
const verification = await resolveReleaseVerificationForInspection({
|
|
673
|
+
auth,
|
|
674
|
+
context: input.context,
|
|
675
|
+
verificationId: input.verificationId ?? null,
|
|
676
|
+
rolloutId: input.rolloutId ?? null
|
|
677
|
+
});
|
|
678
|
+
return {
|
|
679
|
+
authMode: auth.authMode,
|
|
680
|
+
verdict: verification
|
|
681
|
+
? {
|
|
682
|
+
verificationId: verification.verificationId,
|
|
683
|
+
verdict: verification.verdict,
|
|
684
|
+
summary: verification.summary,
|
|
685
|
+
rollbackRecommended: verification.rollbackRecommended,
|
|
686
|
+
warnings: verification.warnings,
|
|
687
|
+
rolloutId: verification.rolloutId,
|
|
688
|
+
backupId: verification.backupId,
|
|
689
|
+
profileVersionId: verification.profileVersionId
|
|
690
|
+
}
|
|
691
|
+
: null
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
async function inspectReleaseEvidence(input) {
|
|
695
|
+
const auth = requireAuthenticatedRolloutClient(await (0, auth_1.resolveAuthenticatedClient)({
|
|
696
|
+
context: input.context,
|
|
697
|
+
allowBootstrap: false
|
|
698
|
+
}));
|
|
699
|
+
return {
|
|
700
|
+
authMode: auth.authMode,
|
|
701
|
+
verification: await resolveReleaseVerificationForInspection({
|
|
702
|
+
auth,
|
|
703
|
+
context: input.context,
|
|
704
|
+
verificationId: input.verificationId ?? null,
|
|
705
|
+
rolloutId: input.rolloutId ?? null
|
|
706
|
+
})
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
async function runRolloutPromote(input) {
|
|
710
|
+
const auth = requireAuthenticatedRolloutClient(await (0, auth_1.resolveAuthenticatedClient)({
|
|
711
|
+
context: input.context,
|
|
712
|
+
allowBootstrap: false
|
|
713
|
+
}));
|
|
714
|
+
const startedAt = nowIso();
|
|
715
|
+
const rolloutId = (0, node_crypto_1.randomUUID)();
|
|
716
|
+
const businessProfileSlug = input.context.manifest.business_slug;
|
|
717
|
+
const codeRefMetadata = (0, shared_1.resolveBusinessProfileGitMetadata)(input.context.root.rootDir);
|
|
718
|
+
const codeRef = {
|
|
719
|
+
repositoryUrl: codeRefMetadata?.repositoryUrl ?? null,
|
|
720
|
+
commitSha: codeRefMetadata?.commitSha ?? null,
|
|
721
|
+
branchName: codeRefMetadata?.branchName ?? null
|
|
722
|
+
};
|
|
723
|
+
const startedBy = auth.accessContext?.operator?.email ?? null;
|
|
724
|
+
const preflight = await inspectRolloutPreflight({
|
|
725
|
+
context: input.context
|
|
726
|
+
});
|
|
727
|
+
if (!preflight.ready) {
|
|
728
|
+
const stopped = completeRolloutRecord({
|
|
729
|
+
rolloutId,
|
|
730
|
+
environment: input.context.environment,
|
|
731
|
+
businessProfileSlug,
|
|
732
|
+
startedBy,
|
|
733
|
+
startedAt,
|
|
734
|
+
status: 'stopped',
|
|
735
|
+
summary: 'Rollout preflight reported blockers and promotion was stopped before mutation.',
|
|
736
|
+
stopReason: preflight.blockers.join(' '),
|
|
737
|
+
readinessStatus: preflight.readiness?.status ?? null,
|
|
738
|
+
codeRef,
|
|
739
|
+
phases: preflight.phases,
|
|
740
|
+
details: {
|
|
741
|
+
blockers: preflight.blockers,
|
|
742
|
+
warnings: preflight.warnings,
|
|
743
|
+
preflight
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
const recorded = await auth.client.ops.recordRolloutExecution({
|
|
747
|
+
rollout: stopped
|
|
748
|
+
});
|
|
749
|
+
return {
|
|
750
|
+
authMode: auth.authMode,
|
|
751
|
+
rollout: recorded.rollout
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
const phases = [];
|
|
755
|
+
let backupArtifact = null;
|
|
756
|
+
let profileVersionId = null;
|
|
757
|
+
let readinessStatus = null;
|
|
758
|
+
let currentPhaseId = 'backup';
|
|
759
|
+
let currentPhaseLabel = 'Pre-release backup';
|
|
760
|
+
try {
|
|
761
|
+
const backupStartedAt = nowIso();
|
|
762
|
+
currentPhaseId = 'backup';
|
|
763
|
+
currentPhaseLabel = 'Pre-release backup';
|
|
764
|
+
const backup = await auth.client.ops.createBackupArtifact({
|
|
765
|
+
environment: input.context.environment,
|
|
766
|
+
backupKind: 'pre_release',
|
|
767
|
+
sourceKind: backupSourceKindForEnvironment(input.context.environment),
|
|
768
|
+
notes: input.notes ?? `Rollout ${rolloutId}`
|
|
769
|
+
});
|
|
770
|
+
backupArtifact = backup.artifact;
|
|
771
|
+
phases.push(buildPhase({
|
|
772
|
+
phaseId: 'backup',
|
|
773
|
+
label: 'Pre-release backup',
|
|
774
|
+
status: 'pass',
|
|
775
|
+
summary: `Created governed pre-release backup ${backup.artifact.backupId}.`,
|
|
776
|
+
startedAt: backupStartedAt,
|
|
777
|
+
completedAt: nowIso(),
|
|
778
|
+
details: {
|
|
779
|
+
artifact: backup.artifact
|
|
780
|
+
}
|
|
781
|
+
}));
|
|
782
|
+
const migrationsStartedAt = nowIso();
|
|
783
|
+
currentPhaseId = 'migrations';
|
|
784
|
+
currentPhaseLabel = 'Schema migrations';
|
|
785
|
+
const migrations = await auth.client.ops.applySchemaMigrations({
|
|
786
|
+
appliedBy: startedBy,
|
|
787
|
+
sourceKind: schemaSourceKindForEnvironment(input.context.environment)
|
|
788
|
+
});
|
|
789
|
+
phases.push(buildPhase({
|
|
790
|
+
phaseId: 'migrations',
|
|
791
|
+
label: 'Schema migrations',
|
|
792
|
+
status: migrations.schema.status === 'fail' ? 'fail' : 'pass',
|
|
793
|
+
summary: migrations.applied.length > 0
|
|
794
|
+
? `Applied ${migrations.applied.length} schema migration(s).`
|
|
795
|
+
: 'Schema ledger already had no pending migrations.',
|
|
796
|
+
startedAt: migrationsStartedAt,
|
|
797
|
+
completedAt: nowIso(),
|
|
798
|
+
details: {
|
|
799
|
+
applied: migrations.applied,
|
|
800
|
+
schema: migrations.schema
|
|
801
|
+
}
|
|
802
|
+
}));
|
|
803
|
+
if (migrations.schema.status === 'fail') {
|
|
804
|
+
throw new Error('Schema migrations finished in fail state.');
|
|
805
|
+
}
|
|
806
|
+
const promotionStartedAt = nowIso();
|
|
807
|
+
currentPhaseId = 'profile_promotion';
|
|
808
|
+
currentPhaseLabel = 'Business-profile promotion';
|
|
809
|
+
const prepared = await (0, project_1.buildProfileBundleForUpload)(input.context);
|
|
810
|
+
const promotion = await auth.client.businessProfiles.uploadBundle({
|
|
811
|
+
bundle: prepared.bundle,
|
|
812
|
+
activateOnUpload: true,
|
|
813
|
+
targetEnvironment: input.context.environment,
|
|
814
|
+
sourceRepositoryUrl: prepared.sourceRepositoryUrl,
|
|
815
|
+
sourceCommitSha: prepared.sourceCommitSha,
|
|
816
|
+
resolvedSecrets: prepared.resolvedSecrets
|
|
817
|
+
});
|
|
818
|
+
profileVersionId = promotion.activeVersion.versionId;
|
|
819
|
+
phases.push(buildPhase({
|
|
820
|
+
phaseId: 'profile_promotion',
|
|
821
|
+
label: 'Business-profile promotion',
|
|
822
|
+
status: 'pass',
|
|
823
|
+
summary: `Uploaded and activated profile version ${promotion.activeVersion.versionId}.`,
|
|
824
|
+
startedAt: promotionStartedAt,
|
|
825
|
+
completedAt: nowIso(),
|
|
826
|
+
details: {
|
|
827
|
+
targetEnvironment: promotion.targetEnvironment,
|
|
828
|
+
serverEnvironment: promotion.serverEnvironment,
|
|
829
|
+
appliedDeclarations: promotion.appliedDeclarations,
|
|
830
|
+
skippedDeclarations: promotion.skippedDeclarations,
|
|
831
|
+
sourceRepositoryUrl: prepared.sourceRepositoryUrl,
|
|
832
|
+
sourceCommitSha: prepared.sourceCommitSha
|
|
833
|
+
}
|
|
834
|
+
}));
|
|
835
|
+
const finalizeStartedAt = nowIso();
|
|
836
|
+
currentPhaseId = 'telegram_finalize';
|
|
837
|
+
currentPhaseLabel = 'Telegram finalize';
|
|
838
|
+
const telegram = await runRolloutFinalizeTelegram({
|
|
839
|
+
context: input.context
|
|
840
|
+
});
|
|
841
|
+
const businessReconcile = await auth.client.ops.reconcileBusiness({
|
|
842
|
+
businessProfileSlug
|
|
843
|
+
});
|
|
844
|
+
phases.push(buildPhase({
|
|
845
|
+
phaseId: 'telegram_finalize',
|
|
846
|
+
label: 'Telegram finalize',
|
|
847
|
+
status: telegram.integrations.length > 0
|
|
848
|
+
? 'pass'
|
|
849
|
+
: 'skipped',
|
|
850
|
+
summary: telegram.integrations.length > 0
|
|
851
|
+
? `Verified and synchronized ${telegram.integrations.length} Telegram integration(s).`
|
|
852
|
+
: 'No environment-compatible Telegram integrations required finalization.',
|
|
853
|
+
startedAt: finalizeStartedAt,
|
|
854
|
+
completedAt: nowIso(),
|
|
855
|
+
details: {
|
|
856
|
+
integrations: telegram.integrations,
|
|
857
|
+
reconcile: businessReconcile
|
|
858
|
+
}
|
|
859
|
+
}));
|
|
860
|
+
const readinessStartedAt = nowIso();
|
|
861
|
+
currentPhaseId = 'readiness';
|
|
862
|
+
currentPhaseLabel = 'Post-promotion readiness';
|
|
863
|
+
const readiness = await auth.client.ops.getReadiness({
|
|
864
|
+
businessProfileSlug
|
|
865
|
+
});
|
|
866
|
+
readinessStatus = readiness.status;
|
|
867
|
+
const readinessPhaseStatus = readinessToRolloutStatus(readiness.status);
|
|
868
|
+
phases.push(buildPhase({
|
|
869
|
+
phaseId: 'readiness',
|
|
870
|
+
label: 'Post-promotion readiness',
|
|
871
|
+
status: readinessPhaseStatus,
|
|
872
|
+
summary: readiness.status === 'fail'
|
|
873
|
+
? 'Readiness failed after promotion.'
|
|
874
|
+
: readiness.status === 'warn'
|
|
875
|
+
? 'Readiness completed with warnings after promotion.'
|
|
876
|
+
: 'Readiness passed after promotion.',
|
|
877
|
+
startedAt: readinessStartedAt,
|
|
878
|
+
completedAt: nowIso(),
|
|
879
|
+
details: {
|
|
880
|
+
readiness
|
|
881
|
+
}
|
|
882
|
+
}));
|
|
883
|
+
const rollout = readinessPhaseStatus === 'fail'
|
|
884
|
+
? completeRolloutRecord({
|
|
885
|
+
rolloutId,
|
|
886
|
+
environment: input.context.environment,
|
|
887
|
+
businessProfileSlug,
|
|
888
|
+
startedBy,
|
|
889
|
+
startedAt,
|
|
890
|
+
status: 'stopped',
|
|
891
|
+
summary: 'Rollout stopped because post-promotion readiness failed.',
|
|
892
|
+
stopReason: 'Post-promotion readiness returned fail.',
|
|
893
|
+
backupId: backupArtifact?.backupId ?? null,
|
|
894
|
+
profileVersionId,
|
|
895
|
+
readinessStatus,
|
|
896
|
+
codeRef,
|
|
897
|
+
phases,
|
|
898
|
+
details: {
|
|
899
|
+
notes: input.notes ?? null
|
|
900
|
+
}
|
|
901
|
+
})
|
|
902
|
+
: completeRolloutRecord({
|
|
903
|
+
rolloutId,
|
|
904
|
+
environment: input.context.environment,
|
|
905
|
+
businessProfileSlug,
|
|
906
|
+
startedBy,
|
|
907
|
+
startedAt,
|
|
908
|
+
status: 'completed',
|
|
909
|
+
summary: readinessPhaseStatus === 'warn'
|
|
910
|
+
? 'Rollout completed with readiness warnings.'
|
|
911
|
+
: 'Rollout completed successfully.',
|
|
912
|
+
backupId: backupArtifact?.backupId ?? null,
|
|
913
|
+
profileVersionId,
|
|
914
|
+
readinessStatus,
|
|
915
|
+
codeRef,
|
|
916
|
+
phases,
|
|
917
|
+
details: {
|
|
918
|
+
notes: input.notes ?? null
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
const recorded = await auth.client.ops.recordRolloutExecution({
|
|
922
|
+
rollout
|
|
923
|
+
});
|
|
924
|
+
return {
|
|
925
|
+
authMode: auth.authMode,
|
|
926
|
+
rollout: recorded.rollout
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
catch (error) {
|
|
930
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
931
|
+
const stopped = completeRolloutRecord({
|
|
932
|
+
rolloutId,
|
|
933
|
+
environment: input.context.environment,
|
|
934
|
+
businessProfileSlug,
|
|
935
|
+
startedBy,
|
|
936
|
+
startedAt,
|
|
937
|
+
status: 'stopped',
|
|
938
|
+
summary: `Rollout stopped during ${currentPhaseId} phase.`,
|
|
939
|
+
stopReason: message,
|
|
940
|
+
backupId: backupArtifact?.backupId ?? null,
|
|
941
|
+
profileVersionId,
|
|
942
|
+
readinessStatus,
|
|
943
|
+
codeRef,
|
|
944
|
+
phases: [
|
|
945
|
+
...phases,
|
|
946
|
+
buildPhase({
|
|
947
|
+
phaseId: currentPhaseId,
|
|
948
|
+
label: currentPhaseLabel,
|
|
949
|
+
status: 'fail',
|
|
950
|
+
summary: message,
|
|
951
|
+
startedAt: nowIso(),
|
|
952
|
+
completedAt: nowIso(),
|
|
953
|
+
details: {
|
|
954
|
+
error: message
|
|
955
|
+
}
|
|
956
|
+
})
|
|
957
|
+
],
|
|
958
|
+
details: {
|
|
959
|
+
notes: input.notes ?? null
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
const recorded = await auth.client.ops.recordRolloutExecution({
|
|
963
|
+
rollout: stopped
|
|
964
|
+
});
|
|
965
|
+
return {
|
|
966
|
+
authMode: auth.authMode,
|
|
967
|
+
rollout: recorded.rollout
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
//# sourceMappingURL=rollout.js.map
|