deepline 0.3.214 → 0.3.215

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.
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
202
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
203
- version: '0.3.214',
203
+ version: '0.3.215',
204
204
  updateSummary:
205
205
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
206
206
  packageCapabilities: {
@@ -91,6 +91,23 @@ export type DeploymentPlayState = {
91
91
  liveRevision: DeploymentPlayState['workingRevision'];
92
92
  };
93
93
 
94
+ export type DeploymentMonitorEntry = {
95
+ /** Repository-relative source path, including the linked target's root. */
96
+ file: string;
97
+ /** The monitor API owns definition validation. */
98
+ definition: unknown;
99
+ };
100
+
101
+ export type DeploymentMonitorOutcome = {
102
+ key: string;
103
+ file: string;
104
+ status: 'planned' | 'applying' | 'applied' | 'stopped' | 'failed';
105
+ jobId?: string;
106
+ warning?: string;
107
+ error?: string;
108
+ credits?: number | null;
109
+ };
110
+
94
111
  export class DeployApiClient {
95
112
  constructor(
96
113
  private readonly config: {
@@ -223,6 +240,29 @@ export class DeployApiClient {
223
240
  });
224
241
  }
225
242
 
243
+ monitors(
244
+ action: 'plan' | 'apply',
245
+ entries: DeploymentMonitorEntry[],
246
+ ): Promise<{ monitors: DeploymentMonitorOutcome[] }> {
247
+ return this.request({
248
+ method: 'POST',
249
+ path: this.deploymentPath('/monitors'),
250
+ body: { action, entries },
251
+ });
252
+ }
253
+
254
+ monitorJob(
255
+ jobId: string,
256
+ monitorKey: string,
257
+ ): Promise<{ monitors: DeploymentMonitorOutcome[] }> {
258
+ return this.request({
259
+ method: 'GET',
260
+ path: this.deploymentPath(
261
+ `/monitors?jobId=${encodeURIComponent(jobId)}&monitorKey=${encodeURIComponent(monitorKey)}`,
262
+ ),
263
+ });
264
+ }
265
+
226
266
  compileManifest(input: {
227
267
  name: string;
228
268
  sourceCode: string;
@@ -4,19 +4,22 @@ import picomatch from 'picomatch';
4
4
 
5
5
  /**
6
6
  * deepline.json — the repo-owned half of deploy configuration. The repo
7
- * describes what plays exist and where; Convex (playProjects) decides what
7
+ * describes what plays and monitors exist and where; Convex (playProjects) decides what
8
8
  * happens when they change (production branch, PR checks, enable/disable).
9
9
  * No field lives in both places.
10
10
  */
11
11
  export type DeeplineRepoManifest = {
12
12
  /** Globs relative to the project root directory. */
13
13
  plays: string[];
14
+ /** Monitor JSON files to discover, relative to the project root directory. */
15
+ monitors: string[];
14
16
  /** Optional tenant guard: the workspace slug this repo expects. */
15
17
  org?: string;
16
18
  };
17
19
 
18
20
  export const DEFAULT_PLAY_GLOBS = ['**/*.play.ts'];
19
- const MAX_DISCOVERED_PLAY_FILES = 200;
21
+ export const DEFAULT_MONITOR_GLOBS = ['**/*.monitor.json'];
22
+ const MAX_DISCOVERED_FILES = 200;
20
23
  const IGNORED_DIRECTORIES = new Set([
21
24
  'node_modules',
22
25
  '.git',
@@ -32,11 +35,33 @@ export class DeeplineManifestError extends Error {
32
35
  }
33
36
  }
34
37
 
38
+ function readGlobs(value: unknown, field: 'plays' | 'monitors'): string[] {
39
+ if (
40
+ !Array.isArray(value) ||
41
+ (field === 'plays' && value.length === 0) ||
42
+ !value.every((glob) => typeof glob === 'string' && glob.trim().length > 0)
43
+ ) {
44
+ throw new DeeplineManifestError(
45
+ `deepline.json "${field}" must be ${field === 'plays' ? 'a non-empty' : 'an'} array of glob strings.`,
46
+ );
47
+ }
48
+ const globs = value.map((glob: string) => glob.trim().replace(/^\.\//, ''));
49
+ if (globs.some((glob) => glob.startsWith('/') || glob.includes('..'))) {
50
+ throw new DeeplineManifestError(
51
+ `deepline.json "${field}" globs must be relative and must not contain "..".`,
52
+ );
53
+ }
54
+ return globs;
55
+ }
56
+
35
57
  export function parseDeeplineManifest(
36
58
  raw: string | null,
37
59
  ): DeeplineRepoManifest {
38
60
  if (raw === null) {
39
- return { plays: DEFAULT_PLAY_GLOBS };
61
+ return {
62
+ plays: DEFAULT_PLAY_GLOBS,
63
+ monitors: DEFAULT_MONITOR_GLOBS,
64
+ };
40
65
  }
41
66
  let parsed: unknown;
42
67
  try {
@@ -49,32 +74,28 @@ export function parseDeeplineManifest(
49
74
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
50
75
  throw new DeeplineManifestError('deepline.json must be a JSON object.');
51
76
  }
52
- const record = parsed as { plays?: unknown; org?: unknown };
53
- let plays = DEFAULT_PLAY_GLOBS;
54
- if (record.plays !== undefined) {
55
- if (
56
- !Array.isArray(record.plays) ||
57
- record.plays.length === 0 ||
58
- !record.plays.every(
59
- (glob) => typeof glob === 'string' && glob.trim().length > 0,
60
- )
61
- ) {
62
- throw new DeeplineManifestError(
63
- 'deepline.json "plays" must be a non-empty array of glob strings, e.g. ["plays/**/*.play.ts"].',
64
- );
65
- }
66
- plays = record.plays.map((glob) => glob.trim().replace(/^\.\//, ''));
67
- }
68
- if (plays.some((glob) => glob.startsWith('/') || glob.includes('..'))) {
69
- throw new DeeplineManifestError(
70
- 'deepline.json "plays" globs must be relative and must not contain "..".',
71
- );
72
- }
77
+ const record = parsed as {
78
+ plays?: unknown;
79
+ monitors?: unknown;
80
+ org?: unknown;
81
+ };
82
+ const plays =
83
+ record.plays === undefined
84
+ ? DEFAULT_PLAY_GLOBS
85
+ : readGlobs(record.plays, 'plays');
86
+ const monitors =
87
+ record.monitors === undefined
88
+ ? DEFAULT_MONITOR_GLOBS
89
+ : readGlobs(record.monitors, 'monitors');
73
90
  const org =
74
91
  typeof record.org === 'string' && record.org.trim()
75
92
  ? record.org.trim()
76
93
  : undefined;
77
- return { plays, ...(org ? { org } : {}) };
94
+ return {
95
+ plays,
96
+ monitors,
97
+ ...(org ? { org } : {}),
98
+ };
78
99
  }
79
100
 
80
101
  export const DEFAULT_MANIFEST_FILE = 'deepline.json';
@@ -121,7 +142,7 @@ export async function readDeeplineManifest(
121
142
  * node_modules or fixtures publishes garbage, so vendored/build directories
122
143
  * are always skipped and discovery is capped loudly.
123
144
  */
124
- export async function discoverPlayFiles(input: {
145
+ export async function discoverDeploymentFiles(input: {
125
146
  projectRoot: string;
126
147
  globs: string[];
127
148
  }): Promise<string[]> {
@@ -142,9 +163,9 @@ export async function discoverPlayFiles(input: {
142
163
  const relativePath = relative(input.projectRoot, absolute);
143
164
  if (matchers.some((match) => match(relativePath))) {
144
165
  found.push(relativePath);
145
- if (found.length > MAX_DISCOVERED_PLAY_FILES) {
166
+ if (found.length > MAX_DISCOVERED_FILES) {
146
167
  throw new DeeplineManifestError(
147
- `deepline.json globs matched more than ${MAX_DISCOVERED_PLAY_FILES} play files. Tighten the globs.`,
168
+ `deepline.json globs matched more than ${MAX_DISCOVERED_FILES} files. Tighten the globs.`,
148
169
  );
149
170
  }
150
171
  }
@@ -160,3 +181,5 @@ export async function discoverPlayFiles(input: {
160
181
  await walk(input.projectRoot);
161
182
  return found.sort();
162
183
  }
184
+
185
+ export const discoverPlayFiles = discoverDeploymentFiles;
@@ -0,0 +1,52 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import type {
4
+ DeploymentMonitorEntry,
5
+ DeploymentMonitorOutcome,
6
+ } from './api-client';
7
+ import { DeeplineManifestError, discoverDeploymentFiles } from './manifest';
8
+
9
+ export async function readMonitorFiles(input: {
10
+ projectRoot: string;
11
+ rootDirectory: string;
12
+ globs: string[];
13
+ }): Promise<DeploymentMonitorEntry[]> {
14
+ const files = await discoverDeploymentFiles(input);
15
+ if (files.length > 100) {
16
+ throw new DeeplineManifestError(
17
+ 'A deployment may contain at most 100 monitor definitions. Split the repository into multiple linked targets.',
18
+ );
19
+ }
20
+ return Promise.all(
21
+ files.map(async (file) => {
22
+ const sourcePath = input.rootDirectory
23
+ ? `${input.rootDirectory}/${file}`
24
+ : file;
25
+ const raw = await readFile(join(input.projectRoot, file), 'utf8');
26
+ try {
27
+ return { file: sourcePath, definition: JSON.parse(raw) as unknown };
28
+ } catch {
29
+ throw new DeeplineManifestError(
30
+ `${sourcePath} must contain a monitor definition as valid JSON.`,
31
+ );
32
+ }
33
+ }),
34
+ );
35
+ }
36
+
37
+ export function monitorDeploymentSummary(
38
+ outcomes: DeploymentMonitorOutcome[],
39
+ ): string {
40
+ return outcomes
41
+ .map(
42
+ (outcome) =>
43
+ `- Monitor \`${outcome.key}\` (\`${outcome.file}\`): ${outcome.status}${
44
+ outcome.credits != null
45
+ ? ` (${outcome.credits} Deepline credits)`
46
+ : ''
47
+ }${outcome.jobId ? ` — job \`${outcome.jobId}\`` : ''}${
48
+ outcome.error ? `\n - ${outcome.error}` : ''
49
+ }${outcome.warning ? `\n - :warning: ${outcome.warning}` : ''}`,
50
+ )
51
+ .join('\n');
52
+ }
@@ -4,6 +4,8 @@ import type { PlayCompilerManifest } from '../compiler-manifest';
4
4
  import {
5
5
  DeployApiClient,
6
6
  DeployApiError,
7
+ type DeploymentMonitorEntry,
8
+ type DeploymentMonitorOutcome,
7
9
  type DeploymentPlayState,
8
10
  type RegisterArtifactEntry,
9
11
  } from './api-client';
@@ -13,6 +15,7 @@ import {
13
15
  DeeplineManifestError,
14
16
  DEFAULT_MANIFEST_FILE,
15
17
  } from './manifest';
18
+ import { monitorDeploymentSummary, readMonitorFiles } from './monitor-files';
16
19
  import type { SerializedPlayDeployment } from './project-serializer';
17
20
  import { resolveOwnedPlayReferenceName } from '../play-reference';
18
21
  import type { BundledPlayFileResult } from '../bundling/index';
@@ -173,6 +176,7 @@ export type PlayDeploymentRunResult = {
173
176
  | 'superseded'
174
177
  | 'cancelled';
175
178
  outcomes: PlayDeployOutcome[];
179
+ monitorOutcomes?: DeploymentMonitorOutcome[];
176
180
  checkRunId?: number;
177
181
  };
178
182
 
@@ -419,6 +423,11 @@ export async function runDeployment(
419
423
  }
420
424
 
421
425
  const buildSeconds = () => Math.max(0, (Date.now() - startedAt) / 1000);
426
+ let monitorOutcomes: DeploymentMonitorOutcome[] = [];
427
+ const monitorResults = () =>
428
+ monitorOutcomes.length > 0 ? { monitors: monitorOutcomes } : {};
429
+ const monitorRunResult = () =>
430
+ monitorOutcomes.length > 0 ? { monitorOutcomes } : {};
422
431
  const finishFailed = async (input: {
423
432
  error: string;
424
433
  outcomes: PlayDeployOutcome[];
@@ -428,7 +437,7 @@ export async function runDeployment(
428
437
  await setStatus({
429
438
  status: 'failed',
430
439
  error: input.error.slice(0, 2000),
431
- results: { plays: input.outcomes },
440
+ results: { plays: input.outcomes, ...monitorResults() },
432
441
  buildSeconds: buildSeconds(),
433
442
  ...(input.checkRunId ? { checkRunId: input.checkRunId } : {}),
434
443
  });
@@ -453,6 +462,7 @@ export async function runDeployment(
453
462
  return {
454
463
  finalStatus: 'failed',
455
464
  outcomes: input.outcomes,
465
+ ...monitorRunResult(),
456
466
  checkRunId: input.checkRunId,
457
467
  };
458
468
  };
@@ -462,13 +472,15 @@ export async function runDeployment(
462
472
  ): Promise<PlayDeploymentRunResult> => {
463
473
  // Carried by the terminal status rather than per-play results so every
464
474
  // reader — API, CLI, dashboard, alerting — sees it without parsing them.
465
- const finalStatus = outcomes.some((outcome) => outcome.warning)
475
+ const finalStatus = [...outcomes, ...monitorOutcomes].some(
476
+ (outcome) => outcome.warning,
477
+ )
466
478
  ? ('succeeded_with_warnings' as const)
467
479
  : ('succeeded' as const);
468
480
  try {
469
481
  await setStatus({
470
482
  status: finalStatus,
471
- results: { plays: outcomes },
483
+ results: { plays: outcomes, ...monitorResults() },
472
484
  buildSeconds: buildSeconds(),
473
485
  ...(checkRun ? { checkRunId: checkRun.checkRunId } : {}),
474
486
  });
@@ -494,6 +506,7 @@ export async function runDeployment(
494
506
  return {
495
507
  finalStatus,
496
508
  outcomes,
509
+ ...monitorRunResult(),
497
510
  checkRunId: checkRun?.checkRunId,
498
511
  };
499
512
  };
@@ -615,8 +628,9 @@ export async function runDeployment(
615
628
  await writeCheck({
616
629
  conclusion: 'neutral',
617
630
  title: `Deployment ${stopped}`,
618
- summary:
619
- 'This deployment was stopped before it finished. Nothing from it went live.',
631
+ summary: monitorOutcomes.some((outcome) => outcome.jobId)
632
+ ? 'This deployment was stopped before it finished. Monitor jobs already submitted may have changed live resources; inspect the jobs in its receipt.'
633
+ : 'This deployment was stopped before it finished. Nothing from it went live.',
620
634
  }).catch(() => undefined);
621
635
  throw revocation;
622
636
  });
@@ -687,6 +701,7 @@ export async function runDeployment(
687
701
  status: 'superseded',
688
702
  error: `Branch ${deploymentBranch} is now at ${head?.slice(0, 12) ?? '<deleted>'}; this deployment's commit is no longer the head.`,
689
703
  buildSeconds: buildSeconds(),
704
+ ...(monitorOutcomes.length > 0 ? { results: monitorResults() } : {}),
690
705
  ...(head ? { observedHeadSha: head } : {}),
691
706
  });
692
707
  return { finalStatus: 'superseded' as const, outcomes: [] };
@@ -703,12 +718,25 @@ export async function runDeployment(
703
718
  const manifestFile = target.manifestPath || DEFAULT_MANIFEST_FILE;
704
719
  let manifest;
705
720
  let playFiles: string[];
721
+ let monitorEntries: DeploymentMonitorEntry[] | undefined;
706
722
  try {
707
723
  manifest = await readDeeplineManifest(projectRoot, manifestFile);
708
724
  playFiles = await discoverPlayFiles({
709
725
  projectRoot,
710
726
  globs: manifest.plays,
711
727
  });
728
+ const discoveredMonitors = await readMonitorFiles({
729
+ projectRoot,
730
+ rootDirectory: target.rootDirectory,
731
+ globs: manifest.monitors,
732
+ });
733
+ if (!project && discoveredMonitors.length > 0) {
734
+ throw new DeeplineManifestError(
735
+ 'Monitor Git deployment requires a linked repository. Deploy individual monitor files with deepline monitors deploy --file.',
736
+ );
737
+ }
738
+ monitorEntries =
739
+ project && discoveredMonitors.length > 0 ? discoveredMonitors : undefined;
712
740
  } catch (error) {
713
741
  if (error instanceof DeeplineManifestError) {
714
742
  return await failWithCheck({
@@ -739,7 +767,29 @@ export async function runDeployment(
739
767
  }
740
768
  }
741
769
 
742
- if (playFiles.length === 0) {
770
+ if (monitorEntries !== undefined) {
771
+ const plan = await client.monitors('plan', monitorEntries);
772
+ monitorOutcomes = plan.monitors;
773
+ const failed = monitorOutcomes.filter(
774
+ (outcome) => outcome.status === 'failed',
775
+ );
776
+ if (failed.length > 0) {
777
+ return await failWithCheck({
778
+ title: 'Monitor validation failed',
779
+ summary: monitorDeploymentSummary(monitorOutcomes),
780
+ error: `${failed.length} monitor definition(s) failed validation.`,
781
+ outcomes: [],
782
+ annotations: failed.map((outcome) =>
783
+ annotationForFailure({
784
+ file: outcome.file,
785
+ message: outcome.error ?? 'Monitor validation failed.',
786
+ }),
787
+ ),
788
+ });
789
+ }
790
+ }
791
+
792
+ if (playFiles.length === 0 && monitorOutcomes.length === 0) {
743
793
  const checkRun = await postCheckOrNull({
744
794
  conclusion: 'neutral',
745
795
  title: 'No plays matched',
@@ -956,7 +1006,10 @@ export async function runDeployment(
956
1006
  }
957
1007
 
958
1008
  await setStatus({ status: 'publishing' }).catch(() => undefined);
959
- const states = await client.playsState(ordered.map((play) => play.name));
1009
+ const states =
1010
+ ordered.length > 0
1011
+ ? await client.playsState(ordered.map((play) => play.name))
1012
+ : { plays: [] };
960
1013
  const stateByName = new Map<string, DeploymentPlayState>(
961
1014
  states.plays.map((play) => [play.name, play]),
962
1015
  );
@@ -1170,13 +1223,21 @@ export async function runDeployment(
1170
1223
  // no validation signal (the deploy token dies with a terminal status).
1171
1224
  const checkRun = await postCheckOrNull({
1172
1225
  conclusion: 'success',
1173
- title: `${ordered.length} play${ordered.length === 1 ? '' : 's'} validated and registered (not deployed)`,
1226
+ title:
1227
+ monitorEntries !== undefined
1228
+ ? `${ordered.length} play(s) validated and registered; ${monitorEntries.length} monitor(s) checked (not deployed)`
1229
+ : `${ordered.length} play${ordered.length === 1 ? '' : 's'} validated and registered (not deployed)`,
1174
1230
  ...(annotations.length > 0 ? { annotations } : {}),
1175
- summary: outcomes
1176
- .map(
1177
- (outcome) =>
1178
- `- \`${outcome.name}\`: ${outcome.status}${outcome.version !== undefined ? ` (v${outcome.version})` : ''}${outcome.sdkUpdated ? ' — SDK updated' : ''}${outcome.warning ? `\n - :warning: ${outcome.warning}` : ''}`,
1179
- )
1231
+ summary: [
1232
+ outcomes
1233
+ .map(
1234
+ (outcome) =>
1235
+ `- \`${outcome.name}\`: ${outcome.status}${outcome.version !== undefined ? ` (v${outcome.version})` : ''}${outcome.sdkUpdated ? ' — SDK updated' : ''}${outcome.warning ? `\n - :warning: ${outcome.warning}` : ''}`,
1236
+ )
1237
+ .join('\n'),
1238
+ monitorDeploymentSummary(monitorOutcomes),
1239
+ ]
1240
+ .filter(Boolean)
1180
1241
  .join('\n'),
1181
1242
  });
1182
1243
  const result = await finishSucceeded(outcomes, checkRun);
@@ -1198,6 +1259,62 @@ export async function runDeployment(
1198
1259
  if (superseded) return superseded;
1199
1260
  }
1200
1261
 
1262
+ if (monitorEntries !== undefined) {
1263
+ monitorOutcomes = (await client.monitors('apply', monitorEntries))
1264
+ .monitors;
1265
+ // Provider changes are not atomic with Play promotion. Retain the exact
1266
+ // jobs in the deployment receipt before waiting or reporting a failure.
1267
+ await setStatus({
1268
+ status: 'publishing',
1269
+ results: { plays: outcomes, ...monitorResults() },
1270
+ });
1271
+ const deadline = Date.now() + 10 * 60_000;
1272
+ while (monitorOutcomes.some((outcome) => outcome.status === 'applying')) {
1273
+ if (Date.now() >= deadline) {
1274
+ throw new RetryableDeploymentError(
1275
+ 'Monitor jobs are still reconciling. Retrying this deployment will resume the same jobs.',
1276
+ );
1277
+ }
1278
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1279
+ for (let index = 0; index < monitorOutcomes.length; index += 1) {
1280
+ const outcome = monitorOutcomes[index];
1281
+ if (outcome.status !== 'applying') continue;
1282
+ if (!outcome.jobId) {
1283
+ throw new Error(
1284
+ `Monitor ${outcome.key} has no reconciliation job.`,
1285
+ );
1286
+ }
1287
+ const result = await client.monitorJob(outcome.jobId, outcome.key);
1288
+ const settled = result.monitors.find(
1289
+ (monitor) =>
1290
+ monitor.key === outcome.key && monitor.jobId === outcome.jobId,
1291
+ );
1292
+ if (!settled) {
1293
+ throw new Error(`Monitor job ${outcome.jobId} returned no result.`);
1294
+ }
1295
+ monitorOutcomes[index] = { ...outcome, ...settled };
1296
+ }
1297
+ }
1298
+ if (
1299
+ monitorOutcomes.some(
1300
+ (outcome) =>
1301
+ outcome.status === 'failed' || outcome.status === 'planned',
1302
+ )
1303
+ ) {
1304
+ return await failWithCheck({
1305
+ title: 'Monitor deployment failed',
1306
+ summary: monitorDeploymentSummary(monitorOutcomes),
1307
+ error:
1308
+ 'Some monitor changes failed. Applied changes remain live; inspect the reported jobs before retrying.',
1309
+ outcomes,
1310
+ });
1311
+ }
1312
+ // Monitor reconciliation may take minutes. Recheck the branch before
1313
+ // publishing Plays; a newer push must not be reverted by this one.
1314
+ const superseded = await supersedeIfHeadMoved();
1315
+ if (superseded) return { ...superseded, ...monitorRunResult() };
1316
+ }
1317
+
1201
1318
  if (toPromote.length > 0) {
1202
1319
  try {
1203
1320
  const promotion = await client.promote(toPromote);
@@ -1280,13 +1397,22 @@ export async function runDeployment(
1280
1397
 
1281
1398
  const checkRun = await postCheckOrNull({
1282
1399
  conclusion: 'success',
1283
- title: `Deployed ${outcomes.filter((o) => o.status === 'promoted').length} play(s) (${outcomes.filter((o) => o.status === 'unchanged').length} unchanged)`,
1400
+ title: `Deployed ${outcomes.filter((o) => o.status === 'promoted').length} play(s) (${outcomes.filter((o) => o.status === 'unchanged').length} unchanged)${
1401
+ monitorEntries !== undefined
1402
+ ? `; ${monitorOutcomes.filter((outcome) => outcome.status === 'applied').length} monitor(s) applied`
1403
+ : ''
1404
+ }`,
1284
1405
  ...(annotations.length > 0 ? { annotations } : {}),
1285
- summary: outcomes
1286
- .map(
1287
- (outcome) =>
1288
- `- \`${outcome.name}\`: ${outcome.status}${outcome.sdkUpdated ? ' — SDK updated' : ''}${outcome.warning ? `\n - :warning: ${outcome.warning}` : ''}`,
1289
- )
1406
+ summary: [
1407
+ outcomes
1408
+ .map(
1409
+ (outcome) =>
1410
+ `- \`${outcome.name}\`: ${outcome.status}${outcome.sdkUpdated ? ' — SDK updated' : ''}${outcome.warning ? `\n - :warning: ${outcome.warning}` : ''}`,
1411
+ )
1412
+ .join('\n'),
1413
+ monitorDeploymentSummary(monitorOutcomes),
1414
+ ]
1415
+ .filter(Boolean)
1290
1416
  .join('\n'),
1291
1417
  });
1292
1418
  return await finishSucceeded(outcomes, checkRun);
@@ -1342,6 +1468,7 @@ export async function runDeployment(
1342
1468
  await setStatus({
1343
1469
  status: 'failed',
1344
1470
  error: message.slice(0, 2000),
1471
+ ...(monitorOutcomes.length > 0 ? { results: monitorResults() } : {}),
1345
1472
  buildSeconds: buildSeconds(),
1346
1473
  ...(checkRunId ? { checkRunId } : {}),
1347
1474
  }).catch(() => undefined);