@sanity/workflow-cli 0.13.0 → 0.14.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +37 -27
  3. package/dist/commands/editorial-workflows/abort.d.ts +1 -0
  4. package/dist/commands/editorial-workflows/abort.js +2 -2
  5. package/dist/commands/editorial-workflows/definition/delete.d.ts +1 -0
  6. package/dist/commands/editorial-workflows/definition/delete.js +2 -2
  7. package/dist/commands/editorial-workflows/definition/diff.d.ts +1 -0
  8. package/dist/commands/editorial-workflows/definition/diff.js +2 -2
  9. package/dist/commands/editorial-workflows/deploy.d.ts +2 -1
  10. package/dist/commands/editorial-workflows/deploy.js +15 -9
  11. package/dist/commands/editorial-workflows/diagnose.d.ts +1 -0
  12. package/dist/commands/editorial-workflows/diagnose.js +2 -2
  13. package/dist/commands/editorial-workflows/fire-action.d.ts +1 -0
  14. package/dist/commands/editorial-workflows/fire-action.js +2 -2
  15. package/dist/commands/editorial-workflows/nuke.d.ts +2 -1
  16. package/dist/commands/editorial-workflows/nuke.js +13 -9
  17. package/dist/commands/editorial-workflows/set-stage.d.ts +1 -0
  18. package/dist/commands/editorial-workflows/set-stage.js +2 -2
  19. package/dist/commands/editorial-workflows/start.d.ts +1 -0
  20. package/dist/commands/editorial-workflows/start.js +2 -2
  21. package/dist/lib/context.d.ts +24 -9
  22. package/dist/lib/context.js +15 -6
  23. package/dist/lib/flags.d.ts +27 -5
  24. package/dist/lib/flags.js +17 -1
  25. package/dist/lib/nuke.d.ts +15 -1
  26. package/dist/lib/nuke.js +22 -1
  27. package/dist/lib/select-deployment.d.ts +37 -15
  28. package/dist/lib/select-deployment.js +49 -23
  29. package/oclif.manifest.json +131 -19
  30. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @sanity/workflow-cli
2
2
 
3
+ ## 0.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 59b40fd: **BREAKING:** deployment selection now keys on the deployment `name`, selected with `--deployment`. Deployment-targeted commands (`start`, `definition diff`, `definition delete`, `nuke`) take `--deployment` as the precise selector; `--tag` still selects while it names exactly one deployment and errors listing the candidates when it spans several. `deploy --tag <tag>` now deploys every deployment carrying the tag (a tag is an environment group); `--deployment` deploys one, `--all-tags` deploys everything, and the three are mutually exclusive. `nuke` requires exactly one of `--deployment` / `--tag`, and refuses when another same-tag deployment sweeps an overlapping resource — guard document ids embed only the tag, so within a shared dataset the sweep cannot tell the deployments' guards apart and would delete the other's live locks. The interactive deploy prompt selects by name with the tag as its description. Read commands keep `--tag` as an optional filter — a repeated tag fans reads out across every resource it is deployed to.
8
+ - de7ccfa: The instance-keyed commands (`diagnose`, `abort`, `set-stage`, `fire-action`) accept `--deployment <name>` to choose which resource to read the instance from — the precise selector consistent with the deployment-targeted commands. `--deployment` names one deployment and reads from the resource it targets; `--tag` stays the optional narrower it was, and the instance's own tag partition still comes from the loaded instance itself, never from the flag. This closes the dead end where a tag repeated across several resources left these commands unable to resolve a resource, and the "spans multiple resources" failures now point at `--deployment` as the remedy.
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies [24b11dd]
13
+ - Updated dependencies [2005ab7]
14
+ - Updated dependencies [24b11dd]
15
+ - Updated dependencies [59b40fd]
16
+ - @sanity/workflow-engine@0.19.0
17
+
3
18
  ## 0.13.0
4
19
 
5
20
  ### Minor Changes
package/README.md CHANGED
@@ -49,7 +49,8 @@ project will fail on cross-resource reads.
49
49
 
50
50
  The CLI is configured by a `sanity.workflow.ts` (or `.js`/`.mjs`) discovered in
51
51
  the directory you run from. It exports a config built with
52
- `defineWorkflowConfig`, declaring one **deployment** per environment:
52
+ `defineWorkflowConfig`, declaring your **deployments** (typically one per
53
+ environment):
53
54
 
54
55
  ```ts
55
56
  import {defineWorkflowConfig} from '@sanity/workflow-engine/define'
@@ -60,7 +61,7 @@ export default defineWorkflowConfig({
60
61
  deployments: [
61
62
  {
62
63
  expectedMinReaderModel: 2,
63
- name: 'production',
64
+ name: 'production', // the deployment's unique identity (lowercase letters, digits, dashes)
64
65
  tag: 'prod', // the environment partition the engine's docs are scoped to
65
66
  workflowResource: {type: 'dataset', id: 'acme.workflows'}, // where those docs live
66
67
  resourceAliases: [
@@ -76,11 +77,20 @@ export default defineWorkflowConfig({
76
77
  })
77
78
  ```
78
79
 
79
- Pick a deployment with `--tag <tag>`; with a single deployment configured you
80
- can omit it. With several configured, a bare interactive `deploy` presents a
81
- keyboard-driven tag selector. In CI or another non-interactive shell, it fails
82
- asking for `--tag` or `--all-tags` instead of blocking for input. `--all-tags`
83
- deploys every deployment in the config in one run: a failure in one doesn't
80
+ `name` is the deployment's identity: unique across the config and constrained
81
+ to the same grammar as `tag`. Tags group deployments by environment and may
82
+ repeat, as long as no two deployments share both a `workflowResource` and a
83
+ `tag` that pair is the storage partition, and the config rejects the
84
+ collision naming both entries.
85
+
86
+ Pick a deployment with `--deployment <name>`; with a single deployment configured you
87
+ can omit it. `--tag <tag>` also works on single-deployment commands while the
88
+ tag names exactly one deployment — on `deploy` it targets every deployment
89
+ carrying the tag (a tag is an environment group). With several configured, a
90
+ bare interactive `deploy` presents a keyboard-driven deployment selector (by
91
+ name, tag alongside). In CI or another non-interactive shell, it fails asking
92
+ for `--deployment`, `--tag`, or `--all-tags` instead of blocking for input.
93
+ `--all-tags` deploys every deployment in the config in one run: a failure in one doesn't
84
94
  stop the rest — the run continues, prints a summary of what failed, and exits
85
95
  non-zero. The client's project + dataset are derived from the deployment's
86
96
  `workflowResource`, and `deploy` expands each definition's `@<handle>:`
@@ -143,26 +153,26 @@ stale compiled output.
143
153
 
144
154
  ## Command status
145
155
 
146
- | Command | Status |
147
- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
148
- | `deploy` | wired — calls `workflow.deployDefinitions` over the selected deployment's definitions |
149
- | `deploy --all-tags` | wired — deploys every deployment in the config, continuing past per-deployment failures |
150
- | `deploy --check` | wired — runs `validateDefinition` over the local batch + a duplicate-name check |
151
- | `deploy --dry-run` | wired — fetches existing docs and renders a coloured JSON diff per change |
152
- | `deploy --only <name>` | wired — filters deploy/check/dry-run to one definition by `name` |
153
- | `start <name>` | wired — calls `workflow.startInstance` (`--field` for input fields) |
154
- | `list` | wired — `client.fetch` over `sanity.workflow.instance` documents (`--definition <name>` to filter) |
155
- | `show <instance-id>` | wired — `client.getDocument` |
156
- | `diagnose <instance-id>` | wired — calls `workflow.diagnose`, classifies why the instance is/isn't progressing |
157
- | `tail <instance-id>` | wired — `client.listen()` over the instance, prints new history entries |
158
- | `abort <instance-id>` | wired — calls `workflow.abortInstance` (hard stop: cancels pending effects, removes guards) |
159
- | `set-stage <instance-id> --to <stage>` | wired — calls `workflow.setStage` (admin override: skips declared transitions/filters; enter lifecycle + cascade still run) |
160
- | `fire-action <instance-id>` | wired — `workflow.availableActions` lists actions; `workflow.fireAction` fires one |
161
- | `definition list` | wired — `client.fetch` over `sanity.workflow.definition` documents |
162
- | `definition show <name>` | wired — `client.fetch`, latest version unless `--version` |
163
- | `definition diff <name>` | wired — diffs the in-code definition against the deployed latest (`--version` to pin) |
164
- | `definition delete <name>` | wired — calls `workflow.deleteDefinition` (refuses on live instances unless `--cascade`) |
165
- | `nuke --tag <tag>` | wired — dev-period reset (exists only until the versioned upgrade framework): deletes every engine-owned doc for the tag (instances, definitions, guards across resources); plan + typed confirm |
156
+ | Command | Status |
157
+ | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
158
+ | `deploy` | wired — calls `workflow.deployDefinitions` over the selected deployment's definitions |
159
+ | `deploy --all-tags` | wired — deploys every deployment in the config, continuing past per-deployment failures |
160
+ | `deploy --check` | wired — runs `validateDefinition` over the local batch + a duplicate-name check |
161
+ | `deploy --dry-run` | wired — fetches existing docs and renders a coloured JSON diff per change |
162
+ | `deploy --only <name>` | wired — filters deploy/check/dry-run to one definition by `name` |
163
+ | `start <name>` | wired — calls `workflow.startInstance` (`--field` for input fields) |
164
+ | `list` | wired — `client.fetch` over `sanity.workflow.instance` documents (`--definition <name>` to filter) |
165
+ | `show <instance-id>` | wired — `client.getDocument` |
166
+ | `diagnose <instance-id>` | wired — calls `workflow.diagnose`, classifies why the instance is/isn't progressing |
167
+ | `tail <instance-id>` | wired — `client.listen()` over the instance, prints new history entries |
168
+ | `abort <instance-id>` | wired — calls `workflow.abortInstance` (hard stop: cancels pending effects, removes guards) |
169
+ | `set-stage <instance-id> --to <stage>` | wired — calls `workflow.setStage` (admin override: skips declared transitions/filters; enter lifecycle + cascade still run) |
170
+ | `fire-action <instance-id>` | wired — `workflow.availableActions` lists actions; `workflow.fireAction` fires one |
171
+ | `definition list` | wired — `client.fetch` over `sanity.workflow.definition` documents |
172
+ | `definition show <name>` | wired — `client.fetch`, latest version unless `--version` |
173
+ | `definition diff <name>` | wired — diffs the in-code definition against the deployed latest (`--version` to pin) |
174
+ | `definition delete <name>` | wired — calls `workflow.deleteDefinition` (refuses on live instances unless `--cascade`) |
175
+ | `nuke --deployment <name>` / `nuke --tag <tag>` | wired — dev-period reset (exists only until the versioned upgrade framework): deletes every engine-owned doc for the deployment's tag (instances, definitions, guards across resources); exactly one selector required; refuses while another same-tag deployment sweeps an overlapping resource (guard ids embed only the tag); plan + typed confirm |
166
176
 
167
177
  ## Telemetry
168
178
 
@@ -10,6 +10,7 @@ export default class Abort extends WorkflowCommand {
10
10
  };
11
11
  static flags: {
12
12
  reason: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ deployment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
14
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
15
  };
15
16
  run(): Promise<void>;
@@ -3,7 +3,7 @@ import { Args, Flags } from '@oclif/core';
3
3
  import { workflow } from '@sanity/workflow-engine';
4
4
  import { WorkflowCommand } from "../../lib/base-command.js";
5
5
  import { resolveInstanceContext } from "../../lib/context.js";
6
- import { tagFlags } from "../../lib/flags.js";
6
+ import { instanceFlags } from "../../lib/flags.js";
7
7
  import { buildOperationArgs } from "../../lib/operation-args.js";
8
8
  import { runWriteVerb } from "../../lib/ops-report.js";
9
9
  export default class Abort extends WorkflowCommand {
@@ -17,7 +17,7 @@ export default class Abort extends WorkflowCommand {
17
17
  instanceId: Args.string({ required: true, description: 'Workflow instance id.' }),
18
18
  };
19
19
  static flags = {
20
- ...tagFlags,
20
+ ...instanceFlags,
21
21
  reason: Flags.string({ description: 'Reason for aborting (recorded in history).' }),
22
22
  };
23
23
  async run() {
@@ -12,6 +12,7 @@ export default class DefinitionDelete extends WorkflowCommand {
12
12
  version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
13
  cascade: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
14
  reason: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ deployment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
16
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
17
  };
17
18
  run(): Promise<void>;
@@ -3,7 +3,7 @@ import { Args, Flags } from '@oclif/core';
3
3
  import { workflow, } from '@sanity/workflow-engine';
4
4
  import { WorkflowCommand } from "../../../lib/base-command.js";
5
5
  import { resolveContext } from "../../../lib/context.js";
6
- import { tagFlags } from "../../../lib/flags.js";
6
+ import { deploymentFlags } from "../../../lib/flags.js";
7
7
  import { baseEngineArgs } from "../../../lib/operation-args.js";
8
8
  import { runWriteVerb } from "../../../lib/ops-report.js";
9
9
  export default class DefinitionDelete extends WorkflowCommand {
@@ -20,7 +20,7 @@ export default class DefinitionDelete extends WorkflowCommand {
20
20
  name: Args.string({ required: true, description: 'Workflow definition name.' }),
21
21
  };
22
22
  static flags = {
23
- ...tagFlags,
23
+ ...deploymentFlags,
24
24
  version: Flags.integer({
25
25
  description: 'Delete only this deployed version (default: every version).',
26
26
  }),
@@ -9,6 +9,7 @@ export default class DefinitionDiff extends WorkflowCommand {
9
9
  };
10
10
  static flags: {
11
11
  version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ deployment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
14
  };
14
15
  run(): Promise<void>;
@@ -5,7 +5,7 @@ import { WorkflowCommand } from "../../../lib/base-command.js";
5
5
  import { resolveContext } from "../../../lib/context.js";
6
6
  import { fetchDeployedDefinition, selectDefinitions, validateOrFail, } from "../../../lib/definitions.js";
7
7
  import { diffReport } from "../../../lib/diff.js";
8
- import { tagFlags } from "../../../lib/flags.js";
8
+ import { deploymentFlags } from "../../../lib/flags.js";
9
9
  import { deploymentToTarget } from "../../../lib/select-deployment.js";
10
10
  export default class DefinitionDiff extends WorkflowCommand {
11
11
  static aliases = ['definition:diff'];
@@ -18,7 +18,7 @@ export default class DefinitionDiff extends WorkflowCommand {
18
18
  name: Args.string({ required: true, description: 'Workflow definition name.' }),
19
19
  };
20
20
  static flags = {
21
- ...tagFlags,
21
+ ...deploymentFlags,
22
22
  version: Flags.integer({ description: 'Deployed version to diff against (default: latest).' }),
23
23
  };
24
24
  async run() {
@@ -15,12 +15,13 @@ export default class Deploy extends WorkflowCommand {
15
15
  static description: string;
16
16
  static examples: string[];
17
17
  static flags: {
18
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
19
  'all-tags': import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
20
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
21
  check: import("@oclif/core/interfaces").BooleanFlag<boolean>;
21
22
  only: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
23
  'share-defs': import("@oclif/core/interfaces").BooleanFlag<boolean>;
23
- tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
+ deployment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
25
  };
25
26
  run(): Promise<void>;
26
27
  /** Deploy (or diff, for `--dry-run`) one deployment's definitions.
@@ -8,9 +8,9 @@ import { clientFor, resolveTokenOrFail } from "../../lib/client.js";
8
8
  import { selectDefinitions, validateOrFail } from "../../lib/definitions.js";
9
9
  import { diffReport } from "../../lib/diff.js";
10
10
  import { fail, isAuthRejection } from "../../lib/fail.js";
11
- import { tagFlags } from "../../lib/flags.js";
11
+ import { deploymentFlags } from "../../lib/flags.js";
12
12
  import { loadWorkflowConfig } from "../../lib/load-config.js";
13
- import { deploymentToTarget, selectDeployments } from "../../lib/select-deployment.js";
13
+ import { deploymentLabel, deploymentToTarget, selectDeployments, } from "../../lib/select-deployment.js";
14
14
  import { shareDefinitionsAfterDeploy } from "../../lib/share-definitions.js";
15
15
  import { cliTelemetry } from "../../lib/telemetry.js";
16
16
  import { groupBanner } from "../../lib/ui.js";
@@ -18,6 +18,7 @@ export default class Deploy extends WorkflowCommand {
18
18
  static aliases = ['deploy'];
19
19
  static description = 'Validate, diff, and deploy workflow definitions to the resource bound by the selected deployment.';
20
20
  static examples = [
21
+ '<%= config.bin %> deploy --deployment review-prod',
21
22
  '<%= config.bin %> deploy --tag prod',
22
23
  '<%= config.bin %> deploy --all-tags',
23
24
  '<%= config.bin %> deploy --check',
@@ -25,11 +26,15 @@ export default class Deploy extends WorkflowCommand {
25
26
  '<%= config.bin %> deploy --only productLaunch',
26
27
  ];
27
28
  static flags = {
28
- ...tagFlags,
29
+ ...deploymentFlags,
30
+ tag: Flags.string({
31
+ description: 'Workflow environment tag (e.g. prod, test) — deploys every deployment carrying the tag (a tag is an environment group).',
32
+ exclusive: ['deployment'],
33
+ }),
29
34
  'all-tags': Flags.boolean({
30
- description: 'Deploy every deployment in the config, not just one tag.',
35
+ description: 'Deploy every deployment in the config, not just a selection.',
31
36
  default: false,
32
- exclusive: ['tag'],
37
+ exclusive: ['tag', 'deployment'],
33
38
  }),
34
39
  'dry-run': Flags.boolean({
35
40
  description: 'Validate + diff against the deployed version; do not write.',
@@ -51,7 +56,11 @@ export default class Deploy extends WorkflowCommand {
51
56
  const { flags } = await this.parse(Deploy);
52
57
  validateModeFlags(flags.check, flags['dry-run']);
53
58
  const config = await loadWorkflowConfig();
54
- const batches = buildBatches(await selectDeployments(config, { tag: flags.tag, allTags: flags['all-tags'] }), flags.only);
59
+ const batches = buildBatches(await selectDeployments(config, {
60
+ name: flags.deployment,
61
+ tag: flags.tag,
62
+ allTags: flags['all-tags'],
63
+ }), flags.only);
55
64
  const log = (line) => this.log(line);
56
65
  if (flags.check) {
57
66
  await reconcileBatches({
@@ -145,9 +154,6 @@ export function validateModeFlags(check, dryRun) {
145
154
  fail('Pass either --check or --dry-run, not both.');
146
155
  }
147
156
  }
148
- function deploymentLabel({ name, tag }) {
149
- return `${name} (${tag})`;
150
- }
151
157
  export function buildBatches(deployments, only) {
152
158
  return deployments.map((deployment) => {
153
159
  const context = deployments.length > 1 ? `${deploymentLabel(deployment)} — ` : '';
@@ -40,6 +40,7 @@ export default class Diagnose extends WorkflowCommand {
40
40
  };
41
41
  static flags: {
42
42
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
43
+ deployment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
43
44
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
44
45
  };
45
46
  run(): Promise<void>;
@@ -5,7 +5,7 @@ import logSymbols from 'log-symbols';
5
5
  import { WorkflowCommand } from "../../lib/base-command.js";
6
6
  import { resolveInstanceContext } from "../../lib/context.js";
7
7
  import { fail, failureDetail } from "../../lib/fail.js";
8
- import { jsonFlags, tagFlags } from "../../lib/flags.js";
8
+ import { instanceFlags, jsonFlags } from "../../lib/flags.js";
9
9
  import { baseEngineArgs } from "../../lib/operation-args.js";
10
10
  import { formatTimestamp, sectionHeader, activityIcon } from "../../lib/ui.js";
11
11
  import { instanceHeader } from "./show.js";
@@ -223,7 +223,7 @@ export default class Diagnose extends WorkflowCommand {
223
223
  instanceId: Args.string({ required: true, description: 'Workflow instance id.' }),
224
224
  };
225
225
  static flags = {
226
- ...tagFlags,
226
+ ...instanceFlags,
227
227
  ...jsonFlags,
228
228
  };
229
229
  async run() {
@@ -41,6 +41,7 @@ export default class FireAction extends WorkflowCommand {
41
41
  activity: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
42
42
  action: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
43
43
  param: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
44
+ deployment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
44
45
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
45
46
  };
46
47
  run(): Promise<void>;
@@ -5,7 +5,7 @@ import logSymbols from 'log-symbols';
5
5
  import { WorkflowCommand } from "../../lib/base-command.js";
6
6
  import { resolveInstanceContext } from "../../lib/context.js";
7
7
  import { fail, failOnThrow, failureDetail } from "../../lib/fail.js";
8
- import { jsonFlags, tagFlags } from "../../lib/flags.js";
8
+ import { instanceFlags, jsonFlags } from "../../lib/flags.js";
9
9
  import { baseEngineArgs } from "../../lib/operation-args.js";
10
10
  import { cascadeTail, opsAppliedLines, runWriteVerb, } from "../../lib/ops-report.js";
11
11
  import { parseParams } from "../../lib/params.js";
@@ -93,7 +93,7 @@ export default class FireAction extends WorkflowCommand {
93
93
  instanceId: Args.string({ required: true, description: 'Workflow instance id.' }),
94
94
  };
95
95
  static flags = {
96
- ...tagFlags,
96
+ ...instanceFlags,
97
97
  activity: Flags.string({
98
98
  description: 'Activity the action belongs to. Required to fire; omit --action to list.',
99
99
  }),
@@ -5,7 +5,8 @@ export default class Nuke extends WorkflowCommand {
5
5
  static description: string;
6
6
  static examples: string[];
7
7
  static flags: {
8
- tag: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ deployment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
11
  };
11
12
  run(): Promise<void>;
@@ -7,7 +7,7 @@ import { clientFor, resolveApiHost, resolveTokenOrFail } from "../../lib/client.
7
7
  import { dedupeResources } from "../../lib/context.js";
8
8
  import { fail } from "../../lib/fail.js";
9
9
  import { loadWorkflowConfig } from "../../lib/load-config.js";
10
- import { confirmationMatches, executeNuke, formatNukeSummary, involvedTargets, planCounts, renderNukePlan, resolveNukePlan, } from "../../lib/nuke.js";
10
+ import { confirmationMatches, executeNuke, formatNukeSummary, involvedTargets, planCounts, refuseOverlappingNuke, renderNukePlan, resolveNukePlan, sweptResources, } from "../../lib/nuke.js";
11
11
  import { runWriteVerb } from "../../lib/ops-report.js";
12
12
  import { canPromptOnStderr } from "../../lib/prompt.js";
13
13
  import { selectDeployment } from "../../lib/select-deployment.js";
@@ -19,13 +19,17 @@ export default class Nuke extends WorkflowCommand {
19
19
  'resource). Content documents are never touched. Prints a dry-run plan, then requires you to ' +
20
20
  'type back every involved dataset (--force skips the prompt; the plan still prints).';
21
21
  static examples = [
22
- '<%= config.bin %> nuke --tag plugin-dev',
22
+ '<%= config.bin %> nuke --deployment plugin-dev',
23
23
  '<%= config.bin %> nuke --tag plugin-dev --force',
24
24
  ];
25
25
  static flags = {
26
+ deployment: Flags.string({
27
+ description: 'The deployment name to reset.',
28
+ exactlyOne: ['deployment', 'tag'],
29
+ }),
26
30
  tag: Flags.string({
27
- description: 'The deployment tag to reset. Required a destructive reset never guesses the environment.',
28
- required: true,
31
+ description: 'The deployment tag to reset (while it names exactly one deployment).',
32
+ exactlyOne: ['deployment', 'tag'],
29
33
  }),
30
34
  force: Flags.boolean({
31
35
  description: 'Skip the confirmation prompt (for scripts/CI). The plan still prints.',
@@ -35,7 +39,8 @@ export default class Nuke extends WorkflowCommand {
35
39
  async run() {
36
40
  const { flags } = await this.parse(Nuke);
37
41
  const config = await loadWorkflowConfig();
38
- const deployment = selectDeployment(config, { tag: flags.tag });
42
+ const deployment = selectDeployment(config, { name: flags.deployment, tag: flags.tag });
43
+ refuseOverlappingNuke(config.deployments, deployment);
39
44
  const targets = buildTargets(deployment, await resolveTokenOrFail());
40
45
  const plan = await resolveNukePlan({ tag: deployment.tag, targets });
41
46
  const apiHost = resolveApiHost() ?? 'https://api.sanity.io (production default)';
@@ -58,12 +63,11 @@ export default class Nuke extends WorkflowCommand {
58
63
  }
59
64
  }
60
65
  function buildTargets(deployment, token) {
61
- const engineResource = deployment.workflowResource;
62
- const aliasResources = (deployment.resourceAliases ?? []).map((binding) => binding.resource);
63
- return dedupeResources([engineResource, ...aliasResources]).map((resource) => ({
66
+ const engineGdr = resourceGdr(deployment.workflowResource);
67
+ return dedupeResources(sweptResources(deployment)).map((resource) => ({
64
68
  resource,
65
69
  client: clientFor(resource, token),
66
- holdsEngineDocs: resourceGdr(resource) === resourceGdr(engineResource),
70
+ holdsEngineDocs: resourceGdr(resource) === engineGdr,
67
71
  }));
68
72
  }
69
73
  async function confirmOrFail(targets) {
@@ -12,6 +12,7 @@ export default class SetStage extends WorkflowCommand {
12
12
  static flags: {
13
13
  to: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
14
  reason: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ deployment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
16
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
17
  };
17
18
  run(): Promise<void>;
@@ -3,7 +3,7 @@ import { Args, Flags } from '@oclif/core';
3
3
  import { workflow } from '@sanity/workflow-engine';
4
4
  import { WorkflowCommand } from "../../lib/base-command.js";
5
5
  import { resolveInstanceContext } from "../../lib/context.js";
6
- import { tagFlags } from "../../lib/flags.js";
6
+ import { instanceFlags } from "../../lib/flags.js";
7
7
  import { buildOperationArgs } from "../../lib/operation-args.js";
8
8
  import { cascadeTail, opsAppliedLines, runWriteVerb, } from "../../lib/ops-report.js";
9
9
  export default class SetStage extends WorkflowCommand {
@@ -20,7 +20,7 @@ export default class SetStage extends WorkflowCommand {
20
20
  }),
21
21
  };
22
22
  static flags = {
23
- ...tagFlags,
23
+ ...instanceFlags,
24
24
  to: Flags.string({
25
25
  required: true,
26
26
  description: 'Target stage name.',
@@ -13,6 +13,7 @@ export default class Start extends WorkflowCommand {
13
13
  version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
14
  field: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
15
15
  'instance-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ deployment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
17
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
18
  };
18
19
  run(): Promise<void>;
@@ -5,7 +5,7 @@ import { WorkflowCommand } from "../../lib/base-command.js";
5
5
  import { resolveContext } from "../../lib/context.js";
6
6
  import { fetchDeployedDefinition } from "../../lib/definitions.js";
7
7
  import { fail, failOnThrow, failureDetail } from "../../lib/fail.js";
8
- import { jsonFlags, tagFlags } from "../../lib/flags.js";
8
+ import { deploymentFlags, jsonFlags } from "../../lib/flags.js";
9
9
  import { baseEngineArgs } from "../../lib/operation-args.js";
10
10
  import { runWriteVerb } from "../../lib/ops-report.js";
11
11
  import { parseParams } from "../../lib/params.js";
@@ -23,7 +23,7 @@ export default class Start extends WorkflowCommand {
23
23
  name: Args.string({ required: true, description: 'Workflow definition name.' }),
24
24
  };
25
25
  static flags = {
26
- ...tagFlags,
26
+ ...deploymentFlags,
27
27
  version: Flags.integer({
28
28
  description: 'Definition version to start from (default: highest deployed).',
29
29
  }),
@@ -13,11 +13,12 @@ export interface InstanceContext {
13
13
  }
14
14
  /**
15
15
  * The resolution a WRITE shares: discover the config, pick the deployment for
16
- * `--tag` (or the sole one), and build an authenticated client for that
17
- * deployment's resource. A write acts on one specific deployment, so a
18
- * single, definite target is exactly what it needs.
16
+ * `--deployment` or `--tag` (or the sole one), and build an authenticated
17
+ * client for that deployment's resource. A write acts on one specific
18
+ * deployment, so a single, definite target is exactly what it needs.
19
19
  */
20
20
  export declare function resolveContext(flags: {
21
+ deployment?: string | undefined;
21
22
  tag?: string | undefined;
22
23
  }): Promise<DeploymentContext>;
23
24
  /** One resource a read inspects, with its authenticated client. */
@@ -34,8 +35,9 @@ export interface ReadTarget {
34
35
  *
35
36
  * Untagged, this is every distinct resource the config mentions — a read is
36
37
  * harmless to fan out, so "show me what's deployed" spans the whole config.
37
- * `--tag` narrows to that deployment's resource. The token resolves once for
38
- * the run, however many targets it spans.
38
+ * `--tag` narrows to the resources deployed under that tag (tags may repeat
39
+ * across deployments). The token resolves once for the run, however many
40
+ * targets it spans.
39
41
  */
40
42
  export declare function resolveReadTargets(flags: {
41
43
  tag?: string | undefined;
@@ -44,9 +46,9 @@ export declare function resolveReadTargets(flags: {
44
46
  * The first occurrence wins its position, so a caller-ordered list (e.g. the
45
47
  * engine resource first) keeps its lead entry. */
46
48
  export declare function dedupeResources(resources: WorkflowResource[]): WorkflowResource[];
47
- /** The distinct resources a read targets: the tagged deployment's sole
48
- * resource, or — untagged — every distinct one the config's deployments
49
- * mention ({@link dedupeResources}). */
49
+ /** The distinct resources a read targets: every resource deployed under the
50
+ * tag (tags may repeat across deployments), or — untagged — every distinct
51
+ * one the config's deployments mention ({@link dedupeResources}). */
50
52
  export declare function resolveReadResources(config: WorkflowConfig, tag: string | undefined): WorkflowResource[];
51
53
  /** The single resource a read should target, or fail asking which when the
52
54
  * config spans more than one. For the paths that need one definite dataset
@@ -60,15 +62,28 @@ export declare function resolveReadResource(config: WorkflowConfig, tag: string
60
62
  * config's declared deployments. An instance id is globally unique and carries
61
63
  * its own `tag`, so a command that names one acts on that instance regardless
62
64
  * of which tags the config happens to deploy; the config only locates the
63
- * resource (via {@link resolveReadResource}, no tag filter).
65
+ * resource (via {@link resolveInstanceResource}, no tag filter).
64
66
  *
65
67
  * Contrast {@link resolveContext}: the deploy/diff/delete path IS scoped to a
66
68
  * declared deployment because it acts on the authored definition set (or a
67
69
  * definition name, which — unlike an instance id — isn't unique across tags).
68
70
  */
69
71
  export declare function resolveInstanceContext(flags: {
72
+ deployment?: string | undefined;
70
73
  tag?: string | undefined;
71
74
  }, instanceId: string): Promise<InstanceContext>;
75
+ /**
76
+ * Which resource an instance-keyed command reads from: the one `--deployment`
77
+ * names (resolved by its unique deployment name), else the sole or
78
+ * `--tag`-narrowed resource ({@link resolveReadResource}). The instance id is
79
+ * globally unique, so this only locates the resource to look in — never the
80
+ * instance's `tag` partition, which the caller takes from the loaded instance.
81
+ * The shared path behind all four instance-keyed commands.
82
+ */
83
+ export declare function resolveInstanceResource(config: WorkflowConfig, { deployment, tag }: {
84
+ deployment?: string | undefined;
85
+ tag?: string | undefined;
86
+ }): WorkflowResource;
72
87
  /** Fetch an instance by id, exiting cleanly when the resource has no such
73
88
  * document — the diagnostic an operator sees on a mistyped id. Split out from
74
89
  * {@link resolveInstanceContext} so the not-found path is unit-testable
@@ -2,11 +2,11 @@ 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 { selectDeployment } from "./select-deployment.js";
5
+ import { availableDeployments, deploymentsForTag, selectDeployment } from "./select-deployment.js";
6
6
  import { resourceLabel } from "./ui.js";
7
7
  export async function resolveContext(flags) {
8
8
  const config = await loadWorkflowConfig();
9
- const deployment = selectDeployment(config, { tag: flags.tag });
9
+ const deployment = selectDeployment(config, { name: flags.deployment, tag: flags.tag });
10
10
  return { deployment, client: clientFor(deployment.workflowResource, await resolveTokenOrFail()) };
11
11
  }
12
12
  export async function resolveReadTargets(flags) {
@@ -20,7 +20,7 @@ export function dedupeResources(resources) {
20
20
  }
21
21
  export function resolveReadResources(config, tag) {
22
22
  if (tag !== undefined) {
23
- return [selectDeployment(config, { tag }).workflowResource];
23
+ return dedupeResources(deploymentsForTag(config, tag).map((deployment) => deployment.workflowResource));
24
24
  }
25
25
  const resources = dedupeResources(config.deployments.map((d) => d.workflowResource));
26
26
  if (resources.length === 0) {
@@ -34,16 +34,25 @@ export function resolveReadResource(config, tag) {
34
34
  if (resources.length === 1 && sole !== undefined) {
35
35
  return sole;
36
36
  }
37
- const tags = config.deployments.map((d) => d.tag).join(', ');
38
- fail('Config spans multiple resources — pass --tag to choose one.', `Available tags: ${tags}`);
37
+ if (tag === undefined) {
38
+ fail('Config spans multiple resources — pass --deployment or --tag to choose one.', availableDeployments(config));
39
+ }
40
+ const carriers = config.deployments.filter((d) => d.tag === tag);
41
+ fail(`Tag "${tag}" spans multiple resources — pass --deployment to choose one.`, availableDeployments(config, carriers));
39
42
  }
40
43
  export async function resolveInstanceContext(flags, instanceId) {
41
44
  const config = await loadWorkflowConfig();
42
- const workflowResource = resolveReadResource(config, flags.tag);
45
+ const workflowResource = resolveInstanceResource(config, flags);
43
46
  const client = clientFor(workflowResource, await resolveTokenOrFail());
44
47
  const instance = await loadInstanceOrFail(client, instanceId);
45
48
  return { client, scope: { tag: instance.tag, workflowResource } };
46
49
  }
50
+ export function resolveInstanceResource(config, { deployment, tag }) {
51
+ if (deployment !== undefined) {
52
+ return selectDeployment(config, { name: deployment, tag: undefined }).workflowResource;
53
+ }
54
+ return resolveReadResource(config, tag);
55
+ }
47
56
  export async function loadInstanceOrFail(client, instanceId) {
48
57
  const instance = await client.getDocument(instanceId, {
49
58
  tag: 'instance.load',
@@ -1,11 +1,33 @@
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. A sole
3
- * deployment needs no tag, and an interactive `deploy` asks when several are
4
- * configured. For read commands it's an optional query filter and the
5
- * resource disambiguator when a config spans more than one. */
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 target for writes; an optional filter for reads.',
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 = {
@@ -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
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 })));
@@ -1,32 +1,54 @@
1
1
  import { type DeployTarget, type WorkflowConfig, type WorkflowDeployment } from '@sanity/workflow-engine';
2
- type ChooseDeploymentTag = (deployments: WorkflowDeployment[]) => Promise<string>;
2
+ 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
- chooseTag?: ChooseDeploymentTag;
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`. Tags are unique across a
11
- * config (enforced by defineWorkflowConfig), so a tag names at most one
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 tag: fall back to the sole deployment when there's exactly one
15
- * (the common single-environment case), otherwise fail asking for `--tag` —
16
- * a multi-deployment config is ambiguous without it. `orAlternative` extends
17
- * that ambiguity message for callers with another way out (deploy's `--all-tags`).
21
+ * With no selector: fall back to the sole deployment when there's exactly one
22
+ * (the common single-environment case), otherwise fail asking for
23
+ * `--deployment` or `--tag` — a multi-deployment config is ambiguous without
24
+ * one. `orAlternative`
25
+ * extends that ambiguity message for callers with another way out (deploy's
26
+ * `--all-tags`).
18
27
  */
19
- export declare function selectDeployment(config: WorkflowConfig, { tag, orAlternative }: {
28
+ export declare function selectDeployment(config: WorkflowConfig, { name, tag, orAlternative, }: {
29
+ name: string | undefined;
20
30
  tag: string | undefined;
21
31
  orAlternative?: string;
22
32
  }): WorkflowDeployment;
23
33
  /**
24
- * The deployments a `deploy` run acts on: every one with `--all-tags`,
25
- * otherwise a single target via {@link selectDeployment}. An ambiguous bare
26
- * interactive run asks for a tag; a run that cannot prompt keeps the explicit
27
- * flag guidance. Deploying everything is an explicit opt-in — never a default.
34
+ * Every deployment carrying `tag`, failing when the tag matches none — the
35
+ * shared resolution for the callers that accept a whole tag group (deploy's
36
+ * tag-as-environment run, the read paths' resource narrowing).
28
37
  */
29
- export declare function selectDeployments(config: WorkflowConfig, { tag, allTags, interactive, chooseTag }: DeploymentSelectionOptions): Promise<WorkflowDeployment[]>;
38
+ export declare function deploymentsForTag(config: WorkflowConfig, tag: string): WorkflowDeployment[];
39
+ /**
40
+ * The deployments a `deploy` run acts on: every one with `--all-tags`, one
41
+ * definite deployment via `--deployment`, every deployment carrying the tag with
42
+ * `--tag` (a tag is an environment group), otherwise the sole deployment. An
43
+ * ambiguous bare interactive run asks which deployment; a run that cannot
44
+ * prompt keeps the explicit flag guidance. Deploying everything is an
45
+ * explicit opt-in — never a default.
46
+ */
47
+ export declare function selectDeployments(config: WorkflowConfig, { name, tag, allTags, interactive, chooseName }: DeploymentSelectionOptions): Promise<WorkflowDeployment[]>;
48
+ /** The "Available deployments: name (tag), …" hint every ambiguity failure
49
+ * prints — one vocabulary across the write and instance-keyed paths, so a
50
+ * reader always sees both the `--deployment` names and their `--tag`s. */
51
+ export declare function availableDeployments(config: WorkflowConfig, deployments?: WorkflowDeployment[]): string;
30
52
  /**
31
53
  * Project a deployment into the engine's {@link DeployTarget} — the shape the
32
54
  * deploy/diff verbs (`deployDefinitions`, `computeDiffEntries`, `diffEntry`)
@@ -2,55 +2,81 @@ import { select } from '@sanity/cli-core/ux';
2
2
  import { resourceAliasesToMap, } from '@sanity/workflow-engine';
3
3
  import { fail } from "./fail.js";
4
4
  import { canPromptOnStderr } from "./prompt.js";
5
- export function selectDeployment(config, { tag, orAlternative = '' }) {
6
- if (tag === undefined) {
7
- if (config.deployments.length > 1) {
8
- fail(`Multiple deployments configured pass --tag${orAlternative}.`, availableTags(config));
5
+ export function deploymentLabel({ name, tag }) {
6
+ return `${name} (${tag})`;
7
+ }
8
+ export function selectDeployment(config, { name, tag, orAlternative = '', }) {
9
+ if (name !== undefined) {
10
+ const deployment = config.deployments.find((candidate) => candidate.name === name);
11
+ if (deployment === undefined) {
12
+ fail(`No deployment named "${name}".`, availableDeployments(config));
9
13
  }
10
- const [sole] = config.deployments;
11
- if (sole === undefined) {
12
- fail('No deployments configured.');
14
+ return deployment;
15
+ }
16
+ if (tag !== undefined) {
17
+ const matches = deploymentsForTag(config, tag);
18
+ const [sole, ...rest] = matches;
19
+ if (sole === undefined || rest.length > 0) {
20
+ fail(`Multiple deployments tagged "${tag}" — pass --deployment to choose one.`, availableDeployments(config, matches));
13
21
  }
14
22
  return sole;
15
23
  }
16
- const deployment = config.deployments.find((candidate) => candidate.tag === tag);
17
- if (deployment === undefined) {
18
- fail(`No deployment for tag "${tag}".`, availableTags(config));
24
+ if (config.deployments.length > 1) {
25
+ fail(`Multiple deployments configured — pass --deployment or --tag${orAlternative}.`, availableDeployments(config));
26
+ }
27
+ const [sole] = config.deployments;
28
+ if (sole === undefined) {
29
+ fail('No deployments configured.');
19
30
  }
20
- return deployment;
31
+ return sole;
21
32
  }
22
- export async function selectDeployments(config, { tag, allTags, interactive, chooseTag = chooseDeploymentTag }) {
33
+ export function deploymentsForTag(config, tag) {
34
+ const matches = config.deployments.filter((candidate) => candidate.tag === tag);
35
+ if (matches.length === 0) {
36
+ fail(`No deployment for tag "${tag}".`, availableDeployments(config));
37
+ }
38
+ return matches;
39
+ }
40
+ export async function selectDeployments(config, { name, tag, allTags, interactive, chooseName = chooseDeploymentName }) {
23
41
  if (allTags) {
24
42
  return config.deployments;
25
43
  }
26
- if (tag === undefined && config.deployments.length > 1) {
44
+ if (tag !== undefined) {
45
+ return deploymentsForTag(config, tag);
46
+ }
47
+ if (name === undefined && config.deployments.length > 1) {
27
48
  if (!(interactive ?? canPromptOnStderr())) {
28
49
  return [
29
50
  selectDeployment(config, {
51
+ name,
30
52
  tag,
31
53
  orAlternative: ', or --all-tags to deploy every deployment',
32
54
  }),
33
55
  ];
34
56
  }
35
- const selectedTag = await chooseTag(config.deployments);
36
- return [selectDeployment(config, { tag: selectedTag })];
57
+ const selectedName = await chooseName(config.deployments);
58
+ return [selectDeployment(config, { name: selectedName, tag: undefined })];
37
59
  }
38
60
  return [
39
- selectDeployment(config, { tag, orAlternative: ', or --all-tags to deploy every deployment' }),
61
+ selectDeployment(config, {
62
+ name,
63
+ tag,
64
+ orAlternative: ', or --all-tags to deploy every deployment',
65
+ }),
40
66
  ];
41
67
  }
42
- async function chooseDeploymentTag(deployments) {
68
+ async function chooseDeploymentName(deployments) {
43
69
  return select({
44
- message: 'Select a deployment tag',
70
+ message: 'Select a deployment',
45
71
  choices: deployments.map(({ name, tag }) => ({
46
- name: tag,
47
- value: tag,
48
- description: name,
72
+ name,
73
+ value: name,
74
+ description: `tag: ${tag}`,
49
75
  })),
50
76
  }, { output: process.stderr });
51
77
  }
52
- function availableTags(config) {
53
- return `Available tags: ${config.deployments.map((candidate) => candidate.tag).join(', ')}`;
78
+ export function availableDeployments(config, deployments = config.deployments) {
79
+ return `Available deployments: ${deployments.map(deploymentLabel).join(', ')}`;
54
80
  }
55
81
  export function deploymentToTarget(deployment) {
56
82
  return {
@@ -17,8 +17,21 @@
17
17
  "<%= config.bin %> abort wf-instance.abc123 --reason 'superseded by relaunch'"
18
18
  ],
19
19
  "flags": {
20
+ "deployment": {
21
+ "description": "Deployment name — read the instance from the resource that deployment targets; the tag partition still comes from the loaded instance.",
22
+ "exclusive": [
23
+ "tag"
24
+ ],
25
+ "name": "deployment",
26
+ "hasDynamicHelp": false,
27
+ "multiple": false,
28
+ "type": "option"
29
+ },
20
30
  "tag": {
21
- "description": "Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.",
31
+ "description": "Workflow environment tag (e.g. prod, test) — an optional query filter, and the resource disambiguator when the config spans several.",
32
+ "exclusive": [
33
+ "deployment"
34
+ ],
22
35
  "name": "tag",
23
36
  "hasDynamicHelp": false,
24
37
  "multiple": false,
@@ -54,6 +67,7 @@
54
67
  "args": {},
55
68
  "description": "Validate, diff, and deploy workflow definitions to the resource bound by the selected deployment.",
56
69
  "examples": [
70
+ "<%= config.bin %> deploy --deployment review-prod",
57
71
  "<%= config.bin %> deploy --tag prod",
58
72
  "<%= config.bin %> deploy --all-tags",
59
73
  "<%= config.bin %> deploy --check",
@@ -61,17 +75,31 @@
61
75
  "<%= config.bin %> deploy --only productLaunch"
62
76
  ],
63
77
  "flags": {
78
+ "deployment": {
79
+ "description": "Deployment name — the unique identity of one deployment in the config.",
80
+ "exclusive": [
81
+ "tag"
82
+ ],
83
+ "name": "deployment",
84
+ "hasDynamicHelp": false,
85
+ "multiple": false,
86
+ "type": "option"
87
+ },
64
88
  "tag": {
65
- "description": "Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.",
89
+ "description": "Workflow environment tag (e.g. prod, test) — deploys every deployment carrying the tag (a tag is an environment group).",
90
+ "exclusive": [
91
+ "deployment"
92
+ ],
66
93
  "name": "tag",
67
94
  "hasDynamicHelp": false,
68
95
  "multiple": false,
69
96
  "type": "option"
70
97
  },
71
98
  "all-tags": {
72
- "description": "Deploy every deployment in the config, not just one tag.",
99
+ "description": "Deploy every deployment in the config, not just a selection.",
73
100
  "exclusive": [
74
- "tag"
101
+ "tag",
102
+ "deployment"
75
103
  ],
76
104
  "name": "all-tags",
77
105
  "allowNo": false,
@@ -136,8 +164,21 @@
136
164
  "<%= config.bin %> diagnose wf-instance.abc123 --json"
137
165
  ],
138
166
  "flags": {
167
+ "deployment": {
168
+ "description": "Deployment name — read the instance from the resource that deployment targets; the tag partition still comes from the loaded instance.",
169
+ "exclusive": [
170
+ "tag"
171
+ ],
172
+ "name": "deployment",
173
+ "hasDynamicHelp": false,
174
+ "multiple": false,
175
+ "type": "option"
176
+ },
139
177
  "tag": {
140
- "description": "Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.",
178
+ "description": "Workflow environment tag (e.g. prod, test) — an optional query filter, and the resource disambiguator when the config spans several.",
179
+ "exclusive": [
180
+ "deployment"
181
+ ],
141
182
  "name": "tag",
142
183
  "hasDynamicHelp": false,
143
184
  "multiple": false,
@@ -183,8 +224,21 @@
183
224
  "<%= config.bin %> fire-action wf-instance.abc123 --activity publish --action publish --param note=shipping"
184
225
  ],
185
226
  "flags": {
227
+ "deployment": {
228
+ "description": "Deployment name — read the instance from the resource that deployment targets; the tag partition still comes from the loaded instance.",
229
+ "exclusive": [
230
+ "tag"
231
+ ],
232
+ "name": "deployment",
233
+ "hasDynamicHelp": false,
234
+ "multiple": false,
235
+ "type": "option"
236
+ },
186
237
  "tag": {
187
- "description": "Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.",
238
+ "description": "Workflow environment tag (e.g. prod, test) — an optional query filter, and the resource disambiguator when the config spans several.",
239
+ "exclusive": [
240
+ "deployment"
241
+ ],
188
242
  "name": "tag",
189
243
  "hasDynamicHelp": false,
190
244
  "multiple": false,
@@ -249,7 +303,7 @@
249
303
  ],
250
304
  "flags": {
251
305
  "tag": {
252
- "description": "Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.",
306
+ "description": "Workflow environment tag (e.g. prod, test) — an optional query filter, and the resource disambiguator when the config spans several.",
253
307
  "name": "tag",
254
308
  "hasDynamicHelp": false,
255
309
  "multiple": false,
@@ -312,14 +366,20 @@
312
366
  "args": {},
313
367
  "description": "The reset for a dataset holding engine documents the versioned upgrade framework cannot yet migrate: deletes the tag's instances, definitions, and guards (across every alias-bound resource). Content documents are never touched. Prints a dry-run plan, then requires you to type back every involved dataset (--force skips the prompt; the plan still prints).",
314
368
  "examples": [
315
- "<%= config.bin %> nuke --tag plugin-dev",
369
+ "<%= config.bin %> nuke --deployment plugin-dev",
316
370
  "<%= config.bin %> nuke --tag plugin-dev --force"
317
371
  ],
318
372
  "flags": {
373
+ "deployment": {
374
+ "description": "The deployment name to reset.",
375
+ "name": "deployment",
376
+ "hasDynamicHelp": false,
377
+ "multiple": false,
378
+ "type": "option"
379
+ },
319
380
  "tag": {
320
- "description": "The deployment tag to reset. Required a destructive reset never guesses the environment.",
381
+ "description": "The deployment tag to reset (while it names exactly one deployment).",
321
382
  "name": "tag",
322
- "required": true,
323
383
  "hasDynamicHelp": false,
324
384
  "multiple": false,
325
385
  "type": "option"
@@ -398,8 +458,21 @@
398
458
  "<%= config.bin %> set-stage wf-instance.abc123 --to ready --reason 'unblock for demo'"
399
459
  ],
400
460
  "flags": {
461
+ "deployment": {
462
+ "description": "Deployment name — read the instance from the resource that deployment targets; the tag partition still comes from the loaded instance.",
463
+ "exclusive": [
464
+ "tag"
465
+ ],
466
+ "name": "deployment",
467
+ "hasDynamicHelp": false,
468
+ "multiple": false,
469
+ "type": "option"
470
+ },
401
471
  "tag": {
402
- "description": "Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.",
472
+ "description": "Workflow environment tag (e.g. prod, test) — an optional query filter, and the resource disambiguator when the config spans several.",
473
+ "exclusive": [
474
+ "deployment"
475
+ ],
403
476
  "name": "tag",
404
477
  "hasDynamicHelp": false,
405
478
  "multiple": false,
@@ -454,7 +527,7 @@
454
527
  ],
455
528
  "flags": {
456
529
  "tag": {
457
- "description": "Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.",
530
+ "description": "Workflow environment tag (e.g. prod, test) — an optional query filter, and the resource disambiguator when the config spans several.",
458
531
  "name": "tag",
459
532
  "hasDynamicHelp": false,
460
533
  "multiple": false,
@@ -506,8 +579,21 @@
506
579
  "<%= config.bin %> start productLaunch --instance-id prod.wf-instance.a1b2c3d4e5f6"
507
580
  ],
508
581
  "flags": {
582
+ "deployment": {
583
+ "description": "Deployment name — the unique identity of one deployment in the config.",
584
+ "exclusive": [
585
+ "tag"
586
+ ],
587
+ "name": "deployment",
588
+ "hasDynamicHelp": false,
589
+ "multiple": false,
590
+ "type": "option"
591
+ },
509
592
  "tag": {
510
- "description": "Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.",
593
+ "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.",
594
+ "exclusive": [
595
+ "deployment"
596
+ ],
511
597
  "name": "tag",
512
598
  "hasDynamicHelp": false,
513
599
  "multiple": false,
@@ -574,7 +660,7 @@
574
660
  ],
575
661
  "flags": {
576
662
  "tag": {
577
- "description": "Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.",
663
+ "description": "Workflow environment tag (e.g. prod, test) — an optional query filter, and the resource disambiguator when the config spans several.",
578
664
  "name": "tag",
579
665
  "hasDynamicHelp": false,
580
666
  "multiple": false,
@@ -614,8 +700,21 @@
614
700
  "<%= config.bin %> definition delete my-workflow --cascade --reason 'workflow retired'"
615
701
  ],
616
702
  "flags": {
703
+ "deployment": {
704
+ "description": "Deployment name — the unique identity of one deployment in the config.",
705
+ "exclusive": [
706
+ "tag"
707
+ ],
708
+ "name": "deployment",
709
+ "hasDynamicHelp": false,
710
+ "multiple": false,
711
+ "type": "option"
712
+ },
617
713
  "tag": {
618
- "description": "Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.",
714
+ "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.",
715
+ "exclusive": [
716
+ "deployment"
717
+ ],
619
718
  "name": "tag",
620
719
  "hasDynamicHelp": false,
621
720
  "multiple": false,
@@ -675,8 +774,21 @@
675
774
  "<%= config.bin %> definition diff productLaunch --version 2"
676
775
  ],
677
776
  "flags": {
777
+ "deployment": {
778
+ "description": "Deployment name — the unique identity of one deployment in the config.",
779
+ "exclusive": [
780
+ "tag"
781
+ ],
782
+ "name": "deployment",
783
+ "hasDynamicHelp": false,
784
+ "multiple": false,
785
+ "type": "option"
786
+ },
678
787
  "tag": {
679
- "description": "Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.",
788
+ "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.",
789
+ "exclusive": [
790
+ "deployment"
791
+ ],
680
792
  "name": "tag",
681
793
  "hasDynamicHelp": false,
682
794
  "multiple": false,
@@ -718,7 +830,7 @@
718
830
  ],
719
831
  "flags": {
720
832
  "tag": {
721
- "description": "Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.",
833
+ "description": "Workflow environment tag (e.g. prod, test) — an optional query filter, and the resource disambiguator when the config spans several.",
722
834
  "name": "tag",
723
835
  "hasDynamicHelp": false,
724
836
  "multiple": false,
@@ -770,7 +882,7 @@
770
882
  "description": "Show a deployed workflow definition.",
771
883
  "flags": {
772
884
  "tag": {
773
- "description": "Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.",
885
+ "description": "Workflow environment tag (e.g. prod, test) — an optional query filter, and the resource disambiguator when the config spans several.",
774
886
  "name": "tag",
775
887
  "hasDynamicHelp": false,
776
888
  "multiple": false,
@@ -801,5 +913,5 @@
801
913
  ]
802
914
  }
803
915
  },
804
- "version": "0.13.0"
916
+ "version": "0.14.0"
805
917
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/workflow-cli",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Command-line tool for deploying, inspecting, and administering Sanity workflow definitions and instances.",
5
5
  "keywords": [
6
6
  "cli",
@@ -56,15 +56,15 @@
56
56
  "jiti": "^2.7.0",
57
57
  "log-symbols": "^7.0.1",
58
58
  "ora": "^9.4.0",
59
- "@sanity/workflow-engine": "0.18.0"
59
+ "@sanity/workflow-engine": "0.19.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/diff": "^8.0.0",
63
63
  "@types/node": "^24.12.4",
64
64
  "oclif": "^4.23.16",
65
65
  "vitest": "^4.1.8",
66
- "@sanity/workflow-engine-test": "0.13.0",
67
- "@sanity/workflow-examples": "0.8.0"
66
+ "@sanity/workflow-engine-test": "0.13.1",
67
+ "@sanity/workflow-examples": "0.8.1"
68
68
  },
69
69
  "oclif": {
70
70
  "bin": "sanity-workflows",