@sanity/workflow-cli 0.10.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 +31 -0
- package/README.md +26 -18
- package/dist/commands/definition/list.js +2 -2
- package/dist/commands/definition/show.d.ts +13 -0
- package/dist/commands/definition/show.js +23 -5
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.js +4 -4
- package/dist/commands/diagnose.d.ts +1 -1
- package/dist/commands/diagnose.js +23 -13
- package/dist/commands/fire-action.js +13 -2
- package/dist/commands/list.d.ts +11 -2
- package/dist/commands/list.js +37 -16
- package/dist/commands/start.d.ts +1 -22
- package/dist/commands/start.js +1 -26
- package/dist/commands/tail.js +15 -12
- package/dist/hooks/prerun/telemetry.d.ts +4 -1
- package/dist/hooks/prerun/telemetry.js +6 -2
- package/dist/lib/definitions.d.ts +10 -0
- package/dist/lib/definitions.js +6 -0
- package/dist/lib/fail.d.ts +7 -0
- package/dist/lib/fail.js +2 -1
- package/dist/lib/share-definitions.d.ts +59 -25
- package/dist/lib/share-definitions.js +102 -21
- package/dist/lib/telemetry-setup.d.ts +4 -0
- package/dist/lib/telemetry-setup.js +18 -11
- package/dist/lib/telemetry.d.ts +33 -2
- package/dist/lib/telemetry.js +9 -4
- package/dist/lib/ui.js +0 -1
- package/oclif.manifest.json +18 -10
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @sanity/workflow-cli
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e5f5b77: **BREAKING:** rendering and the `--json` diagnose payload follow the engine's actions-only payload model.
|
|
8
|
+
- `fire-action`'s list mode renders a cascade-fired action as automation (`⚡ … [automation]` with the engine's canonical cascade-fired detail), never as a fireable command; other disabled reasons keep the shared `actionDisabledDetail` rendering.
|
|
9
|
+
- `diagnose` renders transitions by their `when` trigger (`whenSatisfied` replaces `filterSatisfied` in the `--json` transitions payload), narrates a waiting activity by its caller-fireable actions only (an all-automation activity reads "waiting on automation"), and words failed-effect causes by their action origin.
|
|
10
|
+
- The activity status glyphs drop `pending` — activities are active from stage entry.
|
|
11
|
+
|
|
12
|
+
- 7510fd5: **BREAKING:** `list` speaks the engine's filter vocabulary: the `--in-flight` flag (default: list everything) is replaced by `--include-completed` (default: in-flight only) — the same name, polarity, and default the engine and MCP list surfaces use. `list` also gains `--document <gdr-uri>` to list the instances that reference a document.
|
|
13
|
+
|
|
14
|
+
Fixes: expected failures (rejected write verbs, validation errors, the 401 arm) now exit by throwing through the oclif lifecycle instead of a raw `process.exit`, so the telemetry `finally` hook still completes the command trace and flushes batched events; `tail` serializes its notification handling (a burst of commits could double-print history entries and regress the high-water mark); and an untagged `definition show` whose name exists in several tag partitions of one dataset now errors listing the partitions — it previously picked the highest version across partitions, which could show a prod operator the dev definition.
|
|
15
|
+
|
|
16
|
+
- 6e7f42d: **BREAKING:** Definition sharing on `deploy` flips from opt-in to opt-out, and the flag is renamed. `--share-definitions-with-sanity` becomes the negatable `--share-defs` / `--no-share-defs`. A deploy that creates new definition versions now donates them by default; `--no-share-defs` opts out.
|
|
17
|
+
|
|
18
|
+
With neither flag, an interactive terminal is asked **once** — a blocking acknowledgement whose answer persists in the Sanity user config, so it never repeats — before anything is sent, since notice must precede the send. Every later flagless deploy prints a one-line reminder of the current state and the flag to change it, so an ongoing donation is never silent. Unattended runs (CI, `DO_NOT_TRACK`, non-TTY) can't prompt and donate nothing by default; opt in per run with `--share-defs`.
|
|
19
|
+
|
|
20
|
+
An explicit `--share-defs` also sends that deploy's telemetry regardless of CI / `DO_NOT_TRACK` (a command-line arg beats env vars; account-level `sanity telemetry disable` still applies). Telemetry gains a content-free `Editorial Workflows Definition Sharing Decided` event per invocation — how the decision was reached and what it was (`opt-in` / `opt-out` / `prompt-accepted` / `prompt-declined` / `remembered-share` / `remembered-decline` / `unattended`), whether anything reached the endpoint, and how many versions were eligible — so adoption dashboards can separate explicit opt-outs, prompt declines, and unattended runs and measure the true opt-out rate. The per-definition `Editorial Workflows Definition Shared` marker and the one-POST feedback transport are unchanged.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [11e2d6f]
|
|
25
|
+
- Updated dependencies [7510fd5]
|
|
26
|
+
- Updated dependencies [01e8042]
|
|
27
|
+
- Updated dependencies [79c54db]
|
|
28
|
+
- Updated dependencies [6a46db1]
|
|
29
|
+
- Updated dependencies [35b9b85]
|
|
30
|
+
- Updated dependencies [e5f5b77]
|
|
31
|
+
- Updated dependencies [01e8042]
|
|
32
|
+
- @sanity/workflow-engine@0.16.0
|
|
33
|
+
|
|
3
34
|
## 0.10.0
|
|
4
35
|
|
|
5
36
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ pnpm --filter @sanity/workflow-cli dev <command> [...args]
|
|
|
16
16
|
# Examples:
|
|
17
17
|
pnpm --filter @sanity/workflow-cli dev --help
|
|
18
18
|
pnpm --filter @sanity/workflow-cli dev deploy --dry-run
|
|
19
|
-
pnpm --filter @sanity/workflow-cli dev list --
|
|
19
|
+
pnpm --filter @sanity/workflow-cli dev list --include-completed
|
|
20
20
|
pnpm --filter @sanity/workflow-cli dev show wf-instance.abc123
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -145,9 +145,10 @@ per-command trace (`Editorial Workflows CLI Command Executed` — the command id
|
|
|
145
145
|
names of declared flags used, never their values, and a success flag) plus
|
|
146
146
|
the engine's adoption events from the operations it drives. Consent is the
|
|
147
147
|
account-wide status managed by `npx sanity telemetry enable|disable|status`;
|
|
148
|
-
CI and trueish `DO_NOT_TRACK` suppress everything
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
CI and trueish `DO_NOT_TRACK` suppress everything (except a `deploy --share-defs`,
|
|
149
|
+
which forces that deploy's telemetry — see [Definition sharing](#definition-sharing)),
|
|
150
|
+
and a session that isn't logged in sends nothing. A one-time notice on stderr
|
|
151
|
+
discloses collection on first use.
|
|
151
152
|
|
|
152
153
|
One extension point: supply your own logger as `telemetry` in
|
|
153
154
|
`sanity.workflow.ts` and the built-in pipeline is not constructed at all —
|
|
@@ -156,20 +157,27 @@ consent, suppression, and destination become its business.
|
|
|
156
157
|
|
|
157
158
|
## Definition sharing
|
|
158
159
|
|
|
159
|
-
Separate from telemetry, `deploy
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
Sharing is opt-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
160
|
+
Separate from telemetry, a `deploy` that creates new definition versions
|
|
161
|
+
donates those documents to Sanity — verbatim (structure, names, titles, GROQ
|
|
162
|
+
filters, effect configuration, seeded values), plus their deployment
|
|
163
|
+
coordinates (project and dataset, or resource id) — never content documents,
|
|
164
|
+
instances, or your auth token. The documents go to Sanity's first-party
|
|
165
|
+
definition-feedback endpoint, not the telemetry pipeline; telemetry carries
|
|
166
|
+
only a content-free marker (content hash plus structural counts) per shared
|
|
167
|
+
definition and a per-invocation decision event.
|
|
168
|
+
|
|
169
|
+
Sharing is **opt-out**. Pass `--share-defs` to donate without being asked, or
|
|
170
|
+
`--no-share-defs` to decline. With neither flag, an interactive terminal is
|
|
171
|
+
asked **once** — the answer is remembered in your Sanity user config, so the
|
|
172
|
+
prompt never repeats — before anything is sent; notice always precedes the
|
|
173
|
+
send. Every later flagless deploy then prints a one-line reminder of the
|
|
174
|
+
current state (sharing, or not) and the flag to change it, so an ongoing
|
|
175
|
+
donation is never silent. Unattended runs (CI, `DO_NOT_TRACK`, a non-TTY pipe)
|
|
176
|
+
can't prompt: by default they donate nothing (opt in per run with
|
|
177
|
+
`--share-defs`). An explicit `--share-defs` also sends that deploy's telemetry
|
|
178
|
+
regardless of CI / `DO_NOT_TRACK` — a command-line arg beats env vars;
|
|
179
|
+
account-level `sanity telemetry disable` still applies. A failed share only
|
|
180
|
+
warns, never failing a completed deploy.
|
|
173
181
|
|
|
174
182
|
## Known gaps
|
|
175
183
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import { WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, tagScopeFilter, } from '@sanity/workflow-engine';
|
|
2
|
+
import { WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, inFlightFilter, tagScopeFilter, } from '@sanity/workflow-engine';
|
|
3
3
|
import logSymbols from 'log-symbols';
|
|
4
4
|
import { WorkflowCommand } from "../../lib/base-command.js";
|
|
5
5
|
import { resolveReadTargets } from "../../lib/context.js";
|
|
@@ -31,7 +31,7 @@ export function buildDefinitionListQuery(flags) {
|
|
|
31
31
|
tag,
|
|
32
32
|
_createdAt,
|
|
33
33
|
"stageCount": count(stages),
|
|
34
|
-
"inFlightCount": count(*[${instanceMatch} &&
|
|
34
|
+
"inFlightCount": count(*[${instanceMatch} && ${inFlightFilter()}]),
|
|
35
35
|
"totalInstances": count(*[${instanceMatch}])
|
|
36
36
|
}`;
|
|
37
37
|
return { groq, params };
|
|
@@ -12,6 +12,19 @@ export default class DefinitionShow extends WorkflowCommand {
|
|
|
12
12
|
};
|
|
13
13
|
run(): Promise<void>;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the one definition `show` names, refusing every ambiguity: an
|
|
17
|
+
* untagged name spanning several tag partitions within a dataset
|
|
18
|
+
* ({@link assertSingleTagPartition}) and a name held by several datasets
|
|
19
|
+
* ({@link soleHitOrFail}) both error, never pick. `undefined` means nothing
|
|
20
|
+
* is deployed under the name.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveShownDefinition({ targets, name, tag, version, }: {
|
|
23
|
+
targets: ReadTarget[];
|
|
24
|
+
name: string;
|
|
25
|
+
tag: string | undefined;
|
|
26
|
+
version: number | undefined;
|
|
27
|
+
}): Promise<DeployedDefinition | undefined>;
|
|
15
28
|
/** How a lookup names the version it asked for: `vN` for a pinned
|
|
16
29
|
* `--version`, `latest` when unpinned. */
|
|
17
30
|
export declare function versionLabel(version: number | undefined): string;
|
|
@@ -4,9 +4,10 @@ import { isTerminalStage, } from '@sanity/workflow-engine';
|
|
|
4
4
|
import logSymbols from 'log-symbols';
|
|
5
5
|
import { WorkflowCommand } from "../../lib/base-command.js";
|
|
6
6
|
import { resolveReadTargets, soleHitOrFail } from "../../lib/context.js";
|
|
7
|
-
import { buildDefinitionShowQuery } from "../../lib/definitions.js";
|
|
7
|
+
import { buildDefinitionShowQuery, buildDefinitionTagsQuery } from "../../lib/definitions.js";
|
|
8
|
+
import { fail } from "../../lib/fail.js";
|
|
8
9
|
import { tagFlags } from "../../lib/flags.js";
|
|
9
|
-
import { formatKeyValue, sectionHeader } from "../../lib/ui.js";
|
|
10
|
+
import { formatKeyValue, resourceLabel, sectionHeader } from "../../lib/ui.js";
|
|
10
11
|
export default class DefinitionShow extends WorkflowCommand {
|
|
11
12
|
static description = 'Show a deployed workflow definition.';
|
|
12
13
|
static args = {
|
|
@@ -19,13 +20,12 @@ export default class DefinitionShow extends WorkflowCommand {
|
|
|
19
20
|
async run() {
|
|
20
21
|
const { args, flags } = await this.parse(DefinitionShow);
|
|
21
22
|
const targets = await resolveReadTargets(flags);
|
|
22
|
-
const
|
|
23
|
+
const def = await resolveShownDefinition({
|
|
24
|
+
targets,
|
|
23
25
|
name: args.name,
|
|
24
26
|
tag: flags.tag,
|
|
25
27
|
version: flags.version,
|
|
26
28
|
});
|
|
27
|
-
const hits = await fetchDefinitionHits({ targets, groq, params });
|
|
28
|
-
const def = soleHitOrFail(hits, `Definition "${args.name}"`)?.def;
|
|
29
29
|
if (!def) {
|
|
30
30
|
this.error(`${logSymbols.error} no definition "${args.name}" (${versionLabel(flags.version)})`, {
|
|
31
31
|
exit: 1,
|
|
@@ -38,9 +38,27 @@ export default class DefinitionShow extends WorkflowCommand {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
export async function resolveShownDefinition({ targets, name, tag, version, }) {
|
|
42
|
+
if (tag === undefined) {
|
|
43
|
+
await assertSingleTagPartition({ targets, name });
|
|
44
|
+
}
|
|
45
|
+
const { groq, params } = buildDefinitionShowQuery({ name, tag, version });
|
|
46
|
+
const hits = await fetchDefinitionHits({ targets, groq, params });
|
|
47
|
+
return soleHitOrFail(hits, `Definition "${name}"`)?.def;
|
|
48
|
+
}
|
|
41
49
|
export function versionLabel(version) {
|
|
42
50
|
return version === undefined ? 'latest' : `v${version}`;
|
|
43
51
|
}
|
|
52
|
+
async function assertSingleTagPartition({ targets, name, }) {
|
|
53
|
+
const { groq, params } = buildDefinitionTagsQuery(name);
|
|
54
|
+
await Promise.all(targets.map(async (target) => {
|
|
55
|
+
const tags = await target.client.fetch(groq, params, { tag: 'definition.show' });
|
|
56
|
+
if (tags.length > 1) {
|
|
57
|
+
fail(`Definition "${name}" exists in several tag partitions in ` +
|
|
58
|
+
`${resourceLabel(target.resource)} — pass --tag to choose one.`, tags.toSorted().join('\n'));
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
44
62
|
export async function fetchDefinitionHits({ targets, groq, params, }) {
|
|
45
63
|
const probes = await Promise.all(targets.map(async (target) => {
|
|
46
64
|
const def = await target.client.fetch(groq, params, {
|
|
@@ -18,7 +18,7 @@ export default class Deploy extends WorkflowCommand {
|
|
|
18
18
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
19
|
check: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
20
|
only: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
-
'share-
|
|
21
|
+
'share-defs': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
22
22
|
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
23
23
|
};
|
|
24
24
|
run(): Promise<void>;
|
package/dist/commands/deploy.js
CHANGED
|
@@ -41,9 +41,9 @@ export default class Deploy extends WorkflowCommand {
|
|
|
41
41
|
only: Flags.string({
|
|
42
42
|
description: 'Limit deploy/check/diff to a single workflow definition by name. Every targeted deployment must contain it.',
|
|
43
43
|
}),
|
|
44
|
-
'share-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
'share-defs': Flags.boolean({
|
|
45
|
+
allowNo: true,
|
|
46
|
+
description: 'Share the definition documents newly created by this deploy with Sanity — the full document, verbatim (structure, names, filters, effect configuration, seeded values), plus its deployment coordinates (project and dataset, or resource id); never content documents, instances, or your Sanity auth token. Opt-out: an interactive terminal is asked once (the answer is remembered), while unattended runs (CI / non-TTY / DO_NOT_TRACK) share nothing unless --share-defs is passed. Use --no-share-defs to opt out. An explicit --share-defs also sends telemetry for this deploy regardless of CI / DO_NOT_TRACK.',
|
|
47
47
|
}),
|
|
48
48
|
};
|
|
49
49
|
async run() {
|
|
@@ -90,7 +90,7 @@ export default class Deploy extends WorkflowCommand {
|
|
|
90
90
|
log(line);
|
|
91
91
|
}
|
|
92
92
|
await shareDefinitionsAfterDeploy({
|
|
93
|
-
flag: flags['share-
|
|
93
|
+
flag: flags['share-defs'],
|
|
94
94
|
candidates,
|
|
95
95
|
warn: (message) => this.warn(message),
|
|
96
96
|
});
|
|
@@ -6,7 +6,7 @@ import { WorkflowCommand } from '../lib/base-command.ts';
|
|
|
6
6
|
export declare function rawTransitionProjection(evaluation: WorkflowEvaluation): {
|
|
7
7
|
name: string;
|
|
8
8
|
to: string;
|
|
9
|
-
|
|
9
|
+
whenSatisfied: boolean;
|
|
10
10
|
unevaluable: boolean;
|
|
11
11
|
blockedBy: {
|
|
12
12
|
requirement?: import("@sanity/groq-condition-describe").AtomRequirement;
|
|
@@ -19,8 +19,8 @@ function activityLine(t, assignees) {
|
|
|
19
19
|
return ` ${activityIcon[t.status]} ${t.activity.name} ${styleText('dim', `[${t.status}]`)}${who}`;
|
|
20
20
|
}
|
|
21
21
|
function transitionLine(tr, explanation) {
|
|
22
|
-
const mark = tr.
|
|
23
|
-
const line = ` → ${tr.transition.to} ${styleText('dim', tr.transition.
|
|
22
|
+
const mark = tr.whenSatisfied ? styleText('green', 'ready') : styleText('red', 'false');
|
|
23
|
+
const line = ` → ${tr.transition.to} ${styleText('dim', tr.transition.when)} ${mark}`;
|
|
24
24
|
return explanation !== undefined ? `${line}\n ${styleText('dim', `↳ ${explanation}`)}` : line;
|
|
25
25
|
}
|
|
26
26
|
function currentStageLines(input, explanations) {
|
|
@@ -28,7 +28,7 @@ function currentStageLines(input, explanations) {
|
|
|
28
28
|
`${sectionHeader('Stage')} ${input.instance.currentStage}`,
|
|
29
29
|
sectionHeader('Activities'),
|
|
30
30
|
];
|
|
31
|
-
for (const t of input.activities) {
|
|
31
|
+
for (const t of input.activities.filter((a) => !a.scopedOut)) {
|
|
32
32
|
lines.push(activityLine(t, input.assignees[t.activity.name] ?? []));
|
|
33
33
|
}
|
|
34
34
|
if (input.transitions.length > 0) {
|
|
@@ -42,14 +42,14 @@ function currentStageLines(input, explanations) {
|
|
|
42
42
|
function failedEffectDetail(effect) {
|
|
43
43
|
const ran = effect.durationMs !== undefined ? ` (after ${effect.durationMs}ms)` : '';
|
|
44
44
|
return {
|
|
45
|
-
headline: `a failed effect
|
|
45
|
+
headline: `a failed effect from action '${effect.origin.name}' is blocking its activity`,
|
|
46
46
|
why: [
|
|
47
47
|
styleText('red', `${logSymbols.error} failed effect: ${effect.name}`),
|
|
48
|
-
` queued by
|
|
48
|
+
` queued by action '${effect.origin.name}', failed ${formatTimestamp(effect.ranAt)}${ran}`,
|
|
49
49
|
...(effect.error !== undefined ? [` error: ${effect.error.message}`] : []),
|
|
50
50
|
'',
|
|
51
|
-
`
|
|
52
|
-
`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.`,
|
|
53
53
|
],
|
|
54
54
|
};
|
|
55
55
|
}
|
|
@@ -74,12 +74,22 @@ function failedActivityDetail(activity) {
|
|
|
74
74
|
}
|
|
75
75
|
function waitingHeadline(w) {
|
|
76
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
|
+
}
|
|
77
80
|
return `waiting${who} for action: ${w.actions.join(' or ')}`;
|
|
78
81
|
}
|
|
79
82
|
function waitingLines(w) {
|
|
80
83
|
const who = w.assignees.length > 0
|
|
81
84
|
? `assigned to ${w.assignees.map(formatAssignee).join(', ')}`
|
|
82
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
|
+
}
|
|
83
93
|
return [
|
|
84
94
|
`${logSymbols.info} activity '${w.activity}' is active (${who}),`,
|
|
85
95
|
`waiting for action: ${w.actions.join(' or ')}. This is the normal in-flight`,
|
|
@@ -103,20 +113,20 @@ function blockedLines(b) {
|
|
|
103
113
|
}
|
|
104
114
|
function noTransitionDetail() {
|
|
105
115
|
return {
|
|
106
|
-
headline: `no exit transition's
|
|
116
|
+
headline: `no exit transition's trigger is satisfied`,
|
|
107
117
|
why: [
|
|
108
|
-
`${logSymbols.info} every activity is resolved, but no exit transition's
|
|
109
|
-
`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.`,
|
|
110
120
|
],
|
|
111
121
|
};
|
|
112
122
|
}
|
|
113
123
|
function transitionUnevaluableDetail(transitions) {
|
|
114
124
|
return {
|
|
115
|
-
headline: `an exit transition's
|
|
125
|
+
headline: `an exit transition's trigger could not be evaluated`,
|
|
116
126
|
why: [
|
|
117
127
|
`${logSymbols.info} every activity is resolved, but ${transitions.join(', ')} reads an operand`,
|
|
118
128
|
`that is missing or unreadable (GROQ null), so routing is held rather than`,
|
|
119
|
-
`falling through. Make the data the
|
|
129
|
+
`falling through. Make the data the trigger reads readable — publish the`,
|
|
120
130
|
`subject (or fill the field) — and the instance advances on its own; no`,
|
|
121
131
|
`set-stage needed.`,
|
|
122
132
|
],
|
|
@@ -166,7 +176,7 @@ export function rawTransitionProjection(evaluation) {
|
|
|
166
176
|
return evaluation.currentStage.transitions.map((t) => ({
|
|
167
177
|
name: t.transition.name,
|
|
168
178
|
to: t.transition.to,
|
|
169
|
-
|
|
179
|
+
whenSatisfied: t.whenSatisfied,
|
|
170
180
|
unevaluable: t.unevaluable,
|
|
171
181
|
blockedBy: t.insight.blockedBy.map((atom) => ({
|
|
172
182
|
groq: atom.atom.groq,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
2
|
import { Args, Flags } from '@oclif/core';
|
|
3
|
-
import { actionDisabledDetail, deniedGuardLabels, workflow, } from '@sanity/workflow-engine';
|
|
3
|
+
import { actionRendering, actionDisabledDetail, deniedGuardLabels, workflow, } from '@sanity/workflow-engine';
|
|
4
4
|
import logSymbols from 'log-symbols';
|
|
5
5
|
import { WorkflowCommand } from "../lib/base-command.js";
|
|
6
6
|
import { resolveInstanceContext } from "../lib/context.js";
|
|
@@ -20,8 +20,19 @@ function paramLabel(p) {
|
|
|
20
20
|
return `${p.name}${p.required === true ? '' : '?'}:${p.type}`;
|
|
21
21
|
}
|
|
22
22
|
function actionLines(a) {
|
|
23
|
-
const mark = a.allowed ? logSymbols.success : logSymbols.error;
|
|
24
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;
|
|
25
36
|
const lines = [
|
|
26
37
|
` ${mark} ${a.activity} → ${a.action}${title} ${styleText('dim', `[activity ${a.activityStatus}]`)}`,
|
|
27
38
|
];
|
package/dist/commands/list.d.ts
CHANGED
|
@@ -10,9 +10,10 @@ interface InstanceRow {
|
|
|
10
10
|
lastChangedAt: string;
|
|
11
11
|
}
|
|
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,6 +21,13 @@ 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;
|
|
@@ -38,9 +46,10 @@ export default class List extends WorkflowCommand {
|
|
|
38
46
|
static description: string;
|
|
39
47
|
static examples: string[];
|
|
40
48
|
static flags: {
|
|
41
|
-
'
|
|
49
|
+
'include-completed': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
42
50
|
failed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
43
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>;
|
|
44
53
|
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
45
54
|
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
46
55
|
};
|
package/dist/commands/list.js
CHANGED
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import { WORKFLOW_INSTANCE_TYPE, tagScopeFilter, terminalState, } from '@sanity/workflow-engine';
|
|
2
|
+
import { WORKFLOW_INSTANCE_TYPE, documentPrefilter, inFlightFilter, instanceWatchesDocument, tagScopeFilter, terminalState, } from '@sanity/workflow-engine';
|
|
3
3
|
import logSymbols from 'log-symbols';
|
|
4
4
|
import { WorkflowCommand } from "../lib/base-command.js";
|
|
5
5
|
import { resolveReadTargets } from "../lib/context.js";
|
|
6
|
+
import { failOnThrow } from "../lib/fail.js";
|
|
6
7
|
import { tagFlags } from "../lib/flags.js";
|
|
7
8
|
import { runReadAcrossTargets } from "../lib/read-fanout.js";
|
|
8
9
|
import { clipToLimit, formatAge, formatTable } from "../lib/ui.js";
|
|
9
10
|
export function buildListQuery(flags) {
|
|
10
11
|
const filters = [`_type == "${WORKFLOW_INSTANCE_TYPE}"`];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const params = {};
|
|
13
|
+
if (!flags['include-completed'])
|
|
14
|
+
filters.push(inFlightFilter());
|
|
15
|
+
if (flags.definition) {
|
|
14
16
|
filters.push('definition == $definition');
|
|
17
|
+
params['definition'] = flags.definition;
|
|
18
|
+
}
|
|
15
19
|
if (flags.failed)
|
|
16
20
|
filters.push('count(stages[].activities[status == "failed"]) > 0');
|
|
17
|
-
if (flags.tag)
|
|
21
|
+
if (flags.tag) {
|
|
18
22
|
filters.push(tagScopeFilter());
|
|
23
|
+
params['tag'] = flags.tag;
|
|
24
|
+
}
|
|
25
|
+
if (flags.document) {
|
|
26
|
+
const armParams = {};
|
|
27
|
+
filters.push(`(${documentPrefilter([flags.document], armParams)})`);
|
|
28
|
+
Object.assign(params, armParams);
|
|
29
|
+
return { groq: `*[${filters.join(' && ')}] | order(lastChangedAt desc)`, params };
|
|
30
|
+
}
|
|
31
|
+
params['limit'] = flags.limit + 1;
|
|
19
32
|
const groq = `*[${filters.join(' && ')}] | order(lastChangedAt desc) [0...$limit]{
|
|
20
33
|
_id,
|
|
21
34
|
definition,
|
|
@@ -25,11 +38,6 @@ export function buildListQuery(flags) {
|
|
|
25
38
|
abortedAt,
|
|
26
39
|
lastChangedAt
|
|
27
40
|
}`;
|
|
28
|
-
const params = { limit: flags.limit + 1 };
|
|
29
|
-
if (flags.definition)
|
|
30
|
-
params['definition'] = flags.definition;
|
|
31
|
-
if (flags.tag)
|
|
32
|
-
params['tag'] = flags.tag;
|
|
33
41
|
return { groq, params };
|
|
34
42
|
}
|
|
35
43
|
export function instanceRow(r) {
|
|
@@ -46,17 +54,18 @@ export function instanceRow(r) {
|
|
|
46
54
|
};
|
|
47
55
|
}
|
|
48
56
|
export default class List extends WorkflowCommand {
|
|
49
|
-
static description = 'List workflow instances in the configured dataset.';
|
|
57
|
+
static description = 'List workflow instances in the configured dataset (in-flight by default).';
|
|
50
58
|
static examples = [
|
|
51
59
|
'<%= config.bin %> list',
|
|
52
|
-
'<%= config.bin %> list --
|
|
60
|
+
'<%= config.bin %> list --include-completed',
|
|
53
61
|
'<%= config.bin %> list --definition productLaunch',
|
|
62
|
+
'<%= config.bin %> list --document dataset:proj:ds:article-1',
|
|
54
63
|
'<%= config.bin %> list --tag prod',
|
|
55
64
|
];
|
|
56
65
|
static flags = {
|
|
57
66
|
...tagFlags,
|
|
58
|
-
'
|
|
59
|
-
description: '
|
|
67
|
+
'include-completed': Flags.boolean({
|
|
68
|
+
description: 'Include completed/aborted instances (default: in-flight only).',
|
|
60
69
|
default: false,
|
|
61
70
|
}),
|
|
62
71
|
failed: Flags.boolean({
|
|
@@ -66,6 +75,10 @@ export default class List extends WorkflowCommand {
|
|
|
66
75
|
definition: Flags.string({
|
|
67
76
|
description: "Only instances of this workflow definition (its `name`; the instance's `definition` field).",
|
|
68
77
|
}),
|
|
78
|
+
document: Flags.string({
|
|
79
|
+
description: 'Only instances that reference this document (resource-qualified GDR URI, ' +
|
|
80
|
+
'e.g. "dataset:proj:ds:article-1").',
|
|
81
|
+
}),
|
|
69
82
|
limit: Flags.integer({
|
|
70
83
|
description: 'Maximum rows to return.',
|
|
71
84
|
default: 50,
|
|
@@ -74,12 +87,13 @@ export default class List extends WorkflowCommand {
|
|
|
74
87
|
async run() {
|
|
75
88
|
const { flags } = await this.parse(List);
|
|
76
89
|
const targets = await resolveReadTargets(flags);
|
|
77
|
-
const { groq, params } = buildListQuery(flags);
|
|
90
|
+
const { groq, params } = failOnThrow('Invalid --document:', () => buildListQuery(flags));
|
|
91
|
+
const document = flags.document;
|
|
78
92
|
const failures = await runReadAcrossTargets({
|
|
79
93
|
targets,
|
|
80
94
|
log: (line) => this.log(line),
|
|
81
95
|
run: async ({ client }) => {
|
|
82
|
-
const fetched = await client
|
|
96
|
+
const fetched = await fetchRows({ client, groq, params, document });
|
|
83
97
|
if (fetched.length === 0) {
|
|
84
98
|
this.log(`${logSymbols.info} no instances match`);
|
|
85
99
|
return;
|
|
@@ -109,3 +123,10 @@ export default class List extends WorkflowCommand {
|
|
|
109
123
|
}
|
|
110
124
|
}
|
|
111
125
|
}
|
|
126
|
+
async function fetchRows({ client, groq, params, document, }) {
|
|
127
|
+
if (document === undefined) {
|
|
128
|
+
return client.fetch(groq, params, { tag: 'list' });
|
|
129
|
+
}
|
|
130
|
+
const candidates = await client.fetch(groq, params, { tag: 'list' });
|
|
131
|
+
return candidates.filter((instance) => instanceWatchesDocument(instance, document));
|
|
132
|
+
}
|
package/dist/commands/start.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type InitialFieldValue, type StartInstanceArgs, type WorkflowInstance } from '@sanity/workflow-engine';
|
|
2
2
|
import { WorkflowCommand } from '../lib/base-command.ts';
|
|
3
3
|
import { type EngineScope } from '../lib/operation-args.ts';
|
|
4
4
|
export default class Start extends WorkflowCommand {
|
|
@@ -15,27 +15,6 @@ export default class Start extends WorkflowCommand {
|
|
|
15
15
|
};
|
|
16
16
|
run(): Promise<void>;
|
|
17
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
18
|
/**
|
|
40
19
|
* The shared engine args ({@link baseEngineArgs}) plus the start-specific
|
|
41
20
|
* fields. Optionals spread conditionally because the engine's optional args
|
package/dist/commands/start.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
2
|
import { Args, Flags } from '@oclif/core';
|
|
3
|
-
import {
|
|
3
|
+
import { buildInitialFields, startRefusal, workflow, } from '@sanity/workflow-engine';
|
|
4
4
|
import { WorkflowCommand } from "../lib/base-command.js";
|
|
5
5
|
import { resolveContext } from "../lib/context.js";
|
|
6
6
|
import { fetchDeployedDefinition } from "../lib/definitions.js";
|
|
@@ -82,31 +82,6 @@ async function resolveStartInputs({ client, name, tag, version, values, }) {
|
|
|
82
82
|
}
|
|
83
83
|
return failOnThrow('start usage error:', () => buildInitialFields({ declared: deployed.fields ?? [], values }));
|
|
84
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
85
|
export function buildStartArgs({ scope, name, version, initialFields, }) {
|
|
111
86
|
return {
|
|
112
87
|
...baseEngineArgs(scope),
|