@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.
@@ -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