@sanity/workflow-cli 0.10.0 → 0.12.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 +61 -0
- package/README.md +57 -39
- package/dist/commands/definition/list.d.ts +3 -0
- package/dist/commands/definition/list.js +7 -4
- package/dist/commands/definition/show.d.ts +13 -0
- package/dist/commands/definition/show.js +25 -7
- 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 +13 -2
- package/dist/commands/list.js +43 -17
- package/dist/commands/nuke.d.ts +11 -0
- package/dist/commands/nuke.js +76 -0
- package/dist/commands/start.d.ts +17 -23
- package/dist/commands/start.js +49 -33
- package/dist/commands/tail.js +19 -15
- package/dist/hooks/prerun/telemetry.d.ts +4 -1
- package/dist/hooks/prerun/telemetry.js +6 -2
- package/dist/lib/client.d.ts +8 -0
- package/dist/lib/client.js +1 -1
- package/dist/lib/context.d.ts +6 -2
- package/dist/lib/context.js +9 -7
- package/dist/lib/definitions.d.ts +10 -0
- package/dist/lib/definitions.js +8 -2
- package/dist/lib/fail.d.ts +7 -0
- package/dist/lib/fail.js +2 -1
- package/dist/lib/nuke.d.ts +89 -0
- package/dist/lib/nuke.js +111 -0
- package/dist/lib/operation-args.d.ts +3 -1
- package/dist/lib/ops-report.d.ts +2 -1
- package/dist/lib/prompt.d.ts +10 -0
- package/dist/lib/prompt.js +4 -0
- package/dist/lib/share-definitions.d.ts +59 -25
- package/dist/lib/share-definitions.js +104 -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 +66 -11
- package/package.json +4 -4
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, assertReadableModel, 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,
|
|
@@ -23,13 +36,10 @@ export function buildListQuery(flags) {
|
|
|
23
36
|
currentStage,
|
|
24
37
|
completedAt,
|
|
25
38
|
abortedAt,
|
|
26
|
-
lastChangedAt
|
|
39
|
+
lastChangedAt,
|
|
40
|
+
modelVersion,
|
|
41
|
+
minReaderModel
|
|
27
42
|
}`;
|
|
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
43
|
return { groq, params };
|
|
34
44
|
}
|
|
35
45
|
export function instanceRow(r) {
|
|
@@ -46,17 +56,18 @@ export function instanceRow(r) {
|
|
|
46
56
|
};
|
|
47
57
|
}
|
|
48
58
|
export default class List extends WorkflowCommand {
|
|
49
|
-
static description = 'List workflow instances in the configured dataset.';
|
|
59
|
+
static description = 'List workflow instances in the configured dataset (in-flight by default).';
|
|
50
60
|
static examples = [
|
|
51
61
|
'<%= config.bin %> list',
|
|
52
|
-
'<%= config.bin %> list --
|
|
62
|
+
'<%= config.bin %> list --include-completed',
|
|
53
63
|
'<%= config.bin %> list --definition productLaunch',
|
|
64
|
+
'<%= config.bin %> list --document dataset:proj:ds:article-1',
|
|
54
65
|
'<%= config.bin %> list --tag prod',
|
|
55
66
|
];
|
|
56
67
|
static flags = {
|
|
57
68
|
...tagFlags,
|
|
58
|
-
'
|
|
59
|
-
description: '
|
|
69
|
+
'include-completed': Flags.boolean({
|
|
70
|
+
description: 'Include completed/aborted instances (default: in-flight only).',
|
|
60
71
|
default: false,
|
|
61
72
|
}),
|
|
62
73
|
failed: Flags.boolean({
|
|
@@ -66,6 +77,10 @@ export default class List extends WorkflowCommand {
|
|
|
66
77
|
definition: Flags.string({
|
|
67
78
|
description: "Only instances of this workflow definition (its `name`; the instance's `definition` field).",
|
|
68
79
|
}),
|
|
80
|
+
document: Flags.string({
|
|
81
|
+
description: 'Only instances that reference this document (resource-qualified GDR URI, ' +
|
|
82
|
+
'e.g. "dataset:proj:ds:article-1").',
|
|
83
|
+
}),
|
|
69
84
|
limit: Flags.integer({
|
|
70
85
|
description: 'Maximum rows to return.',
|
|
71
86
|
default: 50,
|
|
@@ -74,12 +89,13 @@ export default class List extends WorkflowCommand {
|
|
|
74
89
|
async run() {
|
|
75
90
|
const { flags } = await this.parse(List);
|
|
76
91
|
const targets = await resolveReadTargets(flags);
|
|
77
|
-
const { groq, params } = buildListQuery(flags);
|
|
92
|
+
const { groq, params } = failOnThrow('Invalid --document:', () => buildListQuery(flags));
|
|
93
|
+
const document = flags.document;
|
|
78
94
|
const failures = await runReadAcrossTargets({
|
|
79
95
|
targets,
|
|
80
96
|
log: (line) => this.log(line),
|
|
81
97
|
run: async ({ client }) => {
|
|
82
|
-
const fetched = await client
|
|
98
|
+
const fetched = await fetchRows({ client, groq, params, document });
|
|
83
99
|
if (fetched.length === 0) {
|
|
84
100
|
this.log(`${logSymbols.info} no instances match`);
|
|
85
101
|
return;
|
|
@@ -109,3 +125,13 @@ export default class List extends WorkflowCommand {
|
|
|
109
125
|
}
|
|
110
126
|
}
|
|
111
127
|
}
|
|
128
|
+
async function fetchRows({ client, groq, params, document, }) {
|
|
129
|
+
if (document === undefined) {
|
|
130
|
+
const rows = await client.fetch(groq, params, { tag: 'list' });
|
|
131
|
+
return rows.map(assertReadableModel);
|
|
132
|
+
}
|
|
133
|
+
const candidates = await client.fetch(groq, params, { tag: 'list' });
|
|
134
|
+
return candidates
|
|
135
|
+
.map(assertReadableModel)
|
|
136
|
+
.filter((instance) => instanceWatchesDocument(instance, document));
|
|
137
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { WorkflowCommand } from '../lib/base-command.ts';
|
|
2
|
+
export default class Nuke extends WorkflowCommand {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { input } from '@sanity/cli-core/ux';
|
|
3
|
+
import { resourceGdr } from '@sanity/workflow-engine';
|
|
4
|
+
import logSymbols from 'log-symbols';
|
|
5
|
+
import { WorkflowCommand } from "../lib/base-command.js";
|
|
6
|
+
import { clientFor, resolveApiHost, resolveTokenOrFail } from "../lib/client.js";
|
|
7
|
+
import { dedupeResources } from "../lib/context.js";
|
|
8
|
+
import { fail } from "../lib/fail.js";
|
|
9
|
+
import { loadWorkflowConfig } from "../lib/load-config.js";
|
|
10
|
+
import { confirmationMatches, executeNuke, formatNukeSummary, involvedTargets, planCounts, renderNukePlan, resolveNukePlan, } from "../lib/nuke.js";
|
|
11
|
+
import { runWriteVerb } from "../lib/ops-report.js";
|
|
12
|
+
import { canPromptOnStderr } from "../lib/prompt.js";
|
|
13
|
+
import { selectDeployment } from "../lib/select-deployment.js";
|
|
14
|
+
export default class Nuke extends WorkflowCommand {
|
|
15
|
+
static summary = 'Delete every engine-owned document for a deployment tag — the dev-period big red button.';
|
|
16
|
+
static description = 'The reset for a dataset holding engine documents the versioned upgrade framework cannot yet ' +
|
|
17
|
+
"migrate: deletes the tag's instances, definitions, and guards (across every alias-bound " +
|
|
18
|
+
'resource). Content documents are never touched. Prints a dry-run plan, then requires you to ' +
|
|
19
|
+
'type back every involved dataset (--force skips the prompt; the plan still prints).';
|
|
20
|
+
static examples = [
|
|
21
|
+
'<%= config.bin %> nuke --tag plugin-dev',
|
|
22
|
+
'<%= config.bin %> nuke --tag plugin-dev --force',
|
|
23
|
+
];
|
|
24
|
+
static flags = {
|
|
25
|
+
tag: Flags.string({
|
|
26
|
+
description: 'The deployment tag to reset. Required — a destructive reset never guesses the environment.',
|
|
27
|
+
required: true,
|
|
28
|
+
}),
|
|
29
|
+
force: Flags.boolean({
|
|
30
|
+
description: 'Skip the confirmation prompt (for scripts/CI). The plan still prints.',
|
|
31
|
+
default: false,
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
async run() {
|
|
35
|
+
const { flags } = await this.parse(Nuke);
|
|
36
|
+
const config = await loadWorkflowConfig();
|
|
37
|
+
const deployment = selectDeployment(config, { tag: flags.tag });
|
|
38
|
+
const targets = buildTargets(deployment, await resolveTokenOrFail());
|
|
39
|
+
const plan = await resolveNukePlan({ tag: deployment.tag, targets });
|
|
40
|
+
const apiHost = resolveApiHost() ?? 'https://api.sanity.io (production default)';
|
|
41
|
+
renderNukePlan(plan, apiHost).forEach((line) => this.log(line));
|
|
42
|
+
const counts = planCounts(plan);
|
|
43
|
+
if (counts.total === 0) {
|
|
44
|
+
this.log(`${logSymbols.info} Nothing to nuke for tag "${deployment.tag}".`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!flags.force)
|
|
48
|
+
await confirmOrFail(involvedTargets(plan));
|
|
49
|
+
await runWriteVerb({
|
|
50
|
+
startLabel: 'Nuking…',
|
|
51
|
+
failLabel: 'Nuke failed',
|
|
52
|
+
failHeadline: 'workflow nuke error:',
|
|
53
|
+
run: () => executeNuke(plan),
|
|
54
|
+
report: () => ({ changed: true, message: formatNukeSummary(deployment.tag, counts) }),
|
|
55
|
+
log: (line) => this.log(line),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function buildTargets(deployment, token) {
|
|
60
|
+
const engineResource = deployment.workflowResource;
|
|
61
|
+
const aliasResources = (deployment.resourceAliases ?? []).map((binding) => binding.resource);
|
|
62
|
+
return dedupeResources([engineResource, ...aliasResources]).map((resource) => ({
|
|
63
|
+
resource,
|
|
64
|
+
client: clientFor(resource, token),
|
|
65
|
+
holdsEngineDocs: resourceGdr(resource) === resourceGdr(engineResource),
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
async function confirmOrFail(targets) {
|
|
69
|
+
if (!canPromptOnStderr()) {
|
|
70
|
+
fail('Refusing to nuke without confirmation in a non-interactive shell.', 'Re-run with --force to skip the prompt.');
|
|
71
|
+
}
|
|
72
|
+
const answer = await input({ message: `Type every target to confirm deletion (space-separated):\n ${targets.join(' ')}\n` }, { output: process.stderr });
|
|
73
|
+
if (!confirmationMatches(answer, targets)) {
|
|
74
|
+
fail('Confirmation did not match — nothing was deleted.', `Expected: ${targets.join(' ')}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
package/dist/commands/start.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StartNotSettledError, 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 {
|
|
@@ -11,41 +11,35 @@ export default class Start extends WorkflowCommand {
|
|
|
11
11
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
12
|
version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
13
|
field: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'instance-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
16
|
};
|
|
16
17
|
run(): Promise<void>;
|
|
17
18
|
}
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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[];
|
|
19
|
+
/** A created-but-unprimed failure is resumable — the engine message names
|
|
20
|
+
* the id and the generic remediation; operators need the CLI's own retry
|
|
21
|
+
* spelling appended. Every other error passes through untouched. */
|
|
22
|
+
export declare function withRetryHint(error: unknown): unknown;
|
|
23
|
+
/** The spinner message for a start whose first cascade failed after the
|
|
24
|
+
* instance was created and primed: the run exists — say so, name the cause,
|
|
25
|
+
* and point at the retry rail instead of reporting a rejection. */
|
|
26
|
+
export declare function startNotSettledReport(error: StartNotSettledError): string;
|
|
27
|
+
/** The `--json` payload for a not-settled start — same `instanceId` key as
|
|
28
|
+
* the success payload so scripts can pick up the retry id either way. */
|
|
29
|
+
export declare function startNotSettledJson(error: StartNotSettledError): Record<string, unknown>;
|
|
39
30
|
/**
|
|
40
31
|
* The shared engine args ({@link baseEngineArgs}) plus the start-specific
|
|
41
32
|
* fields. Optionals spread conditionally because the engine's optional args
|
|
42
33
|
* reject an explicit `undefined` under `exactOptionalPropertyTypes`.
|
|
43
34
|
*/
|
|
44
|
-
export declare function buildStartArgs({ scope, name, version, initialFields, }: {
|
|
35
|
+
export declare function buildStartArgs({ scope, name, version, initialFields, instanceId, }: {
|
|
45
36
|
scope: EngineScope;
|
|
46
37
|
name: string;
|
|
47
38
|
version: number | undefined;
|
|
48
39
|
initialFields: InitialFieldValue[];
|
|
40
|
+
/** The start's idempotency key (`--instance-id`) — a retry carrying it
|
|
41
|
+
* resumes the earlier attempt instead of creating a duplicate. */
|
|
42
|
+
instanceId?: string | undefined;
|
|
49
43
|
}): StartInstanceArgs & EngineScope;
|
|
50
44
|
/** The success spinner message. Pure so the in-flight / immediately-terminal
|
|
51
45
|
* permutations can be asserted without driving a real start. */
|
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, errorMessage, StartNotPrimedError, StartNotSettledError, 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";
|
|
@@ -16,6 +16,7 @@ export default class Start extends WorkflowCommand {
|
|
|
16
16
|
'<%= config.bin %> start productLaunch',
|
|
17
17
|
'<%= config.bin %> start article-review --field subject=\'{"id":"dataset:proj:ds:article-1","type":"article"}\'',
|
|
18
18
|
'<%= config.bin %> start productLaunch --version 2 --tag prod',
|
|
19
|
+
'<%= config.bin %> start productLaunch --instance-id prod.wf-instance.a1b2c3d4e5f6',
|
|
19
20
|
];
|
|
20
21
|
static args = {
|
|
21
22
|
name: Args.string({ required: true, description: 'Workflow definition name.' }),
|
|
@@ -32,6 +33,12 @@ export default class Start extends WorkflowCommand {
|
|
|
32
33
|
multiple: true,
|
|
33
34
|
default: [],
|
|
34
35
|
}),
|
|
36
|
+
'instance-id': Flags.string({
|
|
37
|
+
description: "Start under this instance id — for retries. The id is the start's idempotency key: " +
|
|
38
|
+
'pass the id of a start that failed partway and the engine resumes it instead of ' +
|
|
39
|
+
'creating a duplicate (an already-settled start replays as a no-op). Omit to mint ' +
|
|
40
|
+
'a fresh id.',
|
|
41
|
+
}),
|
|
35
42
|
...jsonFlags,
|
|
36
43
|
};
|
|
37
44
|
async run() {
|
|
@@ -52,25 +59,58 @@ export default class Start extends WorkflowCommand {
|
|
|
52
59
|
name: args.name,
|
|
53
60
|
version: flags.version,
|
|
54
61
|
initialFields,
|
|
62
|
+
instanceId: flags['instance-id'],
|
|
55
63
|
}),
|
|
56
64
|
};
|
|
57
65
|
if (flags.json) {
|
|
58
|
-
const result = await workflow
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
const result = await workflow.startInstance(startArgs).catch((error) => {
|
|
67
|
+
if (error instanceof StartNotSettledError) {
|
|
68
|
+
this.log(JSON.stringify(startNotSettledJson(error), null, 2));
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
fail('start error:', failureDetail(withRetryHint(error)));
|
|
72
|
+
});
|
|
73
|
+
if (result !== undefined) {
|
|
74
|
+
this.log(JSON.stringify(startResultJson(result.instance), null, 2));
|
|
75
|
+
}
|
|
62
76
|
return;
|
|
63
77
|
}
|
|
64
78
|
await runWriteVerb({
|
|
65
79
|
startLabel: `Starting ${args.name}…`,
|
|
66
80
|
failLabel: 'Start rejected',
|
|
67
81
|
failHeadline: 'start error:',
|
|
68
|
-
run: () => workflow.startInstance(startArgs)
|
|
69
|
-
|
|
82
|
+
run: () => workflow.startInstance(startArgs).catch((error) => {
|
|
83
|
+
if (error instanceof StartNotSettledError)
|
|
84
|
+
return error;
|
|
85
|
+
throw withRetryHint(error);
|
|
86
|
+
}),
|
|
87
|
+
report: (result) => result instanceof StartNotSettledError
|
|
88
|
+
? { changed: false, message: startNotSettledReport(result) }
|
|
89
|
+
: { changed: true, message: startReport(result.instance) },
|
|
70
90
|
log: (line) => this.log(line),
|
|
71
91
|
});
|
|
72
92
|
}
|
|
73
93
|
}
|
|
94
|
+
export function withRetryHint(error) {
|
|
95
|
+
return error instanceof StartNotPrimedError
|
|
96
|
+
? new Error(`${errorMessage(error)} (--instance-id ${error.instanceId})`)
|
|
97
|
+
: error;
|
|
98
|
+
}
|
|
99
|
+
export function startNotSettledReport(error) {
|
|
100
|
+
const id = styleText('bold', error.instanceId);
|
|
101
|
+
return (`Started ${id} — but its first auto-advance failed: ${errorMessage(error.cause)}. ` +
|
|
102
|
+
`It settles on the next tick, or retry with --instance-id ${error.instanceId}.`);
|
|
103
|
+
}
|
|
104
|
+
export function startNotSettledJson(error) {
|
|
105
|
+
return {
|
|
106
|
+
instanceId: error.instanceId,
|
|
107
|
+
notSettled: true,
|
|
108
|
+
detail: errorMessage(error.cause),
|
|
109
|
+
...(error.instance !== undefined
|
|
110
|
+
? { currentStage: error.instance.currentStage, definition: error.instance.definition }
|
|
111
|
+
: {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
74
114
|
async function resolveStartInputs({ client, name, tag, version, values, }) {
|
|
75
115
|
const deployed = await fetchDeployedDefinition({ client, name, tag, version });
|
|
76
116
|
if (deployed === undefined) {
|
|
@@ -82,37 +122,13 @@ async function resolveStartInputs({ client, name, tag, version, values, }) {
|
|
|
82
122
|
}
|
|
83
123
|
return failOnThrow('start usage error:', () => buildInitialFields({ declared: deployed.fields ?? [], values }));
|
|
84
124
|
}
|
|
85
|
-
export function
|
|
86
|
-
if (isStartableDefinition(definition)) {
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
89
|
-
return "definition is spawn-only (lifecycle: 'child') — instances of it are spawned by a parent workflow's activity, not started standalone";
|
|
90
|
-
}
|
|
91
|
-
export function buildInitialFields({ declared, values, }) {
|
|
92
|
-
const settable = declared.filter((f) => f.initialValue?.type === 'input');
|
|
93
|
-
const settableNames = settable.map((f) => f.name).join(', ');
|
|
94
|
-
return Object.entries(values).map(([name, value]) => {
|
|
95
|
-
const entry = declared.find((f) => f.name === name);
|
|
96
|
-
if (entry === undefined) {
|
|
97
|
-
throw new Error(settable.length > 0
|
|
98
|
-
? `workflow declares no field "${name}" — input fields: ${settableNames}`
|
|
99
|
-
: `workflow declares no field "${name}" — it declares no input fields at all`);
|
|
100
|
-
}
|
|
101
|
-
if (entry.initialValue?.type !== 'input') {
|
|
102
|
-
throw new Error(`field "${name}" is not input-sourced — the engine would silently ignore the value. ` +
|
|
103
|
-
(settable.length > 0
|
|
104
|
-
? `Input fields: ${settableNames}`
|
|
105
|
-
: 'This workflow has no input fields.'));
|
|
106
|
-
}
|
|
107
|
-
return { type: entry.type, name, value };
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
export function buildStartArgs({ scope, name, version, initialFields, }) {
|
|
125
|
+
export function buildStartArgs({ scope, name, version, initialFields, instanceId, }) {
|
|
111
126
|
return {
|
|
112
127
|
...baseEngineArgs(scope),
|
|
113
128
|
definition: name,
|
|
114
129
|
...(version !== undefined ? { version } : {}),
|
|
115
130
|
...(initialFields.length > 0 ? { initialFields } : {}),
|
|
131
|
+
...(instanceId !== undefined ? { instanceId } : {}),
|
|
116
132
|
};
|
|
117
133
|
}
|
|
118
134
|
export function startReport(instance) {
|
package/dist/commands/tail.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
2
|
import { Args } from '@oclif/core';
|
|
3
|
-
import { displayTitle, errorMessage } from '@sanity/workflow-engine';
|
|
3
|
+
import { assertReadableModel, displayTitle, errorMessage, } from '@sanity/workflow-engine';
|
|
4
4
|
import logSymbols from 'log-symbols';
|
|
5
5
|
import { WorkflowCommand } from "../lib/base-command.js";
|
|
6
6
|
import { findInstance, resolveReadTargets } from "../lib/context.js";
|
|
@@ -31,7 +31,11 @@ export default class Tail extends WorkflowCommand {
|
|
|
31
31
|
this.log(styleText('dim', `tailing ${styleText('bold', args.instanceId)} (${initial.definition} v${initial.pinnedVersion}, ${initial.history.length} prior entries, currently in ${initial.currentStage})`));
|
|
32
32
|
this.log(styleText('dim', 'Ctrl+C to stop'));
|
|
33
33
|
this.log('');
|
|
34
|
-
let
|
|
34
|
+
let rejectListen;
|
|
35
|
+
const listenFailed = new Promise((_, reject) => {
|
|
36
|
+
rejectListen = reject;
|
|
37
|
+
});
|
|
38
|
+
let printChain = Promise.resolve(initial.history.length);
|
|
35
39
|
const subscription = client
|
|
36
40
|
.listen(`*[_id == $id]`, { id: args.instanceId }, {
|
|
37
41
|
includeResult: false,
|
|
@@ -39,17 +43,13 @@ export default class Tail extends WorkflowCommand {
|
|
|
39
43
|
tag: TAIL_TAG,
|
|
40
44
|
})
|
|
41
45
|
.subscribe({
|
|
42
|
-
next:
|
|
43
|
-
|
|
44
|
-
seen = await this.printNewEntries({ client, instanceId: args.instanceId, seen });
|
|
45
|
-
}
|
|
46
|
-
catch (err) {
|
|
46
|
+
next: () => {
|
|
47
|
+
printChain = printChain.then((seen) => this.printNewEntries({ client, instanceId: args.instanceId, seen }).catch((err) => {
|
|
47
48
|
process.stderr.write(`${styleText('red', `${logSymbols.error} fetch failed:`)} ${errorMessage(err)}\n`);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
error: (err) => {
|
|
51
|
-
fail('Listen subscription error:', failureDetail(err));
|
|
49
|
+
return seen;
|
|
50
|
+
}));
|
|
52
51
|
},
|
|
52
|
+
error: rejectListen,
|
|
53
53
|
});
|
|
54
54
|
const shutdown = () => {
|
|
55
55
|
subscription.unsubscribe();
|
|
@@ -58,16 +58,20 @@ export default class Tail extends WorkflowCommand {
|
|
|
58
58
|
};
|
|
59
59
|
process.on('SIGINT', shutdown);
|
|
60
60
|
process.on('SIGTERM', shutdown);
|
|
61
|
-
await
|
|
61
|
+
await listenFailed.catch((err) => {
|
|
62
|
+
subscription.unsubscribe();
|
|
63
|
+
fail('Listen subscription error:', failureDetail(err));
|
|
64
|
+
});
|
|
62
65
|
}
|
|
63
66
|
async printNewEntries({ client, instanceId, seen, }) {
|
|
64
|
-
const
|
|
65
|
-
if (!
|
|
67
|
+
const raw = await client.getDocument(instanceId, { tag: TAIL_TAG });
|
|
68
|
+
if (!raw)
|
|
66
69
|
return seen;
|
|
70
|
+
const next = assertReadableModel(raw);
|
|
67
71
|
const newEntries = next.history.slice(seen);
|
|
68
72
|
for (const entry of newEntries) {
|
|
69
73
|
this.log(`${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${styleText('cyan', displayTitle(entry._type))}`);
|
|
70
74
|
}
|
|
71
|
-
return next.history.length;
|
|
75
|
+
return Math.max(seen, next.history.length);
|
|
72
76
|
}
|
|
73
77
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Hook } from '@oclif/core';
|
|
2
2
|
/** Builds and installs the invocation's telemetry shell before the command
|
|
3
|
-
* runs; the `finally` hook completes the trace and flushes. Never throws.
|
|
3
|
+
* runs; the `finally` hook completes the trace and flushes. Never throws.
|
|
4
|
+
* An explicit `--share-defs` forces the store to send despite CI /
|
|
5
|
+
* `DO_NOT_TRACK` — resolved from raw argv here, since the store is built
|
|
6
|
+
* before oclif parses flags. */
|
|
4
7
|
declare const hook: Hook<'prerun'>;
|
|
5
8
|
export default hook;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { shouldForceShareTelemetry } from "../../lib/share-definitions.js";
|
|
1
2
|
import { setupCliTelemetry } from "../../lib/telemetry-setup.js";
|
|
2
|
-
const hook = async function ({ config }) {
|
|
3
|
-
await setupCliTelemetry({
|
|
3
|
+
const hook = async function ({ config, Command, argv }) {
|
|
4
|
+
await setupCliTelemetry({
|
|
5
|
+
cliVersion: config.version,
|
|
6
|
+
forceSend: shouldForceShareTelemetry({ commandId: Command?.id, argv }),
|
|
7
|
+
});
|
|
4
8
|
};
|
|
5
9
|
export default hook;
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { type SanityClient } from '@sanity/client';
|
|
2
2
|
import { type WorkflowResource } from '@sanity/workflow-engine';
|
|
3
|
+
/**
|
|
4
|
+
* The API host to hit: an explicit `SANITY_API_HOST` wins; otherwise mirror
|
|
5
|
+
* the Sanity CLI — staging routes to the staging API, prod falls through to
|
|
6
|
+
* `@sanity/client`'s default (`undefined` here). Pairs with
|
|
7
|
+
* {@link resolveToken}, which reads the matching session token. Exported so a
|
|
8
|
+
* destructive command can display the environment its clients will hit.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveApiHost(): string | undefined;
|
|
3
11
|
/**
|
|
4
12
|
* Resolve the Sanity auth token. An explicit `SANITY_AUTH_TOKEN` wins (CI /
|
|
5
13
|
* scripted use); otherwise fall back to the `sanity login` session token.
|
package/dist/lib/client.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ENV, envAuthToken } from "./env.js";
|
|
|
5
5
|
import { fail } from "./fail.js";
|
|
6
6
|
const API_VERSION = ENGINE_API_VERSION;
|
|
7
7
|
const STAGING_API_HOST = 'https://api.sanity.work';
|
|
8
|
-
function resolveApiHost() {
|
|
8
|
+
export function resolveApiHost() {
|
|
9
9
|
return process.env[ENV.SANITY_API_HOST] ?? (isStaging() ? STAGING_API_HOST : undefined);
|
|
10
10
|
}
|
|
11
11
|
export async function resolveToken() {
|
package/dist/lib/context.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SanityClient } from '@sanity/client';
|
|
2
|
-
import type
|
|
2
|
+
import { type WorkflowConfig, type WorkflowDeployment, type WorkflowInstance, type WorkflowResource } from '@sanity/workflow-engine';
|
|
3
3
|
import type { EngineScope } from './operation-args.ts';
|
|
4
4
|
export interface DeploymentContext {
|
|
5
5
|
deployment: WorkflowDeployment;
|
|
@@ -40,9 +40,13 @@ export interface ReadTarget {
|
|
|
40
40
|
export declare function resolveReadTargets(flags: {
|
|
41
41
|
tag?: string | undefined;
|
|
42
42
|
}): Promise<ReadTarget[]>;
|
|
43
|
+
/** Distinct resources, keyed by their resource-shaped GDR (`<type>:<id>`).
|
|
44
|
+
* The first occurrence wins its position, so a caller-ordered list (e.g. the
|
|
45
|
+
* engine resource first) keeps its lead entry. */
|
|
46
|
+
export declare function dedupeResources(resources: WorkflowResource[]): WorkflowResource[];
|
|
43
47
|
/** The distinct resources a read targets: the tagged deployment's sole
|
|
44
48
|
* resource, or — untagged — every distinct one the config's deployments
|
|
45
|
-
* mention (
|
|
49
|
+
* mention ({@link dedupeResources}). */
|
|
46
50
|
export declare function resolveReadResources(config: WorkflowConfig, tag: string | undefined): WorkflowResource[];
|
|
47
51
|
/** The single resource a read should target, or fail asking which when the
|
|
48
52
|
* config spans more than one. For the paths that need one definite dataset
|
package/dist/lib/context.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertReadableModel, resourceGdr, } from '@sanity/workflow-engine';
|
|
1
2
|
import { clientFor, resolveTokenOrFail } from "./client.js";
|
|
2
3
|
import { fail } from "./fail.js";
|
|
3
4
|
import { loadWorkflowConfig } from "./load-config.js";
|
|
@@ -14,15 +15,14 @@ export async function resolveReadTargets(flags) {
|
|
|
14
15
|
const token = await resolveTokenOrFail();
|
|
15
16
|
return resources.map((resource) => ({ resource, client: clientFor(resource, token) }));
|
|
16
17
|
}
|
|
18
|
+
export function dedupeResources(resources) {
|
|
19
|
+
return [...new Map(resources.map((resource) => [resourceGdr(resource), resource])).values()];
|
|
20
|
+
}
|
|
17
21
|
export function resolveReadResources(config, tag) {
|
|
18
22
|
if (tag !== undefined) {
|
|
19
23
|
return [selectDeployment(config, { tag }).workflowResource];
|
|
20
24
|
}
|
|
21
|
-
const
|
|
22
|
-
`${d.workflowResource.type}:${d.workflowResource.id}`,
|
|
23
|
-
d.workflowResource,
|
|
24
|
-
]));
|
|
25
|
-
const resources = [...byKey.values()];
|
|
25
|
+
const resources = dedupeResources(config.deployments.map((d) => d.workflowResource));
|
|
26
26
|
if (resources.length === 0) {
|
|
27
27
|
fail('No deployments configured.');
|
|
28
28
|
}
|
|
@@ -51,7 +51,7 @@ export async function loadInstanceOrFail(client, instanceId) {
|
|
|
51
51
|
if (!instance) {
|
|
52
52
|
fail(`Workflow instance ${instanceId} not found`);
|
|
53
53
|
}
|
|
54
|
-
return instance;
|
|
54
|
+
return assertReadableModel(instance);
|
|
55
55
|
}
|
|
56
56
|
export function soleHitOrFail(hits, subject) {
|
|
57
57
|
const [sole, ...rest] = hits;
|
|
@@ -65,7 +65,9 @@ export async function findInstance({ targets, instanceId, requestTag, }) {
|
|
|
65
65
|
const instance = await target.client.getDocument(instanceId, {
|
|
66
66
|
tag: requestTag,
|
|
67
67
|
});
|
|
68
|
-
return instance === undefined
|
|
68
|
+
return instance === undefined
|
|
69
|
+
? undefined
|
|
70
|
+
: { ...target, instance: assertReadableModel(instance) };
|
|
69
71
|
}));
|
|
70
72
|
const hits = probes
|
|
71
73
|
.filter((probe) => probe.status === 'fulfilled')
|
|
@@ -9,6 +9,16 @@ export declare function buildDefinitionShowQuery(flags: DefinitionShowQueryArgs)
|
|
|
9
9
|
groq: string;
|
|
10
10
|
params: Record<string, unknown>;
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* The tag partitions holding any version of a definition name within one
|
|
14
|
+
* dataset. An untagged lookup that would span several partitions must error
|
|
15
|
+
* (the highest version ACROSS partitions is meaningless — it could show a
|
|
16
|
+
* prod operator the dev definition), the same way a cross-dataset hit does.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildDefinitionTagsQuery(name: string): {
|
|
19
|
+
groq: string;
|
|
20
|
+
params: Record<string, unknown>;
|
|
21
|
+
};
|
|
12
22
|
/**
|
|
13
23
|
* Fetch the deployed definition a command targets — latest unless pinned via
|
|
14
24
|
* `version`. An absent pinned version always fails (the caller asked for
|
package/dist/lib/definitions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WORKFLOW_DEFINITION_TYPE, errorMessage, tagScopeFilter, validateDefinition, } from '@sanity/workflow-engine';
|
|
1
|
+
import { WORKFLOW_DEFINITION_TYPE, assertReadableModel, errorMessage, tagScopeFilter, validateDefinition, } from '@sanity/workflow-engine';
|
|
2
2
|
import { fail } from "./fail.js";
|
|
3
3
|
export function buildDefinitionShowQuery(flags) {
|
|
4
4
|
const params = { name: flags.name };
|
|
@@ -14,6 +14,12 @@ export function buildDefinitionShowQuery(flags) {
|
|
|
14
14
|
const groq = `*[${filters.join(' && ')}] | order(version desc) [0]`;
|
|
15
15
|
return { groq, params };
|
|
16
16
|
}
|
|
17
|
+
export function buildDefinitionTagsQuery(name) {
|
|
18
|
+
return {
|
|
19
|
+
groq: `array::unique(*[_type == "${WORKFLOW_DEFINITION_TYPE}" && name == $name].tag)`,
|
|
20
|
+
params: { name },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
17
23
|
export async function fetchDeployedDefinition({ client, name, tag, version, }) {
|
|
18
24
|
const { groq, params } = buildDefinitionShowQuery({ name, tag, version });
|
|
19
25
|
const deployed = await client.fetch(groq, params, {
|
|
@@ -22,7 +28,7 @@ export async function fetchDeployedDefinition({ client, name, tag, version, }) {
|
|
|
22
28
|
if (deployed === null && version !== undefined) {
|
|
23
29
|
fail(`No deployed definition "${name}" v${version}.`);
|
|
24
30
|
}
|
|
25
|
-
return deployed
|
|
31
|
+
return deployed === null ? undefined : assertReadableModel(deployed);
|
|
26
32
|
}
|
|
27
33
|
export function selectDefinitions(allDefinitions, { only, context = '' }) {
|
|
28
34
|
if (!only) {
|
package/dist/lib/fail.d.ts
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
* stderr — no oclif framing, no Node stack — so expected failures
|
|
4
4
|
* (validation, missing input, conflicting flags) read clean.
|
|
5
5
|
*
|
|
6
|
+
* Exits by THROWING oclif's `ExitError`, never `process.exit`: the error
|
|
7
|
+
* rides the normal command lifecycle, so the `finally` hook still runs —
|
|
8
|
+
* completing the command trace and flushing batched telemetry — before
|
|
9
|
+
* oclif's handler exits 1 without printing anything further. A raw
|
|
10
|
+
* `process.exit` here would silently drop the trace and every batched
|
|
11
|
+
* event (including already-logged events from earlier in the command).
|
|
12
|
+
*
|
|
6
13
|
* For unexpected failures (network, engine throw) prefer letting the
|
|
7
14
|
* error propagate so the stack is visible for debugging.
|
|
8
15
|
*/
|