@sanity/workflow-cli 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +485 -0
- package/README.md +62 -18
- 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 +40 -46
- package/dist/commands/definition/show.d.ts +22 -3
- package/dist/commands/definition/show.js +23 -21
- 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 +39 -45
- package/dist/commands/fire-action.d.ts +2 -4
- package/dist/commands/fire-action.js +27 -61
- package/dist/commands/list.d.ts +7 -8
- package/dist/commands/list.js +43 -56
- 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 -7
- package/dist/commands/start.js +18 -54
- package/dist/commands/tail.d.ts +2 -2
- package/dist/commands/tail.js +20 -25
- package/dist/hooks/finally/telemetry.d.ts +8 -0
- package/dist/hooks/finally/telemetry.js +13 -0
- package/dist/hooks/prerun/telemetry.d.ts +5 -0
- package/dist/hooks/prerun/telemetry.js +5 -0
- package/dist/index.js +0 -3
- package/dist/lib/base-command.d.ts +14 -0
- package/dist/lib/base-command.js +10 -0
- package/dist/lib/client.js +13 -30
- package/dist/lib/context.d.ts +49 -13
- package/dist/lib/context.js +49 -48
- package/dist/lib/definitions.js +5 -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 +12 -6
- package/dist/lib/fail.js +24 -25
- 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 +86 -0
- package/dist/lib/share-definitions.js +106 -0
- package/dist/lib/stub.js +0 -7
- package/dist/lib/telemetry-setup.d.ts +66 -0
- package/dist/lib/telemetry-setup.js +92 -0
- package/dist/lib/telemetry.d.ts +100 -0
- package/dist/lib/telemetry.js +89 -0
- package/dist/lib/ui.d.ts +33 -1
- package/dist/lib/ui.js +32 -21
- package/oclif.manifest.json +8 -47
- package/package.json +14 -8
package/README.md
CHANGED
|
@@ -24,6 +24,14 @@ Authenticate once with `sanity login` (the CLI reads that session token); for
|
|
|
24
24
|
CI, set `SANITY_AUTH_TOKEN` instead. Then create a `sanity.workflow.ts` in the
|
|
25
25
|
directory you run from — see [Configuration](#configuration).
|
|
26
26
|
|
|
27
|
+
The token must span **every resource the workflow references**, not just the
|
|
28
|
+
workflow resource. Runtime commands build one client from the deployment
|
|
29
|
+
descriptor; when a workflow's subject or `doc.ref` fields point into other
|
|
30
|
+
datasets (or a canvas / media library), the engine reaches them through
|
|
31
|
+
sibling clients derived from that same token. A `sanity login` session or an
|
|
32
|
+
org-capable token covers this out of the box; a token scoped to a single
|
|
33
|
+
project will fail on cross-resource reads.
|
|
34
|
+
|
|
27
35
|
## Configuration
|
|
28
36
|
|
|
29
37
|
The CLI is configured by a `sanity.workflow.ts` (or `.js`/`.mjs`) discovered in
|
|
@@ -71,10 +79,10 @@ runtime. Rule of thumb: **different resource → bind an alias and reference it
|
|
|
71
79
|
entirely.** So the simplest single-dataset setup is just a `workflowResource`
|
|
72
80
|
and `definitions`, with no `resourceAliases` at all.
|
|
73
81
|
|
|
74
|
-
| Var | Purpose
|
|
75
|
-
| ------------------- |
|
|
76
|
-
| `SANITY_AUTH_TOKEN` | CI
|
|
77
|
-
| `SANITY_API_HOST` | Optional API host override (defaults to the prod API)
|
|
82
|
+
| Var | Purpose |
|
|
83
|
+
| ------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| `SANITY_AUTH_TOKEN` | Explicit token for CI / scripted use — wins over the `sanity login` session (editor role for writes) |
|
|
85
|
+
| `SANITY_API_HOST` | Optional API host override (defaults to the prod API) |
|
|
78
86
|
|
|
79
87
|
Run through the `dev` / `cli` scripts and the CLI itself and your
|
|
80
88
|
`sanity.workflow.ts` load straight from TypeScript source: the scripts set
|
|
@@ -117,19 +125,52 @@ stale compiled output.
|
|
|
117
125
|
| `deploy --check` | wired — runs `validateDefinition` over the local batch + a duplicate-name check |
|
|
118
126
|
| `deploy --dry-run` | wired — fetches existing docs and renders a coloured JSON diff per change |
|
|
119
127
|
| `deploy --only <name>` | wired — filters deploy/check/dry-run to one definition by `name` |
|
|
120
|
-
| `start <name>` | wired — calls `workflow.startInstance` (`--field` for input fields
|
|
121
|
-
| `list` | wired — `client.fetch` over `workflow.instance` documents (`--definition <name>` to filter)
|
|
128
|
+
| `start <name>` | wired — calls `workflow.startInstance` (`--field` for input fields) |
|
|
129
|
+
| `list` | wired — `client.fetch` over `sanity.workflow.instance` documents (`--definition <name>` to filter) |
|
|
122
130
|
| `show <instance-id>` | wired — `client.getDocument` |
|
|
123
|
-
| `diagnose <instance-id>` | wired —
|
|
131
|
+
| `diagnose <instance-id>` | wired — calls `workflow.diagnose`, classifies why the instance is/isn't progressing |
|
|
124
132
|
| `tail <instance-id>` | wired — `client.listen()` over the instance, prints new history entries |
|
|
125
133
|
| `abort <instance-id>` | wired — calls `workflow.abortInstance` (hard stop: cancels pending effects, lifts guards) |
|
|
126
134
|
| `set-stage <instance-id> --to <stage>` | wired — calls `workflow.setStage` (admin override: skips declared transitions/filters; enter lifecycle + cascade still run) |
|
|
127
|
-
| `fire-action <instance-id>` | wired — `workflow.
|
|
128
|
-
| `definition list` | wired — `client.fetch` over `workflow.definition` documents
|
|
135
|
+
| `fire-action <instance-id>` | wired — `workflow.availableActions` lists actions; `workflow.fireAction` fires one |
|
|
136
|
+
| `definition list` | wired — `client.fetch` over `sanity.workflow.definition` documents |
|
|
129
137
|
| `definition show <name>` | wired — `client.fetch`, latest version unless `--version` |
|
|
130
138
|
| `definition diff <name>` | wired — diffs the in-code definition against the deployed latest (`--version` to pin) |
|
|
131
139
|
| `definition delete <name>` | wired — calls `workflow.deleteDefinition` (refuses on live instances unless `--cascade`) |
|
|
132
140
|
|
|
141
|
+
## Telemetry
|
|
142
|
+
|
|
143
|
+
The CLI collects usage telemetry through Sanity's standard pipeline: a
|
|
144
|
+
per-command trace (`Editorial Workflows CLI Command Executed` — the command id, the
|
|
145
|
+
names of declared flags used, never their values, and a success flag) plus
|
|
146
|
+
the engine's adoption events from the operations it drives. Consent is the
|
|
147
|
+
account-wide status managed by `npx sanity telemetry enable|disable|status`;
|
|
148
|
+
CI and trueish `DO_NOT_TRACK` suppress everything, and a session that isn't
|
|
149
|
+
logged in sends nothing. A one-time notice on stderr discloses collection on
|
|
150
|
+
first use.
|
|
151
|
+
|
|
152
|
+
One extension point: supply your own logger as `telemetry` in
|
|
153
|
+
`sanity.workflow.ts` and the built-in pipeline is not constructed at all —
|
|
154
|
+
every event flows to your implementation unconditionally (CI included), and
|
|
155
|
+
consent, suppression, and destination become its business.
|
|
156
|
+
|
|
157
|
+
## Definition sharing
|
|
158
|
+
|
|
159
|
+
Separate from telemetry, `deploy --share-definitions-with-sanity` donates the
|
|
160
|
+
definition documents the deploy newly created to Sanity — verbatim (structure,
|
|
161
|
+
names, titles, GROQ filters, effect configuration, seeded values), plus their
|
|
162
|
+
deployment coordinates (project and dataset, or resource id) — never content
|
|
163
|
+
documents, instances, or your auth token. The documents go to Sanity's
|
|
164
|
+
first-party definition-feedback endpoint, not the telemetry pipeline;
|
|
165
|
+
telemetry carries only a content-free marker (content hash plus structural
|
|
166
|
+
counts) per shared definition.
|
|
167
|
+
|
|
168
|
+
Sharing is opt-in by the flag alone, per invocation: the CLI never prompts
|
|
169
|
+
and nothing persists. An interactive deploy that created new definition
|
|
170
|
+
versions without the flag logs a one-line stderr hint pointing at it — CI /
|
|
171
|
+
non-TTY runs stay silent. Telemetry consent and `DO_NOT_TRACK` play no part:
|
|
172
|
+
sharing is its own explicit consent, given per deploy.
|
|
173
|
+
|
|
133
174
|
## Known gaps
|
|
134
175
|
|
|
135
176
|
- **No `sanity workflow` plugin wiring yet.** The proposal targets the
|
|
@@ -139,15 +180,18 @@ stale compiled output.
|
|
|
139
180
|
- **No true bypass.** `set-stage` is the engine's `setStage` admin
|
|
140
181
|
override: it skips the definition's declared transitions and filters, but
|
|
141
182
|
the target stage's enter lifecycle and the post-move cascade still run. A
|
|
142
|
-
|
|
143
|
-
engine work that hasn't landed.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
183
|
+
true force-set that also bypasses guards + enter effects does not exist
|
|
184
|
+
yet — it needs engine work that hasn't landed.
|
|
185
|
+
|
|
186
|
+
## Identity
|
|
187
|
+
|
|
188
|
+
Identity is the token behind the configured client: the engine resolves the
|
|
189
|
+
acting user (and their roles) from `/users/me`, so every write is attributed
|
|
190
|
+
to whoever the token authenticates — there is no identity injection and no
|
|
191
|
+
`--as` impersonation. Alongside that "who", the CLI declares an advisory
|
|
192
|
+
"via what": every verb carries
|
|
193
|
+
`executionContext: { kind: "cli", id: "workflow-cli" }`, stamped on history
|
|
194
|
+
entries as provenance.
|
|
151
195
|
|
|
152
196
|
## Tech stack
|
|
153
197
|
|
package/bin/run.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Production entry for the published package: loads the compiled commands
|
|
3
|
-
// from ./dist/commands (per the `oclif.commands` field). The dev counterpart,
|
|
4
|
-
// bin/dev.js, passes `development: true` to load TS sources instead.
|
|
5
|
-
|
|
6
2
|
import {execute} from '@oclif/core'
|
|
7
3
|
|
|
8
4
|
await execute({dir: import.meta.url})
|
package/dist/commands/abort.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
1
|
import { type WorkflowInstance } from '@sanity/workflow-engine';
|
|
3
|
-
|
|
2
|
+
import { WorkflowCommand } from '../lib/base-command.ts';
|
|
3
|
+
import { type WriteReport } from '../lib/ops-report.ts';
|
|
4
|
+
export default class Abort extends WorkflowCommand {
|
|
4
5
|
static description: string;
|
|
5
6
|
static examples: string[];
|
|
6
7
|
static args: {
|
|
@@ -21,8 +22,5 @@ interface AbortOutcome {
|
|
|
21
22
|
* aborted / already-terminal permutations can be asserted without driving
|
|
22
23
|
* a real abort.
|
|
23
24
|
*/
|
|
24
|
-
export declare function abortReport(result: AbortOutcome):
|
|
25
|
-
changed: boolean;
|
|
26
|
-
message: string;
|
|
27
|
-
};
|
|
25
|
+
export declare function abortReport(result: AbortOutcome): WriteReport;
|
|
28
26
|
export {};
|
package/dist/commands/abort.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
|
-
import { Args,
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
3
|
import { workflow } from '@sanity/workflow-engine';
|
|
4
|
-
import
|
|
4
|
+
import { WorkflowCommand } from "../lib/base-command.js";
|
|
5
5
|
import { resolveInstanceContext } from "../lib/context.js";
|
|
6
|
-
import { errorMessage, fail } from "../lib/fail.js";
|
|
7
6
|
import { tagFlags } from "../lib/flags.js";
|
|
8
7
|
import { buildOperationArgs } from "../lib/operation-args.js";
|
|
9
|
-
|
|
8
|
+
import { runWriteVerb } from "../lib/ops-report.js";
|
|
9
|
+
export default class Abort extends WorkflowCommand {
|
|
10
10
|
static description = 'Abort an in-flight workflow instance — a hard stop: pending effects are cancelled, stage guards lifted, and the instance is marked terminal where it stands.';
|
|
11
11
|
static examples = [
|
|
12
12
|
'<%= config.bin %> abort wf-instance.abc123',
|
|
@@ -22,34 +22,23 @@ export default class Abort extends Command {
|
|
|
22
22
|
async run() {
|
|
23
23
|
const { args, flags } = await this.parse(Abort);
|
|
24
24
|
const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
await runWriteVerb({
|
|
26
|
+
startLabel: `Aborting ${args.instanceId}…`,
|
|
27
|
+
failLabel: 'Abort rejected',
|
|
28
|
+
failHeadline: 'abort error:',
|
|
29
|
+
run: () => workflow.abortInstance({
|
|
28
30
|
client,
|
|
29
31
|
...buildOperationArgs({
|
|
30
32
|
scope,
|
|
31
33
|
instanceId: args.instanceId,
|
|
32
34
|
reason: flags.reason,
|
|
33
35
|
}),
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
spinner.succeed(report.message);
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
spinner.fail('Abort rejected');
|
|
44
|
-
fail('abort error:', errorMessage(error));
|
|
45
|
-
}
|
|
36
|
+
}),
|
|
37
|
+
report: abortReport,
|
|
38
|
+
log: (line) => this.log(line),
|
|
39
|
+
});
|
|
46
40
|
}
|
|
47
41
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Turn an `abortInstance` result into the spinner message. Pure so the
|
|
50
|
-
* aborted / already-terminal permutations can be asserted without driving
|
|
51
|
-
* a real abort.
|
|
52
|
-
*/
|
|
53
42
|
export function abortReport(result) {
|
|
54
43
|
const stage = styleText('bold', result.instance.currentStage);
|
|
55
44
|
if (!result.changed) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
1
|
import { type DeleteDefinitionArgs, type DeleteDefinitionResult } from '@sanity/workflow-engine';
|
|
2
|
+
import { WorkflowCommand } from '../../lib/base-command.ts';
|
|
3
3
|
import { type EngineScope } from '../../lib/operation-args.ts';
|
|
4
|
-
export default class DefinitionDelete extends
|
|
4
|
+
export default class DefinitionDelete extends WorkflowCommand {
|
|
5
5
|
static description: string;
|
|
6
6
|
static examples: string[];
|
|
7
7
|
static args: {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
|
-
import { Args,
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
3
|
import { workflow, } from '@sanity/workflow-engine';
|
|
4
|
-
import
|
|
4
|
+
import { WorkflowCommand } from "../../lib/base-command.js";
|
|
5
5
|
import { resolveContext } from "../../lib/context.js";
|
|
6
|
-
import { errorMessage, fail } from "../../lib/fail.js";
|
|
7
6
|
import { tagFlags } from "../../lib/flags.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
7
|
+
import { baseEngineArgs } from "../../lib/operation-args.js";
|
|
8
|
+
import { runWriteVerb } from "../../lib/ops-report.js";
|
|
9
|
+
export default class DefinitionDelete extends WorkflowCommand {
|
|
10
10
|
static description = 'Delete a deployed workflow definition (every version, or one via --version). ' +
|
|
11
11
|
'Refuses while non-terminal instances exist unless --cascade aborts them first — ' +
|
|
12
12
|
'instances are aborted in place, never deleted.';
|
|
@@ -35,41 +35,28 @@ export default class DefinitionDelete extends Command {
|
|
|
35
35
|
const { args, flags } = await this.parse(DefinitionDelete);
|
|
36
36
|
const { deployment, client } = await resolveContext(flags);
|
|
37
37
|
const deleteArgs = buildDeleteArgs({ scope: deployment, name: args.name, flags });
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
38
|
+
await runWriteVerb({
|
|
39
|
+
startLabel: `Deleting definition ${deleteTargetLabel(deleteArgs)}…`,
|
|
40
|
+
failLabel: 'Delete rejected',
|
|
41
|
+
failHeadline: 'definition delete error:',
|
|
42
|
+
run: () => workflow.deleteDefinition({ client, ...deleteArgs }),
|
|
43
|
+
report: (result) => ({ changed: true, message: deleteReport(result) }),
|
|
44
|
+
log: (line) => this.log(line),
|
|
45
|
+
});
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
|
-
/**
|
|
50
|
-
* The shared engine args ({@link baseEngineArgs}) plus the delete-specific
|
|
51
|
-
* targeting. `version` and `reason` spread conditionally because the
|
|
52
|
-
* engine's optionals reject an explicit `undefined` under
|
|
53
|
-
* `exactOptionalPropertyTypes`.
|
|
54
|
-
*/
|
|
55
48
|
export function buildDeleteArgs({ scope, name, flags, }) {
|
|
56
49
|
return {
|
|
57
|
-
...baseEngineArgs(scope
|
|
50
|
+
...baseEngineArgs(scope),
|
|
58
51
|
definition: name,
|
|
59
52
|
cascade: flags.cascade,
|
|
60
53
|
...(flags.version !== undefined ? { version: flags.version } : {}),
|
|
61
54
|
...(flags.reason !== undefined ? { reason: flags.reason } : {}),
|
|
62
55
|
};
|
|
63
56
|
}
|
|
64
|
-
/** What the spinner names: the workflow, or one pinned version of it. */
|
|
65
57
|
export function deleteTargetLabel(args) {
|
|
66
58
|
return args.version === undefined ? args.definition : `${args.definition} v${args.version}`;
|
|
67
59
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Turn a `deleteDefinition` result into the spinner message. Pure so the
|
|
70
|
-
* version / cascade / guard permutations can be asserted without driving
|
|
71
|
-
* a real delete.
|
|
72
|
-
*/
|
|
73
60
|
export function deleteReport(result) {
|
|
74
61
|
const versions = result.deletedVersions.map((v) => `v${v}`).join(', ');
|
|
75
62
|
const parts = [`Deleted ${styleText('bold', result.name)} ${versions}`];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
1
|
import { type WorkflowDefinition } from '@sanity/workflow-engine';
|
|
3
|
-
|
|
2
|
+
import { WorkflowCommand } from '../../lib/base-command.ts';
|
|
3
|
+
export default class DefinitionDiff extends WorkflowCommand {
|
|
4
4
|
static description: string;
|
|
5
5
|
static examples: string[];
|
|
6
6
|
static args: {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { Args,
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { diffEntry } from '@sanity/workflow-engine';
|
|
3
3
|
import ora from 'ora';
|
|
4
|
+
import { WorkflowCommand } from "../../lib/base-command.js";
|
|
4
5
|
import { resolveContext } from "../../lib/context.js";
|
|
5
6
|
import { fetchDeployedDefinition, selectDefinitions, validateOrFail } from "../../lib/definitions.js";
|
|
6
7
|
import { diffReport } from "../../lib/diff.js";
|
|
7
8
|
import { tagFlags } from "../../lib/flags.js";
|
|
8
9
|
import { deploymentToTarget } from "../../lib/select-deployment.js";
|
|
9
|
-
export default class DefinitionDiff extends
|
|
10
|
+
export default class DefinitionDiff extends WorkflowCommand {
|
|
10
11
|
static description = 'Diff an in-code definition against the deployed version (latest by default).';
|
|
11
12
|
static examples = [
|
|
12
13
|
'<%= config.bin %> definition diff productLaunch',
|
|
@@ -27,8 +28,6 @@ export default class DefinitionDiff extends Command {
|
|
|
27
28
|
const spinner = ora(`Diffing ${args.name} against deployed…`).start();
|
|
28
29
|
let deployed;
|
|
29
30
|
try {
|
|
30
|
-
// An absent latest is a legitimate "create" diff, so `undefined`
|
|
31
|
-
// flows straight into the diff entry.
|
|
32
31
|
deployed = await fetchDeployedDefinition({
|
|
33
32
|
client,
|
|
34
33
|
name: args.name,
|
|
@@ -45,16 +44,8 @@ export default class DefinitionDiff extends Command {
|
|
|
45
44
|
this.log(line);
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Pick the single definition to diff by name and validate it, failing cleanly
|
|
50
|
-
* when it's missing from the deployment or doesn't pass validation. Extracted
|
|
51
|
-
* from `run` so the command's orchestration stays thin and this stays testable.
|
|
52
|
-
*/
|
|
53
47
|
export function selectDefinitionToDiff(definitions, name) {
|
|
54
48
|
const [def] = selectDefinitions(definitions, { only: name });
|
|
55
|
-
// selectDefinitions already fails on no-match (naming the available defs), so
|
|
56
|
-
// a named lookup always yields at least one — this narrows the type, it is
|
|
57
|
-
// not a reachable failure.
|
|
58
49
|
if (def === undefined) {
|
|
59
50
|
throw new Error(`selectDefinitions returned no match for "${name}"`);
|
|
60
51
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type WorkflowLifecycle } from '@sanity/workflow-engine';
|
|
1
|
+
import { WorkflowCommand } from '../../lib/base-command.ts';
|
|
3
2
|
interface DefinitionListFlags {
|
|
4
3
|
tag?: string | undefined;
|
|
5
4
|
name?: string | undefined;
|
|
@@ -10,16 +9,16 @@ export interface DefinitionListRow {
|
|
|
10
9
|
version: number;
|
|
11
10
|
title: string;
|
|
12
11
|
tag: string;
|
|
13
|
-
lifecycle?: WorkflowLifecycle;
|
|
14
12
|
stageCount: number;
|
|
15
13
|
inFlightCount: number;
|
|
16
14
|
totalInstances: number;
|
|
15
|
+
_createdAt: string;
|
|
17
16
|
}
|
|
18
17
|
export declare function buildDefinitionListQuery(flags: DefinitionListFlags): {
|
|
19
18
|
groq: string;
|
|
20
19
|
params: Record<string, unknown>;
|
|
21
20
|
};
|
|
22
|
-
export default class DefinitionList extends
|
|
21
|
+
export default class DefinitionList extends WorkflowCommand {
|
|
23
22
|
static description: string;
|
|
24
23
|
static examples: string[];
|
|
25
24
|
static flags: {
|
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE,
|
|
3
|
-
import { Table } from 'console-table-printer';
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, tagScopeFilter, } from '@sanity/workflow-engine';
|
|
4
3
|
import logSymbols from 'log-symbols';
|
|
5
|
-
import {
|
|
4
|
+
import { WorkflowCommand } from "../../lib/base-command.js";
|
|
5
|
+
import { resolveReadTargets } from "../../lib/context.js";
|
|
6
6
|
import { tagFlags } from "../../lib/flags.js";
|
|
7
|
+
import { runReadAcrossTargets } from "../../lib/read-fanout.js";
|
|
8
|
+
import { clipToLimit, formatTable } from "../../lib/ui.js";
|
|
7
9
|
export function buildDefinitionListQuery(flags) {
|
|
8
|
-
const params = { limit: flags.limit };
|
|
10
|
+
const params = { limit: flags.limit + 1 };
|
|
9
11
|
const filters = [`_type == "${WORKFLOW_DEFINITION_TYPE}"`];
|
|
10
12
|
const instanceFilters = [
|
|
11
13
|
`_type == "${WORKFLOW_INSTANCE_TYPE}"`,
|
|
12
14
|
'definition == ^.name',
|
|
13
15
|
'pinnedVersion == ^.version',
|
|
14
|
-
// Correlate the count to each definition's own partition. Listing
|
|
15
|
-
// can span partitions (when `--tag` is omitted), and two partitions
|
|
16
|
-
// can hold a same-named, same-version definition — without this, one
|
|
17
|
-
// row's count would bleed in the other partition's instances.
|
|
18
16
|
'tag == ^.tag',
|
|
19
17
|
];
|
|
20
18
|
if (flags.tag) {
|
|
@@ -31,14 +29,14 @@ export function buildDefinitionListQuery(flags) {
|
|
|
31
29
|
version,
|
|
32
30
|
title,
|
|
33
31
|
tag,
|
|
34
|
-
|
|
32
|
+
_createdAt,
|
|
35
33
|
"stageCount": count(stages),
|
|
36
34
|
"inFlightCount": count(*[${instanceMatch} && !defined(completedAt)]),
|
|
37
35
|
"totalInstances": count(*[${instanceMatch}])
|
|
38
36
|
}`;
|
|
39
37
|
return { groq, params };
|
|
40
38
|
}
|
|
41
|
-
export default class DefinitionList extends
|
|
39
|
+
export default class DefinitionList extends WorkflowCommand {
|
|
42
40
|
static description = 'List deployed workflow definitions.';
|
|
43
41
|
static examples = [
|
|
44
42
|
'<%= config.bin %> definition list',
|
|
@@ -56,43 +54,39 @@ export default class DefinitionList extends Command {
|
|
|
56
54
|
};
|
|
57
55
|
async run() {
|
|
58
56
|
const { flags } = await this.parse(DefinitionList);
|
|
59
|
-
const
|
|
57
|
+
const targets = await resolveReadTargets(flags);
|
|
60
58
|
const { groq, params } = buildDefinitionListQuery(flags);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
this.log(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
59
|
+
const failures = await runReadAcrossTargets({
|
|
60
|
+
targets,
|
|
61
|
+
log: (line) => this.log(line),
|
|
62
|
+
run: async ({ client }) => {
|
|
63
|
+
const fetched = await client.fetch(groq, params, {
|
|
64
|
+
tag: 'definition.list',
|
|
65
|
+
});
|
|
66
|
+
if (!fetched.length) {
|
|
67
|
+
this.log(`${logSymbols.info} no definitions found`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const { rows, note } = clipToLimit(fetched, flags.limit);
|
|
71
|
+
const lines = formatTable(['workflow', 'title', 'tag', 'stages', 'in flight', 'instances', 'created'], rows.map((r) => [
|
|
72
|
+
`${r.name} v${r.version}`,
|
|
73
|
+
r.title,
|
|
74
|
+
r.tag,
|
|
75
|
+
String(r.stageCount),
|
|
76
|
+
String(r.inFlightCount),
|
|
77
|
+
String(r.totalInstances),
|
|
78
|
+
r._createdAt,
|
|
79
|
+
]));
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
this.log(line);
|
|
82
|
+
}
|
|
83
|
+
if (note) {
|
|
84
|
+
this.log(note);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
76
87
|
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// repeat Workflow + Tag — and no command takes an `_id` anyway (they take
|
|
80
|
-
// name + --tag/--version). `name vN` is this CLI's identifier format (the
|
|
81
|
-
// `definition show` title). Only declared keys are passed — console-table-
|
|
82
|
-
// printer renders any extra key as a stray column.
|
|
83
|
-
//
|
|
84
|
-
// `lifecycle: 'child'` defs are spawn-only; the Startable column tells an admin
|
|
85
|
-
// which definitions a human can actually start standalone.
|
|
86
|
-
table.addRow({
|
|
87
|
-
workflow: `${r.name} v${r.version}`,
|
|
88
|
-
title: r.title,
|
|
89
|
-
tag: r.tag,
|
|
90
|
-
startable: isStartableDefinition(r) ? 'yes' : 'spawn-only',
|
|
91
|
-
stageCount: r.stageCount,
|
|
92
|
-
inFlightCount: r.inFlightCount,
|
|
93
|
-
totalInstances: r.totalInstances,
|
|
94
|
-
});
|
|
88
|
+
if (failures.length > 0) {
|
|
89
|
+
this.exit(1);
|
|
95
90
|
}
|
|
96
|
-
table.printTable();
|
|
97
91
|
}
|
|
98
92
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { type DeployedDefinition, type WorkflowDefinition, type WorkflowResource } from '@sanity/workflow-engine';
|
|
2
|
+
import { WorkflowCommand } from '../../lib/base-command.ts';
|
|
3
|
+
import { type ReadTarget } from '../../lib/context.ts';
|
|
4
|
+
export default class DefinitionShow extends WorkflowCommand {
|
|
4
5
|
static description: string;
|
|
5
6
|
static args: {
|
|
6
7
|
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
@@ -11,6 +12,24 @@ export default class DefinitionShow extends Command {
|
|
|
11
12
|
};
|
|
12
13
|
run(): Promise<void>;
|
|
13
14
|
}
|
|
15
|
+
/** How a lookup names the version it asked for: `vN` for a pinned
|
|
16
|
+
* `--version`, `latest` when unpinned. */
|
|
17
|
+
export declare function versionLabel(version: number | undefined): string;
|
|
18
|
+
/**
|
|
19
|
+
* One deployed document per target that has the name: the authored
|
|
20
|
+
* {@link WorkflowDefinition} plus the deploy-stamped envelope (`version`,
|
|
21
|
+
* `contentHash`, `tag`). A probe failure propagates — with an unreadable
|
|
22
|
+
* resource, "which dataset holds this name" is unanswerable, unlike an
|
|
23
|
+
* instance-id lookup where any hit IS the instance.
|
|
24
|
+
*/
|
|
25
|
+
export declare function fetchDefinitionHits({ targets, groq, params, }: {
|
|
26
|
+
targets: ReadTarget[];
|
|
27
|
+
groq: string;
|
|
28
|
+
params: Record<string, unknown>;
|
|
29
|
+
}): Promise<{
|
|
30
|
+
resource: WorkflowResource;
|
|
31
|
+
def: DeployedDefinition;
|
|
32
|
+
}[]>;
|
|
14
33
|
/** The summary block at the top of `definition show`: a bold name + version
|
|
15
34
|
* title, then the human title, description, and deploy `tag` as aligned
|
|
16
35
|
* detail rows. `tag` is deployment metadata (the partition), not part of the
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
|
-
import { Args,
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
3
|
import { isTerminalStage, } from '@sanity/workflow-engine';
|
|
4
4
|
import logSymbols from 'log-symbols';
|
|
5
|
-
import {
|
|
5
|
+
import { WorkflowCommand } from "../../lib/base-command.js";
|
|
6
|
+
import { resolveReadTargets, soleHitOrFail } from "../../lib/context.js";
|
|
6
7
|
import { buildDefinitionShowQuery } from "../../lib/definitions.js";
|
|
7
8
|
import { tagFlags } from "../../lib/flags.js";
|
|
8
9
|
import { formatKeyValue, sectionHeader } from "../../lib/ui.js";
|
|
9
|
-
export default class DefinitionShow extends
|
|
10
|
+
export default class DefinitionShow extends WorkflowCommand {
|
|
10
11
|
static description = 'Show a deployed workflow definition.';
|
|
11
12
|
static args = {
|
|
12
13
|
name: Args.string({ required: true, description: 'Workflow definition name.' }),
|
|
@@ -17,18 +18,18 @@ export default class DefinitionShow extends Command {
|
|
|
17
18
|
};
|
|
18
19
|
async run() {
|
|
19
20
|
const { args, flags } = await this.parse(DefinitionShow);
|
|
20
|
-
const
|
|
21
|
+
const targets = await resolveReadTargets(flags);
|
|
21
22
|
const { groq, params } = buildDefinitionShowQuery({
|
|
22
23
|
name: args.name,
|
|
23
24
|
tag: flags.tag,
|
|
24
25
|
version: flags.version,
|
|
25
26
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const def = await client.fetch(groq, params);
|
|
27
|
+
const hits = await fetchDefinitionHits({ targets, groq, params });
|
|
28
|
+
const def = soleHitOrFail(hits, `Definition "${args.name}"`)?.def;
|
|
29
29
|
if (!def) {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
this.error(`${logSymbols.error} no definition "${args.name}" (${versionLabel(flags.version)})`, {
|
|
31
|
+
exit: 1,
|
|
32
|
+
});
|
|
32
33
|
}
|
|
33
34
|
this.log(definitionHeader(def, def.tag));
|
|
34
35
|
this.log('');
|
|
@@ -37,11 +38,19 @@ export default class DefinitionShow extends Command {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
export function versionLabel(version) {
|
|
42
|
+
return version === undefined ? 'latest' : `v${version}`;
|
|
43
|
+
}
|
|
44
|
+
export async function fetchDefinitionHits({ targets, groq, params, }) {
|
|
45
|
+
const probes = await Promise.all(targets.map(async (target) => {
|
|
46
|
+
const def = await target.client.fetch(groq, params, {
|
|
47
|
+
tag: 'definition.show',
|
|
48
|
+
});
|
|
49
|
+
return def === null ? undefined : { resource: target.resource, def };
|
|
50
|
+
}));
|
|
51
|
+
return probes.filter((hit) => hit !== undefined);
|
|
52
|
+
}
|
|
53
|
+
const HEADER_PAD = 11;
|
|
45
54
|
export function definitionHeader(def, tag) {
|
|
46
55
|
const rows = [
|
|
47
56
|
formatKeyValue({ key: 'Title', value: def.title, padTo: HEADER_PAD }),
|
|
@@ -56,18 +65,15 @@ export function definitionHeader(def, tag) {
|
|
|
56
65
|
}
|
|
57
66
|
return [styleText('bold', `${def.name} v${def.version}`), ...rows].join('\n');
|
|
58
67
|
}
|
|
59
|
-
/** One `· action` line, annotated with the terminal status its `status.set` op sets, if any. */
|
|
60
68
|
function actionLine(action) {
|
|
61
69
|
const statusOp = (action.ops ?? []).find((op) => op.type === 'status.set');
|
|
62
70
|
const status = statusOp ? styleText('dim', ` → ${statusOp.status}`) : '';
|
|
63
71
|
return ` · ${action.name}${status}`;
|
|
64
72
|
}
|
|
65
|
-
/** A `- activity` line followed by one line per action it exposes. */
|
|
66
73
|
function activityBlock(activity) {
|
|
67
74
|
const title = activity.title ? styleText('dim', ` — ${activity.title}`) : '';
|
|
68
75
|
return [` - ${activity.name}${title}`, ...(activity.actions ?? []).map(actionLine)];
|
|
69
76
|
}
|
|
70
|
-
/** A `• stage` line (with initial/terminal markers) followed by its activity blocks. */
|
|
71
77
|
function stageBlock(stage, initialStage) {
|
|
72
78
|
const title = stage.title ? styleText('dim', ` — ${stage.title}`) : '';
|
|
73
79
|
const terminal = isTerminalStage(stage) ? styleText('dim', ' (terminal)') : '';
|
|
@@ -77,10 +83,6 @@ function stageBlock(stage, initialStage) {
|
|
|
77
83
|
...(stage.activities ?? []).flatMap(activityBlock),
|
|
78
84
|
];
|
|
79
85
|
}
|
|
80
|
-
/**
|
|
81
|
-
* The body lines for `definition show`: every stage with its activities and each
|
|
82
|
-
* activity's actions. Pure so it can be asserted without driving the oclif command.
|
|
83
|
-
*/
|
|
84
86
|
export function describeDefinition(def) {
|
|
85
87
|
return [
|
|
86
88
|
sectionHeader('Stages'),
|