deepline 0.3.213 → 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.
Files changed (33) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +122 -32
  2. package/dist/bundling-sources/sdk/src/compat.ts +2 -0
  3. package/dist/bundling-sources/sdk/src/deployment-protection.ts +19 -0
  4. package/dist/bundling-sources/sdk/src/http.ts +3 -7
  5. package/dist/bundling-sources/sdk/src/index.ts +7 -0
  6. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  7. package/dist/bundling-sources/sdk/src/types.ts +25 -0
  8. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +18 -0
  9. package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +1 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +33 -7
  11. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +11 -1
  12. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-heartbeat.ts +3 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +53 -1
  14. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +187 -6
  15. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +12 -3
  16. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +5 -2
  17. package/dist/bundling-sources/shared_libs/plays/deploy/api-client.ts +40 -0
  18. package/dist/bundling-sources/shared_libs/plays/deploy/manifest.ts +51 -28
  19. package/dist/bundling-sources/shared_libs/plays/deploy/monitor-files.ts +52 -0
  20. package/dist/bundling-sources/shared_libs/plays/deploy/run-deployment.ts +146 -19
  21. package/dist/bundling-sources/shared_libs/plays/settings-contract.ts +176 -0
  22. package/dist/cli/index.js +1046 -416
  23. package/dist/cli/index.mjs +894 -264
  24. package/dist/index.d.mts +165 -3
  25. package/dist/index.d.ts +165 -3
  26. package/dist/index.js +67 -35
  27. package/dist/index.mjs +67 -35
  28. package/dist/install-integrity.json +3 -0
  29. package/dist/release.d.mts +1 -1
  30. package/dist/release.d.ts +1 -1
  31. package/dist/release.js +1 -1
  32. package/dist/release.mjs +1 -1
  33. package/package.json +3 -2
@@ -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);
@@ -0,0 +1,176 @@
1
+ import { z } from 'zod';
2
+
3
+ /** Keep schema diagnostics readable in both interactive settings and the CLI. */
4
+ export function formatPlaySettingsError(error: unknown): string {
5
+ if (error instanceof z.ZodError)
6
+ return [...new Set(error.issues.map((issue) => issue.message))].join(' ');
7
+ return error instanceof Error ? error.message : String(error);
8
+ }
9
+
10
+ /** Location organizes Plays. Existing workspace permissions still apply. */
11
+ export const PLAY_LOCATION_ROOTS = [
12
+ {
13
+ value: 'person',
14
+ label: 'Personal',
15
+ description:
16
+ 'The creator’s personal root, displayed as their name followed by “’s team”.',
17
+ },
18
+ {
19
+ value: 'team',
20
+ label: 'Team library',
21
+ description:
22
+ 'The shared team library. Publishing moves personal locations here, keeping their folders.',
23
+ },
24
+ ] as const;
25
+
26
+ const folder = z
27
+ .string()
28
+ .min(1)
29
+ .max(128)
30
+ .refine(
31
+ (value) =>
32
+ value === value.trim() &&
33
+ value !== '.' &&
34
+ value !== '..' &&
35
+ !/[\\/\u0000-\u001f\u007f]/u.test(value),
36
+ 'Folder names must be trimmed, cannot be . or .., and cannot contain slashes or control characters.',
37
+ );
38
+ export const playPathSchema = z
39
+ .union([
40
+ z.null(),
41
+ z
42
+ .object({
43
+ root: z.enum(['person', 'team']),
44
+ folders: z.array(folder).max(16).default([]),
45
+ })
46
+ .strict(),
47
+ ])
48
+ .transform((path) =>
49
+ path?.root === 'person' && path.folders.length === 0 ? null : path,
50
+ );
51
+ export type PlayPath = z.output<typeof playPathSchema>;
52
+
53
+ export function parsePlayPath(value: string): PlayPath {
54
+ const [root, ...parts] = value.split('/');
55
+ if (!PLAY_LOCATION_ROOTS.some((option) => option.value === root)) {
56
+ throw new Error(
57
+ 'Path must start with person or team. Example: person/Research/ICP.',
58
+ );
59
+ }
60
+ let folders: string[];
61
+ try {
62
+ folders = parts.map(decodeURIComponent);
63
+ } catch {
64
+ throw new Error(
65
+ 'Path contains an invalid percent escape. Encode a literal % as %25.',
66
+ );
67
+ }
68
+ return playPathSchema.parse({ root, folders });
69
+ }
70
+ export function formatPlayPath(path: PlayPath): string {
71
+ return path === null
72
+ ? 'person'
73
+ : [path.root, ...path.folders.map(encodeURIComponent)].join('/');
74
+ }
75
+ export function personalPlayRootLabel(ownerName?: string | null): string {
76
+ const name = ownerName?.trim();
77
+ return name ? `${name.split(/\s+/)[0]}’s team` : 'Personal';
78
+ }
79
+ export function displayPlayPath(
80
+ path: PlayPath,
81
+ ownerName?: string | null,
82
+ ): string {
83
+ return [
84
+ path?.root === 'team' ? 'Team library' : personalPlayRootLabel(ownerName),
85
+ ...(path?.folders ?? []),
86
+ ].join(' / ');
87
+ }
88
+ export function publishedPlayPath(path: PlayPath): PlayPath {
89
+ return { root: 'team', folders: path?.folders ?? [] };
90
+ }
91
+
92
+ /** One registry drives validation, defaults, CLI keys/help and settings controls. */
93
+ export const PLAY_SETTING_FIELDS = {
94
+ path: {
95
+ schema: playPathSchema
96
+ .default(null)
97
+ .describe(
98
+ 'Play location. Null means the creator’s personal root. Publishing moves it into Team library, preserving folders.',
99
+ ),
100
+ label: 'Location',
101
+ description:
102
+ 'Organize this Play in the creator’s personal root or Team library. Publishing moves it to Team library and keeps its folders. Workspace sharing permissions apply in both roots.',
103
+ get defaultValue(): PlayPath {
104
+ return this.schema.parse(undefined);
105
+ },
106
+ cli: {
107
+ valueSyntax: 'person[/folder/…] | team[/folder/…] | null',
108
+ examples: [
109
+ 'person',
110
+ 'person/Research/ICP',
111
+ 'team',
112
+ 'team/Research',
113
+ 'null',
114
+ ],
115
+ parse: (value: string) =>
116
+ value === 'null' ? null : parsePlayPath(value),
117
+ format: formatPlayPath,
118
+ },
119
+ ui: { control: 'play-location-picker' as const },
120
+ },
121
+ } as const;
122
+ export const playSettingsSchema = z.object(
123
+ Object.fromEntries(
124
+ Object.entries(PLAY_SETTING_FIELDS).map(([key, field]) => [
125
+ key,
126
+ field.schema,
127
+ ]),
128
+ ) as {
129
+ [K in keyof typeof PLAY_SETTING_FIELDS]: (typeof PLAY_SETTING_FIELDS)[K]['schema'];
130
+ },
131
+ ).passthrough();
132
+ export const playSettingsPatchSchema = z
133
+ .object(
134
+ Object.fromEntries(
135
+ Object.entries(PLAY_SETTING_FIELDS).map(([key, field]) => [
136
+ key,
137
+ field.schema.unwrap().optional(),
138
+ ]),
139
+ ) as {
140
+ [K in keyof typeof PLAY_SETTING_FIELDS]: z.ZodOptional<
141
+ ReturnType<(typeof PLAY_SETTING_FIELDS)[K]['schema']['unwrap']>
142
+ >;
143
+ },
144
+ )
145
+ .strict()
146
+ .refine(
147
+ (patch) => Object.keys(patch).length > 0,
148
+ 'Supply at least one setting.',
149
+ );
150
+ export type PlaySettings = {
151
+ [K in keyof typeof PLAY_SETTING_FIELDS]: z.output<
152
+ (typeof PLAY_SETTING_FIELDS)[K]['schema']
153
+ >;
154
+ };
155
+ export type PlaySettingsPatch = z.output<typeof playSettingsPatchSchema>;
156
+ /** Validate registered keys while preserving future keys in stored objects. */
157
+ export function parsePlaySettings(value: unknown): PlaySettings {
158
+ return playSettingsSchema.parse(value) as PlaySettings;
159
+ }
160
+ export const DEFAULT_PLAY_SETTINGS: PlaySettings = parsePlaySettings({});
161
+ export type PlaySettingsResult = {
162
+ playDefinitionId: string;
163
+ ownerUserId: string | null;
164
+ ownerName: string | null;
165
+ settings: PlaySettings;
166
+ };
167
+ export type PlaySettingsPathsPage = {
168
+ paths: Array<{
169
+ path: PlayPath;
170
+ ownerUserId: string | null;
171
+ ownerName: string | null;
172
+ playName: string;
173
+ }>;
174
+ cursor: string | null;
175
+ hasMore: boolean;
176
+ };