@sanity/workflow-cli 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +485 -0
  2. package/README.md +145 -63
  3. package/bin/run.js +0 -4
  4. package/dist/commands/abort.d.ts +8 -12
  5. package/dist/commands/abort.js +24 -34
  6. package/dist/commands/definition/delete.d.ts +6 -6
  7. package/dist/commands/definition/delete.js +17 -33
  8. package/dist/commands/definition/diff.d.ts +10 -3
  9. package/dist/commands/definition/diff.js +21 -29
  10. package/dist/commands/definition/list.d.ts +5 -6
  11. package/dist/commands/definition/list.js +44 -50
  12. package/dist/commands/definition/show.d.ts +22 -13
  13. package/dist/commands/definition/show.js +25 -36
  14. package/dist/commands/deploy.d.ts +67 -8
  15. package/dist/commands/deploy.js +136 -32
  16. package/dist/commands/diagnose.d.ts +22 -4
  17. package/dist/commands/diagnose.js +45 -61
  18. package/dist/commands/fire-action.d.ts +7 -30
  19. package/dist/commands/fire-action.js +42 -115
  20. package/dist/commands/list.d.ts +9 -10
  21. package/dist/commands/list.js +49 -62
  22. package/dist/commands/{retry-activity.d.ts → reset-activity.d.ts} +1 -1
  23. package/dist/commands/{retry-activity.js → reset-activity.js} +2 -3
  24. package/dist/commands/set-stage.d.ts +27 -3
  25. package/dist/commands/set-stage.js +63 -12
  26. package/dist/commands/show.d.ts +3 -2
  27. package/dist/commands/show.js +15 -21
  28. package/dist/commands/start.d.ts +56 -0
  29. package/dist/commands/start.js +133 -0
  30. package/dist/commands/tail.d.ts +5 -2
  31. package/dist/commands/tail.js +25 -26
  32. package/dist/hooks/finally/telemetry.d.ts +8 -0
  33. package/dist/hooks/finally/telemetry.js +13 -0
  34. package/dist/hooks/prerun/telemetry.d.ts +5 -0
  35. package/dist/hooks/prerun/telemetry.js +5 -0
  36. package/dist/index.js +0 -3
  37. package/dist/lib/base-command.d.ts +14 -0
  38. package/dist/lib/base-command.js +10 -0
  39. package/dist/lib/client.d.ts +11 -14
  40. package/dist/lib/client.js +29 -34
  41. package/dist/lib/context.d.ts +98 -0
  42. package/dist/lib/context.js +83 -0
  43. package/dist/lib/definitions.d.ts +38 -29
  44. package/dist/lib/definitions.js +34 -93
  45. package/dist/lib/diff.d.ts +5 -2
  46. package/dist/lib/diff.js +62 -20
  47. package/dist/lib/env.d.ts +4 -6
  48. package/dist/lib/env.js +4 -9
  49. package/dist/lib/fail.d.ts +12 -0
  50. package/dist/lib/fail.js +25 -16
  51. package/dist/lib/flags.d.ts +7 -2
  52. package/dist/lib/flags.js +7 -3
  53. package/dist/lib/load-config.d.ts +15 -0
  54. package/dist/lib/load-config.js +89 -0
  55. package/dist/lib/operation-args.d.ts +20 -15
  56. package/dist/lib/operation-args.js +9 -40
  57. package/dist/lib/ops-report.d.ts +29 -13
  58. package/dist/lib/ops-report.js +20 -17
  59. package/dist/lib/params.d.ts +7 -0
  60. package/dist/lib/params.js +18 -0
  61. package/dist/lib/read-fanout.d.ts +22 -0
  62. package/dist/lib/read-fanout.js +28 -0
  63. package/dist/lib/select-deployment.d.ts +31 -0
  64. package/dist/lib/select-deployment.js +37 -0
  65. package/dist/lib/share-definitions.d.ts +86 -0
  66. package/dist/lib/share-definitions.js +106 -0
  67. package/dist/lib/stub.js +0 -7
  68. package/dist/lib/telemetry-setup.d.ts +66 -0
  69. package/dist/lib/telemetry-setup.js +92 -0
  70. package/dist/lib/telemetry.d.ts +100 -0
  71. package/dist/lib/telemetry.js +89 -0
  72. package/dist/lib/ui.d.ts +33 -1
  73. package/dist/lib/ui.js +32 -21
  74. package/oclif.manifest.json +133 -113
  75. package/package.json +16 -8
  76. package/dist/commands/move-stage.d.ts +0 -52
  77. package/dist/commands/move-stage.js +0 -86
  78. package/dist/lib/config.d.ts +0 -18
  79. package/dist/lib/config.js +0 -50
@@ -1,28 +1,26 @@
1
- import { Command, Flags } from '@oclif/core';
2
- import { WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, isStartableDefinition, tagScopeFilter, } from '@sanity/workflow-engine';
3
- import { Table } from 'console-table-printer';
1
+ import { Flags } from '@oclif/core';
2
+ import { WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, tagScopeFilter, } from '@sanity/workflow-engine';
4
3
  import logSymbols from 'log-symbols';
5
- import { loadClient } from "../../lib/client.js";
4
+ import { WorkflowCommand } from "../../lib/base-command.js";
5
+ import { resolveReadTargets } from "../../lib/context.js";
6
6
  import { tagFlags } from "../../lib/flags.js";
7
+ import { runReadAcrossTargets } from "../../lib/read-fanout.js";
8
+ import { clipToLimit, formatTable } from "../../lib/ui.js";
7
9
  export function buildDefinitionListQuery(flags) {
8
- const params = { limit: flags.limit };
10
+ const params = { limit: flags.limit + 1 };
9
11
  const filters = [`_type == "${WORKFLOW_DEFINITION_TYPE}"`];
10
12
  const instanceFilters = [
11
13
  `_type == "${WORKFLOW_INSTANCE_TYPE}"`,
12
14
  'definition == ^.name',
13
15
  'pinnedVersion == ^.version',
14
- // Correlate the count to each definition's own partition. Listing
15
- // can span partitions (when `--tag` is omitted), and two partitions
16
- // can hold a same-named, same-version definition — without this, one
17
- // row's count would bleed in the other partition's instances.
18
16
  'tag == ^.tag',
19
17
  ];
20
18
  if (flags.tag) {
21
19
  params.tag = flags.tag;
22
20
  filters.push(tagScopeFilter());
23
21
  }
24
- if (flags['workflow-name']) {
25
- params.name = flags['workflow-name'];
22
+ if (flags.name) {
23
+ params.name = flags.name;
26
24
  filters.push('name == $name');
27
25
  }
28
26
  const instanceMatch = instanceFilters.join(' && ');
@@ -31,14 +29,14 @@ export function buildDefinitionListQuery(flags) {
31
29
  version,
32
30
  title,
33
31
  tag,
34
- role,
32
+ _createdAt,
35
33
  "stageCount": count(stages),
36
34
  "inFlightCount": count(*[${instanceMatch} && !defined(completedAt)]),
37
35
  "totalInstances": count(*[${instanceMatch}])
38
36
  }`;
39
37
  return { groq, params };
40
38
  }
41
- export default class DefinitionList extends Command {
39
+ export default class DefinitionList extends WorkflowCommand {
42
40
  static description = 'List deployed workflow definitions.';
43
41
  static examples = [
44
42
  '<%= config.bin %> definition list',
@@ -50,49 +48,45 @@ export default class DefinitionList extends Command {
50
48
  description: 'The maximum number of definitions to return.',
51
49
  default: 100,
52
50
  }),
53
- 'workflow-name': Flags.string({
54
- description: 'Filter to a single workflow definition name (e.g. product-launch)',
51
+ name: Flags.string({
52
+ description: 'Filter to a single workflow definition name (e.g. product-launch).',
55
53
  }),
56
54
  };
57
55
  async run() {
58
56
  const { flags } = await this.parse(DefinitionList);
57
+ const targets = await resolveReadTargets(flags);
59
58
  const { groq, params } = buildDefinitionListQuery(flags);
60
- const client = loadClient();
61
- const rows = await client.fetch(groq, params);
62
- if (!rows.length) {
63
- this.log(`${logSymbols.info} no definitions found`);
64
- return;
65
- }
66
- const table = new Table({
67
- columns: [
68
- { name: 'workflow', title: 'Workflow', alignment: 'left' },
69
- { name: 'title', title: 'Title', alignment: 'left' },
70
- { name: 'tag', title: 'Tag', alignment: 'left' },
71
- { name: 'startable', title: 'Startable', alignment: 'center' },
72
- { name: 'stageCount', title: 'Stages', alignment: 'right' },
73
- { name: 'inFlightCount', title: 'In flight', alignment: 'right' },
74
- { name: 'totalInstances', title: 'Instances', alignment: 'right' },
75
- ],
59
+ const failures = await runReadAcrossTargets({
60
+ targets,
61
+ log: (line) => this.log(line),
62
+ run: async ({ client }) => {
63
+ const fetched = await client.fetch(groq, params, {
64
+ tag: 'definition.list',
65
+ });
66
+ if (!fetched.length) {
67
+ this.log(`${logSymbols.info} no definitions found`);
68
+ return;
69
+ }
70
+ const { rows, note } = clipToLimit(fetched, flags.limit);
71
+ const lines = formatTable(['workflow', 'title', 'tag', 'stages', 'in flight', 'instances', 'created'], rows.map((r) => [
72
+ `${r.name} v${r.version}`,
73
+ r.title,
74
+ r.tag,
75
+ String(r.stageCount),
76
+ String(r.inFlightCount),
77
+ String(r.totalInstances),
78
+ r._createdAt,
79
+ ]));
80
+ for (const line of lines) {
81
+ this.log(line);
82
+ }
83
+ if (note) {
84
+ this.log(note);
85
+ }
86
+ },
76
87
  });
77
- for (const r of rows) {
78
- // The doc `_id` is `tag.name.vN`, so a Definition ID column would just
79
- // repeat Workflow + Tag — and no command takes an `_id` anyway (they take
80
- // name + --tag/--version). `name vN` is this CLI's identifier format (the
81
- // `definition show` title). Only declared keys are passed — console-table-
82
- // printer renders any extra key as a stray column.
83
- //
84
- // `child`-role defs are spawn-only; the Startable column tells an admin
85
- // which definitions a human can actually start standalone.
86
- table.addRow({
87
- workflow: `${r.name} v${r.version}`,
88
- title: r.title,
89
- tag: r.tag,
90
- startable: isStartableDefinition(r) ? 'yes' : 'spawn-only',
91
- stageCount: r.stageCount,
92
- inFlightCount: r.inFlightCount,
93
- totalInstances: r.totalInstances,
94
- });
88
+ if (failures.length > 0) {
89
+ this.exit(1);
95
90
  }
96
- table.printTable();
97
91
  }
98
92
  }
@@ -1,15 +1,7 @@
1
- import { Command } from '@oclif/core';
2
- import { type WorkflowDefinition } from '@sanity/workflow-engine';
3
- interface DefinitionShowFlags {
4
- name: string;
5
- tag?: string | undefined;
6
- version?: number | undefined;
7
- }
8
- export declare function buildDefinitionShowQuery(flags: DefinitionShowFlags): {
9
- groq: string;
10
- params: Record<string, unknown>;
11
- };
12
- export default class DefinitionShow extends Command {
1
+ import { type DeployedDefinition, type WorkflowDefinition, type WorkflowResource } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../../lib/base-command.ts';
3
+ import { type ReadTarget } from '../../lib/context.ts';
4
+ export default class DefinitionShow extends WorkflowCommand {
13
5
  static description: string;
14
6
  static args: {
15
7
  name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
@@ -20,6 +12,24 @@ export default class DefinitionShow extends Command {
20
12
  };
21
13
  run(): Promise<void>;
22
14
  }
15
+ /** How a lookup names the version it asked for: `vN` for a pinned
16
+ * `--version`, `latest` when unpinned. */
17
+ export declare function versionLabel(version: number | undefined): string;
18
+ /**
19
+ * One deployed document per target that has the name: the authored
20
+ * {@link WorkflowDefinition} plus the deploy-stamped envelope (`version`,
21
+ * `contentHash`, `tag`). A probe failure propagates — with an unreadable
22
+ * resource, "which dataset holds this name" is unanswerable, unlike an
23
+ * instance-id lookup where any hit IS the instance.
24
+ */
25
+ export declare function fetchDefinitionHits({ targets, groq, params, }: {
26
+ targets: ReadTarget[];
27
+ groq: string;
28
+ params: Record<string, unknown>;
29
+ }): Promise<{
30
+ resource: WorkflowResource;
31
+ def: DeployedDefinition;
32
+ }[]>;
23
33
  /** The summary block at the top of `definition show`: a bold name + version
24
34
  * title, then the human title, description, and deploy `tag` as aligned
25
35
  * detail rows. `tag` is deployment metadata (the partition), not part of the
@@ -32,4 +42,3 @@ export declare function definitionHeader(def: WorkflowDefinition & {
32
42
  * activity's actions. Pure so it can be asserted without driving the oclif command.
33
43
  */
34
44
  export declare function describeDefinition(def: WorkflowDefinition): string[];
35
- export {};
@@ -1,25 +1,13 @@
1
1
  import { styleText } from 'node:util';
2
- import { Args, Command, Flags } from '@oclif/core';
3
- import { isTerminalStage, WORKFLOW_DEFINITION_TYPE, tagScopeFilter, } from '@sanity/workflow-engine';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { isTerminalStage, } from '@sanity/workflow-engine';
4
4
  import logSymbols from 'log-symbols';
5
- import { loadClient } from "../../lib/client.js";
5
+ import { WorkflowCommand } from "../../lib/base-command.js";
6
+ import { resolveReadTargets, soleHitOrFail } from "../../lib/context.js";
7
+ import { buildDefinitionShowQuery } from "../../lib/definitions.js";
6
8
  import { tagFlags } from "../../lib/flags.js";
7
9
  import { formatKeyValue, sectionHeader } from "../../lib/ui.js";
8
- export function buildDefinitionShowQuery(flags) {
9
- const params = { name: flags.name };
10
- const filters = [`_type == "${WORKFLOW_DEFINITION_TYPE}"`, 'name == $name'];
11
- if (flags.version !== undefined) {
12
- params.version = flags.version;
13
- filters.push('version == $version');
14
- }
15
- if (flags.tag) {
16
- params.tag = flags.tag;
17
- filters.push(tagScopeFilter());
18
- }
19
- const groq = `*[${filters.join(' && ')}] | order(version desc) [0]`;
20
- return { groq, params };
21
- }
22
- export default class DefinitionShow extends Command {
10
+ export default class DefinitionShow extends WorkflowCommand {
23
11
  static description = 'Show a deployed workflow definition.';
24
12
  static args = {
25
13
  name: Args.string({ required: true, description: 'Workflow definition name.' }),
@@ -30,18 +18,18 @@ export default class DefinitionShow extends Command {
30
18
  };
31
19
  async run() {
32
20
  const { args, flags } = await this.parse(DefinitionShow);
21
+ const targets = await resolveReadTargets(flags);
33
22
  const { groq, params } = buildDefinitionShowQuery({
34
23
  name: args.name,
35
24
  tag: flags.tag,
36
25
  version: flags.version,
37
26
  });
38
- const client = loadClient();
39
- // A deployed document: the authored {@link WorkflowDefinition} plus the
40
- // deploy-stamped envelope (`version`, `contentHash`, `tag`).
41
- const def = await client.fetch(groq, params);
27
+ const hits = await fetchDefinitionHits({ targets, groq, params });
28
+ const def = soleHitOrFail(hits, `Definition "${args.name}"`)?.def;
42
29
  if (!def) {
43
- const at = flags.version ? `v${flags.version}` : 'latest';
44
- this.error(`${logSymbols.error} no definition "${args.name}" (${at})`, { exit: 1 });
30
+ this.error(`${logSymbols.error} no definition "${args.name}" (${versionLabel(flags.version)})`, {
31
+ exit: 1,
32
+ });
45
33
  }
46
34
  this.log(definitionHeader(def, def.tag));
47
35
  this.log('');
@@ -50,11 +38,19 @@ export default class DefinitionShow extends Command {
50
38
  }
51
39
  }
52
40
  }
53
- const HEADER_PAD = 11; // "Description" — the longest label in the block
54
- /** The summary block at the top of `definition show`: a bold name + version
55
- * title, then the human title, description, and deploy `tag` as aligned
56
- * detail rows. `tag` is deployment metadata (the partition), not part of the
57
- * authored definition, so it's passed separately and omitted when unknown. */
41
+ export function versionLabel(version) {
42
+ return version === undefined ? 'latest' : `v${version}`;
43
+ }
44
+ export async function fetchDefinitionHits({ targets, groq, params, }) {
45
+ const probes = await Promise.all(targets.map(async (target) => {
46
+ const def = await target.client.fetch(groq, params, {
47
+ tag: 'definition.show',
48
+ });
49
+ return def === null ? undefined : { resource: target.resource, def };
50
+ }));
51
+ return probes.filter((hit) => hit !== undefined);
52
+ }
53
+ const HEADER_PAD = 11;
58
54
  export function definitionHeader(def, tag) {
59
55
  const rows = [
60
56
  formatKeyValue({ key: 'Title', value: def.title, padTo: HEADER_PAD }),
@@ -69,18 +65,15 @@ export function definitionHeader(def, tag) {
69
65
  }
70
66
  return [styleText('bold', `${def.name} v${def.version}`), ...rows].join('\n');
71
67
  }
72
- /** One `· action` line, annotated with the terminal status its `status.set` op sets, if any. */
73
68
  function actionLine(action) {
74
69
  const statusOp = (action.ops ?? []).find((op) => op.type === 'status.set');
75
70
  const status = statusOp ? styleText('dim', ` → ${statusOp.status}`) : '';
76
71
  return ` · ${action.name}${status}`;
77
72
  }
78
- /** A `- activity` line followed by one line per action it exposes. */
79
73
  function activityBlock(activity) {
80
74
  const title = activity.title ? styleText('dim', ` — ${activity.title}`) : '';
81
75
  return [` - ${activity.name}${title}`, ...(activity.actions ?? []).map(actionLine)];
82
76
  }
83
- /** A `• stage` line (with initial/terminal markers) followed by its activity blocks. */
84
77
  function stageBlock(stage, initialStage) {
85
78
  const title = stage.title ? styleText('dim', ` — ${stage.title}`) : '';
86
79
  const terminal = isTerminalStage(stage) ? styleText('dim', ' (terminal)') : '';
@@ -90,10 +83,6 @@ function stageBlock(stage, initialStage) {
90
83
  ...(stage.activities ?? []).flatMap(activityBlock),
91
84
  ];
92
85
  }
93
- /**
94
- * The body lines for `definition show`: every stage with its activities and each
95
- * activity's actions. Pure so it can be asserted without driving the oclif command.
96
- */
97
86
  export function describeDefinition(def) {
98
87
  return [
99
88
  sectionHeader('Stages'),
@@ -1,22 +1,81 @@
1
- import { Command } from '@oclif/core';
2
- export default class Deploy extends Command {
1
+ import { type DeployDefinitionResult, type WorkflowDefinition, type WorkflowDeployment } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
3
+ /** A deployment paired with the definitions selected + validated for it. */
4
+ interface DeployBatch {
5
+ deployment: WorkflowDeployment;
6
+ defs: WorkflowDefinition[];
7
+ }
8
+ /** A deployment whose deploy/diff threw, kept for the closing summary. */
9
+ interface DeployFailure {
10
+ deployment: Pick<WorkflowDeployment, 'name' | 'tag'>;
11
+ message: string;
12
+ }
13
+ export default class Deploy extends WorkflowCommand {
3
14
  static description: string;
4
15
  static examples: string[];
5
16
  static flags: {
17
+ 'all-tags': import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
18
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
19
  check: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
20
  only: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ 'share-definitions-with-sanity': import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
22
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
23
  };
11
24
  run(): Promise<void>;
25
+ /** Deploy (or diff, for `--dry-run`) one deployment's definitions.
26
+ * Returns the per-definition results for a real deploy; `undefined`
27
+ * for a dry run, which writes nothing. */
28
+ private processDeployment;
12
29
  private executeDeploy;
13
30
  private runDryRun;
14
31
  }
15
32
  /** Reject the contradictory `--check --dry-run` combination. */
16
33
  export declare function validateModeFlags(check: boolean, dryRun: boolean): void;
17
- /** One line per deployed definition, symbol-tagged by its result status. */
18
- export declare function deployResultLines(results: {
19
- status: string;
20
- definition: string;
21
- version: number;
22
- }[]): string[];
34
+ /**
35
+ * Select and validate every deployment's definitions up front. Validation runs
36
+ * before any network write ({@link validateOrFail} exits on a bad definition),
37
+ * so a whole-config deploy can never leave a partial reconcile behind a broken
38
+ * batch. In a multi-deployment run the failure names its deployment — it exits
39
+ * before any banner could attribute it.
40
+ */
41
+ export declare function buildBatches(deployments: WorkflowDeployment[], only: string | undefined): DeployBatch[];
42
+ /**
43
+ * Run `processBatch` over each batch, continuing past a failure so one bad
44
+ * deployment can't strand the rest — deployments are independent, and deploys
45
+ * are idempotent, so a partial run is safe to re-run. Failures are flattened
46
+ * to their message for {@link deploySummary}. A single-batch run rethrows
47
+ * instead: it has no summary, so a throw from before the deploy spinner starts
48
+ * would otherwise surface nowhere — and propagating keeps the stack visible.
49
+ * An auth rejection also rethrows regardless of batch count: a dead token is
50
+ * never batch-local, so continuing would just repeat the same failure.
51
+ */
52
+ export declare function reconcileBatches({ batches, log, processBatch, }: {
53
+ batches: DeployBatch[];
54
+ log: (line: string) => void;
55
+ processBatch: (batch: DeployBatch) => Promise<void>;
56
+ }): Promise<DeployFailure[]>;
57
+ /**
58
+ * The closing summary for a multi-deployment run that had failures: the
59
+ * succeeded/failed tally and each failed deployment. Empty for a clean run.
60
+ * The wording is mode-neutral because a `--dry-run` failure lands here too,
61
+ * where nothing was deployed. Single-batch runs never reach it — their lone
62
+ * failure rethrows out of {@link reconcileBatches}.
63
+ */
64
+ export declare function deploySummary({ total, failures, }: {
65
+ total: number;
66
+ failures: DeployFailure[];
67
+ }): string[];
68
+ /**
69
+ * The banner printed above a deployment's output so a multi-deployment run is
70
+ * attributable. `undefined` for a single-target run — it keeps its original
71
+ * unprefixed output.
72
+ */
73
+ export declare function deploymentBanner({ deployment, index, total, }: {
74
+ deployment: Pick<WorkflowDeployment, 'name' | 'tag'>;
75
+ index: number;
76
+ total: number;
77
+ }): string | undefined;
78
+ /** One line per deployed definition, symbol-tagged by its result status,
79
+ * followed by any advisory effect-output lint warnings indented beneath it. */
80
+ export declare function deployResultLines(results: DeployDefinitionResult[]): string[];
81
+ export {};
@@ -1,23 +1,35 @@
1
- import { Command, Flags } from '@oclif/core';
2
- import { computeDiffEntries, workflow } from '@sanity/workflow-engine';
1
+ import { styleText } from 'node:util';
2
+ import { Flags } from '@oclif/core';
3
+ import { computeDiffEntries, errorMessage, workflow, } from '@sanity/workflow-engine';
3
4
  import logSymbols from 'log-symbols';
4
5
  import ora from 'ora';
5
- import { buildClient, loadClient } from "../lib/client.js";
6
- import { loadWorkflowConfig } from "../lib/config.js";
7
- import { loadValidatedDefinitions } from "../lib/definitions.js";
6
+ import { WorkflowCommand } from "../lib/base-command.js";
7
+ import { clientFor, resolveTokenOrFail } from "../lib/client.js";
8
+ import { selectDefinitions, validateOrFail } from "../lib/definitions.js";
8
9
  import { diffReport } from "../lib/diff.js";
9
- import { fail } from "../lib/fail.js";
10
+ import { fail, isAuthRejection } from "../lib/fail.js";
10
11
  import { tagFlags } from "../lib/flags.js";
11
- export default class Deploy extends Command {
12
- static description = 'Validate, diff, and deploy workflow definitions to the configured dataset.';
12
+ import { loadWorkflowConfig } from "../lib/load-config.js";
13
+ import { deploymentToTarget, selectDeployments } from "../lib/select-deployment.js";
14
+ import { shareDefinitionsAfterDeploy } from "../lib/share-definitions.js";
15
+ import { cliTelemetry } from "../lib/telemetry.js";
16
+ import { groupBanner } from "../lib/ui.js";
17
+ export default class Deploy extends WorkflowCommand {
18
+ static description = 'Validate, diff, and deploy workflow definitions to the resource bound by the selected deployment.';
13
19
  static examples = [
14
- '<%= config.bin %> deploy',
20
+ '<%= config.bin %> deploy --tag prod',
21
+ '<%= config.bin %> deploy --all-tags',
15
22
  '<%= config.bin %> deploy --check',
16
23
  '<%= config.bin %> deploy --dry-run',
17
24
  '<%= config.bin %> deploy --only productLaunch',
18
25
  ];
19
26
  static flags = {
20
27
  ...tagFlags,
28
+ 'all-tags': Flags.boolean({
29
+ description: 'Deploy every deployment in the config, not just one tag.',
30
+ default: false,
31
+ exclusive: ['tag'],
32
+ }),
21
33
  'dry-run': Flags.boolean({
22
34
  description: 'Validate + diff against the deployed version; do not write.',
23
35
  default: false,
@@ -27,70 +39,162 @@ export default class Deploy extends Command {
27
39
  default: false,
28
40
  }),
29
41
  only: Flags.string({
30
- description: 'Limit deploy/check/diff to a single workflow definition by name.',
42
+ description: 'Limit deploy/check/diff to a single workflow definition by name. Every targeted deployment must contain it.',
43
+ }),
44
+ 'share-definitions-with-sanity': Flags.boolean({
45
+ description: 'Share the definition documents newly created by this deploy with Sanity — the full document, verbatim (structure, names, filters, effect configuration, seeded values), plus its deployment coordinates (project and dataset, or resource id); never content documents, instances, or your Sanity auth token. Opt-in for this invocation only; the CLI never prompts. Independent of telemetry consent.',
46
+ default: false,
31
47
  }),
32
48
  };
33
49
  async run() {
34
50
  const { flags } = await this.parse(Deploy);
35
51
  validateModeFlags(flags.check, flags['dry-run']);
36
- const config = loadWorkflowConfig(flags.tag);
37
- const selected = await loadValidatedDefinitions(flags.only);
52
+ const config = await loadWorkflowConfig();
53
+ const batches = buildBatches(selectDeployments(config, { tag: flags.tag, allTags: flags['all-tags'] }), flags.only);
54
+ const log = (line) => this.log(line);
38
55
  if (flags.check) {
39
- this.log(`${logSymbols.success} ${selected.length} definition(s) passed validation (check only — dataset not contacted).`);
56
+ await reconcileBatches({
57
+ batches,
58
+ log,
59
+ processBatch: async ({ defs }) => {
60
+ log(`${logSymbols.success} ${defs.length} definition(s) passed validation (check only — dataset not contacted).`);
61
+ },
62
+ });
40
63
  return;
41
64
  }
42
- const client = loadClient();
43
- if (flags['dry-run']) {
44
- await this.runDryRun({ client, defs: selected, config });
45
- return;
65
+ const token = await resolveTokenOrFail();
66
+ const candidates = [];
67
+ const failures = await reconcileBatches({
68
+ batches,
69
+ log,
70
+ processBatch: async ({ deployment, defs }) => {
71
+ const client = clientFor(deployment.workflowResource, token);
72
+ const deployed = await this.processDeployment({
73
+ deployment,
74
+ defs,
75
+ dryRun: flags['dry-run'],
76
+ client,
77
+ });
78
+ if (deployed !== undefined) {
79
+ candidates.push({
80
+ client,
81
+ tag: deployment.tag,
82
+ resource: deployment.workflowResource,
83
+ results: deployed.results,
84
+ deployId: deployed.deployId,
85
+ });
86
+ }
87
+ },
88
+ });
89
+ for (const line of deploySummary({ total: batches.length, failures })) {
90
+ log(line);
46
91
  }
47
- await this.executeDeploy({ client, defs: selected, config });
92
+ await shareDefinitionsAfterDeploy({
93
+ flag: flags['share-definitions-with-sanity'],
94
+ candidates,
95
+ warn: (message) => this.warn(message),
96
+ });
97
+ if (failures.length > 0) {
98
+ this.exit(1);
99
+ }
100
+ }
101
+ async processDeployment({ deployment, defs, dryRun, client, }) {
102
+ const target = deploymentToTarget(deployment);
103
+ if (dryRun) {
104
+ await this.runDryRun({ client, defs, target });
105
+ return undefined;
106
+ }
107
+ return this.executeDeploy({ client, defs, target });
48
108
  }
49
- async executeDeploy({ client, defs, config, }) {
109
+ async executeDeploy({ client, defs, target, }) {
50
110
  const spinner = ora(`Deploying ${defs.length} definition(s)…`).start();
51
111
  try {
52
- const { results } = await workflow.deployDefinitions({
53
- workflowResource: config.workflowResource,
54
- tag: config.tag,
112
+ const deployed = await workflow.deployDefinitions({
113
+ ...target,
55
114
  client,
56
115
  definitions: defs,
116
+ telemetry: cliTelemetry().logger,
57
117
  });
58
- spinner.succeed(`Processed ${results.length} definition(s)`);
59
- for (const line of deployResultLines(results))
118
+ spinner.succeed(`Processed ${deployed.results.length} definition(s)`);
119
+ for (const line of deployResultLines(deployed.results))
60
120
  this.log(line);
121
+ return deployed;
61
122
  }
62
123
  catch (error) {
63
- spinner.fail('Deploy failed');
124
+ spinner.fail(`Deploy failed — ${errorMessage(error)}`);
64
125
  throw error;
65
126
  }
66
127
  }
67
- async runDryRun({ client, defs, config, }) {
128
+ async runDryRun({ client, defs, target, }) {
68
129
  const spinner = ora(`Diffing ${defs.length} definition(s) against dataset…`).start();
69
130
  try {
70
- const entries = await computeDiffEntries({ client, defs, target: config });
131
+ const entries = await computeDiffEntries({ client, defs, target });
71
132
  spinner.succeed(`Diffed ${defs.length} definition(s)`);
72
133
  for (const line of diffReport(entries))
73
134
  this.log(line);
74
135
  }
75
136
  catch (error) {
76
- spinner.fail('Diff failed');
137
+ spinner.fail(`Diff failed — ${errorMessage(error)}`);
77
138
  throw error;
78
139
  }
79
140
  }
80
141
  }
81
- /** Reject the contradictory `--check --dry-run` combination. */
82
142
  export function validateModeFlags(check, dryRun) {
83
143
  if (check && dryRun) {
84
144
  fail('Pass either --check or --dry-run, not both.');
85
145
  }
86
146
  }
87
- /** One line per deployed definition, symbol-tagged by its result status. */
147
+ function deploymentLabel({ name, tag }) {
148
+ return `${name} (${tag})`;
149
+ }
150
+ export function buildBatches(deployments, only) {
151
+ return deployments.map((deployment) => {
152
+ const context = deployments.length > 1 ? `${deploymentLabel(deployment)} — ` : '';
153
+ const defs = selectDefinitions(deployment.definitions, { only, context });
154
+ validateOrFail(defs, context);
155
+ return { deployment, defs };
156
+ });
157
+ }
158
+ export async function reconcileBatches({ batches, log, processBatch, }) {
159
+ const failures = [];
160
+ for (const [index, batch] of batches.entries()) {
161
+ const banner = deploymentBanner({ deployment: batch.deployment, index, total: batches.length });
162
+ if (banner !== undefined) {
163
+ log(banner);
164
+ }
165
+ try {
166
+ await processBatch(batch);
167
+ }
168
+ catch (error) {
169
+ if (batches.length === 1 || isAuthRejection(error)) {
170
+ throw error;
171
+ }
172
+ failures.push({ deployment: batch.deployment, message: errorMessage(error) });
173
+ }
174
+ }
175
+ return failures;
176
+ }
177
+ export function deploySummary({ total, failures, }) {
178
+ if (failures.length === 0) {
179
+ return [];
180
+ }
181
+ return [
182
+ '',
183
+ styleText('bold', `${total - failures.length} of ${total} deployments succeeded — ${failures.length} failed:`),
184
+ ...failures.map((f) => styleText('red', ` ${logSymbols.error} ${deploymentLabel(f.deployment)} — ${f.message}`)),
185
+ ];
186
+ }
187
+ export function deploymentBanner({ deployment, index, total, }) {
188
+ return groupBanner({ label: deploymentLabel(deployment), index, total });
189
+ }
88
190
  export function deployResultLines(results) {
89
191
  const symbols = {
90
192
  created: logSymbols.success,
91
193
  };
92
- return results.map((r) => {
194
+ return results.flatMap((r) => {
93
195
  const symbol = symbols[r.status] ?? logSymbols.warning;
94
- return ` ${symbol} ${r.status.padEnd(9)} ${r.definition} v${r.version}`;
196
+ const head = ` ${symbol} ${r.status.padEnd(9)} ${r.name} v${r.version}`;
197
+ const warnings = (r.warnings ?? []).map((w) => ` ${logSymbols.warning} ${w}`);
198
+ return [head, ...warnings];
95
199
  });
96
200
  }