@sanity/workflow-cli 0.8.0 → 0.9.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/README.md +99 -61
- package/dist/commands/abort.d.ts +5 -7
- package/dist/commands/abort.js +15 -14
- package/dist/commands/definition/delete.d.ts +10 -6
- package/dist/commands/definition/delete.js +8 -11
- package/dist/commands/definition/diff.d.ts +8 -1
- package/dist/commands/definition/diff.js +30 -24
- package/dist/commands/definition/list.d.ts +4 -4
- package/dist/commands/definition/list.js +8 -8
- package/dist/commands/definition/show.d.ts +0 -10
- package/dist/commands/definition/show.js +11 -20
- package/dist/commands/deploy.d.ts +58 -5
- package/dist/commands/deploy.js +134 -27
- package/dist/commands/diagnose.d.ts +5 -1
- package/dist/commands/diagnose.js +18 -28
- package/dist/commands/fire-action.d.ts +15 -28
- package/dist/commands/fire-action.js +37 -72
- package/dist/commands/list.d.ts +2 -2
- package/dist/commands/list.js +8 -8
- package/dist/commands/{retry-activity.d.ts → reset-activity.d.ts} +1 -1
- package/dist/commands/{retry-activity.js → reset-activity.js} +2 -2
- package/dist/commands/set-stage.d.ts +27 -3
- package/dist/commands/set-stage.js +80 -13
- package/dist/commands/show.d.ts +1 -0
- package/dist/commands/show.js +14 -8
- package/dist/commands/start.d.ts +59 -0
- package/dist/commands/start.js +169 -0
- package/dist/commands/tail.d.ts +3 -0
- package/dist/commands/tail.js +9 -5
- package/dist/lib/client.d.ts +11 -14
- package/dist/lib/client.js +45 -33
- package/dist/lib/context.d.ts +62 -0
- package/dist/lib/context.js +82 -0
- package/dist/lib/definitions.d.ts +38 -29
- package/dist/lib/definitions.js +45 -85
- package/dist/lib/diff.js +8 -0
- package/dist/lib/env.d.ts +0 -6
- package/dist/lib/env.js +3 -9
- package/dist/lib/fail.d.ts +6 -0
- package/dist/lib/fail.js +11 -1
- package/dist/lib/flags.d.ts +14 -2
- package/dist/lib/flags.js +24 -3
- package/dist/lib/load-config.d.ts +11 -0
- package/dist/lib/load-config.js +69 -0
- package/dist/lib/operation-args.d.ts +25 -7
- package/dist/lib/operation-args.js +12 -11
- package/dist/lib/ops-report.d.ts +19 -7
- package/dist/lib/ops-report.js +6 -7
- package/dist/lib/params.d.ts +7 -0
- package/dist/lib/params.js +26 -0
- package/dist/lib/select-deployment.d.ts +31 -0
- package/dist/lib/select-deployment.js +58 -0
- package/dist/lib/ui.d.ts +3 -1
- package/dist/lib/ui.js +1 -3
- package/oclif.manifest.json +145 -86
- package/package.json +6 -4
- package/dist/commands/move-stage.d.ts +0 -47
- package/dist/commands/move-stage.js +0 -77
- package/dist/lib/config.d.ts +0 -18
- package/dist/lib/config.js +0 -50
package/dist/lib/definitions.js
CHANGED
|
@@ -1,103 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { WORKFLOW_DEFINITION_TYPE, tagScopeFilter, validateDefinition, } from '@sanity/workflow-engine';
|
|
2
|
+
import { errorMessage, fail } from "./fail.js";
|
|
3
|
+
export function buildDefinitionShowQuery(flags) {
|
|
4
|
+
const params = { name: flags.name };
|
|
5
|
+
const filters = [`_type == "${WORKFLOW_DEFINITION_TYPE}"`, 'name == $name'];
|
|
6
|
+
if (flags.version !== undefined) {
|
|
7
|
+
params.version = flags.version;
|
|
8
|
+
filters.push('version == $version');
|
|
6
9
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import { pathToFileURL } from 'node:url';
|
|
11
|
-
import { validateDefinition } from '@sanity/workflow-engine';
|
|
12
|
-
import { loadDefinitionsSource } from "./config.js";
|
|
13
|
-
import { fail } from "./fail.js";
|
|
14
|
-
/**
|
|
15
|
-
* Turn a configured definitions source into a specifier `import()` can
|
|
16
|
-
* load. Path-like values (relative or absolute) resolve against `cwd` and
|
|
17
|
-
* become file URLs so they're imported as files, not bare package names;
|
|
18
|
-
* anything else passes through as a module specifier (e.g. a workspace
|
|
19
|
-
* package such as `@sanity/workflow-examples`).
|
|
20
|
-
*/
|
|
21
|
-
export function resolveDefinitionsSpecifier(source, cwd) {
|
|
22
|
-
if (source.startsWith('.') || isAbsolute(source)) {
|
|
23
|
-
return pathToFileURL(resolve(cwd, source)).href;
|
|
10
|
+
if (flags.tag) {
|
|
11
|
+
params.tag = flags.tag;
|
|
12
|
+
filters.push(tagScopeFilter());
|
|
24
13
|
}
|
|
25
|
-
|
|
14
|
+
const groq = `*[${filters.join(' && ')}] | order(version desc) [0]`;
|
|
15
|
+
return { groq, params };
|
|
26
16
|
}
|
|
27
17
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
18
|
+
* Fetch the deployed definition a command targets — latest unless pinned via
|
|
19
|
+
* `version`. An absent pinned version always fails (the caller asked for
|
|
20
|
+
* something specific); an absent latest returns `undefined` so the caller
|
|
21
|
+
* decides whether that's fatal.
|
|
32
22
|
*/
|
|
33
|
-
export function
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
23
|
+
export async function fetchDeployedDefinition({ client, name, tag, version, }) {
|
|
24
|
+
const { groq, params } = buildDefinitionShowQuery({ name, tag, version });
|
|
25
|
+
const deployed = await client.fetch(groq, params);
|
|
26
|
+
if (deployed === null && version !== undefined) {
|
|
27
|
+
fail(`No deployed definition "${name}" v${version}.`);
|
|
37
28
|
}
|
|
38
|
-
return
|
|
29
|
+
return deployed ?? undefined;
|
|
39
30
|
}
|
|
40
31
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* crash with a raw stack trace.
|
|
32
|
+
* Narrow a batch to one definition name (`only`), failing with the available
|
|
33
|
+
* names when there's no match. With `only` unset the whole batch passes through
|
|
34
|
+
* (a fresh array, so callers can mutate without aliasing). `context` prefixes
|
|
35
|
+
* the failure so callers whose run spans several deployments can attribute it.
|
|
46
36
|
*/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
let mod;
|
|
50
|
-
try {
|
|
51
|
-
mod = await import(__rewriteRelativeImportExtension(specifier, true));
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
55
|
-
fail(`Failed to load definitions from "${source}":`, message);
|
|
56
|
-
}
|
|
57
|
-
return extractDefinitions(mod, source);
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Narrow the local batch to one definition `name`, failing with the
|
|
61
|
-
* available names when there's no match. With `only` unset the whole batch passes
|
|
62
|
-
* through (a fresh array, so callers can mutate without aliasing).
|
|
63
|
-
*/
|
|
64
|
-
export function selectDefinitions(allDefinitions, only) {
|
|
65
|
-
if (!only)
|
|
37
|
+
export function selectDefinitions(allDefinitions, { only, context = '' }) {
|
|
38
|
+
if (!only) {
|
|
66
39
|
return [...allDefinitions];
|
|
40
|
+
}
|
|
67
41
|
const filtered = allDefinitions.filter((d) => d.name === only);
|
|
68
42
|
if (filtered.length === 0) {
|
|
69
43
|
const available = allDefinitions.map((d) => d.name).join(', ');
|
|
70
|
-
fail(
|
|
44
|
+
fail(`${context}No definition named "${only}".`, `Available: ${available}`);
|
|
71
45
|
}
|
|
72
46
|
return filtered;
|
|
73
47
|
}
|
|
74
48
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*/
|
|
79
|
-
export async function loadValidatedDefinitions(only) {
|
|
80
|
-
const allDefinitions = await importDefinitions(loadDefinitionsSource());
|
|
81
|
-
const selected = selectDefinitions(allDefinitions, only);
|
|
82
|
-
const errors = collectValidationErrors(selected);
|
|
83
|
-
if (errors.length > 0) {
|
|
84
|
-
fail('Validation failed:', errors.join('\n'));
|
|
85
|
-
}
|
|
86
|
-
return selected;
|
|
87
|
-
}
|
|
88
|
-
/** {@link loadValidatedDefinitions} narrowed to exactly one definition name. */
|
|
89
|
-
export async function loadValidatedDefinition(name) {
|
|
90
|
-
const [def] = await loadValidatedDefinitions(name);
|
|
91
|
-
// selectDefinitions already failed on no match; this narrows the
|
|
92
|
-
// indexed access for the type checker.
|
|
93
|
-
if (!def)
|
|
94
|
-
fail(`No definition named "${name}".`);
|
|
95
|
-
return def;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Per-definition structural/GROQ checks plus one batch-level check the
|
|
99
|
-
* engine doesn't do for us: duplicate `name` within the local set.
|
|
100
|
-
* Returns the human-readable error lines; empty means the batch is valid.
|
|
49
|
+
* Per-definition structural/GROQ checks plus one batch-level check the engine
|
|
50
|
+
* doesn't do for us: duplicate `name` within the set. Returns the
|
|
51
|
+
* human-readable error lines; empty means the batch is valid.
|
|
101
52
|
*/
|
|
102
53
|
export function collectValidationErrors(defs) {
|
|
103
54
|
const errors = [];
|
|
@@ -114,9 +65,18 @@ export function collectValidationErrors(defs) {
|
|
|
114
65
|
validateDefinition(d);
|
|
115
66
|
}
|
|
116
67
|
catch (err) {
|
|
117
|
-
|
|
118
|
-
errors.push(` · ${d.name}: ${message}`);
|
|
68
|
+
errors.push(` · ${d.name}: ${errorMessage(err)}`);
|
|
119
69
|
}
|
|
120
70
|
}
|
|
121
71
|
return errors;
|
|
122
72
|
}
|
|
73
|
+
/** Validate a set of definitions, exiting with the collected errors on any
|
|
74
|
+
* failure. The single validate-then-fail gate `deploy` and `definition diff`
|
|
75
|
+
* share, so the two can't drift into divergent failure messages. `context`
|
|
76
|
+
* prefixes the failure with the deployment it belongs to in multi-target runs. */
|
|
77
|
+
export function validateOrFail(defs, context = '') {
|
|
78
|
+
const errors = collectValidationErrors(defs);
|
|
79
|
+
if (errors.length > 0) {
|
|
80
|
+
fail(`${context}Validation failed:`, errors.join('\n'));
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/lib/diff.js
CHANGED
|
@@ -33,6 +33,13 @@ export function diffLines(oldObj, newObj) {
|
|
|
33
33
|
}
|
|
34
34
|
return out;
|
|
35
35
|
}
|
|
36
|
+
/** Footnote for the detail diff: `_id`, `version`, and `contentHash` are
|
|
37
|
+
* stamped by deploy as a function of the content — never authored. Without
|
|
38
|
+
* this an operator can read their `+`/`-` lines as fields they edited (e.g. a
|
|
39
|
+
* `version` they never set), when they only reflect the content change. */
|
|
40
|
+
function derivedFieldsNote() {
|
|
41
|
+
return `${logSymbols.info} ${styleText('dim', '_id, version and contentHash are deploy-generated — they change automatically with the content, not fields you edit.')}`;
|
|
42
|
+
}
|
|
36
43
|
/** The full diff report: the summary block plus a per-change diff. */
|
|
37
44
|
export function diffReport(entries) {
|
|
38
45
|
const lines = ['', sectionHeader('Summary'), ...summaryLines(entries)];
|
|
@@ -46,5 +53,6 @@ export function diffReport(entries) {
|
|
|
46
53
|
// doc as added lines so the operator sees what would land.
|
|
47
54
|
lines.push('', styleText('bold', `── ${e.name} v${e.version} ──`), ...diffLines(e.existing ?? {}, e.expected));
|
|
48
55
|
}
|
|
56
|
+
lines.push('', derivedFieldsNote());
|
|
49
57
|
return lines;
|
|
50
58
|
}
|
package/dist/lib/env.d.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
export declare const ENV: {
|
|
2
|
-
readonly SANITY_PROJECT_ID: "SANITY_PROJECT_ID";
|
|
3
2
|
readonly SANITY_AUTH_TOKEN: "SANITY_AUTH_TOKEN";
|
|
4
|
-
readonly SANITY_DATASET: "SANITY_DATASET";
|
|
5
3
|
readonly SANITY_API_HOST: "SANITY_API_HOST";
|
|
6
|
-
readonly WORKFLOW_RESOURCE_TYPE: "WORKFLOW_RESOURCE_TYPE";
|
|
7
|
-
readonly WORKFLOW_RESOURCE_ID: "WORKFLOW_RESOURCE_ID";
|
|
8
|
-
readonly WORKFLOW_TAG: "WORKFLOW_TAG";
|
|
9
|
-
readonly WORKFLOW_DEFS: "WORKFLOW_DEFS";
|
|
10
4
|
};
|
package/dist/lib/env.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
// Typed env var definitions
|
|
1
|
+
// Typed env var definitions. Client config (projectId/dataset) is derived from
|
|
2
|
+
// the deployment's resource, and the tag comes from `--tag`/the config — so the
|
|
3
|
+
// only env vars left are auth + host overrides.
|
|
2
4
|
export const ENV = {
|
|
3
|
-
// Sanity client
|
|
4
|
-
SANITY_PROJECT_ID: 'SANITY_PROJECT_ID',
|
|
5
5
|
SANITY_AUTH_TOKEN: 'SANITY_AUTH_TOKEN',
|
|
6
|
-
SANITY_DATASET: 'SANITY_DATASET',
|
|
7
6
|
SANITY_API_HOST: 'SANITY_API_HOST',
|
|
8
|
-
// Workflow config
|
|
9
|
-
WORKFLOW_RESOURCE_TYPE: 'WORKFLOW_RESOURCE_TYPE',
|
|
10
|
-
WORKFLOW_RESOURCE_ID: 'WORKFLOW_RESOURCE_ID',
|
|
11
|
-
WORKFLOW_TAG: 'WORKFLOW_TAG',
|
|
12
|
-
WORKFLOW_DEFS: 'WORKFLOW_DEFS',
|
|
13
7
|
};
|
package/dist/lib/fail.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The human-readable message of a caught `unknown`. Strips control characters
|
|
3
|
+
* (keeping newlines and tabs) so server-derived error text can't smuggle
|
|
4
|
+
* terminal escape sequences into CLI output.
|
|
5
|
+
*/
|
|
6
|
+
export declare function errorMessage(error: unknown): string;
|
|
1
7
|
/**
|
|
2
8
|
* Render a user-facing error and exit non-zero. Writes straight to
|
|
3
9
|
* stderr — no oclif framing, no Node stack — so expected failures
|
package/dist/lib/fail.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
2
|
import logSymbols from 'log-symbols';
|
|
3
|
+
/**
|
|
4
|
+
* The human-readable message of a caught `unknown`. Strips control characters
|
|
5
|
+
* (keeping newlines and tabs) so server-derived error text can't smuggle
|
|
6
|
+
* terminal escape sequences into CLI output.
|
|
7
|
+
*/
|
|
8
|
+
export function errorMessage(error) {
|
|
9
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10
|
+
// [^\P{Cc}\n\t] = every control character except \n and \t.
|
|
11
|
+
return message.replace(/[^\P{Cc}\n\t]/gu, '');
|
|
12
|
+
}
|
|
3
13
|
/**
|
|
4
14
|
* Render a user-facing error and exit non-zero. Writes straight to
|
|
5
15
|
* stderr — no oclif framing, no Node stack — so expected failures
|
|
@@ -28,6 +38,6 @@ export function failOnThrow(headline, fn) {
|
|
|
28
38
|
return fn();
|
|
29
39
|
}
|
|
30
40
|
catch (err) {
|
|
31
|
-
fail(headline,
|
|
41
|
+
fail(headline, errorMessage(err));
|
|
32
42
|
}
|
|
33
43
|
}
|
package/dist/lib/flags.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
1
|
+
/** The environment tag (e.g. prod, test). For write commands it selects which
|
|
2
|
+
* deployment in the discovered `sanity.workflow` config to act on (omittable
|
|
3
|
+
* when only one is configured); for read commands it's an optional query
|
|
4
|
+
* filter — and the resource disambiguator when a config spans more than one. */
|
|
3
5
|
export declare const tagFlags: {
|
|
4
6
|
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
5
7
|
};
|
|
8
|
+
/** Actor attribution for write verbs — name a real user (plus roles) instead
|
|
9
|
+
* of the pinned workflow-cli system actor, so assignee- and role-gated
|
|
10
|
+
* filters pass. Commands resolve the pair with `resolveActor`. */
|
|
11
|
+
export declare const actorFlags: {
|
|
12
|
+
as: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'as-role': import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
};
|
|
15
|
+
export declare const jsonFlags: {
|
|
16
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
package/dist/lib/flags.js
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
2
|
+
/** The environment tag (e.g. prod, test). For write commands it selects which
|
|
3
|
+
* deployment in the discovered `sanity.workflow` config to act on (omittable
|
|
4
|
+
* when only one is configured); for read commands it's an optional query
|
|
5
|
+
* filter — and the resource disambiguator when a config spans more than one. */
|
|
4
6
|
export const tagFlags = {
|
|
5
7
|
tag: Flags.string({
|
|
6
|
-
description: 'Workflow environment tag (e.g. prod, test)
|
|
8
|
+
description: 'Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.',
|
|
9
|
+
}),
|
|
10
|
+
};
|
|
11
|
+
/** Actor attribution for write verbs — name a real user (plus roles) instead
|
|
12
|
+
* of the pinned workflow-cli system actor, so assignee- and role-gated
|
|
13
|
+
* filters pass. Commands resolve the pair with `resolveActor`. */
|
|
14
|
+
export const actorFlags = {
|
|
15
|
+
as: Flags.string({
|
|
16
|
+
description: 'Attribute the write to this Sanity user id (default: a workflow-cli system actor).',
|
|
17
|
+
}),
|
|
18
|
+
'as-role': Flags.string({
|
|
19
|
+
description: 'Role to attribute to the --as user (repeatable), so role-gated filters pass.',
|
|
20
|
+
multiple: true,
|
|
21
|
+
default: [],
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
export const jsonFlags = {
|
|
25
|
+
json: Flags.boolean({
|
|
26
|
+
description: 'Emit structured JSON instead of rendered output.',
|
|
27
|
+
default: false,
|
|
7
28
|
}),
|
|
8
29
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WorkflowConfig } from '@sanity/workflow-engine';
|
|
2
|
+
/**
|
|
3
|
+
* Discover the `sanity.workflow.{ts,js,mjs}` in `cwd`, import its default
|
|
4
|
+
* export, and validate it through {@link defineWorkflowConfig}.
|
|
5
|
+
*
|
|
6
|
+
* Re-validating here (rather than trusting the export) means a config authored
|
|
7
|
+
* as a raw object — `export default {deployments: [...]}` without the helper —
|
|
8
|
+
* is still checked, and a malformed one fails with the engine's path-prefixed
|
|
9
|
+
* message at the CLI boundary instead of mid-deploy.
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadWorkflowConfig(cwd?: string): Promise<WorkflowConfig>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { basename, dirname, join } from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
import { defineWorkflowConfig } from '@sanity/workflow-engine/define';
|
|
13
|
+
import { createJiti } from 'jiti';
|
|
14
|
+
import { fail } from "./fail.js";
|
|
15
|
+
const CONFIG_FILE_NAMES = [
|
|
16
|
+
'sanity.workflow.ts',
|
|
17
|
+
'sanity.workflow.js',
|
|
18
|
+
'sanity.workflow.mjs',
|
|
19
|
+
];
|
|
20
|
+
/** First `sanity.workflow.{ts,js,mjs}` in `cwd`, or undefined. */
|
|
21
|
+
function findConfigFile(cwd) {
|
|
22
|
+
for (const name of CONFIG_FILE_NAMES) {
|
|
23
|
+
const candidate = join(cwd, name);
|
|
24
|
+
if (existsSync(candidate)) {
|
|
25
|
+
return candidate;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Import the config module's default export. `.ts` goes through jiti (no build
|
|
32
|
+
* step); `.js`/`.mjs` use native dynamic import. A throw inside the module — a
|
|
33
|
+
* syntax error, a `defineWorkflowConfig` that rejected — surfaces as a clean
|
|
34
|
+
* CLI error rather than an oclif stack trace.
|
|
35
|
+
*/
|
|
36
|
+
async function importDefault(filePath) {
|
|
37
|
+
const url = pathToFileURL(filePath).href;
|
|
38
|
+
try {
|
|
39
|
+
if (filePath.endsWith('.ts')) {
|
|
40
|
+
return await createJiti(dirname(filePath)).import(url, { default: true });
|
|
41
|
+
}
|
|
42
|
+
return (await import(__rewriteRelativeImportExtension(url, true))).default;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
fail(`Failed to load ${basename(filePath)}:`, err instanceof Error ? err.message : String(err));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Discover the `sanity.workflow.{ts,js,mjs}` in `cwd`, import its default
|
|
50
|
+
* export, and validate it through {@link defineWorkflowConfig}.
|
|
51
|
+
*
|
|
52
|
+
* Re-validating here (rather than trusting the export) means a config authored
|
|
53
|
+
* as a raw object — `export default {deployments: [...]}` without the helper —
|
|
54
|
+
* is still checked, and a malformed one fails with the engine's path-prefixed
|
|
55
|
+
* message at the CLI boundary instead of mid-deploy.
|
|
56
|
+
*/
|
|
57
|
+
export async function loadWorkflowConfig(cwd = process.cwd()) {
|
|
58
|
+
const filePath = findConfigFile(cwd);
|
|
59
|
+
if (filePath === undefined) {
|
|
60
|
+
fail(`No ${CONFIG_FILE_NAMES[0]} found in ${cwd}.`, 'Create one that `export default defineWorkflowConfig({deployments: [...]})`.');
|
|
61
|
+
}
|
|
62
|
+
const exported = await importDefault(filePath);
|
|
63
|
+
try {
|
|
64
|
+
return defineWorkflowConfig(exported);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
fail(`Invalid config in ${basename(filePath)}:`, err instanceof Error ? err.message : String(err));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
import type { Actor, OperationArgs } from '@sanity/workflow-engine';
|
|
2
|
-
|
|
1
|
+
import type { Actor, EngineScopeArgs, OperationArgs } from '@sanity/workflow-engine';
|
|
2
|
+
/**
|
|
3
|
+
* The engine-scope a CLI write needs: which tag partition and which resource
|
|
4
|
+
* the engine's own data lives in — {@link EngineScopeArgs} minus the client.
|
|
5
|
+
* A {@link import('@sanity/workflow-engine').WorkflowDeployment} satisfies it,
|
|
6
|
+
* so commands pass their resolved deployment straight through.
|
|
7
|
+
*/
|
|
8
|
+
export type EngineScope = Pick<EngineScopeArgs, 'tag' | 'workflowResource'>;
|
|
9
|
+
/**
|
|
10
|
+
* The synthetic actor a CLI write is attributed to when the operator
|
|
11
|
+
* doesn't name a real user. The engine resolves a real actor from
|
|
12
|
+
* `/users/me`, which a token-only CLI can't reach; until the CLI hooks
|
|
13
|
+
* into Sanity auth proper, writes act as this system identity.
|
|
14
|
+
*/
|
|
15
|
+
export declare const SYSTEM_ACTOR: Actor;
|
|
3
16
|
/**
|
|
4
17
|
* The write-verb arguments every engine operation shares: tag scope,
|
|
5
|
-
* workflow resource, and the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
18
|
+
* workflow resource, and the actor the write is attributed to — commands
|
|
19
|
+
* with `--as` resolve one via {@link resolveActor}; the rest pass
|
|
20
|
+
* {@link SYSTEM_ACTOR} explicitly. Spread by both the instance verbs
|
|
21
|
+
* ({@link buildOperationArgs}) and the definition verbs (e.g. `definition delete`).
|
|
8
22
|
*/
|
|
9
|
-
export declare function baseEngineArgs(
|
|
23
|
+
export declare function baseEngineArgs(scope: EngineScope, actor: Actor): EngineScope & Pick<OperationArgs, 'access'>;
|
|
10
24
|
/**
|
|
11
25
|
* Resolve the actor a write is attributed to. With a named user the fire
|
|
12
26
|
* is pinned to it (plus any roles, so role-gated action filters pass);
|
|
@@ -20,6 +34,10 @@ export declare function resolveActor(as: string | undefined, roles: string[]): A
|
|
|
20
34
|
* conditionally because the engine's optional field rejects an explicit
|
|
21
35
|
* `undefined` under `exactOptionalPropertyTypes`.
|
|
22
36
|
*/
|
|
23
|
-
export declare function buildOperationArgs(
|
|
37
|
+
export declare function buildOperationArgs({ scope, instanceId, reason, }: {
|
|
38
|
+
scope: EngineScope;
|
|
39
|
+
instanceId: string;
|
|
40
|
+
reason: string | undefined;
|
|
41
|
+
}): OperationArgs & EngineScope & {
|
|
24
42
|
reason?: string;
|
|
25
43
|
};
|
|
@@ -4,18 +4,19 @@
|
|
|
4
4
|
* `/users/me`, which a token-only CLI can't reach; until the CLI hooks
|
|
5
5
|
* into Sanity auth proper, writes act as this system identity.
|
|
6
6
|
*/
|
|
7
|
-
const SYSTEM_ACTOR = { kind: 'system', id: 'workflow-cli' };
|
|
7
|
+
export const SYSTEM_ACTOR = { kind: 'system', id: 'workflow-cli' };
|
|
8
8
|
/**
|
|
9
9
|
* The write-verb arguments every engine operation shares: tag scope,
|
|
10
|
-
* workflow resource, and the
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* workflow resource, and the actor the write is attributed to — commands
|
|
11
|
+
* with `--as` resolve one via {@link resolveActor}; the rest pass
|
|
12
|
+
* {@link SYSTEM_ACTOR} explicitly. Spread by both the instance verbs
|
|
13
|
+
* ({@link buildOperationArgs}) and the definition verbs (e.g. `definition delete`).
|
|
13
14
|
*/
|
|
14
|
-
export function baseEngineArgs(
|
|
15
|
+
export function baseEngineArgs(scope, actor) {
|
|
15
16
|
return {
|
|
16
|
-
tag:
|
|
17
|
-
workflowResource:
|
|
18
|
-
access: { actor
|
|
17
|
+
tag: scope.tag,
|
|
18
|
+
workflowResource: scope.workflowResource,
|
|
19
|
+
access: { actor },
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
@@ -31,7 +32,7 @@ export function resolveActor(as, roles) {
|
|
|
31
32
|
}
|
|
32
33
|
return SYSTEM_ACTOR;
|
|
33
34
|
}
|
|
34
|
-
return { kind: '
|
|
35
|
+
return { kind: 'person', id: as, ...(roles.length > 0 ? { roles } : {}) };
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
37
38
|
* {@link baseEngineArgs} plus the instance-operation fields (everything
|
|
@@ -39,9 +40,9 @@ export function resolveActor(as, roles) {
|
|
|
39
40
|
* conditionally because the engine's optional field rejects an explicit
|
|
40
41
|
* `undefined` under `exactOptionalPropertyTypes`.
|
|
41
42
|
*/
|
|
42
|
-
export function buildOperationArgs(
|
|
43
|
+
export function buildOperationArgs({ scope, instanceId, reason, }) {
|
|
43
44
|
return {
|
|
44
|
-
...baseEngineArgs(
|
|
45
|
+
...baseEngineArgs(scope, SYSTEM_ACTOR),
|
|
45
46
|
instanceId,
|
|
46
47
|
...(reason !== undefined ? { reason } : {}),
|
|
47
48
|
};
|
package/dist/lib/ops-report.d.ts
CHANGED
|
@@ -9,22 +9,34 @@ export interface RanOp {
|
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* The "ops applied" block shared by write-verb reports (
|
|
12
|
+
* The "ops applied" block shared by write-verb reports (set-stage,
|
|
13
13
|
* fire-action): a blank separator, a header, and one line per primitive
|
|
14
14
|
* that ran. Empty when nothing ran, so callers append it unconditionally.
|
|
15
15
|
*/
|
|
16
16
|
export declare function opsAppliedLines(ranOps: RanOp[] | undefined): string[];
|
|
17
|
+
/** The subset of an engine `OperationResult` the write-verb reports read. */
|
|
18
|
+
export interface WriteOutcome {
|
|
19
|
+
changed: boolean;
|
|
20
|
+
cascaded: number;
|
|
21
|
+
instance: {
|
|
22
|
+
currentStage: string;
|
|
23
|
+
};
|
|
24
|
+
ranOps?: RanOp[];
|
|
25
|
+
}
|
|
17
26
|
/** The spinner message + ops block a write verb prints once its engine
|
|
18
27
|
* call returns. {@link opsAppliedLines} builds `opsLines`. */
|
|
19
28
|
export interface WriteReport {
|
|
20
|
-
|
|
29
|
+
changed: boolean;
|
|
21
30
|
message: string;
|
|
22
31
|
opsLines: string[];
|
|
23
32
|
}
|
|
24
33
|
/**
|
|
25
|
-
* Emit a write-verb report through its spinner: warn and bail
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* consistent.
|
|
34
|
+
* Emit a write-verb report through its spinner: warn and bail on a no-op
|
|
35
|
+
* (`changed: false`), otherwise succeed and print the ops block. Shared by
|
|
36
|
+
* every write command so the changed / no-op branches stay consistent.
|
|
29
37
|
*/
|
|
30
|
-
export declare function emitWriteReport(spinner
|
|
38
|
+
export declare function emitWriteReport({ spinner, report, log, }: {
|
|
39
|
+
spinner: Ora;
|
|
40
|
+
report: WriteReport;
|
|
41
|
+
log: (line: string) => void;
|
|
42
|
+
}): void;
|
package/dist/lib/ops-report.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
2
|
import logSymbols from 'log-symbols';
|
|
3
3
|
/**
|
|
4
|
-
* The "ops applied" block shared by write-verb reports (
|
|
4
|
+
* The "ops applied" block shared by write-verb reports (set-stage,
|
|
5
5
|
* fire-action): a blank separator, a header, and one line per primitive
|
|
6
6
|
* that ran. Empty when nothing ran, so callers append it unconditionally.
|
|
7
7
|
*/
|
|
@@ -13,13 +13,12 @@ export function opsAppliedLines(ranOps) {
|
|
|
13
13
|
return details.length > 0 ? ['', styleText('dim', 'ops applied:'), ...details] : [];
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Emit a write-verb report through its spinner: warn and bail
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* consistent.
|
|
16
|
+
* Emit a write-verb report through its spinner: warn and bail on a no-op
|
|
17
|
+
* (`changed: false`), otherwise succeed and print the ops block. Shared by
|
|
18
|
+
* every write command so the changed / no-op branches stay consistent.
|
|
20
19
|
*/
|
|
21
|
-
export function emitWriteReport(spinner, report, log) {
|
|
22
|
-
if (!report.
|
|
20
|
+
export function emitWriteReport({ spinner, report, log, }) {
|
|
21
|
+
if (!report.changed) {
|
|
23
22
|
spinner.warn(report.message);
|
|
24
23
|
return;
|
|
25
24
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse repeated `key=value` flag pairs (`--param`, `--field`) into an
|
|
3
|
+
* object. Each value is JSON-parsed so numbers/booleans/objects type
|
|
4
|
+
* correctly, falling back to the raw string. The engine validates the
|
|
5
|
+
* result against the declared types the consuming flag targets.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseParams(pairs: string[]): Record<string, unknown>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function coerceValue(raw) {
|
|
2
|
+
try {
|
|
3
|
+
return JSON.parse(raw);
|
|
4
|
+
}
|
|
5
|
+
catch {
|
|
6
|
+
// Not valid JSON — treat it as a plain string so `--param note=hi`
|
|
7
|
+
// needs no quoting. The engine validates the typed value either way.
|
|
8
|
+
return raw;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function parsePair(pair) {
|
|
12
|
+
const eq = pair.indexOf('=');
|
|
13
|
+
if (eq < 1) {
|
|
14
|
+
throw new Error(`expected key=value, got "${pair}"`);
|
|
15
|
+
}
|
|
16
|
+
return [pair.slice(0, eq), coerceValue(pair.slice(eq + 1))];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse repeated `key=value` flag pairs (`--param`, `--field`) into an
|
|
20
|
+
* object. Each value is JSON-parsed so numbers/booleans/objects type
|
|
21
|
+
* correctly, falling back to the raw string. The engine validates the
|
|
22
|
+
* result against the declared types the consuming flag targets.
|
|
23
|
+
*/
|
|
24
|
+
export function parseParams(pairs) {
|
|
25
|
+
return Object.fromEntries(pairs.map(parsePair));
|
|
26
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type DeployTarget, type WorkflowConfig, type WorkflowDeployment } from '@sanity/workflow-engine';
|
|
2
|
+
/**
|
|
3
|
+
* Pick the deployment for the requested `--tag`. Tags are unique across a
|
|
4
|
+
* config (enforced by defineWorkflowConfig), so a tag names at most one
|
|
5
|
+
* deployment.
|
|
6
|
+
*
|
|
7
|
+
* With no tag: fall back to the sole deployment when there's exactly one
|
|
8
|
+
* (the common single-environment case), otherwise fail asking for `--tag` —
|
|
9
|
+
* a multi-deployment config is ambiguous without it. `orAlternative` extends
|
|
10
|
+
* that ambiguity message for callers with another way out (deploy's `--all-tags`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function selectDeployment(config: WorkflowConfig, { tag, orAlternative }: {
|
|
13
|
+
tag: string | undefined;
|
|
14
|
+
orAlternative?: string;
|
|
15
|
+
}): WorkflowDeployment;
|
|
16
|
+
/**
|
|
17
|
+
* The deployments a `deploy` run acts on: every one with `--all-tags`,
|
|
18
|
+
* otherwise a single target via {@link selectDeployment} and its failure
|
|
19
|
+
* modes. Deploying everything is an explicit opt-in — never a default — so an
|
|
20
|
+
* ambiguous bare `deploy` can't fan a write out across environments.
|
|
21
|
+
*/
|
|
22
|
+
export declare function selectDeployments(config: WorkflowConfig, { tag, allTags }: {
|
|
23
|
+
tag: string | undefined;
|
|
24
|
+
allTags: boolean;
|
|
25
|
+
}): WorkflowDeployment[];
|
|
26
|
+
/**
|
|
27
|
+
* Project a deployment into the engine's {@link DeployTarget} — the shape the
|
|
28
|
+
* deploy/diff verbs (`deployDefinitions`, `computeDiffEntries`, `diffEntry`)
|
|
29
|
+
* consume — expanding its handle bindings into the alias map in the same step.
|
|
30
|
+
*/
|
|
31
|
+
export declare function deploymentToTarget(deployment: WorkflowDeployment): DeployTarget;
|