@sanity/workflow-cli 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +485 -0
- package/README.md +145 -63
- package/bin/run.js +0 -4
- package/dist/commands/abort.d.ts +8 -12
- package/dist/commands/abort.js +24 -34
- package/dist/commands/definition/delete.d.ts +6 -6
- package/dist/commands/definition/delete.js +17 -33
- package/dist/commands/definition/diff.d.ts +10 -3
- package/dist/commands/definition/diff.js +21 -29
- package/dist/commands/definition/list.d.ts +5 -6
- package/dist/commands/definition/list.js +44 -50
- package/dist/commands/definition/show.d.ts +22 -13
- package/dist/commands/definition/show.js +25 -36
- package/dist/commands/deploy.d.ts +67 -8
- package/dist/commands/deploy.js +136 -32
- package/dist/commands/diagnose.d.ts +22 -4
- package/dist/commands/diagnose.js +45 -61
- package/dist/commands/fire-action.d.ts +7 -30
- package/dist/commands/fire-action.js +42 -115
- package/dist/commands/list.d.ts +9 -10
- package/dist/commands/list.js +49 -62
- package/dist/commands/{retry-activity.d.ts → reset-activity.d.ts} +1 -1
- package/dist/commands/{retry-activity.js → reset-activity.js} +2 -3
- package/dist/commands/set-stage.d.ts +27 -3
- package/dist/commands/set-stage.js +63 -12
- package/dist/commands/show.d.ts +3 -2
- package/dist/commands/show.js +15 -21
- package/dist/commands/start.d.ts +56 -0
- package/dist/commands/start.js +133 -0
- package/dist/commands/tail.d.ts +5 -2
- package/dist/commands/tail.js +25 -26
- package/dist/hooks/finally/telemetry.d.ts +8 -0
- package/dist/hooks/finally/telemetry.js +13 -0
- package/dist/hooks/prerun/telemetry.d.ts +5 -0
- package/dist/hooks/prerun/telemetry.js +5 -0
- package/dist/index.js +0 -3
- package/dist/lib/base-command.d.ts +14 -0
- package/dist/lib/base-command.js +10 -0
- package/dist/lib/client.d.ts +11 -14
- package/dist/lib/client.js +29 -34
- package/dist/lib/context.d.ts +98 -0
- package/dist/lib/context.js +83 -0
- package/dist/lib/definitions.d.ts +38 -29
- package/dist/lib/definitions.js +34 -93
- package/dist/lib/diff.d.ts +5 -2
- package/dist/lib/diff.js +62 -20
- package/dist/lib/env.d.ts +4 -6
- package/dist/lib/env.js +4 -9
- package/dist/lib/fail.d.ts +12 -0
- package/dist/lib/fail.js +25 -16
- package/dist/lib/flags.d.ts +7 -2
- package/dist/lib/flags.js +7 -3
- package/dist/lib/load-config.d.ts +15 -0
- package/dist/lib/load-config.js +89 -0
- package/dist/lib/operation-args.d.ts +20 -15
- package/dist/lib/operation-args.js +9 -40
- package/dist/lib/ops-report.d.ts +29 -13
- package/dist/lib/ops-report.js +20 -17
- package/dist/lib/params.d.ts +7 -0
- package/dist/lib/params.js +18 -0
- package/dist/lib/read-fanout.d.ts +22 -0
- package/dist/lib/read-fanout.js +28 -0
- package/dist/lib/select-deployment.d.ts +31 -0
- package/dist/lib/select-deployment.js +37 -0
- package/dist/lib/share-definitions.d.ts +86 -0
- package/dist/lib/share-definitions.js +106 -0
- package/dist/lib/stub.js +0 -7
- package/dist/lib/telemetry-setup.d.ts +66 -0
- package/dist/lib/telemetry-setup.js +92 -0
- package/dist/lib/telemetry.d.ts +100 -0
- package/dist/lib/telemetry.js +89 -0
- package/dist/lib/ui.d.ts +33 -1
- package/dist/lib/ui.js +32 -21
- package/oclif.manifest.json +133 -113
- package/package.json +16 -8
- package/dist/commands/move-stage.d.ts +0 -52
- package/dist/commands/move-stage.js +0 -86
- package/dist/lib/config.d.ts +0 -18
- package/dist/lib/config.js +0 -50
|
@@ -1,28 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE,
|
|
3
|
-
import { Table } from 'console-table-printer';
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, tagScopeFilter, } from '@sanity/workflow-engine';
|
|
4
3
|
import logSymbols from 'log-symbols';
|
|
5
|
-
import {
|
|
4
|
+
import { WorkflowCommand } from "../../lib/base-command.js";
|
|
5
|
+
import { resolveReadTargets } from "../../lib/context.js";
|
|
6
6
|
import { tagFlags } from "../../lib/flags.js";
|
|
7
|
+
import { runReadAcrossTargets } from "../../lib/read-fanout.js";
|
|
8
|
+
import { clipToLimit, formatTable } from "../../lib/ui.js";
|
|
7
9
|
export function buildDefinitionListQuery(flags) {
|
|
8
|
-
const params = { limit: flags.limit };
|
|
10
|
+
const params = { limit: flags.limit + 1 };
|
|
9
11
|
const filters = [`_type == "${WORKFLOW_DEFINITION_TYPE}"`];
|
|
10
12
|
const instanceFilters = [
|
|
11
13
|
`_type == "${WORKFLOW_INSTANCE_TYPE}"`,
|
|
12
14
|
'definition == ^.name',
|
|
13
15
|
'pinnedVersion == ^.version',
|
|
14
|
-
// Correlate the count to each definition's own partition. Listing
|
|
15
|
-
// can span partitions (when `--tag` is omitted), and two partitions
|
|
16
|
-
// can hold a same-named, same-version definition — without this, one
|
|
17
|
-
// row's count would bleed in the other partition's instances.
|
|
18
16
|
'tag == ^.tag',
|
|
19
17
|
];
|
|
20
18
|
if (flags.tag) {
|
|
21
19
|
params.tag = flags.tag;
|
|
22
20
|
filters.push(tagScopeFilter());
|
|
23
21
|
}
|
|
24
|
-
if (flags
|
|
25
|
-
params.name = flags
|
|
22
|
+
if (flags.name) {
|
|
23
|
+
params.name = flags.name;
|
|
26
24
|
filters.push('name == $name');
|
|
27
25
|
}
|
|
28
26
|
const instanceMatch = instanceFilters.join(' && ');
|
|
@@ -31,14 +29,14 @@ export function buildDefinitionListQuery(flags) {
|
|
|
31
29
|
version,
|
|
32
30
|
title,
|
|
33
31
|
tag,
|
|
34
|
-
|
|
32
|
+
_createdAt,
|
|
35
33
|
"stageCount": count(stages),
|
|
36
34
|
"inFlightCount": count(*[${instanceMatch} && !defined(completedAt)]),
|
|
37
35
|
"totalInstances": count(*[${instanceMatch}])
|
|
38
36
|
}`;
|
|
39
37
|
return { groq, params };
|
|
40
38
|
}
|
|
41
|
-
export default class DefinitionList extends
|
|
39
|
+
export default class DefinitionList extends WorkflowCommand {
|
|
42
40
|
static description = 'List deployed workflow definitions.';
|
|
43
41
|
static examples = [
|
|
44
42
|
'<%= config.bin %> definition list',
|
|
@@ -50,49 +48,45 @@ export default class DefinitionList extends Command {
|
|
|
50
48
|
description: 'The maximum number of definitions to return.',
|
|
51
49
|
default: 100,
|
|
52
50
|
}),
|
|
53
|
-
|
|
54
|
-
description: 'Filter to a single workflow definition name (e.g. product-launch)',
|
|
51
|
+
name: Flags.string({
|
|
52
|
+
description: 'Filter to a single workflow definition name (e.g. product-launch).',
|
|
55
53
|
}),
|
|
56
54
|
};
|
|
57
55
|
async run() {
|
|
58
56
|
const { flags } = await this.parse(DefinitionList);
|
|
57
|
+
const targets = await resolveReadTargets(flags);
|
|
59
58
|
const { groq, params } = buildDefinitionListQuery(flags);
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
{
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
59
|
+
const failures = await runReadAcrossTargets({
|
|
60
|
+
targets,
|
|
61
|
+
log: (line) => this.log(line),
|
|
62
|
+
run: async ({ client }) => {
|
|
63
|
+
const fetched = await client.fetch(groq, params, {
|
|
64
|
+
tag: 'definition.list',
|
|
65
|
+
});
|
|
66
|
+
if (!fetched.length) {
|
|
67
|
+
this.log(`${logSymbols.info} no definitions found`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const { rows, note } = clipToLimit(fetched, flags.limit);
|
|
71
|
+
const lines = formatTable(['workflow', 'title', 'tag', 'stages', 'in flight', 'instances', 'created'], rows.map((r) => [
|
|
72
|
+
`${r.name} v${r.version}`,
|
|
73
|
+
r.title,
|
|
74
|
+
r.tag,
|
|
75
|
+
String(r.stageCount),
|
|
76
|
+
String(r.inFlightCount),
|
|
77
|
+
String(r.totalInstances),
|
|
78
|
+
r._createdAt,
|
|
79
|
+
]));
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
this.log(line);
|
|
82
|
+
}
|
|
83
|
+
if (note) {
|
|
84
|
+
this.log(note);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
76
87
|
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// repeat Workflow + Tag — and no command takes an `_id` anyway (they take
|
|
80
|
-
// name + --tag/--version). `name vN` is this CLI's identifier format (the
|
|
81
|
-
// `definition show` title). Only declared keys are passed — console-table-
|
|
82
|
-
// printer renders any extra key as a stray column.
|
|
83
|
-
//
|
|
84
|
-
// `child`-role defs are spawn-only; the Startable column tells an admin
|
|
85
|
-
// which definitions a human can actually start standalone.
|
|
86
|
-
table.addRow({
|
|
87
|
-
workflow: `${r.name} v${r.version}`,
|
|
88
|
-
title: r.title,
|
|
89
|
-
tag: r.tag,
|
|
90
|
-
startable: isStartableDefinition(r) ? 'yes' : 'spawn-only',
|
|
91
|
-
stageCount: r.stageCount,
|
|
92
|
-
inFlightCount: r.inFlightCount,
|
|
93
|
-
totalInstances: r.totalInstances,
|
|
94
|
-
});
|
|
88
|
+
if (failures.length > 0) {
|
|
89
|
+
this.exit(1);
|
|
95
90
|
}
|
|
96
|
-
table.printTable();
|
|
97
91
|
}
|
|
98
92
|
}
|
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
tag?: string | undefined;
|
|
6
|
-
version?: number | undefined;
|
|
7
|
-
}
|
|
8
|
-
export declare function buildDefinitionShowQuery(flags: DefinitionShowFlags): {
|
|
9
|
-
groq: string;
|
|
10
|
-
params: Record<string, unknown>;
|
|
11
|
-
};
|
|
12
|
-
export default class DefinitionShow extends Command {
|
|
1
|
+
import { type DeployedDefinition, type WorkflowDefinition, type WorkflowResource } from '@sanity/workflow-engine';
|
|
2
|
+
import { WorkflowCommand } from '../../lib/base-command.ts';
|
|
3
|
+
import { type ReadTarget } from '../../lib/context.ts';
|
|
4
|
+
export default class DefinitionShow extends WorkflowCommand {
|
|
13
5
|
static description: string;
|
|
14
6
|
static args: {
|
|
15
7
|
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
@@ -20,6 +12,24 @@ export default class DefinitionShow extends Command {
|
|
|
20
12
|
};
|
|
21
13
|
run(): Promise<void>;
|
|
22
14
|
}
|
|
15
|
+
/** How a lookup names the version it asked for: `vN` for a pinned
|
|
16
|
+
* `--version`, `latest` when unpinned. */
|
|
17
|
+
export declare function versionLabel(version: number | undefined): string;
|
|
18
|
+
/**
|
|
19
|
+
* One deployed document per target that has the name: the authored
|
|
20
|
+
* {@link WorkflowDefinition} plus the deploy-stamped envelope (`version`,
|
|
21
|
+
* `contentHash`, `tag`). A probe failure propagates — with an unreadable
|
|
22
|
+
* resource, "which dataset holds this name" is unanswerable, unlike an
|
|
23
|
+
* instance-id lookup where any hit IS the instance.
|
|
24
|
+
*/
|
|
25
|
+
export declare function fetchDefinitionHits({ targets, groq, params, }: {
|
|
26
|
+
targets: ReadTarget[];
|
|
27
|
+
groq: string;
|
|
28
|
+
params: Record<string, unknown>;
|
|
29
|
+
}): Promise<{
|
|
30
|
+
resource: WorkflowResource;
|
|
31
|
+
def: DeployedDefinition;
|
|
32
|
+
}[]>;
|
|
23
33
|
/** The summary block at the top of `definition show`: a bold name + version
|
|
24
34
|
* title, then the human title, description, and deploy `tag` as aligned
|
|
25
35
|
* detail rows. `tag` is deployment metadata (the partition), not part of the
|
|
@@ -32,4 +42,3 @@ export declare function definitionHeader(def: WorkflowDefinition & {
|
|
|
32
42
|
* activity's actions. Pure so it can be asserted without driving the oclif command.
|
|
33
43
|
*/
|
|
34
44
|
export declare function describeDefinition(def: WorkflowDefinition): string[];
|
|
35
|
-
export {};
|
|
@@ -1,25 +1,13 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
|
-
import { Args,
|
|
3
|
-
import { isTerminalStage,
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { isTerminalStage, } from '@sanity/workflow-engine';
|
|
4
4
|
import logSymbols from 'log-symbols';
|
|
5
|
-
import {
|
|
5
|
+
import { WorkflowCommand } from "../../lib/base-command.js";
|
|
6
|
+
import { resolveReadTargets, soleHitOrFail } from "../../lib/context.js";
|
|
7
|
+
import { buildDefinitionShowQuery } from "../../lib/definitions.js";
|
|
6
8
|
import { tagFlags } from "../../lib/flags.js";
|
|
7
9
|
import { formatKeyValue, sectionHeader } from "../../lib/ui.js";
|
|
8
|
-
export
|
|
9
|
-
const params = { name: flags.name };
|
|
10
|
-
const filters = [`_type == "${WORKFLOW_DEFINITION_TYPE}"`, 'name == $name'];
|
|
11
|
-
if (flags.version !== undefined) {
|
|
12
|
-
params.version = flags.version;
|
|
13
|
-
filters.push('version == $version');
|
|
14
|
-
}
|
|
15
|
-
if (flags.tag) {
|
|
16
|
-
params.tag = flags.tag;
|
|
17
|
-
filters.push(tagScopeFilter());
|
|
18
|
-
}
|
|
19
|
-
const groq = `*[${filters.join(' && ')}] | order(version desc) [0]`;
|
|
20
|
-
return { groq, params };
|
|
21
|
-
}
|
|
22
|
-
export default class DefinitionShow extends Command {
|
|
10
|
+
export default class DefinitionShow extends WorkflowCommand {
|
|
23
11
|
static description = 'Show a deployed workflow definition.';
|
|
24
12
|
static args = {
|
|
25
13
|
name: Args.string({ required: true, description: 'Workflow definition name.' }),
|
|
@@ -30,18 +18,18 @@ export default class DefinitionShow extends Command {
|
|
|
30
18
|
};
|
|
31
19
|
async run() {
|
|
32
20
|
const { args, flags } = await this.parse(DefinitionShow);
|
|
21
|
+
const targets = await resolveReadTargets(flags);
|
|
33
22
|
const { groq, params } = buildDefinitionShowQuery({
|
|
34
23
|
name: args.name,
|
|
35
24
|
tag: flags.tag,
|
|
36
25
|
version: flags.version,
|
|
37
26
|
});
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
// deploy-stamped envelope (`version`, `contentHash`, `tag`).
|
|
41
|
-
const def = await client.fetch(groq, params);
|
|
27
|
+
const hits = await fetchDefinitionHits({ targets, groq, params });
|
|
28
|
+
const def = soleHitOrFail(hits, `Definition "${args.name}"`)?.def;
|
|
42
29
|
if (!def) {
|
|
43
|
-
|
|
44
|
-
|
|
30
|
+
this.error(`${logSymbols.error} no definition "${args.name}" (${versionLabel(flags.version)})`, {
|
|
31
|
+
exit: 1,
|
|
32
|
+
});
|
|
45
33
|
}
|
|
46
34
|
this.log(definitionHeader(def, def.tag));
|
|
47
35
|
this.log('');
|
|
@@ -50,11 +38,19 @@ export default class DefinitionShow extends Command {
|
|
|
50
38
|
}
|
|
51
39
|
}
|
|
52
40
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
41
|
+
export function versionLabel(version) {
|
|
42
|
+
return version === undefined ? 'latest' : `v${version}`;
|
|
43
|
+
}
|
|
44
|
+
export async function fetchDefinitionHits({ targets, groq, params, }) {
|
|
45
|
+
const probes = await Promise.all(targets.map(async (target) => {
|
|
46
|
+
const def = await target.client.fetch(groq, params, {
|
|
47
|
+
tag: 'definition.show',
|
|
48
|
+
});
|
|
49
|
+
return def === null ? undefined : { resource: target.resource, def };
|
|
50
|
+
}));
|
|
51
|
+
return probes.filter((hit) => hit !== undefined);
|
|
52
|
+
}
|
|
53
|
+
const HEADER_PAD = 11;
|
|
58
54
|
export function definitionHeader(def, tag) {
|
|
59
55
|
const rows = [
|
|
60
56
|
formatKeyValue({ key: 'Title', value: def.title, padTo: HEADER_PAD }),
|
|
@@ -69,18 +65,15 @@ export function definitionHeader(def, tag) {
|
|
|
69
65
|
}
|
|
70
66
|
return [styleText('bold', `${def.name} v${def.version}`), ...rows].join('\n');
|
|
71
67
|
}
|
|
72
|
-
/** One `· action` line, annotated with the terminal status its `status.set` op sets, if any. */
|
|
73
68
|
function actionLine(action) {
|
|
74
69
|
const statusOp = (action.ops ?? []).find((op) => op.type === 'status.set');
|
|
75
70
|
const status = statusOp ? styleText('dim', ` → ${statusOp.status}`) : '';
|
|
76
71
|
return ` · ${action.name}${status}`;
|
|
77
72
|
}
|
|
78
|
-
/** A `- activity` line followed by one line per action it exposes. */
|
|
79
73
|
function activityBlock(activity) {
|
|
80
74
|
const title = activity.title ? styleText('dim', ` — ${activity.title}`) : '';
|
|
81
75
|
return [` - ${activity.name}${title}`, ...(activity.actions ?? []).map(actionLine)];
|
|
82
76
|
}
|
|
83
|
-
/** A `• stage` line (with initial/terminal markers) followed by its activity blocks. */
|
|
84
77
|
function stageBlock(stage, initialStage) {
|
|
85
78
|
const title = stage.title ? styleText('dim', ` — ${stage.title}`) : '';
|
|
86
79
|
const terminal = isTerminalStage(stage) ? styleText('dim', ' (terminal)') : '';
|
|
@@ -90,10 +83,6 @@ function stageBlock(stage, initialStage) {
|
|
|
90
83
|
...(stage.activities ?? []).flatMap(activityBlock),
|
|
91
84
|
];
|
|
92
85
|
}
|
|
93
|
-
/**
|
|
94
|
-
* The body lines for `definition show`: every stage with its activities and each
|
|
95
|
-
* activity's actions. Pure so it can be asserted without driving the oclif command.
|
|
96
|
-
*/
|
|
97
86
|
export function describeDefinition(def) {
|
|
98
87
|
return [
|
|
99
88
|
sectionHeader('Stages'),
|
|
@@ -1,22 +1,81 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { type DeployDefinitionResult, type WorkflowDefinition, type WorkflowDeployment } from '@sanity/workflow-engine';
|
|
2
|
+
import { WorkflowCommand } from '../lib/base-command.ts';
|
|
3
|
+
/** A deployment paired with the definitions selected + validated for it. */
|
|
4
|
+
interface DeployBatch {
|
|
5
|
+
deployment: WorkflowDeployment;
|
|
6
|
+
defs: WorkflowDefinition[];
|
|
7
|
+
}
|
|
8
|
+
/** A deployment whose deploy/diff threw, kept for the closing summary. */
|
|
9
|
+
interface DeployFailure {
|
|
10
|
+
deployment: Pick<WorkflowDeployment, 'name' | 'tag'>;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
export default class Deploy extends WorkflowCommand {
|
|
3
14
|
static description: string;
|
|
4
15
|
static examples: string[];
|
|
5
16
|
static flags: {
|
|
17
|
+
'all-tags': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
18
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
19
|
check: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
20
|
only: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
+
'share-definitions-with-sanity': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
22
|
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
23
|
};
|
|
11
24
|
run(): Promise<void>;
|
|
25
|
+
/** Deploy (or diff, for `--dry-run`) one deployment's definitions.
|
|
26
|
+
* Returns the per-definition results for a real deploy; `undefined`
|
|
27
|
+
* for a dry run, which writes nothing. */
|
|
28
|
+
private processDeployment;
|
|
12
29
|
private executeDeploy;
|
|
13
30
|
private runDryRun;
|
|
14
31
|
}
|
|
15
32
|
/** Reject the contradictory `--check --dry-run` combination. */
|
|
16
33
|
export declare function validateModeFlags(check: boolean, dryRun: boolean): void;
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Select and validate every deployment's definitions up front. Validation runs
|
|
36
|
+
* before any network write ({@link validateOrFail} exits on a bad definition),
|
|
37
|
+
* so a whole-config deploy can never leave a partial reconcile behind a broken
|
|
38
|
+
* batch. In a multi-deployment run the failure names its deployment — it exits
|
|
39
|
+
* before any banner could attribute it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildBatches(deployments: WorkflowDeployment[], only: string | undefined): DeployBatch[];
|
|
42
|
+
/**
|
|
43
|
+
* Run `processBatch` over each batch, continuing past a failure so one bad
|
|
44
|
+
* deployment can't strand the rest — deployments are independent, and deploys
|
|
45
|
+
* are idempotent, so a partial run is safe to re-run. Failures are flattened
|
|
46
|
+
* to their message for {@link deploySummary}. A single-batch run rethrows
|
|
47
|
+
* instead: it has no summary, so a throw from before the deploy spinner starts
|
|
48
|
+
* would otherwise surface nowhere — and propagating keeps the stack visible.
|
|
49
|
+
* An auth rejection also rethrows regardless of batch count: a dead token is
|
|
50
|
+
* never batch-local, so continuing would just repeat the same failure.
|
|
51
|
+
*/
|
|
52
|
+
export declare function reconcileBatches({ batches, log, processBatch, }: {
|
|
53
|
+
batches: DeployBatch[];
|
|
54
|
+
log: (line: string) => void;
|
|
55
|
+
processBatch: (batch: DeployBatch) => Promise<void>;
|
|
56
|
+
}): Promise<DeployFailure[]>;
|
|
57
|
+
/**
|
|
58
|
+
* The closing summary for a multi-deployment run that had failures: the
|
|
59
|
+
* succeeded/failed tally and each failed deployment. Empty for a clean run.
|
|
60
|
+
* The wording is mode-neutral because a `--dry-run` failure lands here too,
|
|
61
|
+
* where nothing was deployed. Single-batch runs never reach it — their lone
|
|
62
|
+
* failure rethrows out of {@link reconcileBatches}.
|
|
63
|
+
*/
|
|
64
|
+
export declare function deploySummary({ total, failures, }: {
|
|
65
|
+
total: number;
|
|
66
|
+
failures: DeployFailure[];
|
|
67
|
+
}): string[];
|
|
68
|
+
/**
|
|
69
|
+
* The banner printed above a deployment's output so a multi-deployment run is
|
|
70
|
+
* attributable. `undefined` for a single-target run — it keeps its original
|
|
71
|
+
* unprefixed output.
|
|
72
|
+
*/
|
|
73
|
+
export declare function deploymentBanner({ deployment, index, total, }: {
|
|
74
|
+
deployment: Pick<WorkflowDeployment, 'name' | 'tag'>;
|
|
75
|
+
index: number;
|
|
76
|
+
total: number;
|
|
77
|
+
}): string | undefined;
|
|
78
|
+
/** One line per deployed definition, symbol-tagged by its result status,
|
|
79
|
+
* followed by any advisory effect-output lint warnings indented beneath it. */
|
|
80
|
+
export declare function deployResultLines(results: DeployDefinitionResult[]): string[];
|
|
81
|
+
export {};
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,23 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { computeDiffEntries, errorMessage, workflow, } from '@sanity/workflow-engine';
|
|
3
4
|
import logSymbols from 'log-symbols';
|
|
4
5
|
import ora from 'ora';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { WorkflowCommand } from "../lib/base-command.js";
|
|
7
|
+
import { clientFor, resolveTokenOrFail } from "../lib/client.js";
|
|
8
|
+
import { selectDefinitions, validateOrFail } from "../lib/definitions.js";
|
|
8
9
|
import { diffReport } from "../lib/diff.js";
|
|
9
|
-
import { fail } from "../lib/fail.js";
|
|
10
|
+
import { fail, isAuthRejection } from "../lib/fail.js";
|
|
10
11
|
import { tagFlags } from "../lib/flags.js";
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
import { loadWorkflowConfig } from "../lib/load-config.js";
|
|
13
|
+
import { deploymentToTarget, selectDeployments } from "../lib/select-deployment.js";
|
|
14
|
+
import { shareDefinitionsAfterDeploy } from "../lib/share-definitions.js";
|
|
15
|
+
import { cliTelemetry } from "../lib/telemetry.js";
|
|
16
|
+
import { groupBanner } from "../lib/ui.js";
|
|
17
|
+
export default class Deploy extends WorkflowCommand {
|
|
18
|
+
static description = 'Validate, diff, and deploy workflow definitions to the resource bound by the selected deployment.';
|
|
13
19
|
static examples = [
|
|
14
|
-
'<%= config.bin %> deploy',
|
|
20
|
+
'<%= config.bin %> deploy --tag prod',
|
|
21
|
+
'<%= config.bin %> deploy --all-tags',
|
|
15
22
|
'<%= config.bin %> deploy --check',
|
|
16
23
|
'<%= config.bin %> deploy --dry-run',
|
|
17
24
|
'<%= config.bin %> deploy --only productLaunch',
|
|
18
25
|
];
|
|
19
26
|
static flags = {
|
|
20
27
|
...tagFlags,
|
|
28
|
+
'all-tags': Flags.boolean({
|
|
29
|
+
description: 'Deploy every deployment in the config, not just one tag.',
|
|
30
|
+
default: false,
|
|
31
|
+
exclusive: ['tag'],
|
|
32
|
+
}),
|
|
21
33
|
'dry-run': Flags.boolean({
|
|
22
34
|
description: 'Validate + diff against the deployed version; do not write.',
|
|
23
35
|
default: false,
|
|
@@ -27,70 +39,162 @@ export default class Deploy extends Command {
|
|
|
27
39
|
default: false,
|
|
28
40
|
}),
|
|
29
41
|
only: Flags.string({
|
|
30
|
-
description: 'Limit deploy/check/diff to a single workflow definition by name.',
|
|
42
|
+
description: 'Limit deploy/check/diff to a single workflow definition by name. Every targeted deployment must contain it.',
|
|
43
|
+
}),
|
|
44
|
+
'share-definitions-with-sanity': Flags.boolean({
|
|
45
|
+
description: 'Share the definition documents newly created by this deploy with Sanity — the full document, verbatim (structure, names, filters, effect configuration, seeded values), plus its deployment coordinates (project and dataset, or resource id); never content documents, instances, or your Sanity auth token. Opt-in for this invocation only; the CLI never prompts. Independent of telemetry consent.',
|
|
46
|
+
default: false,
|
|
31
47
|
}),
|
|
32
48
|
};
|
|
33
49
|
async run() {
|
|
34
50
|
const { flags } = await this.parse(Deploy);
|
|
35
51
|
validateModeFlags(flags.check, flags['dry-run']);
|
|
36
|
-
const config = loadWorkflowConfig(
|
|
37
|
-
const
|
|
52
|
+
const config = await loadWorkflowConfig();
|
|
53
|
+
const batches = buildBatches(selectDeployments(config, { tag: flags.tag, allTags: flags['all-tags'] }), flags.only);
|
|
54
|
+
const log = (line) => this.log(line);
|
|
38
55
|
if (flags.check) {
|
|
39
|
-
|
|
56
|
+
await reconcileBatches({
|
|
57
|
+
batches,
|
|
58
|
+
log,
|
|
59
|
+
processBatch: async ({ defs }) => {
|
|
60
|
+
log(`${logSymbols.success} ${defs.length} definition(s) passed validation (check only — dataset not contacted).`);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
40
63
|
return;
|
|
41
64
|
}
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
65
|
+
const token = await resolveTokenOrFail();
|
|
66
|
+
const candidates = [];
|
|
67
|
+
const failures = await reconcileBatches({
|
|
68
|
+
batches,
|
|
69
|
+
log,
|
|
70
|
+
processBatch: async ({ deployment, defs }) => {
|
|
71
|
+
const client = clientFor(deployment.workflowResource, token);
|
|
72
|
+
const deployed = await this.processDeployment({
|
|
73
|
+
deployment,
|
|
74
|
+
defs,
|
|
75
|
+
dryRun: flags['dry-run'],
|
|
76
|
+
client,
|
|
77
|
+
});
|
|
78
|
+
if (deployed !== undefined) {
|
|
79
|
+
candidates.push({
|
|
80
|
+
client,
|
|
81
|
+
tag: deployment.tag,
|
|
82
|
+
resource: deployment.workflowResource,
|
|
83
|
+
results: deployed.results,
|
|
84
|
+
deployId: deployed.deployId,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
for (const line of deploySummary({ total: batches.length, failures })) {
|
|
90
|
+
log(line);
|
|
46
91
|
}
|
|
47
|
-
await
|
|
92
|
+
await shareDefinitionsAfterDeploy({
|
|
93
|
+
flag: flags['share-definitions-with-sanity'],
|
|
94
|
+
candidates,
|
|
95
|
+
warn: (message) => this.warn(message),
|
|
96
|
+
});
|
|
97
|
+
if (failures.length > 0) {
|
|
98
|
+
this.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async processDeployment({ deployment, defs, dryRun, client, }) {
|
|
102
|
+
const target = deploymentToTarget(deployment);
|
|
103
|
+
if (dryRun) {
|
|
104
|
+
await this.runDryRun({ client, defs, target });
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return this.executeDeploy({ client, defs, target });
|
|
48
108
|
}
|
|
49
|
-
async executeDeploy({ client, defs,
|
|
109
|
+
async executeDeploy({ client, defs, target, }) {
|
|
50
110
|
const spinner = ora(`Deploying ${defs.length} definition(s)…`).start();
|
|
51
111
|
try {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
tag: config.tag,
|
|
112
|
+
const deployed = await workflow.deployDefinitions({
|
|
113
|
+
...target,
|
|
55
114
|
client,
|
|
56
115
|
definitions: defs,
|
|
116
|
+
telemetry: cliTelemetry().logger,
|
|
57
117
|
});
|
|
58
|
-
spinner.succeed(`Processed ${results.length} definition(s)`);
|
|
59
|
-
for (const line of deployResultLines(results))
|
|
118
|
+
spinner.succeed(`Processed ${deployed.results.length} definition(s)`);
|
|
119
|
+
for (const line of deployResultLines(deployed.results))
|
|
60
120
|
this.log(line);
|
|
121
|
+
return deployed;
|
|
61
122
|
}
|
|
62
123
|
catch (error) {
|
|
63
|
-
spinner.fail(
|
|
124
|
+
spinner.fail(`Deploy failed — ${errorMessage(error)}`);
|
|
64
125
|
throw error;
|
|
65
126
|
}
|
|
66
127
|
}
|
|
67
|
-
async runDryRun({ client, defs,
|
|
128
|
+
async runDryRun({ client, defs, target, }) {
|
|
68
129
|
const spinner = ora(`Diffing ${defs.length} definition(s) against dataset…`).start();
|
|
69
130
|
try {
|
|
70
|
-
const entries = await computeDiffEntries({ client, defs, target
|
|
131
|
+
const entries = await computeDiffEntries({ client, defs, target });
|
|
71
132
|
spinner.succeed(`Diffed ${defs.length} definition(s)`);
|
|
72
133
|
for (const line of diffReport(entries))
|
|
73
134
|
this.log(line);
|
|
74
135
|
}
|
|
75
136
|
catch (error) {
|
|
76
|
-
spinner.fail(
|
|
137
|
+
spinner.fail(`Diff failed — ${errorMessage(error)}`);
|
|
77
138
|
throw error;
|
|
78
139
|
}
|
|
79
140
|
}
|
|
80
141
|
}
|
|
81
|
-
/** Reject the contradictory `--check --dry-run` combination. */
|
|
82
142
|
export function validateModeFlags(check, dryRun) {
|
|
83
143
|
if (check && dryRun) {
|
|
84
144
|
fail('Pass either --check or --dry-run, not both.');
|
|
85
145
|
}
|
|
86
146
|
}
|
|
87
|
-
|
|
147
|
+
function deploymentLabel({ name, tag }) {
|
|
148
|
+
return `${name} (${tag})`;
|
|
149
|
+
}
|
|
150
|
+
export function buildBatches(deployments, only) {
|
|
151
|
+
return deployments.map((deployment) => {
|
|
152
|
+
const context = deployments.length > 1 ? `${deploymentLabel(deployment)} — ` : '';
|
|
153
|
+
const defs = selectDefinitions(deployment.definitions, { only, context });
|
|
154
|
+
validateOrFail(defs, context);
|
|
155
|
+
return { deployment, defs };
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
export async function reconcileBatches({ batches, log, processBatch, }) {
|
|
159
|
+
const failures = [];
|
|
160
|
+
for (const [index, batch] of batches.entries()) {
|
|
161
|
+
const banner = deploymentBanner({ deployment: batch.deployment, index, total: batches.length });
|
|
162
|
+
if (banner !== undefined) {
|
|
163
|
+
log(banner);
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
await processBatch(batch);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (batches.length === 1 || isAuthRejection(error)) {
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
failures.push({ deployment: batch.deployment, message: errorMessage(error) });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return failures;
|
|
176
|
+
}
|
|
177
|
+
export function deploySummary({ total, failures, }) {
|
|
178
|
+
if (failures.length === 0) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
return [
|
|
182
|
+
'',
|
|
183
|
+
styleText('bold', `${total - failures.length} of ${total} deployments succeeded — ${failures.length} failed:`),
|
|
184
|
+
...failures.map((f) => styleText('red', ` ${logSymbols.error} ${deploymentLabel(f.deployment)} — ${f.message}`)),
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
export function deploymentBanner({ deployment, index, total, }) {
|
|
188
|
+
return groupBanner({ label: deploymentLabel(deployment), index, total });
|
|
189
|
+
}
|
|
88
190
|
export function deployResultLines(results) {
|
|
89
191
|
const symbols = {
|
|
90
192
|
created: logSymbols.success,
|
|
91
193
|
};
|
|
92
|
-
return results.
|
|
194
|
+
return results.flatMap((r) => {
|
|
93
195
|
const symbol = symbols[r.status] ?? logSymbols.warning;
|
|
94
|
-
|
|
196
|
+
const head = ` ${symbol} ${r.status.padEnd(9)} ${r.name} v${r.version}`;
|
|
197
|
+
const warnings = (r.warnings ?? []).map((w) => ` ${logSymbols.warning} ${w}`);
|
|
198
|
+
return [head, ...warnings];
|
|
95
199
|
});
|
|
96
200
|
}
|