@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
package/dist/commands/list.js
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { WORKFLOW_INSTANCE_TYPE, tagScopeFilter } from '@sanity/workflow-engine';
|
|
4
|
-
import { Table } from 'console-table-printer';
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { WORKFLOW_INSTANCE_TYPE, tagScopeFilter, terminalState, } from '@sanity/workflow-engine';
|
|
5
3
|
import logSymbols from 'log-symbols';
|
|
6
|
-
import {
|
|
4
|
+
import { WorkflowCommand } from "../lib/base-command.js";
|
|
5
|
+
import { resolveReadTargets } from "../lib/context.js";
|
|
7
6
|
import { tagFlags } from "../lib/flags.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
* Build the GROQ query + params for `list` from its flags. Pure so the
|
|
11
|
-
* filter combinations can be asserted without a live dataset. `--tag` is an
|
|
12
|
-
* optional filter: omit it and the listing spans every partition.
|
|
13
|
-
*/
|
|
7
|
+
import { runReadAcrossTargets } from "../lib/read-fanout.js";
|
|
8
|
+
import { clipToLimit, formatAge, formatTable } from "../lib/ui.js";
|
|
14
9
|
export function buildListQuery(flags) {
|
|
15
10
|
const filters = [`_type == "${WORKFLOW_INSTANCE_TYPE}"`];
|
|
16
11
|
if (flags['in-flight'])
|
|
17
12
|
filters.push('!defined(completedAt)');
|
|
18
|
-
if (flags
|
|
13
|
+
if (flags.definition)
|
|
19
14
|
filters.push('definition == $definition');
|
|
20
|
-
// Cheap approximation of --failed: any activity in any stage with status "failed".
|
|
21
15
|
if (flags.failed)
|
|
22
16
|
filters.push('count(stages[].activities[status == "failed"]) > 0');
|
|
23
17
|
if (flags.tag)
|
|
@@ -31,44 +25,32 @@ export function buildListQuery(flags) {
|
|
|
31
25
|
abortedAt,
|
|
32
26
|
lastChangedAt
|
|
33
27
|
}`;
|
|
34
|
-
const params = { limit: flags.limit };
|
|
35
|
-
if (flags
|
|
36
|
-
params['definition'] = flags
|
|
28
|
+
const params = { limit: flags.limit + 1 };
|
|
29
|
+
if (flags.definition)
|
|
30
|
+
params['definition'] = flags.definition;
|
|
37
31
|
if (flags.tag)
|
|
38
32
|
params['tag'] = flags.tag;
|
|
39
33
|
return { groq, params };
|
|
40
34
|
}
|
|
41
|
-
/** Aborted instances also carry `completedAt`, so check `abortedAt` first. */
|
|
42
|
-
function deriveStatus(r) {
|
|
43
|
-
if (r.abortedAt)
|
|
44
|
-
return 'aborted';
|
|
45
|
-
if (r.completedAt)
|
|
46
|
-
return 'completed';
|
|
47
|
-
return 'in-flight';
|
|
48
|
-
}
|
|
49
|
-
/** Map a queried instance to its display row (derives the status column).
|
|
50
|
-
* Pure — status is the semantic value; the table colors it at render. */
|
|
51
35
|
export function instanceRow(r) {
|
|
52
36
|
return {
|
|
53
37
|
_id: r._id,
|
|
54
38
|
definition: r.definition,
|
|
55
39
|
tag: r.tag,
|
|
56
40
|
currentStage: r.currentStage,
|
|
57
|
-
status:
|
|
41
|
+
status: terminalState({
|
|
42
|
+
...(r.completedAt != null ? { completedAt: r.completedAt } : {}),
|
|
43
|
+
...(r.abortedAt != null ? { abortedAt: r.abortedAt } : {}),
|
|
44
|
+
}),
|
|
58
45
|
lastChangedAt: r.lastChangedAt,
|
|
59
46
|
};
|
|
60
47
|
}
|
|
61
|
-
|
|
62
|
-
completed: 'green',
|
|
63
|
-
aborted: 'dim',
|
|
64
|
-
'in-flight': 'cyan',
|
|
65
|
-
};
|
|
66
|
-
export default class List extends Command {
|
|
48
|
+
export default class List extends WorkflowCommand {
|
|
67
49
|
static description = 'List workflow instances in the configured dataset.';
|
|
68
50
|
static examples = [
|
|
69
51
|
'<%= config.bin %> list',
|
|
70
52
|
'<%= config.bin %> list --in-flight',
|
|
71
|
-
'<%= config.bin %> list --
|
|
53
|
+
'<%= config.bin %> list --definition productLaunch',
|
|
72
54
|
'<%= config.bin %> list --tag prod',
|
|
73
55
|
];
|
|
74
56
|
static flags = {
|
|
@@ -81,8 +63,8 @@ export default class List extends Command {
|
|
|
81
63
|
description: 'Only instances with at least one failed activity.',
|
|
82
64
|
default: false,
|
|
83
65
|
}),
|
|
84
|
-
|
|
85
|
-
description:
|
|
66
|
+
definition: Flags.string({
|
|
67
|
+
description: "Only instances of this workflow definition (its `name`; the instance's `definition` field).",
|
|
86
68
|
}),
|
|
87
69
|
limit: Flags.integer({
|
|
88
70
|
description: 'Maximum rows to return.',
|
|
@@ -91,34 +73,39 @@ export default class List extends Command {
|
|
|
91
73
|
};
|
|
92
74
|
async run() {
|
|
93
75
|
const { flags } = await this.parse(List);
|
|
76
|
+
const targets = await resolveReadTargets(flags);
|
|
94
77
|
const { groq, params } = buildListQuery(flags);
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
{
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
78
|
+
const failures = await runReadAcrossTargets({
|
|
79
|
+
targets,
|
|
80
|
+
log: (line) => this.log(line),
|
|
81
|
+
run: async ({ client }) => {
|
|
82
|
+
const fetched = await client.fetch(groq, params, { tag: 'list' });
|
|
83
|
+
if (fetched.length === 0) {
|
|
84
|
+
this.log(`${logSymbols.info} no instances match`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const { rows, note } = clipToLimit(fetched, flags.limit);
|
|
88
|
+
const lines = formatTable(['instance', 'workflow', 'tag', 'stage', 'status', 'updated'], rows.map((r) => {
|
|
89
|
+
const row = instanceRow(r);
|
|
90
|
+
return [
|
|
91
|
+
row._id,
|
|
92
|
+
row.definition,
|
|
93
|
+
row.tag,
|
|
94
|
+
row.currentStage,
|
|
95
|
+
row.status,
|
|
96
|
+
formatAge(row.lastChangedAt),
|
|
97
|
+
];
|
|
98
|
+
}));
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
this.log(line);
|
|
101
|
+
}
|
|
102
|
+
if (note) {
|
|
103
|
+
this.log(note);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
110
106
|
});
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
table.addRow({
|
|
114
|
-
_id: row._id,
|
|
115
|
-
definition: row.definition,
|
|
116
|
-
tag: row.tag,
|
|
117
|
-
currentStage: row.currentStage,
|
|
118
|
-
status: styleText(STATUS_COLOR[row.status], row.status),
|
|
119
|
-
updated: formatAge(row.lastChangedAt),
|
|
120
|
-
});
|
|
107
|
+
if (failures.length > 0) {
|
|
108
|
+
this.exit(1);
|
|
121
109
|
}
|
|
122
|
-
table.printTable();
|
|
123
110
|
}
|
|
124
111
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import { StubCommand } from "../lib/stub.js";
|
|
3
|
-
export default class
|
|
4
|
-
// Stubbed, so kept off the help + published surface until it's wired.
|
|
3
|
+
export default class ResetActivity extends StubCommand {
|
|
5
4
|
static hidden = true;
|
|
6
|
-
static description = '
|
|
5
|
+
static description = 'Reset a failed activity on an in-flight instance — back to pending (re-run) or to skipped (bypass).';
|
|
7
6
|
static args = {
|
|
8
7
|
instanceId: Args.string({ required: true, description: 'Workflow instance id.' }),
|
|
9
8
|
activity: Args.string({ required: true, description: 'Activity name within the current stage.' }),
|
|
@@ -1,12 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { type SetStageArgs, type WorkflowInstance } from '@sanity/workflow-engine';
|
|
2
|
+
import { WorkflowCommand } from '../lib/base-command.ts';
|
|
3
|
+
import { type EngineScope } from '../lib/operation-args.ts';
|
|
4
|
+
import { type WriteOutcome, type WriteReport } from '../lib/ops-report.ts';
|
|
5
|
+
export default class SetStage extends WorkflowCommand {
|
|
4
6
|
static description: string;
|
|
7
|
+
static examples: string[];
|
|
5
8
|
static args: {
|
|
6
9
|
instanceId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
10
|
};
|
|
8
11
|
static flags: {
|
|
9
12
|
to: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
13
|
reason: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
15
|
};
|
|
16
|
+
run(): Promise<void>;
|
|
12
17
|
}
|
|
18
|
+
/** The shared operation args ({@link buildOperationArgs}) plus the
|
|
19
|
+
* setStage-specific target. */
|
|
20
|
+
export declare function buildSetStageArgs({ scope, instanceId, targetStage, reason, }: {
|
|
21
|
+
scope: EngineScope;
|
|
22
|
+
instanceId: string;
|
|
23
|
+
targetStage: string;
|
|
24
|
+
reason: string | undefined;
|
|
25
|
+
}): SetStageArgs & EngineScope;
|
|
26
|
+
/** {@link WriteOutcome} with the terminal stamp the no-op copy reads. */
|
|
27
|
+
interface SetStageOutcome extends WriteOutcome {
|
|
28
|
+
instance: Pick<WorkflowInstance, 'currentStage' | 'completedAt'>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Turn a `setStage` result into the spinner message + ops block. Pure so
|
|
32
|
+
* the changed / terminal / already-at-target / cascaded / ops permutations
|
|
33
|
+
* can be asserted without driving a real move.
|
|
34
|
+
*/
|
|
35
|
+
export declare function setStageReport(result: SetStageOutcome, to: string): WriteReport;
|
|
36
|
+
export {};
|
|
@@ -1,18 +1,69 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
1
2
|
import { Args, Flags } from '@oclif/core';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
static
|
|
3
|
+
import { workflow } from '@sanity/workflow-engine';
|
|
4
|
+
import { WorkflowCommand } from "../lib/base-command.js";
|
|
5
|
+
import { resolveInstanceContext } from "../lib/context.js";
|
|
6
|
+
import { tagFlags } from "../lib/flags.js";
|
|
7
|
+
import { buildOperationArgs } from "../lib/operation-args.js";
|
|
8
|
+
import { cascadeTail, opsAppliedLines, runWriteVerb, } from "../lib/ops-report.js";
|
|
9
|
+
export default class SetStage extends WorkflowCommand {
|
|
10
|
+
static description = "Force an instance into a stage, regardless of its declared transitions and filters — the engine's setStage admin override. The target stage's enter lifecycle still runs (auto-activities start, stage guards reconcile), and the post-move cascade can immediately auto-transition the instance onward.";
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> set-stage wf-instance.abc123 --to ready',
|
|
13
|
+
"<%= config.bin %> set-stage wf-instance.abc123 --to ready --reason 'unblock for demo'",
|
|
14
|
+
];
|
|
11
15
|
static args = {
|
|
12
|
-
instanceId: Args.string({
|
|
16
|
+
instanceId: Args.string({
|
|
17
|
+
required: true,
|
|
18
|
+
description: 'Workflow instance id to move.',
|
|
19
|
+
}),
|
|
13
20
|
};
|
|
14
21
|
static flags = {
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
...tagFlags,
|
|
23
|
+
to: Flags.string({
|
|
24
|
+
required: true,
|
|
25
|
+
description: 'Target stage name.',
|
|
26
|
+
}),
|
|
27
|
+
reason: Flags.string({
|
|
28
|
+
description: 'Free-text reason — recorded on the history entry for audit.',
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
async run() {
|
|
32
|
+
const { args, flags } = await this.parse(SetStage);
|
|
33
|
+
const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
|
|
34
|
+
await runWriteVerb({
|
|
35
|
+
startLabel: `Setting ${args.instanceId} → ${flags.to}…`,
|
|
36
|
+
failLabel: 'Stage move rejected',
|
|
37
|
+
failHeadline: 'set-stage error:',
|
|
38
|
+
run: () => workflow.setStage({
|
|
39
|
+
client,
|
|
40
|
+
...buildSetStageArgs({
|
|
41
|
+
scope,
|
|
42
|
+
instanceId: args.instanceId,
|
|
43
|
+
targetStage: flags.to,
|
|
44
|
+
reason: flags.reason,
|
|
45
|
+
}),
|
|
46
|
+
}),
|
|
47
|
+
report: (result) => setStageReport(result, flags.to),
|
|
48
|
+
log: (line) => this.log(line),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function buildSetStageArgs({ scope, instanceId, targetStage, reason, }) {
|
|
53
|
+
return { ...buildOperationArgs({ scope, instanceId, reason }), targetStage };
|
|
54
|
+
}
|
|
55
|
+
export function setStageReport(result, to) {
|
|
56
|
+
const stage = styleText('bold', result.instance.currentStage);
|
|
57
|
+
if (!result.changed) {
|
|
58
|
+
const { completedAt } = result.instance;
|
|
59
|
+
const message = completedAt !== undefined
|
|
60
|
+
? `setStage changed nothing — instance is terminal (at ${stage} since ${completedAt})`
|
|
61
|
+
: `setStage changed nothing — instance is already at ${stage}`;
|
|
62
|
+
return { changed: false, message, opsLines: [] };
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
changed: true,
|
|
66
|
+
message: `Now at ${stage} (was → ${to}${cascadeTail(result.cascaded)})`,
|
|
67
|
+
opsLines: opsAppliedLines(result.ranOps),
|
|
17
68
|
};
|
|
18
69
|
}
|
package/dist/commands/show.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
1
|
import { type WorkflowInstance } from '@sanity/workflow-engine';
|
|
3
|
-
|
|
2
|
+
import { WorkflowCommand } from '../lib/base-command.ts';
|
|
3
|
+
export default class Show extends WorkflowCommand {
|
|
4
4
|
static description: string;
|
|
5
5
|
static examples: string[];
|
|
6
6
|
static args: {
|
|
@@ -8,6 +8,7 @@ export default class Show extends Command {
|
|
|
8
8
|
};
|
|
9
9
|
static flags: {
|
|
10
10
|
include: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
};
|
|
12
13
|
run(): Promise<void>;
|
|
13
14
|
}
|
package/dist/commands/show.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
|
-
import { Args,
|
|
3
|
-
import { abortReason } from '@sanity/workflow-engine';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { abortReason, displayTitle, terminalState, } from '@sanity/workflow-engine';
|
|
4
4
|
import logSymbols from 'log-symbols';
|
|
5
|
-
import {
|
|
5
|
+
import { WorkflowCommand } from "../lib/base-command.js";
|
|
6
|
+
import { findInstance, resolveReadTargets } from "../lib/context.js";
|
|
7
|
+
import { tagFlags } from "../lib/flags.js";
|
|
6
8
|
import { formatKeyValue, formatTimestamp, sectionHeader, activityIcon } from "../lib/ui.js";
|
|
7
|
-
export default class Show extends
|
|
9
|
+
export default class Show extends WorkflowCommand {
|
|
8
10
|
static description = 'Show the state, activities, and effects of a workflow instance.';
|
|
9
11
|
static examples = [
|
|
10
12
|
'<%= config.bin %> show wf-instance.abc123',
|
|
@@ -17,6 +19,7 @@ export default class Show extends Command {
|
|
|
17
19
|
}),
|
|
18
20
|
};
|
|
19
21
|
static flags = {
|
|
22
|
+
...tagFlags,
|
|
20
23
|
include: Flags.string({
|
|
21
24
|
description: 'Optional sections to include in output.',
|
|
22
25
|
options: ['history'],
|
|
@@ -26,11 +29,12 @@ export default class Show extends Command {
|
|
|
26
29
|
};
|
|
27
30
|
async run() {
|
|
28
31
|
const { args, flags } = await this.parse(Show);
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
32
|
+
const targets = await resolveReadTargets(flags);
|
|
33
|
+
const hit = await findInstance({ targets, instanceId: args.instanceId, requestTag: 'show' });
|
|
34
|
+
if (!hit) {
|
|
32
35
|
this.error(`${logSymbols.error} no instance with id "${args.instanceId}"`, { exit: 1 });
|
|
33
36
|
}
|
|
37
|
+
const { instance } = hit;
|
|
34
38
|
this.log(instanceHeader(instance));
|
|
35
39
|
this.log('');
|
|
36
40
|
for (const line of describeInstance(instance, {
|
|
@@ -40,19 +44,14 @@ export default class Show extends Command {
|
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
|
-
const HEADER_PAD = 9;
|
|
44
|
-
/** The summary block at the top of `show` (and reused by `diagnose` /
|
|
45
|
-
* `fire-action`): a bold workflow + id title, then aligned detail rows. */
|
|
47
|
+
const HEADER_PAD = 9;
|
|
46
48
|
export function instanceHeader(instance) {
|
|
47
49
|
const title = `${styleText('bold', `${instance.definition} v${instance.pinnedVersion}`)} ${styleText('cyan', instance._id)}`;
|
|
48
50
|
const rows = [
|
|
49
51
|
formatKeyValue({ key: 'Stage', value: instance.currentStage, padTo: HEADER_PAD }),
|
|
50
52
|
formatKeyValue({ key: 'Started', value: formatTimestamp(instance.startedAt), padTo: HEADER_PAD }),
|
|
51
53
|
];
|
|
52
|
-
|
|
53
|
-
// abort line stands in for completion — showing a green "Completed" too would
|
|
54
|
-
// misread as success.
|
|
55
|
-
if (instance.abortedAt) {
|
|
54
|
+
if (terminalState(instance) === 'aborted' && instance.abortedAt !== undefined) {
|
|
56
55
|
const reason = abortReason(instance);
|
|
57
56
|
const tail = reason ? ` — ${reason}` : '';
|
|
58
57
|
const value = styleText('yellow', `${formatTimestamp(instance.abortedAt)}${tail}`);
|
|
@@ -67,11 +66,6 @@ export function instanceHeader(instance) {
|
|
|
67
66
|
rows.push(formatKeyValue({ key: 'Tag', value: instance.tag, padTo: HEADER_PAD }));
|
|
68
67
|
return [title, ...rows].join('\n');
|
|
69
68
|
}
|
|
70
|
-
/**
|
|
71
|
-
* The per-instance body lines: every stage with its activities, any pending
|
|
72
|
-
* effects, and (optionally) the history log. Pure so it can be asserted
|
|
73
|
-
* without driving the oclif command.
|
|
74
|
-
*/
|
|
75
69
|
export function describeInstance(instance, options) {
|
|
76
70
|
const lines = [sectionHeader('Stages')];
|
|
77
71
|
for (const stage of instance.stages) {
|
|
@@ -79,7 +73,7 @@ export function describeInstance(instance, options) {
|
|
|
79
73
|
? styleText('dim', ` (exited ${formatTimestamp(stage.exitedAt)})`)
|
|
80
74
|
: styleText('cyan', ' (current)');
|
|
81
75
|
lines.push(` • ${stage.name}${marker}`);
|
|
82
|
-
for (const activity of stage.activities) {
|
|
76
|
+
for (const activity of stage.activities ?? []) {
|
|
83
77
|
lines.push(` ${activityIcon[activity.status]} ${activity.name} ${styleText('dim', `[${activity.status}]`)}`);
|
|
84
78
|
}
|
|
85
79
|
}
|
|
@@ -92,7 +86,7 @@ export function describeInstance(instance, options) {
|
|
|
92
86
|
if (options.includeHistory) {
|
|
93
87
|
lines.push('', sectionHeader('History'));
|
|
94
88
|
for (const entry of instance.history) {
|
|
95
|
-
lines.push(` ${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${entry._type}`);
|
|
89
|
+
lines.push(` ${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${displayTitle(entry._type)}`);
|
|
96
90
|
}
|
|
97
91
|
}
|
|
98
92
|
return lines;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type FieldEntry, type InitialFieldValue, type StartInstanceArgs, type WorkflowInstance, type WorkflowLifecycle } from '@sanity/workflow-engine';
|
|
2
|
+
import { WorkflowCommand } from '../lib/base-command.ts';
|
|
3
|
+
import { type EngineScope } from '../lib/operation-args.ts';
|
|
4
|
+
export default class Start extends WorkflowCommand {
|
|
5
|
+
static description: string;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static args: {
|
|
8
|
+
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
9
|
+
};
|
|
10
|
+
static flags: {
|
|
11
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
field: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Why a definition can't be started standalone, or `undefined` when it can.
|
|
20
|
+
* Advisory, mirroring the Startable column in `definition list` — the engine
|
|
21
|
+
* itself does not refuse ({@link isStartableDefinition}), but the CLI has no
|
|
22
|
+
* way to supply the parent context a spawn-only child expects.
|
|
23
|
+
*/
|
|
24
|
+
export declare function startRefusal(definition: {
|
|
25
|
+
lifecycle?: WorkflowLifecycle | undefined;
|
|
26
|
+
}): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Type each `--field name=value` against the workflow's declared field
|
|
29
|
+
* entries — the engine takes typed {@link InitialFieldValue}s, and a value's
|
|
30
|
+
* type IS its declared entry's kind. Only `input`-sourced entries read
|
|
31
|
+
* caller values (the engine silently ignores the rest), so anything else
|
|
32
|
+
* fails here, naming the fields that ARE settable; value validation stays
|
|
33
|
+
* in the engine.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildInitialFields({ declared, values, }: {
|
|
36
|
+
declared: Pick<FieldEntry, 'type' | 'name' | 'initialValue'>[];
|
|
37
|
+
values: Record<string, unknown>;
|
|
38
|
+
}): InitialFieldValue[];
|
|
39
|
+
/**
|
|
40
|
+
* The shared engine args ({@link baseEngineArgs}) plus the start-specific
|
|
41
|
+
* fields. Optionals spread conditionally because the engine's optional args
|
|
42
|
+
* reject an explicit `undefined` under `exactOptionalPropertyTypes`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function buildStartArgs({ scope, name, version, initialFields, }: {
|
|
45
|
+
scope: EngineScope;
|
|
46
|
+
name: string;
|
|
47
|
+
version: number | undefined;
|
|
48
|
+
initialFields: InitialFieldValue[];
|
|
49
|
+
}): StartInstanceArgs & EngineScope;
|
|
50
|
+
/** The success spinner message. Pure so the in-flight / immediately-terminal
|
|
51
|
+
* permutations can be asserted without driving a real start. */
|
|
52
|
+
export declare function startReport(instance: Pick<WorkflowInstance, '_id' | 'currentStage' | 'completedAt'>): string;
|
|
53
|
+
/** The `--json` payload. Keys follow the CLI's `--json` vocabulary (matching
|
|
54
|
+
* `fire-action`): `instanceId` carries the document `_id`, `version` the
|
|
55
|
+
* pinned definition version; the rest mirror the instance fields. */
|
|
56
|
+
export declare function startResultJson(instance: Pick<WorkflowInstance, '_id' | 'definition' | 'pinnedVersion' | 'currentStage' | 'completedAt'>): Record<string, unknown>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { isStartableDefinition, workflow, } from '@sanity/workflow-engine';
|
|
4
|
+
import { WorkflowCommand } from "../lib/base-command.js";
|
|
5
|
+
import { resolveContext } from "../lib/context.js";
|
|
6
|
+
import { fetchDeployedDefinition } from "../lib/definitions.js";
|
|
7
|
+
import { fail, failOnThrow, failureDetail } from "../lib/fail.js";
|
|
8
|
+
import { jsonFlags, tagFlags } from "../lib/flags.js";
|
|
9
|
+
import { baseEngineArgs } from "../lib/operation-args.js";
|
|
10
|
+
import { runWriteVerb } from "../lib/ops-report.js";
|
|
11
|
+
import { parseParams } from "../lib/params.js";
|
|
12
|
+
export default class Start extends WorkflowCommand {
|
|
13
|
+
static description = 'Start a workflow instance from a deployed definition. Supply values for the ' +
|
|
14
|
+
"workflow's input-sourced fields with --field (e.g. the subject document ref).";
|
|
15
|
+
static examples = [
|
|
16
|
+
'<%= config.bin %> start productLaunch',
|
|
17
|
+
'<%= config.bin %> start article-review --field subject=\'{"id":"dataset:proj:ds:article-1","type":"article"}\'',
|
|
18
|
+
'<%= config.bin %> start productLaunch --version 2 --tag prod',
|
|
19
|
+
];
|
|
20
|
+
static args = {
|
|
21
|
+
name: Args.string({ required: true, description: 'Workflow definition name.' }),
|
|
22
|
+
};
|
|
23
|
+
static flags = {
|
|
24
|
+
...tagFlags,
|
|
25
|
+
version: Flags.integer({
|
|
26
|
+
description: 'Definition version to start from (default: highest deployed).',
|
|
27
|
+
}),
|
|
28
|
+
field: Flags.string({
|
|
29
|
+
description: 'Initial value for a declared input-sourced field, as name=value (repeatable). Values ' +
|
|
30
|
+
'are JSON-parsed, falling back to a string; ref kinds take a JSON object with a GDR ' +
|
|
31
|
+
'`id` and doc `type`.',
|
|
32
|
+
multiple: true,
|
|
33
|
+
default: [],
|
|
34
|
+
}),
|
|
35
|
+
...jsonFlags,
|
|
36
|
+
};
|
|
37
|
+
async run() {
|
|
38
|
+
const { args, flags } = await this.parse(Start);
|
|
39
|
+
const { deployment, client } = await resolveContext(flags);
|
|
40
|
+
const values = failOnThrow('Invalid --field:', () => parseParams(flags.field));
|
|
41
|
+
const initialFields = await resolveStartInputs({
|
|
42
|
+
client,
|
|
43
|
+
name: args.name,
|
|
44
|
+
tag: deployment.tag,
|
|
45
|
+
version: flags.version,
|
|
46
|
+
values,
|
|
47
|
+
});
|
|
48
|
+
const startArgs = {
|
|
49
|
+
client,
|
|
50
|
+
...buildStartArgs({
|
|
51
|
+
scope: deployment,
|
|
52
|
+
name: args.name,
|
|
53
|
+
version: flags.version,
|
|
54
|
+
initialFields,
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
if (flags.json) {
|
|
58
|
+
const result = await workflow
|
|
59
|
+
.startInstance(startArgs)
|
|
60
|
+
.catch((error) => fail('start error:', failureDetail(error)));
|
|
61
|
+
this.log(JSON.stringify(startResultJson(result.instance), null, 2));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await runWriteVerb({
|
|
65
|
+
startLabel: `Starting ${args.name}…`,
|
|
66
|
+
failLabel: 'Start rejected',
|
|
67
|
+
failHeadline: 'start error:',
|
|
68
|
+
run: () => workflow.startInstance(startArgs),
|
|
69
|
+
report: (result) => ({ changed: true, message: startReport(result.instance) }),
|
|
70
|
+
log: (line) => this.log(line),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function resolveStartInputs({ client, name, tag, version, values, }) {
|
|
75
|
+
const deployed = await fetchDeployedDefinition({ client, name, tag, version });
|
|
76
|
+
if (deployed === undefined) {
|
|
77
|
+
fail(`No deployed definition "${name}".`);
|
|
78
|
+
}
|
|
79
|
+
const refusal = startRefusal(deployed);
|
|
80
|
+
if (refusal !== undefined) {
|
|
81
|
+
fail('start error:', refusal);
|
|
82
|
+
}
|
|
83
|
+
return failOnThrow('start usage error:', () => buildInitialFields({ declared: deployed.fields ?? [], values }));
|
|
84
|
+
}
|
|
85
|
+
export function startRefusal(definition) {
|
|
86
|
+
if (isStartableDefinition(definition)) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
return "definition is spawn-only (lifecycle: 'child') — instances of it are spawned by a parent workflow's activity, not started standalone";
|
|
90
|
+
}
|
|
91
|
+
export function buildInitialFields({ declared, values, }) {
|
|
92
|
+
const settable = declared.filter((f) => f.initialValue?.type === 'input');
|
|
93
|
+
const settableNames = settable.map((f) => f.name).join(', ');
|
|
94
|
+
return Object.entries(values).map(([name, value]) => {
|
|
95
|
+
const entry = declared.find((f) => f.name === name);
|
|
96
|
+
if (entry === undefined) {
|
|
97
|
+
throw new Error(settable.length > 0
|
|
98
|
+
? `workflow declares no field "${name}" — input fields: ${settableNames}`
|
|
99
|
+
: `workflow declares no field "${name}" — it declares no input fields at all`);
|
|
100
|
+
}
|
|
101
|
+
if (entry.initialValue?.type !== 'input') {
|
|
102
|
+
throw new Error(`field "${name}" is not input-sourced — the engine would silently ignore the value. ` +
|
|
103
|
+
(settable.length > 0
|
|
104
|
+
? `Input fields: ${settableNames}`
|
|
105
|
+
: 'This workflow has no input fields.'));
|
|
106
|
+
}
|
|
107
|
+
return { type: entry.type, name, value };
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export function buildStartArgs({ scope, name, version, initialFields, }) {
|
|
111
|
+
return {
|
|
112
|
+
...baseEngineArgs(scope),
|
|
113
|
+
definition: name,
|
|
114
|
+
...(version !== undefined ? { version } : {}),
|
|
115
|
+
...(initialFields.length > 0 ? { initialFields } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function startReport(instance) {
|
|
119
|
+
const id = styleText('bold', instance._id);
|
|
120
|
+
const stage = styleText('bold', instance.currentStage);
|
|
121
|
+
return instance.completedAt !== undefined
|
|
122
|
+
? `Started ${id} — completed immediately at ${stage}`
|
|
123
|
+
: `Started ${id} — now at ${stage}`;
|
|
124
|
+
}
|
|
125
|
+
export function startResultJson(instance) {
|
|
126
|
+
return {
|
|
127
|
+
instanceId: instance._id,
|
|
128
|
+
definition: instance.definition,
|
|
129
|
+
version: instance.pinnedVersion,
|
|
130
|
+
currentStage: instance.currentStage,
|
|
131
|
+
...(instance.completedAt !== undefined ? { completedAt: instance.completedAt } : {}),
|
|
132
|
+
};
|
|
133
|
+
}
|
package/dist/commands/tail.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export default class Tail extends
|
|
1
|
+
import { WorkflowCommand } from '../lib/base-command.ts';
|
|
2
|
+
export default class Tail extends WorkflowCommand {
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static args: {
|
|
6
6
|
instanceId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
7
|
};
|
|
8
|
+
static flags: {
|
|
9
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
8
11
|
run(): Promise<void>;
|
|
9
12
|
/**
|
|
10
13
|
* Fetch the current instance state, print history entries past the
|