@sanity/workflow-cli 0.9.0 → 0.11.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 +516 -0
- package/README.md +71 -19
- package/bin/run.js +0 -4
- package/dist/commands/abort.d.ts +4 -6
- package/dist/commands/abort.js +13 -24
- package/dist/commands/definition/delete.d.ts +2 -2
- package/dist/commands/definition/delete.js +14 -27
- package/dist/commands/definition/diff.d.ts +2 -2
- package/dist/commands/definition/diff.js +3 -12
- package/dist/commands/definition/list.d.ts +3 -4
- package/dist/commands/definition/list.js +41 -47
- package/dist/commands/definition/show.d.ts +35 -3
- package/dist/commands/definition/show.js +44 -24
- package/dist/commands/deploy.d.ts +11 -5
- package/dist/commands/deploy.js +54 -57
- package/dist/commands/diagnose.d.ts +22 -4
- package/dist/commands/diagnose.js +60 -56
- package/dist/commands/fire-action.d.ts +2 -4
- package/dist/commands/fire-action.js +39 -62
- package/dist/commands/list.d.ts +18 -10
- package/dist/commands/list.js +77 -69
- package/dist/commands/reset-activity.js +0 -1
- package/dist/commands/set-stage.d.ts +2 -2
- package/dist/commands/set-stage.js +14 -30
- package/dist/commands/show.d.ts +2 -2
- package/dist/commands/show.js +12 -24
- package/dist/commands/start.d.ts +4 -28
- package/dist/commands/start.js +19 -80
- package/dist/commands/tail.d.ts +2 -2
- package/dist/commands/tail.js +34 -36
- package/dist/hooks/finally/telemetry.d.ts +8 -0
- package/dist/hooks/finally/telemetry.js +13 -0
- package/dist/hooks/prerun/telemetry.d.ts +8 -0
- package/dist/hooks/prerun/telemetry.js +9 -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.js +13 -30
- package/dist/lib/context.d.ts +49 -13
- package/dist/lib/context.js +49 -48
- package/dist/lib/definitions.d.ts +10 -0
- package/dist/lib/definitions.js +11 -24
- package/dist/lib/diff.d.ts +5 -2
- package/dist/lib/diff.js +61 -27
- package/dist/lib/env.d.ts +4 -0
- package/dist/lib/env.js +4 -3
- package/dist/lib/fail.d.ts +19 -6
- package/dist/lib/fail.js +26 -26
- package/dist/lib/flags.d.ts +0 -7
- package/dist/lib/flags.js +0 -17
- package/dist/lib/load-config.d.ts +11 -7
- package/dist/lib/load-config.js +40 -20
- package/dist/lib/operation-args.d.ts +14 -23
- package/dist/lib/operation-args.js +6 -38
- package/dist/lib/ops-report.d.ts +18 -10
- package/dist/lib/ops-report.js +20 -16
- package/dist/lib/params.js +0 -8
- package/dist/lib/read-fanout.d.ts +22 -0
- package/dist/lib/read-fanout.js +28 -0
- package/dist/lib/select-deployment.js +0 -21
- package/dist/lib/share-definitions.d.ts +120 -0
- package/dist/lib/share-definitions.js +187 -0
- package/dist/lib/stub.js +0 -7
- package/dist/lib/telemetry-setup.d.ts +70 -0
- package/dist/lib/telemetry-setup.js +99 -0
- package/dist/lib/telemetry.d.ts +131 -0
- package/dist/lib/telemetry.js +94 -0
- package/dist/lib/ui.d.ts +33 -1
- package/dist/lib/ui.js +32 -22
- package/oclif.manifest.json +21 -52
- package/package.json +14 -8
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
|
-
import { Args
|
|
3
|
-
import { diagnoseInputFromEvaluation, workflow, } from '@sanity/workflow-engine';
|
|
2
|
+
import { Args } from '@oclif/core';
|
|
3
|
+
import { diagnoseInputFromEvaluation, workflow, unsatisfiedTransitionSummaries, } from '@sanity/workflow-engine';
|
|
4
4
|
import logSymbols from 'log-symbols';
|
|
5
|
+
import { WorkflowCommand } from "../lib/base-command.js";
|
|
5
6
|
import { resolveInstanceContext } from "../lib/context.js";
|
|
6
|
-
import {
|
|
7
|
+
import { fail, failureDetail } from "../lib/fail.js";
|
|
7
8
|
import { jsonFlags, tagFlags } from "../lib/flags.js";
|
|
9
|
+
import { baseEngineArgs } from "../lib/operation-args.js";
|
|
8
10
|
import { formatTimestamp, sectionHeader, activityIcon } from "../lib/ui.js";
|
|
9
11
|
import { instanceHeader } from "./show.js";
|
|
10
12
|
function formatAssignee(a) {
|
|
@@ -16,22 +18,23 @@ function activityLine(t, assignees) {
|
|
|
16
18
|
: '';
|
|
17
19
|
return ` ${activityIcon[t.status]} ${t.activity.name} ${styleText('dim', `[${t.status}]`)}${who}`;
|
|
18
20
|
}
|
|
19
|
-
function transitionLine(tr) {
|
|
20
|
-
const mark = tr.
|
|
21
|
-
|
|
21
|
+
function transitionLine(tr, explanation) {
|
|
22
|
+
const mark = tr.whenSatisfied ? styleText('green', 'ready') : styleText('red', 'false');
|
|
23
|
+
const line = ` → ${tr.transition.to} ${styleText('dim', tr.transition.when)} ${mark}`;
|
|
24
|
+
return explanation !== undefined ? `${line}\n ${styleText('dim', `↳ ${explanation}`)}` : line;
|
|
22
25
|
}
|
|
23
|
-
function currentStageLines(input) {
|
|
26
|
+
function currentStageLines(input, explanations) {
|
|
24
27
|
const lines = [
|
|
25
28
|
`${sectionHeader('Stage')} ${input.instance.currentStage}`,
|
|
26
29
|
sectionHeader('Activities'),
|
|
27
30
|
];
|
|
28
|
-
for (const t of input.activities) {
|
|
31
|
+
for (const t of input.activities.filter((a) => !a.scopedOut)) {
|
|
29
32
|
lines.push(activityLine(t, input.assignees[t.activity.name] ?? []));
|
|
30
33
|
}
|
|
31
34
|
if (input.transitions.length > 0) {
|
|
32
35
|
lines.push('', sectionHeader('Exit transitions'));
|
|
33
36
|
for (const tr of input.transitions) {
|
|
34
|
-
lines.push(transitionLine(tr));
|
|
37
|
+
lines.push(transitionLine(tr, explanations?.get(tr.transition.name)));
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
return lines;
|
|
@@ -39,14 +42,14 @@ function currentStageLines(input) {
|
|
|
39
42
|
function failedEffectDetail(effect) {
|
|
40
43
|
const ran = effect.durationMs !== undefined ? ` (after ${effect.durationMs}ms)` : '';
|
|
41
44
|
return {
|
|
42
|
-
headline: `a failed effect
|
|
45
|
+
headline: `a failed effect from action '${effect.origin.name}' is blocking its activity`,
|
|
43
46
|
why: [
|
|
44
47
|
styleText('red', `${logSymbols.error} failed effect: ${effect.name}`),
|
|
45
|
-
` queued by
|
|
48
|
+
` queued by action '${effect.origin.name}', failed ${formatTimestamp(effect.ranAt)}${ran}`,
|
|
46
49
|
...(effect.error !== undefined ? [` error: ${effect.error.message}`] : []),
|
|
47
50
|
'',
|
|
48
|
-
`
|
|
49
|
-
`external system, so the activity never
|
|
51
|
+
`The activity that fired '${effect.origin.name}' is waiting on this effect. It failed`,
|
|
52
|
+
`against an external system, so the activity never resolves and the stage can't advance.`,
|
|
50
53
|
],
|
|
51
54
|
};
|
|
52
55
|
}
|
|
@@ -71,12 +74,22 @@ function failedActivityDetail(activity) {
|
|
|
71
74
|
}
|
|
72
75
|
function waitingHeadline(w) {
|
|
73
76
|
const who = w.assignees.length > 0 ? ` on ${w.assignees.map(formatAssignee).join(', ')}` : '';
|
|
77
|
+
if (w.actions.length === 0) {
|
|
78
|
+
return `waiting${who} on automation — a trigger fires on its own when its condition holds`;
|
|
79
|
+
}
|
|
74
80
|
return `waiting${who} for action: ${w.actions.join(' or ')}`;
|
|
75
81
|
}
|
|
76
82
|
function waitingLines(w) {
|
|
77
83
|
const who = w.assignees.length > 0
|
|
78
84
|
? `assigned to ${w.assignees.map(formatAssignee).join(', ')}`
|
|
79
85
|
: 'unassigned — anyone with permission can act';
|
|
86
|
+
if (w.actions.length === 0) {
|
|
87
|
+
return [
|
|
88
|
+
`${logSymbols.info} activity '${w.activity}' is active (${who}),`,
|
|
89
|
+
`waiting on automation: its cascade-fired action(s) fire on their own when their`,
|
|
90
|
+
`trigger conditions hold — nothing for a caller to fire.`,
|
|
91
|
+
];
|
|
92
|
+
}
|
|
80
93
|
return [
|
|
81
94
|
`${logSymbols.info} activity '${w.activity}' is active (${who}),`,
|
|
82
95
|
`waiting for action: ${w.actions.join(' or ')}. This is the normal in-flight`,
|
|
@@ -100,36 +113,25 @@ function blockedLines(b) {
|
|
|
100
113
|
}
|
|
101
114
|
function noTransitionDetail() {
|
|
102
115
|
return {
|
|
103
|
-
headline: `no exit transition's
|
|
116
|
+
headline: `no exit transition's trigger is satisfied`,
|
|
104
117
|
why: [
|
|
105
|
-
`${logSymbols.info} every activity is resolved, but no exit transition's
|
|
106
|
-
`Likely a routing state value a
|
|
118
|
+
`${logSymbols.info} every activity is resolved, but no exit transition's \`when\` is true.`,
|
|
119
|
+
`Likely a routing state value a trigger reads never got written.`,
|
|
107
120
|
],
|
|
108
121
|
};
|
|
109
122
|
}
|
|
110
|
-
/** An unevaluable exit filter is a *recoverable* hold, not a dead end: it
|
|
111
|
-
* clears itself once the operand becomes readable, and no engine verb
|
|
112
|
-
* unsticks it (so the engine emits no remediation). The "what to do" —
|
|
113
|
-
* make the operand readable — lives here in the why, since there is no
|
|
114
|
-
* runnable fix to render. */
|
|
115
123
|
function transitionUnevaluableDetail(transitions) {
|
|
116
124
|
return {
|
|
117
|
-
headline: `an exit transition's
|
|
125
|
+
headline: `an exit transition's trigger could not be evaluated`,
|
|
118
126
|
why: [
|
|
119
127
|
`${logSymbols.info} every activity is resolved, but ${transitions.join(', ')} reads an operand`,
|
|
120
128
|
`that is missing or unreadable (GROQ null), so routing is held rather than`,
|
|
121
|
-
`falling through. Make the data the
|
|
129
|
+
`falling through. Make the data the trigger reads readable — publish the`,
|
|
122
130
|
`subject (or fill the field) — and the instance advances on its own; no`,
|
|
123
131
|
`set-stage needed.`,
|
|
124
132
|
],
|
|
125
133
|
};
|
|
126
134
|
}
|
|
127
|
-
/** Render the engine's structured remediations as the "suggested fix" block.
|
|
128
|
-
* Verbs render under their engine names — the same vocabulary `--json`
|
|
129
|
-
* emits, so scripts and humans read one language. Only runnable ones reach
|
|
130
|
-
* here: {@link renderDiagnosis} filters on the engine's `available` flag,
|
|
131
|
-
* so verbs that aren't callable engine operations yet never render as a
|
|
132
|
-
* fix (the full set, planned verbs included, still rides `--json`). */
|
|
133
135
|
function remediationLines(remediations) {
|
|
134
136
|
return remediations.map((r) => ` • ${r.verb} — ${r.rationale}`);
|
|
135
137
|
}
|
|
@@ -147,6 +149,11 @@ function causeDetail(cause) {
|
|
|
147
149
|
return transitionUnevaluableDetail(cause.transitions);
|
|
148
150
|
}
|
|
149
151
|
}
|
|
152
|
+
function liveChildrenTail(diagnosis) {
|
|
153
|
+
if (diagnosis.liveChildren === undefined)
|
|
154
|
+
return '';
|
|
155
|
+
return ` ${styleText('yellow', `${diagnosis.liveChildren} spawned child workflow(s) still running.`)}`;
|
|
156
|
+
}
|
|
150
157
|
function statusLine(diagnosis) {
|
|
151
158
|
switch (diagnosis.state) {
|
|
152
159
|
case 'progressing':
|
|
@@ -156,31 +163,37 @@ function statusLine(diagnosis) {
|
|
|
156
163
|
case 'blocked':
|
|
157
164
|
return `${logSymbols.info} ${styleText('cyan', 'blocked')} — ${blockedHeadline(diagnosis)}`;
|
|
158
165
|
case 'completed':
|
|
159
|
-
return `${logSymbols.success} ${styleText('green', 'completed')} at ${formatTimestamp(diagnosis.at)}
|
|
166
|
+
return `${logSymbols.success} ${styleText('green', 'completed')} at ${formatTimestamp(diagnosis.at)}.${liveChildrenTail(diagnosis)}`;
|
|
160
167
|
case 'aborted': {
|
|
161
168
|
const tail = diagnosis.reason !== undefined ? ` — ${diagnosis.reason}` : '';
|
|
162
|
-
return `${logSymbols.info} ${styleText('dim', 'aborted')} at ${formatTimestamp(diagnosis.at)}${tail}
|
|
169
|
+
return `${logSymbols.info} ${styleText('dim', 'aborted')} at ${formatTimestamp(diagnosis.at)}${tail}.${liveChildrenTail(diagnosis)}`;
|
|
163
170
|
}
|
|
164
171
|
case 'stuck':
|
|
165
172
|
return `${logSymbols.warning} ${styleText('yellow', 'STUCK')} — ${causeDetail(diagnosis.cause).headline}`;
|
|
166
173
|
}
|
|
167
174
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
export function rawTransitionProjection(evaluation) {
|
|
176
|
+
return evaluation.currentStage.transitions.map((t) => ({
|
|
177
|
+
name: t.transition.name,
|
|
178
|
+
to: t.transition.to,
|
|
179
|
+
whenSatisfied: t.whenSatisfied,
|
|
180
|
+
unevaluable: t.unevaluable,
|
|
181
|
+
blockedBy: t.insight.blockedBy.map((atom) => ({
|
|
182
|
+
groq: atom.atom.groq,
|
|
183
|
+
negated: atom.atom.negated,
|
|
184
|
+
pivotal: atom.pivotal,
|
|
185
|
+
...(atom.requirement !== undefined ? { requirement: atom.requirement } : {}),
|
|
186
|
+
})),
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
export function renderDiagnosis({ diagnosis, input, remediations, explanations, }) {
|
|
177
190
|
const lines = [statusLine(diagnosis)];
|
|
178
191
|
const inFlight = diagnosis.state === 'progressing' ||
|
|
179
192
|
diagnosis.state === 'waiting' ||
|
|
180
193
|
diagnosis.state === 'blocked' ||
|
|
181
194
|
diagnosis.state === 'stuck';
|
|
182
195
|
if (inFlight) {
|
|
183
|
-
lines.push('', ...currentStageLines(input));
|
|
196
|
+
lines.push('', ...currentStageLines(input, explanations));
|
|
184
197
|
}
|
|
185
198
|
if (diagnosis.state === 'waiting') {
|
|
186
199
|
lines.push('', ...waitingLines(diagnosis));
|
|
@@ -192,16 +205,13 @@ export function renderDiagnosis({ diagnosis, input, remediations, }) {
|
|
|
192
205
|
const detail = causeDetail(diagnosis.cause);
|
|
193
206
|
const runnable = remediations.filter((r) => r.available);
|
|
194
207
|
lines.push('', sectionHeader("Why it's stuck"), ...detail.why);
|
|
195
|
-
// A recoverable hold (e.g. an unevaluable transition filter) has no
|
|
196
|
-
// runnable remediation — its "what to do" is in the why above, so skip
|
|
197
|
-
// the empty fix header rather than dangle it.
|
|
198
208
|
if (runnable.length > 0) {
|
|
199
209
|
lines.push('', sectionHeader('Suggested fix'), ...remediationLines(runnable));
|
|
200
210
|
}
|
|
201
211
|
}
|
|
202
212
|
return lines;
|
|
203
213
|
}
|
|
204
|
-
export default class Diagnose extends
|
|
214
|
+
export default class Diagnose extends WorkflowCommand {
|
|
205
215
|
static description = "Explain why a workflow instance is or isn't progressing, and what would unstick it.";
|
|
206
216
|
static examples = [
|
|
207
217
|
'<%= config.bin %> diagnose wf-instance.abc123',
|
|
@@ -221,18 +231,10 @@ export default class Diagnose extends Command {
|
|
|
221
231
|
const { evaluation, diagnosis, remediations } = await workflow
|
|
222
232
|
.diagnose({
|
|
223
233
|
client,
|
|
224
|
-
|
|
225
|
-
workflowResource: scope.workflowResource,
|
|
234
|
+
...baseEngineArgs(scope),
|
|
226
235
|
instanceId: args.instanceId,
|
|
227
|
-
// Token-auth CLI can't hit /users/me; pin the same system actor the
|
|
228
|
-
// write verbs use. Transition filters and activity status are actor-
|
|
229
|
-
// independent. A requirement that reads $actor/$assigned/$can is the
|
|
230
|
-
// exception — it's evaluated against this system actor, so a `blocked`
|
|
231
|
-
// verdict on such a requirement reflects the engine's view, not a
|
|
232
|
-
// specific assignee's.
|
|
233
|
-
access: { actor: { kind: 'system', id: 'workflow-cli' } },
|
|
234
236
|
})
|
|
235
|
-
.catch((error) => fail('diagnose failed:',
|
|
237
|
+
.catch((error) => fail('diagnose failed:', failureDetail(error)));
|
|
236
238
|
const input = diagnoseInputFromEvaluation(evaluation);
|
|
237
239
|
if (flags.json) {
|
|
238
240
|
this.log(JSON.stringify({
|
|
@@ -240,12 +242,14 @@ export default class Diagnose extends Command {
|
|
|
240
242
|
stage: evaluation.instance.currentStage,
|
|
241
243
|
diagnosis,
|
|
242
244
|
remediations,
|
|
245
|
+
transitions: rawTransitionProjection(evaluation),
|
|
243
246
|
}, null, 2));
|
|
244
247
|
return;
|
|
245
248
|
}
|
|
246
249
|
this.log(instanceHeader(evaluation.instance));
|
|
247
250
|
this.log('');
|
|
248
|
-
|
|
251
|
+
const explanations = new Map(unsatisfiedTransitionSummaries(evaluation).map((entry) => [entry.transition, entry.summary]));
|
|
252
|
+
for (const line of renderDiagnosis({ diagnosis, input, remediations, explanations })) {
|
|
249
253
|
this.log(line);
|
|
250
254
|
}
|
|
251
255
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
1
|
import { type AvailableAction } from '@sanity/workflow-engine';
|
|
2
|
+
import { WorkflowCommand } from '../lib/base-command.ts';
|
|
3
3
|
import { type WriteOutcome, type WriteReport } from '../lib/ops-report.ts';
|
|
4
4
|
/**
|
|
5
5
|
* The list-mode body: every action on the current stage's activities, marked
|
|
@@ -29,7 +29,7 @@ export declare function fireActionReport({ result, activity, action, }: {
|
|
|
29
29
|
activity: string;
|
|
30
30
|
action: string;
|
|
31
31
|
}): WriteReport;
|
|
32
|
-
export default class FireAction extends
|
|
32
|
+
export default class FireAction extends WorkflowCommand {
|
|
33
33
|
static description: string;
|
|
34
34
|
static examples: string[];
|
|
35
35
|
static args: {
|
|
@@ -37,8 +37,6 @@ export default class FireAction extends Command {
|
|
|
37
37
|
};
|
|
38
38
|
static flags: {
|
|
39
39
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
40
|
-
as: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
41
|
-
'as-role': import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
42
40
|
activity: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
43
41
|
action: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
44
42
|
param: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,37 +1,38 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
|
-
import { Args,
|
|
3
|
-
import { deniedGuardLabels, workflow, } from '@sanity/workflow-engine';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { actionRendering, actionDisabledDetail, deniedGuardLabels, workflow, } from '@sanity/workflow-engine';
|
|
4
4
|
import logSymbols from 'log-symbols';
|
|
5
|
-
import
|
|
5
|
+
import { WorkflowCommand } from "../lib/base-command.js";
|
|
6
6
|
import { resolveInstanceContext } from "../lib/context.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { baseEngineArgs
|
|
10
|
-
import {
|
|
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 { cascadeTail, opsAppliedLines, runWriteVerb, } from "../lib/ops-report.js";
|
|
11
11
|
import { parseParams } from "../lib/params.js";
|
|
12
12
|
import { instanceHeader } from "./show.js";
|
|
13
13
|
function reasonText(reason) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return `action filter not satisfied${reason.detail ? ` (${reason.detail})` : ''}`;
|
|
17
|
-
case 'activity-not-active':
|
|
18
|
-
return `activity is ${reason.status}`;
|
|
19
|
-
case 'stage-terminal':
|
|
20
|
-
return `stage '${reason.stage}' is terminal`;
|
|
21
|
-
case 'mutation-guard-denied':
|
|
22
|
-
return `blocked by mutation guard(s) ${deniedGuardLabels(reason.denied).join(', ')}`;
|
|
23
|
-
case 'instance-completed':
|
|
24
|
-
return `instance completed at ${reason.completedAt}`;
|
|
25
|
-
case 'requirements-unmet':
|
|
26
|
-
return `unmet requirement(s): ${reason.unmetRequirements.join(', ')}`;
|
|
14
|
+
if (reason.kind === 'mutation-guard-denied') {
|
|
15
|
+
return `blocked by mutation guard(s) ${deniedGuardLabels(reason.denied).join(', ')}`;
|
|
27
16
|
}
|
|
17
|
+
return actionDisabledDetail(reason);
|
|
28
18
|
}
|
|
29
19
|
function paramLabel(p) {
|
|
30
20
|
return `${p.name}${p.required === true ? '' : '?'}:${p.type}`;
|
|
31
21
|
}
|
|
32
22
|
function actionLines(a) {
|
|
33
|
-
const mark = a.allowed ? logSymbols.success : logSymbols.error;
|
|
34
23
|
const title = a.title !== undefined ? styleText('dim', ` (${a.title})`) : '';
|
|
24
|
+
const rendering = actionRendering(a);
|
|
25
|
+
if (rendering === 'absent')
|
|
26
|
+
return [];
|
|
27
|
+
if (rendering === 'automation') {
|
|
28
|
+
return [
|
|
29
|
+
` ${styleText('cyan', '⚡')} ${a.activity} → ${a.action}${title} ${styleText('dim', '[automation]')}`,
|
|
30
|
+
...(a.disabledReason?.kind === 'cascade-fired'
|
|
31
|
+
? [styleText('dim', ` ${actionDisabledDetail(a.disabledReason)}`)]
|
|
32
|
+
: []),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
const mark = a.allowed ? logSymbols.success : logSymbols.error;
|
|
35
36
|
const lines = [
|
|
36
37
|
` ${mark} ${a.activity} → ${a.action}${title} ${styleText('dim', `[activity ${a.activityStatus}]`)}`,
|
|
37
38
|
];
|
|
@@ -43,12 +44,6 @@ function actionLines(a) {
|
|
|
43
44
|
}
|
|
44
45
|
return lines;
|
|
45
46
|
}
|
|
46
|
-
/**
|
|
47
|
-
* The list-mode body: every action on the current stage's activities, marked
|
|
48
|
-
* fireable or not (with the disabling reason), plus a hint showing the
|
|
49
|
-
* flags to fire one. Pure so the populated/empty permutations assert
|
|
50
|
-
* without a live evaluation.
|
|
51
|
-
*/
|
|
52
47
|
export function renderAvailableActions({ actions, stage, instanceId, }) {
|
|
53
48
|
if (actions.length === 0) {
|
|
54
49
|
return [`No actions on the activities of the current stage ('${stage}').`];
|
|
@@ -60,8 +55,6 @@ export function renderAvailableActions({ actions, stage, instanceId, }) {
|
|
|
60
55
|
lines.push('', styleText('dim', 'Fire one with:'), styleText('dim', ` fire-action ${instanceId} --activity <activity> --action <action>`));
|
|
61
56
|
return lines;
|
|
62
57
|
}
|
|
63
|
-
/** The `--json` payload for a fired action — the structured counterpart of
|
|
64
|
-
* {@link fireActionReport}. Pure so its shape is asserted directly. */
|
|
65
58
|
export function fireResultJson(result, ids) {
|
|
66
59
|
return {
|
|
67
60
|
instanceId: ids.instanceId,
|
|
@@ -73,11 +66,6 @@ export function fireResultJson(result, ids) {
|
|
|
73
66
|
...(result.ranOps !== undefined ? { ranOps: result.ranOps } : {}),
|
|
74
67
|
};
|
|
75
68
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Turn a `fireAction` result into the spinner message + ops block. Pure so
|
|
78
|
-
* the fired / no-op / cascaded / ops permutations assert without a live
|
|
79
|
-
* fire.
|
|
80
|
-
*/
|
|
81
69
|
export function fireActionReport({ result, activity, action, }) {
|
|
82
70
|
const stage = styleText('bold', result.instance.currentStage);
|
|
83
71
|
if (!result.changed) {
|
|
@@ -87,19 +75,17 @@ export function fireActionReport({ result, activity, action, }) {
|
|
|
87
75
|
opsLines: [],
|
|
88
76
|
};
|
|
89
77
|
}
|
|
90
|
-
const tail = result.cascaded > 0 ? `, then cascaded ${result.cascaded} auto-transition(s)` : '';
|
|
91
78
|
return {
|
|
92
79
|
changed: true,
|
|
93
|
-
message: `Fired ${styleText('bold', action)} on ${styleText('bold', activity)} — now at ${stage}${
|
|
80
|
+
message: `Fired ${styleText('bold', action)} on ${styleText('bold', activity)} — now at ${stage}${cascadeTail(result.cascaded)}`,
|
|
94
81
|
opsLines: opsAppliedLines(result.ranOps),
|
|
95
82
|
};
|
|
96
83
|
}
|
|
97
|
-
export default class FireAction extends
|
|
98
|
-
static description =
|
|
84
|
+
export default class FireAction extends WorkflowCommand {
|
|
85
|
+
static description = 'Fire an action on an instance to unstick a waiting activity — the write acts as the configured token. Omit --action to list what can be fired.';
|
|
99
86
|
static examples = [
|
|
100
87
|
'<%= config.bin %> fire-action wf-instance.abc123',
|
|
101
88
|
'<%= config.bin %> fire-action wf-instance.abc123 --activity approve --action approve',
|
|
102
|
-
'<%= config.bin %> fire-action wf-instance.abc123 --activity approve --action approve --as user.xyz --as-role editor',
|
|
103
89
|
'<%= config.bin %> fire-action wf-instance.abc123 --activity publish --action publish --param note=shipping',
|
|
104
90
|
];
|
|
105
91
|
static args = {
|
|
@@ -118,19 +104,16 @@ export default class FireAction extends Command {
|
|
|
118
104
|
multiple: true,
|
|
119
105
|
default: [],
|
|
120
106
|
}),
|
|
121
|
-
...actorFlags,
|
|
122
107
|
...jsonFlags,
|
|
123
108
|
};
|
|
124
109
|
async run() {
|
|
125
110
|
const { args, flags } = await this.parse(FireAction);
|
|
126
111
|
const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
|
|
127
|
-
const actor = failOnThrow('fire-action usage error:', () => resolveActor(flags.as, flags['as-role']));
|
|
128
112
|
if (flags.action === undefined) {
|
|
129
113
|
await this.listActions({
|
|
130
114
|
client,
|
|
131
115
|
scope,
|
|
132
116
|
instanceId: args.instanceId,
|
|
133
|
-
actor,
|
|
134
117
|
json: flags.json,
|
|
135
118
|
});
|
|
136
119
|
return;
|
|
@@ -142,21 +125,20 @@ export default class FireAction extends Command {
|
|
|
142
125
|
client,
|
|
143
126
|
scope,
|
|
144
127
|
instanceId: args.instanceId,
|
|
145
|
-
actor,
|
|
146
128
|
activity: flags.activity,
|
|
147
129
|
action: flags.action,
|
|
148
130
|
params: flags.param,
|
|
149
131
|
json: flags.json,
|
|
150
132
|
});
|
|
151
133
|
}
|
|
152
|
-
async listActions({ client, scope, instanceId,
|
|
134
|
+
async listActions({ client, scope, instanceId, json, }) {
|
|
153
135
|
const { evaluation, actions } = await workflow
|
|
154
136
|
.availableActions({
|
|
155
137
|
client,
|
|
156
|
-
...baseEngineArgs(scope
|
|
138
|
+
...baseEngineArgs(scope),
|
|
157
139
|
instanceId,
|
|
158
140
|
})
|
|
159
|
-
.catch((error) => fail('fire-action failed:',
|
|
141
|
+
.catch((error) => fail('fire-action failed:', failureDetail(error)));
|
|
160
142
|
const stage = evaluation.instance.currentStage;
|
|
161
143
|
if (json) {
|
|
162
144
|
this.log(JSON.stringify({ instanceId, stage, actions }, null, 2));
|
|
@@ -169,11 +151,11 @@ export default class FireAction extends Command {
|
|
|
169
151
|
}
|
|
170
152
|
}
|
|
171
153
|
async fireOne(opts) {
|
|
172
|
-
const { client, scope, instanceId,
|
|
154
|
+
const { client, scope, instanceId, activity, action, json } = opts;
|
|
173
155
|
const params = failOnThrow('Invalid --param:', () => parseParams(opts.params));
|
|
174
156
|
const fireArgs = {
|
|
175
157
|
client,
|
|
176
|
-
...baseEngineArgs(scope
|
|
158
|
+
...baseEngineArgs(scope),
|
|
177
159
|
instanceId,
|
|
178
160
|
activity,
|
|
179
161
|
action,
|
|
@@ -182,22 +164,17 @@ export default class FireAction extends Command {
|
|
|
182
164
|
if (json) {
|
|
183
165
|
const result = await workflow
|
|
184
166
|
.fireAction(fireArgs)
|
|
185
|
-
.catch((error) => fail('fire-action error:',
|
|
167
|
+
.catch((error) => fail('fire-action error:', failureDetail(error)));
|
|
186
168
|
this.log(JSON.stringify(fireResultJson(result, { instanceId, activity, action }), null, 2));
|
|
187
169
|
return;
|
|
188
170
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
catch (error) {
|
|
199
|
-
spinner.fail('Action rejected');
|
|
200
|
-
fail('fire-action error:', errorMessage(error));
|
|
201
|
-
}
|
|
171
|
+
await runWriteVerb({
|
|
172
|
+
startLabel: `Firing ${action} on ${activity}…`,
|
|
173
|
+
failLabel: 'Action rejected',
|
|
174
|
+
failHeadline: 'fire-action error:',
|
|
175
|
+
run: () => workflow.fireAction(fireArgs),
|
|
176
|
+
report: (result) => fireActionReport({ result, activity, action }),
|
|
177
|
+
log: (line) => this.log(line),
|
|
178
|
+
});
|
|
202
179
|
}
|
|
203
180
|
}
|
package/dist/commands/list.d.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type TerminalState } from '@sanity/workflow-engine';
|
|
2
|
+
import { WorkflowCommand } from '../lib/base-command.ts';
|
|
2
3
|
interface InstanceRow {
|
|
3
4
|
_id: string;
|
|
4
5
|
definition: string;
|
|
5
6
|
tag: string;
|
|
6
7
|
currentStage: string;
|
|
7
|
-
completedAt?: string;
|
|
8
|
-
abortedAt?: string;
|
|
8
|
+
completedAt?: string | null;
|
|
9
|
+
abortedAt?: string | null;
|
|
9
10
|
lastChangedAt: string;
|
|
10
11
|
}
|
|
11
|
-
type InstanceStatus = 'aborted' | 'completed' | 'in-flight';
|
|
12
12
|
interface ListFlags {
|
|
13
|
-
'
|
|
13
|
+
'include-completed': boolean;
|
|
14
14
|
failed: boolean;
|
|
15
15
|
definition?: string | undefined;
|
|
16
|
+
document?: string | undefined;
|
|
16
17
|
tag?: string | undefined;
|
|
17
18
|
limit: number;
|
|
18
19
|
}
|
|
@@ -20,28 +21,35 @@ interface ListFlags {
|
|
|
20
21
|
* Build the GROQ query + params for `list` from its flags. Pure so the
|
|
21
22
|
* filter combinations can be asserted without a live dataset. `--tag` is an
|
|
22
23
|
* optional filter: omit it and the listing spans every partition.
|
|
24
|
+
*
|
|
25
|
+
* With `--document` the lake filter is the engine's prefilter arm — a
|
|
26
|
+
* deliberate superset narrowed to the exact watch-set in JS
|
|
27
|
+
* ({@link instanceWatchesDocument}) — so the rows come back as full
|
|
28
|
+
* documents (the narrowing reads fields/stages/ancestors) and UNCAPPED: a
|
|
29
|
+
* capped superset could silently hide matches. The display limit applies
|
|
30
|
+
* after narrowing.
|
|
23
31
|
*/
|
|
24
32
|
export declare function buildListQuery(flags: ListFlags): {
|
|
25
33
|
groq: string;
|
|
26
34
|
params: Record<string, unknown>;
|
|
27
35
|
};
|
|
28
|
-
/** Map a queried instance to its display row (derives the status column).
|
|
29
|
-
* Pure — status is the semantic value; the table colors it at render. */
|
|
36
|
+
/** Map a queried instance to its display row (derives the status column). */
|
|
30
37
|
export declare function instanceRow(r: InstanceRow): {
|
|
31
38
|
_id: string;
|
|
32
39
|
definition: string;
|
|
33
40
|
tag: string;
|
|
34
41
|
currentStage: string;
|
|
35
|
-
status:
|
|
42
|
+
status: TerminalState;
|
|
36
43
|
lastChangedAt: string;
|
|
37
44
|
};
|
|
38
|
-
export default class List extends
|
|
45
|
+
export default class List extends WorkflowCommand {
|
|
39
46
|
static description: string;
|
|
40
47
|
static examples: string[];
|
|
41
48
|
static flags: {
|
|
42
|
-
'
|
|
49
|
+
'include-completed': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
43
50
|
failed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
44
51
|
definition: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
52
|
+
document: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
45
53
|
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
46
54
|
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
47
55
|
};
|