@sanity/workflow-cli 0.13.0 → 0.20.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 +37 -0
- package/README.md +40 -27
- package/dist/commands/editorial-workflows/abort.d.ts +1 -0
- package/dist/commands/editorial-workflows/abort.js +2 -2
- package/dist/commands/editorial-workflows/definition/delete.d.ts +1 -0
- package/dist/commands/editorial-workflows/definition/delete.js +2 -2
- package/dist/commands/editorial-workflows/definition/diff.d.ts +1 -0
- package/dist/commands/editorial-workflows/definition/diff.js +2 -2
- package/dist/commands/editorial-workflows/deploy.d.ts +2 -1
- package/dist/commands/editorial-workflows/deploy.js +15 -9
- package/dist/commands/editorial-workflows/diagnose.d.ts +1 -0
- package/dist/commands/editorial-workflows/diagnose.js +2 -2
- package/dist/commands/editorial-workflows/fire-action.d.ts +1 -0
- package/dist/commands/editorial-workflows/fire-action.js +2 -2
- package/dist/commands/editorial-workflows/nuke.d.ts +2 -1
- package/dist/commands/editorial-workflows/nuke.js +13 -9
- package/dist/commands/editorial-workflows/set-stage.d.ts +1 -0
- package/dist/commands/editorial-workflows/set-stage.js +2 -2
- package/dist/commands/editorial-workflows/start.d.ts +1 -0
- package/dist/commands/editorial-workflows/start.js +2 -2
- package/dist/lib/context.d.ts +36 -13
- package/dist/lib/context.js +27 -6
- package/dist/lib/fail.js +1 -1
- package/dist/lib/flags.d.ts +27 -5
- package/dist/lib/flags.js +17 -1
- package/dist/lib/nuke.d.ts +15 -14
- package/dist/lib/nuke.js +27 -8
- package/dist/lib/prompt.d.ts +14 -0
- package/dist/lib/prompt.js +4 -0
- package/dist/lib/select-deployment.d.ts +41 -16
- package/dist/lib/select-deployment.js +55 -38
- package/oclif.manifest.json +131 -19
- package/package.json +8 -5
package/dist/lib/context.js
CHANGED
|
@@ -2,11 +2,12 @@ import { assertReadableModel, resourceGdr, } from '@sanity/workflow-engine';
|
|
|
2
2
|
import { clientFor, resolveTokenOrFail } from "./client.js";
|
|
3
3
|
import { fail } from "./fail.js";
|
|
4
4
|
import { loadWorkflowConfig } from "./load-config.js";
|
|
5
|
-
import {
|
|
5
|
+
import { canPromptOnStderr } from "./prompt.js";
|
|
6
|
+
import { availableDeployments, deploymentsForTag, selectDeployment, } from "./select-deployment.js";
|
|
6
7
|
import { resourceLabel } from "./ui.js";
|
|
7
8
|
export async function resolveContext(flags) {
|
|
8
9
|
const config = await loadWorkflowConfig();
|
|
9
|
-
const deployment = selectDeployment(config, { tag: flags.tag });
|
|
10
|
+
const deployment = await selectDeployment(config, { name: flags.deployment, tag: flags.tag });
|
|
10
11
|
return { deployment, client: clientFor(deployment.workflowResource, await resolveTokenOrFail()) };
|
|
11
12
|
}
|
|
12
13
|
export async function resolveReadTargets(flags) {
|
|
@@ -20,7 +21,7 @@ export function dedupeResources(resources) {
|
|
|
20
21
|
}
|
|
21
22
|
export function resolveReadResources(config, tag) {
|
|
22
23
|
if (tag !== undefined) {
|
|
23
|
-
return
|
|
24
|
+
return dedupeResources(deploymentsForTag(config, tag).map((deployment) => deployment.workflowResource));
|
|
24
25
|
}
|
|
25
26
|
const resources = dedupeResources(config.deployments.map((d) => d.workflowResource));
|
|
26
27
|
if (resources.length === 0) {
|
|
@@ -34,16 +35,36 @@ export function resolveReadResource(config, tag) {
|
|
|
34
35
|
if (resources.length === 1 && sole !== undefined) {
|
|
35
36
|
return sole;
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
if (tag === undefined) {
|
|
39
|
+
fail('Config spans multiple resources — pass --deployment or --tag to choose one.', availableDeployments(config));
|
|
40
|
+
}
|
|
41
|
+
const carriers = config.deployments.filter((d) => d.tag === tag);
|
|
42
|
+
fail(`Tag "${tag}" spans multiple resources — pass --deployment to choose one.`, availableDeployments(config, carriers));
|
|
39
43
|
}
|
|
40
44
|
export async function resolveInstanceContext(flags, instanceId) {
|
|
41
45
|
const config = await loadWorkflowConfig();
|
|
42
|
-
const workflowResource =
|
|
46
|
+
const workflowResource = await resolveInstanceResource(config, flags);
|
|
43
47
|
const client = clientFor(workflowResource, await resolveTokenOrFail());
|
|
44
48
|
const instance = await loadInstanceOrFail(client, instanceId);
|
|
45
49
|
return { client, scope: { tag: instance.tag, workflowResource } };
|
|
46
50
|
}
|
|
51
|
+
export async function resolveInstanceResource(config, { deployment, tag, interactive, chooseDeployment, }) {
|
|
52
|
+
if (deployment !== undefined) {
|
|
53
|
+
return (await selectDeployment(config, { name: deployment, tag: undefined })).workflowResource;
|
|
54
|
+
}
|
|
55
|
+
if (tag === undefined &&
|
|
56
|
+
(interactive ?? canPromptOnStderr()) &&
|
|
57
|
+
resolveReadResources(config, undefined).length > 1) {
|
|
58
|
+
const chosen = await selectDeployment(config, {
|
|
59
|
+
name: undefined,
|
|
60
|
+
tag: undefined,
|
|
61
|
+
interactive: true,
|
|
62
|
+
chooseName: chooseDeployment,
|
|
63
|
+
});
|
|
64
|
+
return chosen.workflowResource;
|
|
65
|
+
}
|
|
66
|
+
return resolveReadResource(config, tag);
|
|
67
|
+
}
|
|
47
68
|
export async function loadInstanceOrFail(client, instanceId) {
|
|
48
69
|
const instance = await client.getDocument(instanceId, {
|
|
49
70
|
tag: 'instance.load',
|
package/dist/lib/fail.js
CHANGED
|
@@ -8,7 +8,7 @@ export function fail(headline, detail) {
|
|
|
8
8
|
process.stderr.write(`${styleText('red', `${logSymbols.error} ${headline}`)}\n`);
|
|
9
9
|
if (detail !== undefined && detail !== '') {
|
|
10
10
|
for (const line of detail.split('\n')) {
|
|
11
|
-
process.stderr.write(` ${styleText(
|
|
11
|
+
process.stderr.write(` ${styleText('red', line)}\n`);
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
return Errors.exit(1);
|
package/dist/lib/flags.d.ts
CHANGED
|
@@ -1,11 +1,33 @@
|
|
|
1
|
-
/** The environment tag (e.g. prod, test).
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
1
|
+
/** The environment tag (e.g. prod, test). Tags group deployments by
|
|
2
|
+
* environment and may repeat across a config. For the read commands that
|
|
3
|
+
* consume this directly it's an optional query filter — and the resource
|
|
4
|
+
* disambiguator when a config spans more than one. Write commands layer
|
|
5
|
+
* selection semantics on top via {@link deploymentFlags}; instance-keyed
|
|
6
|
+
* commands add `--deployment` alongside it via {@link instanceFlags}. */
|
|
6
7
|
export declare const tagFlags: {
|
|
7
8
|
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
};
|
|
10
|
+
/** The selectors an instance-keyed command (`diagnose`, `abort`, `set-stage`,
|
|
11
|
+
* `fire-action`) takes. An instance id is globally unique and carries its own
|
|
12
|
+
* `tag`, so these only pick WHICH resource to read it from — never the
|
|
13
|
+
* instance's partition, which always comes from the loaded instance.
|
|
14
|
+
* `--deployment` names one deployment and reads from the resource it targets;
|
|
15
|
+
* `--tag` stays the optional narrower {@link tagFlags} describes. Mutually
|
|
16
|
+
* exclusive; a sole-resource config needs neither. */
|
|
17
|
+
export declare const instanceFlags: {
|
|
18
|
+
deployment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
};
|
|
21
|
+
/** The selectors a single-deployment (write) command takes: `--deployment` —
|
|
22
|
+
* the deployment's unique name identity — or `--tag`, which resolves while the
|
|
23
|
+
* tag names exactly one deployment and errors asking for `--deployment` when
|
|
24
|
+
* it spans several. Mutually exclusive; a sole-deployment config needs neither.
|
|
25
|
+
* `deploy` overrides the `tag` description with its group semantics (every
|
|
26
|
+
* deployment carrying the tag). */
|
|
27
|
+
export declare const deploymentFlags: {
|
|
28
|
+
deployment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
29
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
30
|
+
};
|
|
9
31
|
export declare const jsonFlags: {
|
|
10
32
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
33
|
};
|
package/dist/lib/flags.js
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
+
const tagAsFilterDescription = 'Workflow environment tag (e.g. prod, test) — an optional query filter, and the resource disambiguator when the config spans several.';
|
|
2
3
|
export const tagFlags = {
|
|
4
|
+
tag: Flags.string({ description: tagAsFilterDescription }),
|
|
5
|
+
};
|
|
6
|
+
export const instanceFlags = {
|
|
7
|
+
deployment: Flags.string({
|
|
8
|
+
description: 'Deployment name — read the instance from the resource that deployment targets; the tag partition still comes from the loaded instance.',
|
|
9
|
+
exclusive: ['tag'],
|
|
10
|
+
}),
|
|
11
|
+
tag: Flags.string({ description: tagAsFilterDescription, exclusive: ['deployment'] }),
|
|
12
|
+
};
|
|
13
|
+
export const deploymentFlags = {
|
|
14
|
+
deployment: Flags.string({
|
|
15
|
+
description: 'Deployment name — the unique identity of one deployment in the config.',
|
|
16
|
+
exclusive: ['tag'],
|
|
17
|
+
}),
|
|
3
18
|
tag: Flags.string({
|
|
4
|
-
description: 'Workflow environment tag (e.g. prod, test) — the deployment to
|
|
19
|
+
description: 'Workflow environment tag (e.g. prod, test) — selects the deployment to act on while the tag names exactly one; pass --deployment when it spans several.',
|
|
20
|
+
exclusive: ['deployment'],
|
|
5
21
|
}),
|
|
6
22
|
};
|
|
7
23
|
export const jsonFlags = {
|
package/dist/lib/nuke.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type WorkflowClient, type WorkflowResource } from '@sanity/workflow-engine';
|
|
1
|
+
import { type WorkflowClient, type WorkflowDeployment, type WorkflowResource } from '@sanity/workflow-engine';
|
|
2
2
|
/** A resource a nuke may delete from, with the client that reads/writes it and
|
|
3
3
|
* whether it is the engine's own resource — the only one that holds instances
|
|
4
4
|
* and definitions (guards can co-locate with subjects in any resource). */
|
|
@@ -32,6 +32,20 @@ export interface PlanCounts {
|
|
|
32
32
|
datasets: number;
|
|
33
33
|
total: number;
|
|
34
34
|
}
|
|
35
|
+
/** Every resource a deployment's nuke sweeps: its own workflow resource (which
|
|
36
|
+
* holds instances + definitions + guards) plus each alias-bound resource
|
|
37
|
+
* (guards co-locate with the subjects they lock). */
|
|
38
|
+
export declare function sweptResources(deployment: WorkflowDeployment): WorkflowResource[];
|
|
39
|
+
/**
|
|
40
|
+
* Fail before any nuke work when another same-tag deployment sweeps a resource
|
|
41
|
+
* the selected one does. Guard document ids embed only the tag
|
|
42
|
+
* (`temp.system.guard.<tag>.wf-instance.*`), so within a shared resource the
|
|
43
|
+
* sweep cannot tell such deployments' guards apart — nuking one would delete
|
|
44
|
+
* the others' live locks, invisibly to the plan. Config validation already
|
|
45
|
+
* forbids sharing tag + workflow resource, so an overlap can only arrive
|
|
46
|
+
* through alias bindings.
|
|
47
|
+
*/
|
|
48
|
+
export declare function refuseOverlappingNuke(deployments: WorkflowDeployment[], selected: WorkflowDeployment): void;
|
|
35
49
|
/**
|
|
36
50
|
* Resolve what a tag-scoped nuke would delete, per resource — the dry-run plan
|
|
37
51
|
* printed before any deletion. Instances and definitions are read only from the
|
|
@@ -45,19 +59,6 @@ export declare function resolveNukePlan(args: {
|
|
|
45
59
|
tag: string;
|
|
46
60
|
targets: NukeTarget[];
|
|
47
61
|
}): Promise<NukePlan>;
|
|
48
|
-
/**
|
|
49
|
-
* Does a guard doc belong to `tag`? A guard's `sourceInstanceId` is the id of
|
|
50
|
-
* the instance that registered it (`<tag>.wf-instance.<random>`), and its own
|
|
51
|
-
* `_id` embeds that same instance id
|
|
52
|
-
* (`temp.system.guard.<tag>.wf-instance.<random>.<name>`). Matching on either —
|
|
53
|
-
* with the trailing `.` so `prod` never matches `prod-eu` — catches guards
|
|
54
|
-
* whose `sourceInstanceId` was never written as well as guards whose instance
|
|
55
|
-
* is already gone.
|
|
56
|
-
*/
|
|
57
|
-
export declare function guardMatchesTag(guard: {
|
|
58
|
-
_id: string;
|
|
59
|
-
sourceInstanceId?: string;
|
|
60
|
-
}, tag: string): boolean;
|
|
61
62
|
/** The `project.dataset` labels of resources the plan actually deletes from —
|
|
62
63
|
* the exact set the operator must type back to confirm. A resource with
|
|
63
64
|
* nothing to delete is not "involved" and is omitted. */
|
package/dist/lib/nuke.js
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
|
-
import { GUARD_DOC_TYPE, WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, tagScopeFilter, } from '@sanity/workflow-engine';
|
|
2
|
+
import { GUARD_DOC_TYPE, WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, resourceGdr, tagScopeFilter, } from '@sanity/workflow-engine';
|
|
3
|
+
import { fail } from "./fail.js";
|
|
4
|
+
import { deploymentLabel } from "./select-deployment.js";
|
|
3
5
|
import { formatTable, resourceLabel, sectionHeader } from "./ui.js";
|
|
4
6
|
const CHUNK = 200;
|
|
5
7
|
const REQUEST_TAG = 'nuke';
|
|
8
|
+
export function sweptResources(deployment) {
|
|
9
|
+
return [
|
|
10
|
+
deployment.workflowResource,
|
|
11
|
+
...(deployment.resourceAliases ?? []).map((binding) => binding.resource),
|
|
12
|
+
];
|
|
13
|
+
}
|
|
14
|
+
export function refuseOverlappingNuke(deployments, selected) {
|
|
15
|
+
const swept = new Set(sweptResources(selected).map(resourceGdr));
|
|
16
|
+
const clashing = deployments.filter((candidate) => candidate.name !== selected.name &&
|
|
17
|
+
candidate.tag === selected.tag &&
|
|
18
|
+
sweptResources(candidate).some((resource) => swept.has(resourceGdr(resource))));
|
|
19
|
+
if (clashing.length === 0) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
fail(`Refusing to nuke "${deploymentLabel(selected)}" — its guards are indistinguishable ` +
|
|
23
|
+
`from ${clashing.map(deploymentLabel).join(', ')}'s in the shared resource(s).`, `Guard ids embed only the tag, and these deployments share tag "${selected.tag}" plus ` +
|
|
24
|
+
"a swept resource — the sweep would delete the other deployments' live guards. " +
|
|
25
|
+
'Give them distinct tags first.');
|
|
26
|
+
}
|
|
6
27
|
export async function resolveNukePlan(args) {
|
|
7
28
|
const { tag, targets } = args;
|
|
8
29
|
const resources = await Promise.all(targets.map((target) => resolveResourcePlan({ tag, target })));
|
|
@@ -31,14 +52,12 @@ function engineDocIds(args) {
|
|
|
31
52
|
return args.client.fetch(`*[_type == $type && ${tagScopeFilter()}]._id`, { type: args.type, tag: args.tag }, { perspective: 'raw', tag: REQUEST_TAG });
|
|
32
53
|
}
|
|
33
54
|
async function tagGuardIds(args) {
|
|
34
|
-
const
|
|
35
|
-
return guards.filter((guard) => guardMatchesTag(guard, args.tag)).map((guard) => guard._id);
|
|
36
|
-
}
|
|
37
|
-
export function guardMatchesTag(guard, tag) {
|
|
38
|
-
const instancePrefix = `${tag}.wf-instance.`;
|
|
55
|
+
const instancePrefix = `${args.tag}.wf-instance.`;
|
|
39
56
|
const guardIdPrefix = `${GUARD_DOC_TYPE}.${instancePrefix}`;
|
|
40
|
-
return
|
|
41
|
-
|
|
57
|
+
return args.client.fetch(`*[_type == $type && (
|
|
58
|
+
string::startsWith(sourceInstanceId, $instancePrefix) ||
|
|
59
|
+
string::startsWith(_id, $guardIdPrefix)
|
|
60
|
+
)]._id`, { type: GUARD_DOC_TYPE, instancePrefix, guardIdPrefix }, { perspective: 'raw', tag: REQUEST_TAG });
|
|
42
61
|
}
|
|
43
62
|
export function involvedTargets(plan) {
|
|
44
63
|
return plan.resources
|
package/dist/lib/prompt.d.ts
CHANGED
|
@@ -9,3 +9,17 @@
|
|
|
9
9
|
* prompts such as nuke's confirmation.
|
|
10
10
|
*/
|
|
11
11
|
export declare function canPromptOnStderr(): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Ask a `select` question on stderr — the channel this package renders prompts
|
|
14
|
+
* on so a redirected stdout can't swallow the question (the display side of the
|
|
15
|
+
* {@link canPromptOnStderr} gate). The single site that routes a `select` to
|
|
16
|
+
* stderr, so no `select` prompt can silently regress to stdout.
|
|
17
|
+
*/
|
|
18
|
+
export declare function selectOnStderr(config: {
|
|
19
|
+
message: string;
|
|
20
|
+
choices: readonly {
|
|
21
|
+
name: string;
|
|
22
|
+
value: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
}[];
|
|
25
|
+
}): Promise<string>;
|
package/dist/lib/prompt.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { isInteractive } from '@sanity/cli-core';
|
|
2
|
+
import { select } from '@sanity/cli-core/ux';
|
|
2
3
|
export function canPromptOnStderr() {
|
|
3
4
|
return isInteractive() && process.stderr.isTTY === true;
|
|
4
5
|
}
|
|
6
|
+
export function selectOnStderr(config) {
|
|
7
|
+
return select(config, { output: process.stderr });
|
|
8
|
+
}
|
|
@@ -1,32 +1,57 @@
|
|
|
1
1
|
import { type DeployTarget, type WorkflowConfig, type WorkflowDeployment } from '@sanity/workflow-engine';
|
|
2
|
-
type
|
|
2
|
+
export type ChooseDeploymentName = (deployments: WorkflowDeployment[]) => Promise<string>;
|
|
3
3
|
interface DeploymentSelectionOptions {
|
|
4
|
+
name: string | undefined;
|
|
4
5
|
tag: string | undefined;
|
|
5
6
|
allTags: boolean;
|
|
6
7
|
interactive?: boolean;
|
|
7
|
-
|
|
8
|
+
chooseName?: ChooseDeploymentName;
|
|
8
9
|
}
|
|
10
|
+
/** The one identity format for a deployment in output — banners, failure
|
|
11
|
+
* summaries, and "available deployments" listings all render it the same
|
|
12
|
+
* way. */
|
|
13
|
+
export declare function deploymentLabel({ name, tag }: Pick<WorkflowDeployment, 'name' | 'tag'>): string;
|
|
9
14
|
/**
|
|
10
|
-
* Pick the deployment for the requested `--tag`.
|
|
11
|
-
* config (enforced by defineWorkflowConfig), so a
|
|
12
|
-
* deployment.
|
|
15
|
+
* Pick the deployment for the requested `--deployment` or `--tag`. Names are
|
|
16
|
+
* unique across a config (enforced by defineWorkflowConfig), so a name resolves
|
|
17
|
+
* exactly one deployment. Tags are repeatable — a tag resolves only while it
|
|
18
|
+
* names exactly one deployment; when it spans several, the failure lists them
|
|
19
|
+
* so `--deployment` can disambiguate.
|
|
13
20
|
*
|
|
14
|
-
* With no
|
|
15
|
-
* (the common single-environment case)
|
|
16
|
-
*
|
|
17
|
-
* that
|
|
21
|
+
* With no selector: fall back to the sole deployment when there's exactly one
|
|
22
|
+
* (the common single-environment case). When several are configured, an
|
|
23
|
+
* interactive terminal is prompted to pick one ({@link canPromptOnStderr}); a
|
|
24
|
+
* run that cannot prompt fails asking for `--deployment` or `--tag` — a
|
|
25
|
+
* multi-deployment config is ambiguous without one. `orAlternative` extends
|
|
26
|
+
* that ambiguity message for callers with another way out (deploy's
|
|
27
|
+
* `--all-tags`).
|
|
18
28
|
*/
|
|
19
|
-
export declare function selectDeployment(config: WorkflowConfig, { tag, orAlternative }: {
|
|
29
|
+
export declare function selectDeployment(config: WorkflowConfig, { name, tag, orAlternative, interactive, chooseName, }: {
|
|
30
|
+
name: string | undefined;
|
|
20
31
|
tag: string | undefined;
|
|
21
32
|
orAlternative?: string;
|
|
22
|
-
|
|
33
|
+
interactive?: boolean | undefined;
|
|
34
|
+
chooseName?: ChooseDeploymentName | undefined;
|
|
35
|
+
}): Promise<WorkflowDeployment>;
|
|
23
36
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* flag guidance. Deploying everything is an explicit opt-in — never a default.
|
|
37
|
+
* Every deployment carrying `tag`, failing when the tag matches none — the
|
|
38
|
+
* shared resolution for the callers that accept a whole tag group (deploy's
|
|
39
|
+
* tag-as-environment run, the read paths' resource narrowing).
|
|
28
40
|
*/
|
|
29
|
-
export declare function
|
|
41
|
+
export declare function deploymentsForTag(config: WorkflowConfig, tag: string): WorkflowDeployment[];
|
|
42
|
+
/**
|
|
43
|
+
* The deployments a `deploy` run acts on: every one with `--all-tags`, one
|
|
44
|
+
* definite deployment via `--deployment`, every deployment carrying the tag with
|
|
45
|
+
* `--tag` (a tag is an environment group), otherwise the sole deployment. An
|
|
46
|
+
* ambiguous bare interactive run asks which deployment; a run that cannot
|
|
47
|
+
* prompt keeps the explicit flag guidance. Deploying everything is an
|
|
48
|
+
* explicit opt-in — never a default.
|
|
49
|
+
*/
|
|
50
|
+
export declare function selectDeployments(config: WorkflowConfig, { name, tag, allTags, interactive, chooseName }: DeploymentSelectionOptions): Promise<WorkflowDeployment[]>;
|
|
51
|
+
/** The "Available deployments: name (tag), …" hint every ambiguity failure
|
|
52
|
+
* prints — one vocabulary across the write and instance-keyed paths, so a
|
|
53
|
+
* reader always sees both the `--deployment` names and their `--tag`s. */
|
|
54
|
+
export declare function availableDeployments(config: WorkflowConfig, deployments?: WorkflowDeployment[]): string;
|
|
30
55
|
/**
|
|
31
56
|
* Project a deployment into the engine's {@link DeployTarget} — the shape the
|
|
32
57
|
* deploy/diff verbs (`deployDefinitions`, `computeDiffEntries`, `diffEntry`)
|
|
@@ -1,56 +1,73 @@
|
|
|
1
|
-
import { select } from '@sanity/cli-core/ux';
|
|
2
1
|
import { resourceAliasesToMap, } from '@sanity/workflow-engine';
|
|
3
2
|
import { fail } from "./fail.js";
|
|
4
|
-
import { canPromptOnStderr } from "./prompt.js";
|
|
5
|
-
export function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import { canPromptOnStderr, selectOnStderr } from "./prompt.js";
|
|
4
|
+
export function deploymentLabel({ name, tag }) {
|
|
5
|
+
return `${name} (${tag})`;
|
|
6
|
+
}
|
|
7
|
+
export async function selectDeployment(config, { name, tag, orAlternative = '', interactive, chooseName = chooseDeploymentName, }) {
|
|
8
|
+
if (name !== undefined) {
|
|
9
|
+
const deployment = config.deployments.find((candidate) => candidate.name === name);
|
|
10
|
+
if (deployment === undefined) {
|
|
11
|
+
fail(`No deployment named "${name}".`, availableDeployments(config));
|
|
9
12
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
return deployment;
|
|
14
|
+
}
|
|
15
|
+
if (tag !== undefined) {
|
|
16
|
+
const matches = deploymentsForTag(config, tag);
|
|
17
|
+
const [sole, ...rest] = matches;
|
|
18
|
+
if (sole === undefined || rest.length > 0) {
|
|
19
|
+
fail(`Multiple deployments tagged "${tag}" — pass --deployment to choose one.`, availableDeployments(config, matches));
|
|
13
20
|
}
|
|
14
21
|
return sole;
|
|
15
22
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
return selectDefaultDeployment(config, { orAlternative, interactive, chooseName });
|
|
24
|
+
}
|
|
25
|
+
async function selectDefaultDeployment(config, { orAlternative, interactive, chooseName, }) {
|
|
26
|
+
if (config.deployments.length > 1) {
|
|
27
|
+
if (interactive ?? canPromptOnStderr()) {
|
|
28
|
+
const selectedName = await chooseName(config.deployments);
|
|
29
|
+
return selectDeployment(config, { name: selectedName, tag: undefined });
|
|
30
|
+
}
|
|
31
|
+
fail(`Multiple deployments configured — pass --deployment or --tag${orAlternative}.`, availableDeployments(config));
|
|
32
|
+
}
|
|
33
|
+
const [sole] = config.deployments;
|
|
34
|
+
if (sole === undefined) {
|
|
35
|
+
fail('No deployments configured.');
|
|
19
36
|
}
|
|
20
|
-
return
|
|
37
|
+
return sole;
|
|
21
38
|
}
|
|
22
|
-
export
|
|
39
|
+
export function deploymentsForTag(config, tag) {
|
|
40
|
+
const matches = config.deployments.filter((candidate) => candidate.tag === tag);
|
|
41
|
+
if (matches.length === 0) {
|
|
42
|
+
fail(`No deployment for tag "${tag}".`, availableDeployments(config));
|
|
43
|
+
}
|
|
44
|
+
return matches;
|
|
45
|
+
}
|
|
46
|
+
export async function selectDeployments(config, { name, tag, allTags, interactive, chooseName }) {
|
|
23
47
|
if (allTags) {
|
|
24
48
|
return config.deployments;
|
|
25
49
|
}
|
|
26
|
-
if (tag
|
|
27
|
-
|
|
28
|
-
return [
|
|
29
|
-
selectDeployment(config, {
|
|
30
|
-
tag,
|
|
31
|
-
orAlternative: ', or --all-tags to deploy every deployment',
|
|
32
|
-
}),
|
|
33
|
-
];
|
|
34
|
-
}
|
|
35
|
-
const selectedTag = await chooseTag(config.deployments);
|
|
36
|
-
return [selectDeployment(config, { tag: selectedTag })];
|
|
50
|
+
if (tag !== undefined) {
|
|
51
|
+
return deploymentsForTag(config, tag);
|
|
37
52
|
}
|
|
38
53
|
return [
|
|
39
|
-
selectDeployment(config, {
|
|
54
|
+
await selectDeployment(config, {
|
|
55
|
+
name,
|
|
56
|
+
tag,
|
|
57
|
+
orAlternative: ', or --all-tags to deploy every deployment',
|
|
58
|
+
interactive,
|
|
59
|
+
chooseName,
|
|
60
|
+
}),
|
|
40
61
|
];
|
|
41
62
|
}
|
|
42
|
-
async function
|
|
43
|
-
return
|
|
44
|
-
message: 'Select a deployment
|
|
45
|
-
choices: deployments.map(({ name, tag }) => ({
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}, { output: process.stderr });
|
|
51
|
-
}
|
|
52
|
-
function availableTags(config) {
|
|
53
|
-
return `Available tags: ${config.deployments.map((candidate) => candidate.tag).join(', ')}`;
|
|
63
|
+
async function chooseDeploymentName(deployments) {
|
|
64
|
+
return selectOnStderr({
|
|
65
|
+
message: 'Select a deployment',
|
|
66
|
+
choices: deployments.map(({ name, tag }) => ({ name, value: name, description: `tag: ${tag}` })),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export function availableDeployments(config, deployments = config.deployments) {
|
|
70
|
+
return `Available deployments: ${deployments.map(deploymentLabel).join(', ')}`;
|
|
54
71
|
}
|
|
55
72
|
export function deploymentToTarget(deployment) {
|
|
56
73
|
return {
|