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.
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/plays/deploy/api-client.ts +40 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/manifest.ts +51 -28
- package/dist/bundling-sources/shared_libs/plays/deploy/monitor-files.ts +52 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/run-deployment.ts +146 -19
- package/dist/cli/index.js +487 -325
- package/dist/cli/index.mjs +335 -173
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/install-integrity.json +1 -0
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
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
|
|
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 {
|
|
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 {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 {
|
|
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
|
|
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 >
|
|
166
|
+
if (found.length > MAX_DISCOVERED_FILES) {
|
|
146
167
|
throw new DeeplineManifestError(
|
|
147
|
-
`deepline.json globs matched more than ${
|
|
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(
|
|
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.
|
|
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 (
|
|
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 =
|
|
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:
|
|
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:
|
|
1176
|
-
|
|
1177
|
-
(
|
|
1178
|
-
|
|
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:
|
|
1286
|
-
|
|
1287
|
-
(
|
|
1288
|
-
|
|
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);
|