@sanity/workflow-cli 0.31.0 → 0.33.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 +183 -0
- package/README.md +244 -42
- package/dist/commands/workflows/blueprint/generate.d.ts +9 -0
- package/dist/commands/workflows/blueprint/generate.js +28 -0
- package/dist/commands/workflows/diagnose.d.ts +4 -2
- package/dist/commands/workflows/diagnose.js +26 -88
- package/dist/commands/workflows/list.d.ts +10 -2
- package/dist/commands/workflows/list.js +102 -35
- package/dist/commands/workflows/show.d.ts +3 -5
- package/dist/commands/workflows/show.js +35 -7
- package/dist/commands/workflows/tail.js +2 -2
- package/dist/hooks/prerun/telemetry.d.ts +6 -0
- package/dist/hooks/prerun/telemetry.js +14 -7
- package/dist/lib/blueprint-emit.d.ts +30 -0
- package/dist/lib/blueprint-emit.js +147 -0
- package/dist/lib/blueprint-needs.d.ts +2 -0
- package/dist/lib/blueprint-needs.js +109 -0
- package/dist/lib/cause-detail.d.ts +9 -0
- package/dist/lib/cause-detail.js +111 -0
- package/dist/lib/load-config.d.ts +20 -0
- package/dist/lib/load-config.js +16 -13
- package/dist/lib/share-definitions.d.ts +2 -2
- package/dist/lib/share-definitions.js +2 -2
- package/dist/lib/telemetry-setup.d.ts +2 -0
- package/dist/lib/telemetry-setup.js +2 -2
- package/dist/lib/telemetry.d.ts +5 -2
- package/dist/lib/telemetry.js +13 -4
- package/dist/lib/ui.d.ts +0 -6
- package/dist/lib/ui.js +0 -17
- package/dist/standalone-argv.d.ts +2 -2
- package/dist/standalone-argv.js +1 -0
- package/oclif.manifest.json +60 -1
- package/package.json +18 -8
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
2
|
import { Args } from '@oclif/core';
|
|
3
|
+
import { formatDateTime } from '@sanity/cli-core/dates';
|
|
3
4
|
import { diagnoseInputFromEvaluation, workflow, unsatisfiedTransitionSummaries, } from '@sanity/workflow-engine';
|
|
4
5
|
import logSymbols from 'log-symbols';
|
|
5
6
|
import { WorkflowCommand } from "../../lib/base-command.js";
|
|
7
|
+
import { stuckDiagnosisLines, stuckHeadline } from "../../lib/cause-detail.js";
|
|
6
8
|
import { resolveInstanceContext } from "../../lib/context.js";
|
|
7
9
|
import { fail, failureDetail } from "../../lib/fail.js";
|
|
8
10
|
import { instanceFlags, jsonFlags } from "../../lib/flags.js";
|
|
9
11
|
import { baseEngineArgs } from "../../lib/operation-args.js";
|
|
10
|
-
import {
|
|
12
|
+
import { sectionHeader, activityIcon } from "../../lib/ui.js";
|
|
11
13
|
import { instanceHeader } from "./show.js";
|
|
12
14
|
function formatAssignee(a) {
|
|
13
15
|
return a.type === 'user' ? `user:${a.id}` : `role:${a.role}`;
|
|
@@ -39,57 +41,32 @@ function currentStageLines(input, explanations) {
|
|
|
39
41
|
}
|
|
40
42
|
return lines;
|
|
41
43
|
}
|
|
42
|
-
function failedEffectDetail(effect) {
|
|
43
|
-
const ran = effect.durationMs !== undefined ? ` (after ${effect.durationMs}ms)` : '';
|
|
44
|
-
return {
|
|
45
|
-
headline: `a failed effect from action '${effect.origin.name}' is blocking its activity`,
|
|
46
|
-
why: [
|
|
47
|
-
styleText('red', `${logSymbols.error} failed effect: ${effect.name}`),
|
|
48
|
-
` queued by action '${effect.origin.name}', failed ${formatTimestamp(effect.ranAt)}${ran}`,
|
|
49
|
-
...(effect.error !== undefined ? [` error: ${effect.error.message}`] : []),
|
|
50
|
-
'',
|
|
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.`,
|
|
53
|
-
],
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
function hungEffectDetail(effect) {
|
|
57
|
-
return {
|
|
58
|
-
headline: `effect '${effect.name}' was claimed but never completed`,
|
|
59
|
-
why: [
|
|
60
|
-
styleText('yellow', `${logSymbols.warning} hung effect: ${effect.name}`),
|
|
61
|
-
` claimed ${formatTimestamp(effect.claim?.claimedAt ?? '?')} but never reported back — the`,
|
|
62
|
-
` drainer likely died mid-dispatch, so it won't drain on its own.`,
|
|
63
|
-
],
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
function failedActivityDetail(activity) {
|
|
67
|
-
return {
|
|
68
|
-
headline: `activity '${activity}' failed`,
|
|
69
|
-
why: [
|
|
70
|
-
styleText('red', `${logSymbols.error} activity '${activity}' is in a terminal failed state.`),
|
|
71
|
-
`Any exit transition gated on '${activity}' being done can never fire.`,
|
|
72
|
-
],
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
44
|
function waitingHeadline(w) {
|
|
76
45
|
const who = w.assignees.length > 0 ? ` on ${w.assignees.map(formatAssignee).join(', ')}` : '';
|
|
77
|
-
if (w.
|
|
46
|
+
if (w.waitingFor === 'automation') {
|
|
78
47
|
return `waiting${who} on automation — a trigger fires on its own when its condition holds`;
|
|
79
48
|
}
|
|
49
|
+
if (w.waitingFor === 'manual-action')
|
|
50
|
+
return `waiting${who} for an eligible caller to act`;
|
|
80
51
|
return `waiting${who} for action: ${w.actions.join(' or ')}`;
|
|
81
52
|
}
|
|
82
53
|
function waitingLines(w) {
|
|
83
54
|
const who = w.assignees.length > 0
|
|
84
55
|
? `assigned to ${w.assignees.map(formatAssignee).join(', ')}`
|
|
85
|
-
: 'unassigned —
|
|
86
|
-
if (w.
|
|
56
|
+
: 'unassigned — no holder is selected';
|
|
57
|
+
if (w.waitingFor === 'automation') {
|
|
87
58
|
return [
|
|
88
59
|
`${logSymbols.info} activity '${w.activity}' is active (${who}),`,
|
|
89
60
|
`waiting on automation: its cascade-fired action(s) fire on their own when their`,
|
|
90
61
|
`trigger conditions hold — nothing for a caller to fire.`,
|
|
91
62
|
];
|
|
92
63
|
}
|
|
64
|
+
if (w.waitingFor === 'manual-action') {
|
|
65
|
+
return [
|
|
66
|
+
`${logSymbols.info} activity '${w.activity}' is active (${who}),`,
|
|
67
|
+
`waiting for a manual action, but none is available to this caller right now.`,
|
|
68
|
+
];
|
|
69
|
+
}
|
|
93
70
|
return [
|
|
94
71
|
`${logSymbols.info} activity '${w.activity}' is active (${who}),`,
|
|
95
72
|
`waiting for action: ${w.actions.join(' or ')}. This is the normal in-flight`,
|
|
@@ -111,44 +88,6 @@ function blockedLines(b) {
|
|
|
111
88
|
`activity completing, required content landing). It will not advance on its own.`,
|
|
112
89
|
];
|
|
113
90
|
}
|
|
114
|
-
function noTransitionDetail() {
|
|
115
|
-
return {
|
|
116
|
-
headline: `no exit transition's trigger is satisfied`,
|
|
117
|
-
why: [
|
|
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.`,
|
|
120
|
-
],
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
function transitionUnevaluableDetail(transitions) {
|
|
124
|
-
return {
|
|
125
|
-
headline: `an exit transition's trigger could not be evaluated`,
|
|
126
|
-
why: [
|
|
127
|
-
`${logSymbols.info} every activity is resolved, but ${transitions.join(', ')} reads an operand`,
|
|
128
|
-
`that is missing or unreadable (GROQ null), so routing is held rather than`,
|
|
129
|
-
`falling through. Make the data the trigger reads readable — publish the`,
|
|
130
|
-
`subject (or fill the field) — and the instance advances on its own; no`,
|
|
131
|
-
`set-stage needed.`,
|
|
132
|
-
],
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
function remediationLines(remediations) {
|
|
136
|
-
return remediations.map((r) => ` • ${r.verb} — ${r.rationale}`);
|
|
137
|
-
}
|
|
138
|
-
function causeDetail(cause) {
|
|
139
|
-
switch (cause.kind) {
|
|
140
|
-
case 'failed-effect':
|
|
141
|
-
return failedEffectDetail(cause.effect);
|
|
142
|
-
case 'hung-effect':
|
|
143
|
-
return hungEffectDetail(cause.effect);
|
|
144
|
-
case 'failed-activity':
|
|
145
|
-
return failedActivityDetail(cause.activity);
|
|
146
|
-
case 'no-transition-fires':
|
|
147
|
-
return noTransitionDetail();
|
|
148
|
-
case 'transition-unevaluable':
|
|
149
|
-
return transitionUnevaluableDetail(cause.transitions);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
91
|
function liveChildrenTail(diagnosis) {
|
|
153
92
|
if (diagnosis.liveChildren === undefined)
|
|
154
93
|
return '';
|
|
@@ -163,13 +102,13 @@ function statusLine(diagnosis) {
|
|
|
163
102
|
case 'blocked':
|
|
164
103
|
return `${logSymbols.info} ${styleText('cyan', 'blocked')} — ${blockedHeadline(diagnosis)}`;
|
|
165
104
|
case 'completed':
|
|
166
|
-
return `${logSymbols.success} ${styleText('green', 'completed')} at ${
|
|
105
|
+
return `${logSymbols.success} ${styleText('green', 'completed')} at ${formatDateTime(diagnosis.at)}.${liveChildrenTail(diagnosis)}`;
|
|
167
106
|
case 'aborted': {
|
|
168
107
|
const tail = diagnosis.reason !== undefined ? ` — ${diagnosis.reason}` : '';
|
|
169
|
-
return `${logSymbols.info} ${styleText('dim', 'aborted')} at ${
|
|
108
|
+
return `${logSymbols.info} ${styleText('dim', 'aborted')} at ${formatDateTime(diagnosis.at)}${tail}.${liveChildrenTail(diagnosis)}`;
|
|
170
109
|
}
|
|
171
110
|
case 'stuck':
|
|
172
|
-
return
|
|
111
|
+
return stuckHeadline(diagnosis.cause);
|
|
173
112
|
}
|
|
174
113
|
}
|
|
175
114
|
export function rawTransitionProjection(evaluation) {
|
|
@@ -186,7 +125,7 @@ export function rawTransitionProjection(evaluation) {
|
|
|
186
125
|
})),
|
|
187
126
|
}));
|
|
188
127
|
}
|
|
189
|
-
export function renderDiagnosis({ diagnosis, input, remediations, explanations, }) {
|
|
128
|
+
export function renderDiagnosis({ diagnosis, input, remediations, explanations, allMissingDocuments, }) {
|
|
190
129
|
const lines = [statusLine(diagnosis)];
|
|
191
130
|
const inFlight = diagnosis.state === 'progressing' ||
|
|
192
131
|
diagnosis.state === 'waiting' ||
|
|
@@ -201,14 +140,7 @@ export function renderDiagnosis({ diagnosis, input, remediations, explanations,
|
|
|
201
140
|
if (diagnosis.state === 'blocked') {
|
|
202
141
|
lines.push('', ...blockedLines(diagnosis));
|
|
203
142
|
}
|
|
204
|
-
|
|
205
|
-
const detail = causeDetail(diagnosis.cause);
|
|
206
|
-
const runnable = remediations.filter((r) => r.available);
|
|
207
|
-
lines.push('', sectionHeader("Why it's stuck"), ...detail.why);
|
|
208
|
-
if (runnable.length > 0) {
|
|
209
|
-
lines.push('', sectionHeader('Suggested fix'), ...remediationLines(runnable));
|
|
210
|
-
}
|
|
211
|
-
}
|
|
143
|
+
lines.push(...stuckDiagnosisLines({ diagnosis, allMissingDocuments, remediations }));
|
|
212
144
|
return lines;
|
|
213
145
|
}
|
|
214
146
|
export default class Diagnose extends WorkflowCommand {
|
|
@@ -249,7 +181,13 @@ export default class Diagnose extends WorkflowCommand {
|
|
|
249
181
|
this.log(instanceHeader(evaluation.instance));
|
|
250
182
|
this.log('');
|
|
251
183
|
const explanations = new Map(unsatisfiedTransitionSummaries(evaluation).map((entry) => [entry.transition, entry.summary]));
|
|
252
|
-
for (const line of renderDiagnosis({
|
|
184
|
+
for (const line of renderDiagnosis({
|
|
185
|
+
diagnosis,
|
|
186
|
+
input,
|
|
187
|
+
remediations,
|
|
188
|
+
explanations,
|
|
189
|
+
allMissingDocuments: evaluation.missingDocuments,
|
|
190
|
+
})) {
|
|
253
191
|
this.log(line);
|
|
254
192
|
}
|
|
255
193
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type GdrUri, type TerminalState } from '@sanity/workflow-engine';
|
|
1
|
+
import { type AssignmentIdentity, type AssignmentStateCounts, type GdrUri, type StageEntry, type TerminalState } from '@sanity/workflow-engine';
|
|
2
2
|
import { WorkflowCommand } from '../../lib/base-command.ts';
|
|
3
3
|
interface InstanceRow {
|
|
4
4
|
_id: string;
|
|
@@ -10,12 +10,16 @@ interface InstanceRow {
|
|
|
10
10
|
lastChangedAt: string;
|
|
11
11
|
modelVersion?: number | null;
|
|
12
12
|
minReaderModel?: number | null;
|
|
13
|
+
stages?: StageEntry[];
|
|
13
14
|
}
|
|
14
15
|
interface ListFlags {
|
|
15
16
|
'include-completed': boolean;
|
|
16
17
|
failed: boolean;
|
|
17
18
|
definition?: string | undefined;
|
|
18
19
|
document?: string | undefined;
|
|
20
|
+
'assignment-user'?: string | undefined;
|
|
21
|
+
'assignment-role'?: string[] | undefined;
|
|
22
|
+
'assignment-state'?: string[] | undefined;
|
|
19
23
|
tag?: string | undefined;
|
|
20
24
|
limit: number;
|
|
21
25
|
}
|
|
@@ -38,13 +42,14 @@ export declare function buildListQuery(flags: ListFlags): {
|
|
|
38
42
|
params: Record<string, unknown>;
|
|
39
43
|
};
|
|
40
44
|
/** Map a queried instance to its display row (derives the status column). */
|
|
41
|
-
export declare function instanceRow(r: InstanceRow): {
|
|
45
|
+
export declare function instanceRow(r: InstanceRow, identity?: AssignmentIdentity): {
|
|
42
46
|
_id: string;
|
|
43
47
|
definition: string;
|
|
44
48
|
tag: string;
|
|
45
49
|
currentStage: string;
|
|
46
50
|
status: TerminalState;
|
|
47
51
|
lastChangedAt: string;
|
|
52
|
+
assignment?: AssignmentStateCounts;
|
|
48
53
|
};
|
|
49
54
|
export default class List extends WorkflowCommand {
|
|
50
55
|
static description: string;
|
|
@@ -55,6 +60,9 @@ export default class List extends WorkflowCommand {
|
|
|
55
60
|
failed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
56
61
|
definition: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
57
62
|
document: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
63
|
+
'assignment-user': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
64
|
+
'assignment-role': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
65
|
+
'assignment-state': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
58
66
|
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
59
67
|
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
60
68
|
};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import {
|
|
2
|
+
import { formatTimeAgo } from '@sanity/cli-core/dates';
|
|
3
|
+
import { WORKFLOW_INSTANCE_TYPE, assignmentPrefilter, assertReadableModel, documentPrefilter, inFlightFilter, instanceWatchesDocument, instanceAssignmentStateCounts, tagScopeFilter, terminalState, } from '@sanity/workflow-engine';
|
|
3
4
|
import logSymbols from 'log-symbols';
|
|
4
5
|
import { WorkflowCommand } from "../../lib/base-command.js";
|
|
5
6
|
import { resolveReadTargets } from "../../lib/context.js";
|
|
6
7
|
import { failOnThrow } from "../../lib/fail.js";
|
|
7
8
|
import { jsonFlags, tagFlags } from "../../lib/flags.js";
|
|
8
9
|
import { logJsonListing, runReadAcrossTargets } from "../../lib/read-fanout.js";
|
|
9
|
-
import {
|
|
10
|
+
import { logClippedTable } from "../../lib/ui.js";
|
|
10
11
|
const DOCUMENT_LIST_PROJECTION = `{
|
|
11
12
|
_type,
|
|
12
13
|
_id,
|
|
@@ -39,6 +40,12 @@ export function buildListQuery(flags) {
|
|
|
39
40
|
filters.push(tagScopeFilter());
|
|
40
41
|
params['tag'] = flags.tag;
|
|
41
42
|
}
|
|
43
|
+
const assignment = assignmentFilter(flags);
|
|
44
|
+
if (assignment !== undefined) {
|
|
45
|
+
const armParams = {};
|
|
46
|
+
filters.push(assignmentPrefilter(assignment, armParams));
|
|
47
|
+
Object.assign(params, armParams);
|
|
48
|
+
}
|
|
42
49
|
if (flags.document) {
|
|
43
50
|
const armParams = {};
|
|
44
51
|
filters.push(`(${documentPrefilter([flags.document], armParams)})`);
|
|
@@ -60,10 +67,28 @@ export function buildListQuery(flags) {
|
|
|
60
67
|
lastChangedAt,
|
|
61
68
|
modelVersion,
|
|
62
69
|
minReaderModel
|
|
70
|
+
${assignment === undefined ? '' : ', stages'}
|
|
63
71
|
}`;
|
|
64
72
|
return { groq, params };
|
|
65
73
|
}
|
|
66
|
-
|
|
74
|
+
function assignmentFilter(flags) {
|
|
75
|
+
const userId = flags['assignment-user'];
|
|
76
|
+
const roles = flags['assignment-role'];
|
|
77
|
+
const rawStates = flags['assignment-state'];
|
|
78
|
+
if (userId === undefined) {
|
|
79
|
+
if (roles !== undefined || rawStates !== undefined) {
|
|
80
|
+
throw new Error('--assignment-role and --assignment-state require --assignment-user');
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const states = rawStates;
|
|
85
|
+
return {
|
|
86
|
+
userId,
|
|
87
|
+
...(roles === undefined ? {} : { roles }),
|
|
88
|
+
...(states === undefined ? {} : { states }),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export function instanceRow(r, identity) {
|
|
67
92
|
return {
|
|
68
93
|
_id: r._id,
|
|
69
94
|
definition: r.definition,
|
|
@@ -74,8 +99,57 @@ export function instanceRow(r) {
|
|
|
74
99
|
...(r.abortedAt != null ? { abortedAt: r.abortedAt } : {}),
|
|
75
100
|
}),
|
|
76
101
|
lastChangedAt: r.lastChangedAt,
|
|
102
|
+
...(identity === undefined ? {} : { assignment: assignmentCounts(r, identity) }),
|
|
77
103
|
};
|
|
78
104
|
}
|
|
105
|
+
function assignmentCounts(row, identity) {
|
|
106
|
+
return instanceAssignmentStateCounts({ currentStage: row.currentStage, stages: row.stages ?? [] }, identity);
|
|
107
|
+
}
|
|
108
|
+
function assignmentIdentityFor(flags) {
|
|
109
|
+
const assignment = assignmentFilter(flags);
|
|
110
|
+
return assignment === undefined
|
|
111
|
+
? undefined
|
|
112
|
+
: { userId: assignment.userId, roles: assignment.roles ?? [] };
|
|
113
|
+
}
|
|
114
|
+
async function logTargetInstances(args) {
|
|
115
|
+
const fetched = await fetchRows(args);
|
|
116
|
+
if (fetched.rows.length === 0) {
|
|
117
|
+
const message = fetched.hasMore
|
|
118
|
+
? `no matches in the first ${args.limit} candidates — raise --limit to search further`
|
|
119
|
+
: 'no instances match';
|
|
120
|
+
args.log(`${logSymbols.info} ${message}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
logClippedTable({
|
|
124
|
+
rows: fetched.rows,
|
|
125
|
+
limit: args.limit,
|
|
126
|
+
moreAvailable: fetched.hasMore,
|
|
127
|
+
headers: [
|
|
128
|
+
'instance',
|
|
129
|
+
'workflow',
|
|
130
|
+
'tag',
|
|
131
|
+
'stage',
|
|
132
|
+
'status',
|
|
133
|
+
'updated',
|
|
134
|
+
...(args.identity === undefined ? [] : ['assignment u/r/h']),
|
|
135
|
+
],
|
|
136
|
+
toCells: (candidate) => {
|
|
137
|
+
const row = instanceRow(candidate, args.identity);
|
|
138
|
+
return [
|
|
139
|
+
row._id,
|
|
140
|
+
row.definition,
|
|
141
|
+
row.tag,
|
|
142
|
+
row.currentStage,
|
|
143
|
+
row.status,
|
|
144
|
+
formatTimeAgo(new Date(row.lastChangedAt)),
|
|
145
|
+
...(row.assignment === undefined
|
|
146
|
+
? []
|
|
147
|
+
: [`${row.assignment.unrouted}/${row.assignment.routed}/${row.assignment.held}`]),
|
|
148
|
+
];
|
|
149
|
+
},
|
|
150
|
+
log: args.log,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
79
153
|
export default class List extends WorkflowCommand {
|
|
80
154
|
static description = 'List workflow instances in the configured dataset (in-flight by default).';
|
|
81
155
|
static examples = [
|
|
@@ -83,6 +157,7 @@ export default class List extends WorkflowCommand {
|
|
|
83
157
|
'<%= config.bin %> workflows list --include-completed',
|
|
84
158
|
'<%= config.bin %> workflows list --definition productLaunch',
|
|
85
159
|
'<%= config.bin %> workflows list --document dataset:proj:ds:article-1',
|
|
160
|
+
'<%= config.bin %> workflows list --assignment-user gAda --assignment-role legal --assignment-state routed',
|
|
86
161
|
'<%= config.bin %> workflows list --tag prod',
|
|
87
162
|
'<%= config.bin %> workflows list --json',
|
|
88
163
|
];
|
|
@@ -103,6 +178,18 @@ export default class List extends WorkflowCommand {
|
|
|
103
178
|
description: 'Only instances that reference this document (resource-qualified GDR URI, ' +
|
|
104
179
|
'e.g. "dataset:proj:ds:article-1").',
|
|
105
180
|
}),
|
|
181
|
+
'assignment-user': Flags.string({
|
|
182
|
+
description: 'Account-global user id used for viewer-scoped assignment filtering and counts.',
|
|
183
|
+
}),
|
|
184
|
+
'assignment-role': Flags.string({
|
|
185
|
+
description: 'A literal project role held by --assignment-user. Repeat for multiple roles.',
|
|
186
|
+
multiple: true,
|
|
187
|
+
}),
|
|
188
|
+
'assignment-state': Flags.string({
|
|
189
|
+
description: 'Assignment state to include: unrouted, routed (offered through a supplied role), or held. Repeat for multiple states.',
|
|
190
|
+
multiple: true,
|
|
191
|
+
options: ['unrouted', 'routed', 'held'],
|
|
192
|
+
}),
|
|
106
193
|
limit: Flags.integer({
|
|
107
194
|
description: 'Maximum rows to return.',
|
|
108
195
|
default: 50,
|
|
@@ -112,8 +199,9 @@ export default class List extends WorkflowCommand {
|
|
|
112
199
|
async run() {
|
|
113
200
|
const { flags } = await this.parse(List);
|
|
114
201
|
const targets = await resolveReadTargets(flags);
|
|
115
|
-
const { groq, params } = failOnThrow('Invalid
|
|
202
|
+
const { groq, params } = failOnThrow('Invalid list filter:', () => buildListQuery(flags));
|
|
116
203
|
const document = flags.document;
|
|
204
|
+
const identity = assignmentIdentityFor(flags);
|
|
117
205
|
if (flags.json) {
|
|
118
206
|
await logJsonListing({
|
|
119
207
|
targets,
|
|
@@ -121,43 +209,22 @@ export default class List extends WorkflowCommand {
|
|
|
121
209
|
limit: flags.limit,
|
|
122
210
|
log: (line) => this.log(line),
|
|
123
211
|
fetch: ({ client }) => fetchRows({ client, groq, params, document, limit: flags.limit }),
|
|
124
|
-
toRow: instanceRow,
|
|
212
|
+
toRow: (row) => instanceRow(row, identity),
|
|
125
213
|
});
|
|
126
214
|
return;
|
|
127
215
|
}
|
|
128
216
|
const failures = await runReadAcrossTargets({
|
|
129
217
|
targets,
|
|
130
218
|
log: (line) => this.log(line),
|
|
131
|
-
run:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
logClippedTable({
|
|
143
|
-
rows: fetched.rows,
|
|
144
|
-
limit: flags.limit,
|
|
145
|
-
moreAvailable: fetched.hasMore,
|
|
146
|
-
headers: ['instance', 'workflow', 'tag', 'stage', 'status', 'updated'],
|
|
147
|
-
toCells: (r) => {
|
|
148
|
-
const row = instanceRow(r);
|
|
149
|
-
return [
|
|
150
|
-
row._id,
|
|
151
|
-
row.definition,
|
|
152
|
-
row.tag,
|
|
153
|
-
row.currentStage,
|
|
154
|
-
row.status,
|
|
155
|
-
formatAge(row.lastChangedAt),
|
|
156
|
-
];
|
|
157
|
-
},
|
|
158
|
-
log: (line) => this.log(line),
|
|
159
|
-
});
|
|
160
|
-
},
|
|
219
|
+
run: ({ client }) => logTargetInstances({
|
|
220
|
+
client,
|
|
221
|
+
groq,
|
|
222
|
+
params,
|
|
223
|
+
document,
|
|
224
|
+
limit: flags.limit,
|
|
225
|
+
identity,
|
|
226
|
+
log: (line) => this.log(line),
|
|
227
|
+
}),
|
|
161
228
|
});
|
|
162
229
|
if (failures.length > 0) {
|
|
163
230
|
this.exit(1);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type WorkflowInstance } from '@sanity/workflow-engine';
|
|
2
2
|
import { WorkflowCommand } from '../../lib/base-command.ts';
|
|
3
|
+
import { type InstanceDiagnostic } from '../../lib/cause-detail.ts';
|
|
3
4
|
export default class Show extends WorkflowCommand {
|
|
4
5
|
static description: string;
|
|
5
6
|
static examples: string[];
|
|
@@ -12,15 +13,12 @@ export default class Show extends WorkflowCommand {
|
|
|
12
13
|
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
14
|
};
|
|
14
15
|
run(): Promise<void>;
|
|
16
|
+
private diagnostic;
|
|
15
17
|
}
|
|
16
18
|
/** The summary block at the top of `show` (and reused by `diagnose` /
|
|
17
19
|
* `fire-action`): a bold workflow + id title, then aligned detail rows. */
|
|
18
20
|
export declare function instanceHeader(instance: WorkflowInstance): string;
|
|
19
|
-
/**
|
|
20
|
-
* The per-instance body lines: every stage with its activities, any pending
|
|
21
|
-
* effects, and (optionally) the history log. Pure so it can be asserted
|
|
22
|
-
* without driving the oclif command.
|
|
23
|
-
*/
|
|
24
21
|
export declare function describeInstance(instance: WorkflowInstance, options: {
|
|
25
22
|
includeHistory: boolean;
|
|
23
|
+
diagnostic?: InstanceDiagnostic | undefined;
|
|
26
24
|
}): string[];
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
2
|
import { Args, Flags } from '@oclif/core';
|
|
3
|
-
import {
|
|
3
|
+
import { formatDateTime } from '@sanity/cli-core/dates';
|
|
4
|
+
import { abortReason, workflow, errorMessage, displayTitle, terminalState, } from '@sanity/workflow-engine';
|
|
4
5
|
import logSymbols from 'log-symbols';
|
|
5
6
|
import { WorkflowCommand } from "../../lib/base-command.js";
|
|
7
|
+
import { stuckDiagnosisLines, stuckHeadline, } from "../../lib/cause-detail.js";
|
|
6
8
|
import { findInstance, resolveReadTargets } from "../../lib/context.js";
|
|
7
9
|
import { jsonFlags, tagFlags } from "../../lib/flags.js";
|
|
8
|
-
import {
|
|
10
|
+
import { baseEngineArgs } from "../../lib/operation-args.js";
|
|
11
|
+
import { formatKeyValue, sectionHeader, activityIcon } from "../../lib/ui.js";
|
|
9
12
|
export default class Show extends WorkflowCommand {
|
|
10
13
|
static description = 'Show the state, activities, and effects of a workflow instance.';
|
|
11
14
|
static examples = [
|
|
@@ -41,30 +44,49 @@ export default class Show extends WorkflowCommand {
|
|
|
41
44
|
this.log(JSON.stringify(instance, null, 2));
|
|
42
45
|
return;
|
|
43
46
|
}
|
|
47
|
+
const diagnostic = await this.diagnostic(hit);
|
|
44
48
|
this.log(instanceHeader(instance));
|
|
45
49
|
this.log('');
|
|
46
50
|
for (const line of describeInstance(instance, {
|
|
47
51
|
includeHistory: flags.include.includes('history'),
|
|
52
|
+
diagnostic,
|
|
48
53
|
})) {
|
|
49
54
|
this.log(line);
|
|
50
55
|
}
|
|
51
56
|
}
|
|
57
|
+
async diagnostic(hit) {
|
|
58
|
+
const { instance } = hit;
|
|
59
|
+
if (terminalState(instance) !== 'in-flight')
|
|
60
|
+
return undefined;
|
|
61
|
+
try {
|
|
62
|
+
const { diagnosis, evaluation, remediations } = await workflow.diagnose({
|
|
63
|
+
client: hit.client,
|
|
64
|
+
...baseEngineArgs({ tag: instance.tag, workflowResource: instance.workflowResource }),
|
|
65
|
+
instanceId: instance._id,
|
|
66
|
+
});
|
|
67
|
+
return { diagnosis, remediations, allMissingDocuments: evaluation.missingDocuments };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
this.warn(`Could not evaluate the instance: ${errorMessage(error)}. Showing stored state; reference availability is unknown.`);
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
52
74
|
}
|
|
53
75
|
const HEADER_PAD = 9;
|
|
54
76
|
export function instanceHeader(instance) {
|
|
55
77
|
const title = `${styleText('bold', `${instance.definition} v${instance.pinnedVersion}`)} ${styleText('cyan', instance._id)}`;
|
|
56
78
|
const rows = [
|
|
57
79
|
formatKeyValue({ key: 'Stage', value: instance.currentStage, padTo: HEADER_PAD }),
|
|
58
|
-
formatKeyValue({ key: 'Started', value:
|
|
80
|
+
formatKeyValue({ key: 'Started', value: formatDateTime(instance.startedAt), padTo: HEADER_PAD }),
|
|
59
81
|
];
|
|
60
82
|
if (terminalState(instance) === 'aborted' && instance.abortedAt !== undefined) {
|
|
61
83
|
const reason = abortReason(instance);
|
|
62
84
|
const tail = reason ? ` — ${reason}` : '';
|
|
63
|
-
const value = `${
|
|
85
|
+
const value = `${formatDateTime(instance.abortedAt)}${tail}`;
|
|
64
86
|
rows.push(formatKeyValue({ key: 'Aborted', value, padTo: HEADER_PAD, emphasis: 'yellow' }));
|
|
65
87
|
}
|
|
66
88
|
else if (instance.completedAt) {
|
|
67
|
-
const value =
|
|
89
|
+
const value = formatDateTime(instance.completedAt);
|
|
68
90
|
rows.push(formatKeyValue({ key: 'Completed', value, padTo: HEADER_PAD, emphasis: 'green' }));
|
|
69
91
|
}
|
|
70
92
|
else {
|
|
@@ -76,11 +98,17 @@ export function instanceHeader(instance) {
|
|
|
76
98
|
}
|
|
77
99
|
export function describeInstance(instance, options) {
|
|
78
100
|
return [
|
|
101
|
+
...diagnosticLines(options.diagnostic),
|
|
79
102
|
...stageLines(instance),
|
|
80
103
|
...pendingEffectLines(instance),
|
|
81
104
|
...(options.includeHistory ? historyLines(instance) : []),
|
|
82
105
|
];
|
|
83
106
|
}
|
|
107
|
+
function diagnosticLines(diagnostic) {
|
|
108
|
+
if (diagnostic?.diagnosis.state !== 'stuck')
|
|
109
|
+
return [];
|
|
110
|
+
return [stuckHeadline(diagnostic.diagnosis.cause), ...stuckDiagnosisLines(diagnostic), ''];
|
|
111
|
+
}
|
|
84
112
|
function stageLines(instance) {
|
|
85
113
|
return [
|
|
86
114
|
sectionHeader('Stages'),
|
|
@@ -92,7 +120,7 @@ function stageLines(instance) {
|
|
|
92
120
|
}
|
|
93
121
|
function stageMarker(stage) {
|
|
94
122
|
return stage.exitedAt
|
|
95
|
-
? styleText('dim', ` (exited ${
|
|
123
|
+
? styleText('dim', ` (exited ${formatDateTime(stage.exitedAt)})`)
|
|
96
124
|
: styleText('cyan', ' (current)');
|
|
97
125
|
}
|
|
98
126
|
function pendingEffectLines(instance) {
|
|
@@ -109,6 +137,6 @@ function historyLines(instance) {
|
|
|
109
137
|
return [
|
|
110
138
|
'',
|
|
111
139
|
sectionHeader('History'),
|
|
112
|
-
...instance.history.map((entry) => ` ${styleText('dim', `[${
|
|
140
|
+
...instance.history.map((entry) => ` ${styleText('dim', `[${formatDateTime(entry.at)}]`)} ${displayTitle(entry._type)}`),
|
|
113
141
|
];
|
|
114
142
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
2
|
import { Args } from '@oclif/core';
|
|
3
|
+
import { formatDateTime } from '@sanity/cli-core/dates';
|
|
3
4
|
import { assertReadableModel, displayTitle, errorMessage, } from '@sanity/workflow-engine';
|
|
4
5
|
import logSymbols from 'log-symbols';
|
|
5
6
|
import { WorkflowCommand } from "../../lib/base-command.js";
|
|
6
7
|
import { findInstance, resolveReadTargets } from "../../lib/context.js";
|
|
7
8
|
import { fail, failureDetail } from "../../lib/fail.js";
|
|
8
9
|
import { tagFlags } from "../../lib/flags.js";
|
|
9
|
-
import { formatTimestamp } from "../../lib/ui.js";
|
|
10
10
|
const TAIL_TAG = 'tail';
|
|
11
11
|
export default class Tail extends WorkflowCommand {
|
|
12
12
|
static description = 'Stream new history entries on a workflow instance as they land in the dataset.';
|
|
@@ -70,7 +70,7 @@ export default class Tail extends WorkflowCommand {
|
|
|
70
70
|
const next = assertReadableModel(raw);
|
|
71
71
|
const newEntries = next.history.slice(seen);
|
|
72
72
|
for (const entry of newEntries) {
|
|
73
|
-
this.log(`${styleText('dim', `[${
|
|
73
|
+
this.log(`${styleText('dim', `[${formatDateTime(entry.at)}]`)} ${styleText('cyan', displayTitle(entry._type))}`);
|
|
74
74
|
}
|
|
75
75
|
return Math.max(seen, next.history.length);
|
|
76
76
|
}
|
|
@@ -6,5 +6,11 @@ import type { Hook } from '@oclif/core';
|
|
|
6
6
|
* resolved from raw argv because the store is built before oclif parses
|
|
7
7
|
* flags. Skips non-workflows commands when this package is mounted as a
|
|
8
8
|
* host plugin so `sanity dataset list` does not inherit the shell. */
|
|
9
|
+
export declare function installPrerunTelemetry(args: {
|
|
10
|
+
bin: string;
|
|
11
|
+
version: string;
|
|
12
|
+
commandId: string | undefined;
|
|
13
|
+
argv: string[];
|
|
14
|
+
}): Promise<void>;
|
|
9
15
|
declare const hook: Hook<'prerun'>;
|
|
10
16
|
export default hook;
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { shouldRunWorkflowCliTelemetry } from "../../command-ids.js";
|
|
2
2
|
import { shouldForceShareTelemetry } from "../../lib/share-definitions.js";
|
|
3
3
|
import { setupCliTelemetry } from "../../lib/telemetry-setup.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
commandId: Command?.id,
|
|
8
|
-
})) {
|
|
4
|
+
export async function installPrerunTelemetry(args) {
|
|
5
|
+
const { bin, version, commandId, argv } = args;
|
|
6
|
+
if (!shouldRunWorkflowCliTelemetry({ bin, commandId })) {
|
|
9
7
|
return;
|
|
10
8
|
}
|
|
11
9
|
await setupCliTelemetry({
|
|
12
|
-
cliVersion:
|
|
13
|
-
forceSend: shouldForceShareTelemetry({ commandId
|
|
10
|
+
cliVersion: version,
|
|
11
|
+
forceSend: shouldForceShareTelemetry({ commandId, argv }),
|
|
12
|
+
...(commandId !== undefined ? { commandId } : {}),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
const hook = async function ({ config, Command, argv }) {
|
|
16
|
+
await installPrerunTelemetry({
|
|
17
|
+
bin: config.bin,
|
|
18
|
+
version: config.version,
|
|
19
|
+
commandId: Command?.id,
|
|
20
|
+
argv,
|
|
14
21
|
});
|
|
15
22
|
};
|
|
16
23
|
export default hook;
|