@sanity/workflow-cli 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/bin/run.js +8 -0
- package/dist/commands/abort.d.ts +30 -0
- package/dist/commands/abort.js +62 -0
- package/dist/commands/definition/delete.d.ts +36 -0
- package/dist/commands/definition/delete.js +86 -0
- package/dist/commands/definition/diff.d.ts +13 -0
- package/dist/commands/definition/diff.js +57 -0
- package/dist/commands/definition/list.d.ts +31 -0
- package/dist/commands/definition/list.js +83 -0
- package/dist/commands/definition/show.d.ts +33 -0
- package/dist/commands/definition/show.js +118 -0
- package/dist/commands/deploy.d.ts +22 -0
- package/dist/commands/deploy.js +97 -0
- package/dist/commands/diagnose.d.ts +23 -0
- package/dist/commands/diagnose.js +269 -0
- package/dist/commands/fire-action.d.ts +63 -0
- package/dist/commands/fire-action.js +241 -0
- package/dist/commands/list.d.ts +46 -0
- package/dist/commands/list.js +106 -0
- package/dist/commands/move-stage.d.ts +47 -0
- package/dist/commands/move-stage.js +77 -0
- package/dist/commands/retry-task.d.ts +9 -0
- package/dist/commands/retry-task.js +11 -0
- package/dist/commands/set-stage.d.ts +12 -0
- package/dist/commands/set-stage.js +18 -0
- package/dist/commands/show.d.ts +23 -0
- package/dist/commands/show.js +85 -0
- package/dist/commands/tail.d.ts +15 -0
- package/dist/commands/tail.js +73 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/lib/client.d.ts +17 -0
- package/dist/lib/client.js +40 -0
- package/dist/lib/config.d.ts +18 -0
- package/dist/lib/config.js +50 -0
- package/dist/lib/definitions.d.ts +36 -0
- package/dist/lib/definitions.js +122 -0
- package/dist/lib/diff.d.ts +7 -0
- package/dist/lib/diff.js +49 -0
- package/dist/lib/env.d.ts +10 -0
- package/dist/lib/env.js +13 -0
- package/dist/lib/fail.d.ts +16 -0
- package/dist/lib/fail.js +33 -0
- package/dist/lib/flags.d.ts +5 -0
- package/dist/lib/flags.js +8 -0
- package/dist/lib/operation-args.d.ts +25 -0
- package/dist/lib/operation-args.js +48 -0
- package/dist/lib/ops-report.d.ts +30 -0
- package/dist/lib/ops-report.js +28 -0
- package/dist/lib/stub.d.ts +10 -0
- package/dist/lib/stub.js +24 -0
- package/oclif.manifest.json +684 -0
- package/package.json +94 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sanity, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @sanity/workflow-cli
|
|
2
|
+
|
|
3
|
+
Command-line tool for deploying, inspecting, and administering Sanity workflow
|
|
4
|
+
definitions and instances.
|
|
5
|
+
|
|
6
|
+
> [!WARNING]
|
|
7
|
+
> Early access, restricted. `retry-task` and `set-stage` are still stubs —
|
|
8
|
+
> they print their parsed input and exit non-zero. The "wired" commands below
|
|
9
|
+
> talk to a real Sanity dataset using `SANITY_*` env vars.
|
|
10
|
+
|
|
11
|
+
## Run
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm --filter @sanity/workflow-cli dev <command> [...args]
|
|
15
|
+
|
|
16
|
+
# Examples:
|
|
17
|
+
pnpm --filter @sanity/workflow-cli dev --help
|
|
18
|
+
pnpm --filter @sanity/workflow-cli dev deploy --dry-run
|
|
19
|
+
pnpm --filter @sanity/workflow-cli dev list --in-flight
|
|
20
|
+
pnpm --filter @sanity/workflow-cli dev show wf-instance.abc123
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Copy the template and fill it in first: `cp ../../.env.example ../../.env`
|
|
24
|
+
(from the repo root, `cp .env.example .env`). Env vars are read from
|
|
25
|
+
the root `.env` (`SANITY_PROJECT_ID`, `SANITY_DATASET`,
|
|
26
|
+
`SANITY_AUTH_TOKEN`). The dev script loads them via `tsx --env-file`.
|
|
27
|
+
|
|
28
|
+
### Workflow target (resource + tags)
|
|
29
|
+
|
|
30
|
+
The CLI targets one engine "resource" (a dataset, canvas, media
|
|
31
|
+
library, or dashboard) tagged with one or more engine tags. Defaults
|
|
32
|
+
match the bench fixtures the rest of this repo uses.
|
|
33
|
+
|
|
34
|
+
| Env var | Default | Allowed values |
|
|
35
|
+
| ------------------------ | ------------ | ------------------------------------------------------- |
|
|
36
|
+
| `WORKFLOW_RESOURCE_TYPE` | `dataset` | `dataset` / `canvas` / `media-library` / `dashboard` |
|
|
37
|
+
| `WORKFLOW_RESOURCE_ID` | `test.test` | any string |
|
|
38
|
+
| `WORKFLOW_TAG` | _(required)_ | single environment tag, e.g. `prod` (`--tag` overrides) |
|
|
39
|
+
| `WORKFLOW_DEFS` | _(required)_ | module path or package exporting `allDefinitions` |
|
|
40
|
+
|
|
41
|
+
Invalid values fail with a clean error before the command runs.
|
|
42
|
+
|
|
43
|
+
`WORKFLOW_DEFS` has no default — `deploy` is agnostic about which
|
|
44
|
+
definitions it ships, so it must be told where to find them. Point it at a
|
|
45
|
+
module (a path like `./src/workflows.ts`, or a package name) whose
|
|
46
|
+
`allDefinitions` export is a `WorkflowDefinition[]`. This repo's own demo
|
|
47
|
+
set lives in `@sanity/workflow-examples`, so `WORKFLOW_DEFS=@sanity/workflow-examples`
|
|
48
|
+
deploys it.
|
|
49
|
+
|
|
50
|
+
Run through the `dev` / `cli` scripts and this loads straight from
|
|
51
|
+
TypeScript source — no build step between editing a definition and
|
|
52
|
+
deploying it. Those scripts set `NODE_OPTIONS='--conditions=development'`
|
|
53
|
+
and run under `tsx`, so a package specifier resolves to its `src/` via the
|
|
54
|
+
`development` export condition and the `.ts` is transpiled on the fly. A
|
|
55
|
+
built or standalone CLI instead resolves the package's compiled `dist`.
|
|
56
|
+
|
|
57
|
+
> [!NOTE]
|
|
58
|
+
> pnpm intercepts a handful of its own flag names (`--check`,
|
|
59
|
+
> `--filter`, `--recursive`, …) when they appear after a script name.
|
|
60
|
+
> To invoke `deploy --check`, run from inside the package or via
|
|
61
|
+
> `pnpm exec`:
|
|
62
|
+
>
|
|
63
|
+
> ```bash
|
|
64
|
+
> # from inside packages/workflow-cli
|
|
65
|
+
> pnpm dev deploy --check
|
|
66
|
+
>
|
|
67
|
+
> # or, from anywhere
|
|
68
|
+
> pnpm --filter @sanity/workflow-cli exec \
|
|
69
|
+
> env NODE_OPTIONS='--conditions=development' \
|
|
70
|
+
> tsx --env-file-if-exists=../../.env bin/dev.js deploy --check
|
|
71
|
+
> ```
|
|
72
|
+
>
|
|
73
|
+
> `--dry-run` and `--only` don't collide with pnpm flags and work
|
|
74
|
+
> through the normal `pnpm --filter … dev …` invocation.
|
|
75
|
+
|
|
76
|
+
## Command status
|
|
77
|
+
|
|
78
|
+
| Command | Status |
|
|
79
|
+
| ------------------------------------------ | ----------------------------------------------------------------------------------------- |
|
|
80
|
+
| `deploy` | wired — calls `workflow.deployDefinitions` over the `WORKFLOW_DEFS` module |
|
|
81
|
+
| `deploy --check` | wired — runs `validateDefinition` over the local batch + a duplicate-id check |
|
|
82
|
+
| `deploy --dry-run` | wired — fetches existing docs and renders a coloured JSON diff per change |
|
|
83
|
+
| `deploy --only <id>` | wired — filters deploy/check/dry-run to one `definition` |
|
|
84
|
+
| `list` | wired — `client.fetch` over `workflow.instance` documents |
|
|
85
|
+
| `show <instance-id>` | wired — `client.getDocument` |
|
|
86
|
+
| `diagnose <instance-id>` | wired — one `workflow.evaluate` call, classifies why the instance is/isn't progressing |
|
|
87
|
+
| `tail <instance-id>` | wired — `client.listen()` over the instance, prints new history entries |
|
|
88
|
+
| `abort <instance-id>` | wired — calls `workflow.abortInstance` (hard stop: cancels pending effects, lifts guards) |
|
|
89
|
+
| `retry-task <instance-id> <task-id>` | stub |
|
|
90
|
+
| `move-stage <instance-id> --to <stage-id>` | wired — calls `workflow.setStage` (runs guards + onEnter effects) |
|
|
91
|
+
| `fire-action <instance-id>` | wired — `workflow.evaluate` lists actions; `workflow.fireAction` fires one |
|
|
92
|
+
| `set-stage <instance-id> --to <stage-id>` | stub (engine has no guard-bypass path yet) |
|
|
93
|
+
| `definition list` | wired — `client.fetch` over `workflow.definition` documents |
|
|
94
|
+
| `definition show <id>` | wired — `client.fetch`, latest version unless `--version` |
|
|
95
|
+
| `definition diff <id>` | wired — diffs the in-code definition against the deployed latest (`--version` to pin) |
|
|
96
|
+
| `definition delete <name>` | wired — calls `workflow.deleteDefinition` (refuses on live instances unless `--cascade`) |
|
|
97
|
+
|
|
98
|
+
## Known gaps
|
|
99
|
+
|
|
100
|
+
- **No `sanity workflow` plugin wiring yet.** The proposal targets the
|
|
101
|
+
Sanity CLI as a plugin host. The package publishes as a standalone
|
|
102
|
+
`sanity-workflows` binary today; plugging into `sanity` as an oclif
|
|
103
|
+
plugin is a separate piece of work.
|
|
104
|
+
- **`set-stage` manual override.** The engine's `workflow.setStage`
|
|
105
|
+
always runs guards + effects; the proposal's "bypass" semantics
|
|
106
|
+
need engine work before this command can do anything useful.
|
|
107
|
+
- **Actor identity.** Write paths default the actor to
|
|
108
|
+
`{ kind: "system", id: "workflow-cli" }` rather than resolving the real
|
|
109
|
+
user via `/users/me` (which a token-only CLI can't reach). `fire-action`
|
|
110
|
+
takes `--as <userId>` / `--as-role <role>` to attribute a fire to a real
|
|
111
|
+
user, so assignee- and role-gated action filters pass. Once we host inside
|
|
112
|
+
the Sanity CLI's auth flow, the real user can flow through everywhere.
|
|
113
|
+
|
|
114
|
+
## Tech stack
|
|
115
|
+
|
|
116
|
+
Mirrors the Sanity CLI's stack so the eventual plugin port is small:
|
|
117
|
+
|
|
118
|
+
- [`@oclif/core`](https://oclif.io) for command definitions, flags,
|
|
119
|
+
help.
|
|
120
|
+
- `@oclif/plugin-help` for help output.
|
|
121
|
+
- `ora`, `boxen`, `log-symbols`, `console-table-printer`, `picocolors`
|
|
122
|
+
for terminal UX.
|
|
123
|
+
- `diff` for the colored JSON unified diff in `deploy --dry-run`.
|
package/bin/run.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Production entry for the published package: loads the compiled commands
|
|
3
|
+
// from ./dist/commands (per the `oclif.commands` field). The dev counterpart,
|
|
4
|
+
// bin/dev.js, passes `development: true` to load TS sources instead.
|
|
5
|
+
|
|
6
|
+
import {execute} from '@oclif/core'
|
|
7
|
+
|
|
8
|
+
await execute({dir: import.meta.url})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Abort extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
instanceId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
reason: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
interface AbortOutcome {
|
|
15
|
+
fired: boolean;
|
|
16
|
+
instance: {
|
|
17
|
+
currentStage: string;
|
|
18
|
+
completedAt?: string | undefined;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Turn an `abortInstance` result into the spinner message. Pure so the
|
|
23
|
+
* fired / already-terminal permutations can be asserted without driving
|
|
24
|
+
* a real abort.
|
|
25
|
+
*/
|
|
26
|
+
export declare function abortReport(result: AbortOutcome): {
|
|
27
|
+
fired: boolean;
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { workflow } from '@sanity/workflow-engine';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { loadClient } from "../lib/client.js";
|
|
6
|
+
import { loadWorkflowConfig } from "../lib/config.js";
|
|
7
|
+
import { fail } from "../lib/fail.js";
|
|
8
|
+
import { tagFlags } from "../lib/flags.js";
|
|
9
|
+
import { buildOperationArgs } from "../lib/operation-args.js";
|
|
10
|
+
export default class Abort extends Command {
|
|
11
|
+
static description = 'Abort an in-flight workflow instance — a hard stop: pending effects are cancelled, stage guards lifted, and the instance is marked terminal where it stands.';
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> abort wf-instance.abc123',
|
|
14
|
+
"<%= config.bin %> abort wf-instance.abc123 --reason 'superseded by relaunch'",
|
|
15
|
+
];
|
|
16
|
+
static args = {
|
|
17
|
+
instanceId: Args.string({ required: true, description: 'Workflow instance id.' }),
|
|
18
|
+
};
|
|
19
|
+
static flags = {
|
|
20
|
+
...tagFlags,
|
|
21
|
+
reason: Flags.string({ description: 'Reason for aborting (recorded in history).' }),
|
|
22
|
+
};
|
|
23
|
+
async run() {
|
|
24
|
+
const { args, flags } = await this.parse(Abort);
|
|
25
|
+
const config = loadWorkflowConfig(flags.tag);
|
|
26
|
+
const client = loadClient();
|
|
27
|
+
const spinner = ora(`Aborting ${args.instanceId}…`).start();
|
|
28
|
+
try {
|
|
29
|
+
const result = await workflow.abortInstance({
|
|
30
|
+
client,
|
|
31
|
+
...buildOperationArgs(config, args.instanceId, flags.reason),
|
|
32
|
+
});
|
|
33
|
+
const report = abortReport(result);
|
|
34
|
+
if (!report.fired) {
|
|
35
|
+
spinner.warn(report.message);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
spinner.succeed(report.message);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
spinner.fail('Abort rejected');
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
fail('abortInstance error:', message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Turn an `abortInstance` result into the spinner message. Pure so the
|
|
49
|
+
* fired / already-terminal permutations can be asserted without driving
|
|
50
|
+
* a real abort.
|
|
51
|
+
*/
|
|
52
|
+
export function abortReport(result) {
|
|
53
|
+
const stage = pc.bold(result.instance.currentStage);
|
|
54
|
+
if (!result.fired) {
|
|
55
|
+
const at = result.instance.completedAt ?? 'unknown';
|
|
56
|
+
return {
|
|
57
|
+
fired: false,
|
|
58
|
+
message: `abortInstance returned without firing — instance is already terminal (at ${stage} since ${at})`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { fired: true, message: `Aborted — instance hard-stopped at ${stage}` };
|
|
62
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { type DeleteDefinitionArgs, type DeleteDefinitionResult } from '@sanity/workflow-engine';
|
|
3
|
+
import { type WorkflowConfig } from '../../lib/config.ts';
|
|
4
|
+
export default class DefinitionDelete extends Command {
|
|
5
|
+
static description: string;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static args: {
|
|
8
|
+
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
9
|
+
};
|
|
10
|
+
static flags: {
|
|
11
|
+
version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
cascade: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
reason: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The shared engine args ({@link baseEngineArgs}) plus the delete-specific
|
|
20
|
+
* targeting. `version` and `reason` spread conditionally because the
|
|
21
|
+
* engine's optionals reject an explicit `undefined` under
|
|
22
|
+
* `exactOptionalPropertyTypes`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildDeleteArgs(config: WorkflowConfig, name: string, flags: {
|
|
25
|
+
version?: number | undefined;
|
|
26
|
+
cascade: boolean;
|
|
27
|
+
reason?: string | undefined;
|
|
28
|
+
}): Omit<DeleteDefinitionArgs, 'client'>;
|
|
29
|
+
/** What the spinner names: the workflow, or one pinned version of it. */
|
|
30
|
+
export declare function deleteTargetLabel(args: Pick<DeleteDefinitionArgs, 'definition' | 'version'>): string;
|
|
31
|
+
/**
|
|
32
|
+
* Turn a `deleteDefinition` result into the spinner message. Pure so the
|
|
33
|
+
* version / cascade / guard permutations can be asserted without driving
|
|
34
|
+
* a real delete.
|
|
35
|
+
*/
|
|
36
|
+
export declare function deleteReport(result: DeleteDefinitionResult): string;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { workflow, } from '@sanity/workflow-engine';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { loadClient } from "../../lib/client.js";
|
|
6
|
+
import { loadWorkflowConfig } from "../../lib/config.js";
|
|
7
|
+
import { fail } from "../../lib/fail.js";
|
|
8
|
+
import { tagFlags } from "../../lib/flags.js";
|
|
9
|
+
import { baseEngineArgs } from "../../lib/operation-args.js";
|
|
10
|
+
export default class DefinitionDelete extends Command {
|
|
11
|
+
static description = 'Delete a deployed workflow definition (every version, or one via --version). ' +
|
|
12
|
+
'Refuses while non-terminal instances exist unless --cascade aborts them first — ' +
|
|
13
|
+
'instances are aborted in place, never deleted.';
|
|
14
|
+
static examples = [
|
|
15
|
+
'<%= config.bin %> definition delete my-workflow',
|
|
16
|
+
'<%= config.bin %> definition delete my-workflow --version 2',
|
|
17
|
+
"<%= config.bin %> definition delete my-workflow --cascade --reason 'workflow retired'",
|
|
18
|
+
];
|
|
19
|
+
static args = {
|
|
20
|
+
name: Args.string({ required: true, description: 'Workflow definition name.' }),
|
|
21
|
+
};
|
|
22
|
+
static flags = {
|
|
23
|
+
...tagFlags,
|
|
24
|
+
version: Flags.integer({
|
|
25
|
+
description: 'Delete only this deployed version (default: every version).',
|
|
26
|
+
}),
|
|
27
|
+
cascade: Flags.boolean({
|
|
28
|
+
description: 'Abort every non-terminal instance pinned to the targeted versions, then delete.',
|
|
29
|
+
default: false,
|
|
30
|
+
}),
|
|
31
|
+
reason: Flags.string({
|
|
32
|
+
description: 'Free-text reason — recorded on each cascade-abort history entry.',
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
async run() {
|
|
36
|
+
const { args, flags } = await this.parse(DefinitionDelete);
|
|
37
|
+
const config = loadWorkflowConfig(flags.tag);
|
|
38
|
+
const client = loadClient();
|
|
39
|
+
const deleteArgs = buildDeleteArgs(config, args.name, flags);
|
|
40
|
+
const spinner = ora(`Deleting definition ${deleteTargetLabel(deleteArgs)}…`).start();
|
|
41
|
+
try {
|
|
42
|
+
const result = await workflow.deleteDefinition({ client, ...deleteArgs });
|
|
43
|
+
spinner.succeed(deleteReport(result));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
spinner.fail('Delete rejected');
|
|
47
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
48
|
+
fail('deleteDefinition error:', message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The shared engine args ({@link baseEngineArgs}) plus the delete-specific
|
|
54
|
+
* targeting. `version` and `reason` spread conditionally because the
|
|
55
|
+
* engine's optionals reject an explicit `undefined` under
|
|
56
|
+
* `exactOptionalPropertyTypes`.
|
|
57
|
+
*/
|
|
58
|
+
export function buildDeleteArgs(config, name, flags) {
|
|
59
|
+
return {
|
|
60
|
+
...baseEngineArgs(config),
|
|
61
|
+
definition: name,
|
|
62
|
+
cascade: flags.cascade,
|
|
63
|
+
...(flags.version !== undefined ? { version: flags.version } : {}),
|
|
64
|
+
...(flags.reason !== undefined ? { reason: flags.reason } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** What the spinner names: the workflow, or one pinned version of it. */
|
|
68
|
+
export function deleteTargetLabel(args) {
|
|
69
|
+
return args.version === undefined ? args.definition : `${args.definition} v${args.version}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Turn a `deleteDefinition` result into the spinner message. Pure so the
|
|
73
|
+
* version / cascade / guard permutations can be asserted without driving
|
|
74
|
+
* a real delete.
|
|
75
|
+
*/
|
|
76
|
+
export function deleteReport(result) {
|
|
77
|
+
const versions = result.deletedVersions.map((v) => `v${v}`).join(', ');
|
|
78
|
+
const parts = [`Deleted ${pc.bold(result.name)} ${versions}`];
|
|
79
|
+
if (result.abortedInstanceIds.length > 0) {
|
|
80
|
+
parts.push(`aborted ${result.abortedInstanceIds.length} instance(s)`);
|
|
81
|
+
}
|
|
82
|
+
if (result.deletedGuardCount > 0) {
|
|
83
|
+
parts.push(`removed ${result.deletedGuardCount} orphaned guard doc(s)`);
|
|
84
|
+
}
|
|
85
|
+
return parts.join(' — ');
|
|
86
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class DefinitionDiff extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
definition: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { diffEntry } from '@sanity/workflow-engine';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { loadClient } from "../../lib/client.js";
|
|
5
|
+
import { loadWorkflowConfig } from "../../lib/config.js";
|
|
6
|
+
import { loadValidatedDefinition } from "../../lib/definitions.js";
|
|
7
|
+
import { diffReport } from "../../lib/diff.js";
|
|
8
|
+
import { fail } from "../../lib/fail.js";
|
|
9
|
+
import { tagFlags } from "../../lib/flags.js";
|
|
10
|
+
import { buildDefinitionShowQuery } from "./show.js";
|
|
11
|
+
export default class DefinitionDiff extends Command {
|
|
12
|
+
static description = 'Diff an in-code definition against the deployed version (latest by default).';
|
|
13
|
+
static examples = [
|
|
14
|
+
'<%= config.bin %> definition diff productLaunch',
|
|
15
|
+
'<%= config.bin %> definition diff productLaunch --version 2',
|
|
16
|
+
];
|
|
17
|
+
static args = {
|
|
18
|
+
definition: Args.string({ required: true, description: 'Workflow definition name.' }),
|
|
19
|
+
};
|
|
20
|
+
static flags = {
|
|
21
|
+
...tagFlags,
|
|
22
|
+
version: Flags.integer({ description: 'Deployed version to diff against (default: latest).' }),
|
|
23
|
+
};
|
|
24
|
+
async run() {
|
|
25
|
+
const { args, flags } = await this.parse(DefinitionDiff);
|
|
26
|
+
const config = loadWorkflowConfig(flags.tag);
|
|
27
|
+
const def = await loadValidatedDefinition(args.definition);
|
|
28
|
+
const client = loadClient();
|
|
29
|
+
const spinner = ora(`Diffing ${args.definition} against deployed…`).start();
|
|
30
|
+
let deployed;
|
|
31
|
+
try {
|
|
32
|
+
deployed = await fetchDeployedDefinition(client, args.definition, config, flags.version);
|
|
33
|
+
spinner.succeed(`Diffed ${args.definition} v${def.version}`);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
spinner.fail('Diff failed');
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
for (const line of diffReport([diffEntry(def, deployed, config)]))
|
|
40
|
+
this.log(line);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Fetch the deployed document to compare against. Unlike `deploy
|
|
45
|
+
* --dry-run` (which looks up the exact docId, so a version bump always
|
|
46
|
+
* reads as "create"), diff targets the deployed version that would be
|
|
47
|
+
* superseded — latest unless pinned via `version`. An absent latest is a
|
|
48
|
+
* legitimate "create" diff, but an absent *pinned* version is an error.
|
|
49
|
+
*/
|
|
50
|
+
async function fetchDeployedDefinition(client, name, config, version) {
|
|
51
|
+
const { groq, params } = buildDefinitionShowQuery({ name, tag: config.tag, version });
|
|
52
|
+
const deployed = await client.fetch(groq, params);
|
|
53
|
+
if (deployed === null && version !== undefined) {
|
|
54
|
+
fail(`No deployed definition "${name}" v${version}.`);
|
|
55
|
+
}
|
|
56
|
+
return deployed ?? undefined;
|
|
57
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
interface DefinitionListFlags {
|
|
3
|
+
tag?: string | undefined;
|
|
4
|
+
'workflow-id'?: string | undefined;
|
|
5
|
+
limit: number;
|
|
6
|
+
}
|
|
7
|
+
export interface DefinitionListRow {
|
|
8
|
+
_id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
version: number;
|
|
11
|
+
title: string;
|
|
12
|
+
tag: string;
|
|
13
|
+
stageCount: number;
|
|
14
|
+
inFlightCount: number;
|
|
15
|
+
totalInstances: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function buildDefinitionListQuery(flags: DefinitionListFlags): {
|
|
18
|
+
groq: string;
|
|
19
|
+
params: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
export default class DefinitionList extends Command {
|
|
22
|
+
static description: string;
|
|
23
|
+
static examples: string[];
|
|
24
|
+
static flags: {
|
|
25
|
+
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
26
|
+
'workflow-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
27
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
28
|
+
};
|
|
29
|
+
run(): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, tagScopeFilter, } from '@sanity/workflow-engine';
|
|
3
|
+
import { Table } from 'console-table-printer';
|
|
4
|
+
import logSymbols from 'log-symbols';
|
|
5
|
+
import { loadClient } from "../../lib/client.js";
|
|
6
|
+
import { tagFlags } from "../../lib/flags.js";
|
|
7
|
+
export function buildDefinitionListQuery(flags) {
|
|
8
|
+
const params = { limit: flags.limit };
|
|
9
|
+
const filters = [`_type == "${WORKFLOW_DEFINITION_TYPE}"`];
|
|
10
|
+
const instanceFilters = [
|
|
11
|
+
`_type == "${WORKFLOW_INSTANCE_TYPE}"`,
|
|
12
|
+
'definition == ^.name',
|
|
13
|
+
'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
|
+
'tag == ^.tag',
|
|
19
|
+
];
|
|
20
|
+
if (flags.tag) {
|
|
21
|
+
params.tag = flags.tag;
|
|
22
|
+
filters.push(tagScopeFilter());
|
|
23
|
+
}
|
|
24
|
+
if (flags['workflow-id']) {
|
|
25
|
+
params.name = flags['workflow-id'];
|
|
26
|
+
filters.push('name == $name');
|
|
27
|
+
}
|
|
28
|
+
const instanceMatch = instanceFilters.join(' && ');
|
|
29
|
+
const groq = `*[${filters.join(' && ')}] | order(name asc, version desc) [0...$limit]{
|
|
30
|
+
_id,
|
|
31
|
+
name,
|
|
32
|
+
version,
|
|
33
|
+
title,
|
|
34
|
+
tag,
|
|
35
|
+
"stageCount": count(stages),
|
|
36
|
+
"inFlightCount": count(*[${instanceMatch} && !defined(completedAt)]),
|
|
37
|
+
"totalInstances": count(*[${instanceMatch}])
|
|
38
|
+
}`;
|
|
39
|
+
return { groq, params };
|
|
40
|
+
}
|
|
41
|
+
export default class DefinitionList extends Command {
|
|
42
|
+
static description = 'List deployed workflow definitions.';
|
|
43
|
+
static examples = [
|
|
44
|
+
'<%= config.bin %> definition list',
|
|
45
|
+
'<%= config.bin %> definition list --tag prod',
|
|
46
|
+
];
|
|
47
|
+
static flags = {
|
|
48
|
+
...tagFlags,
|
|
49
|
+
limit: Flags.integer({
|
|
50
|
+
description: 'The maximum number of definitions to return.',
|
|
51
|
+
default: 100,
|
|
52
|
+
}),
|
|
53
|
+
'workflow-id': Flags.string({
|
|
54
|
+
description: 'Filter to a single workflow definition name (e.g. product-launch)',
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
async run() {
|
|
58
|
+
const { flags } = await this.parse(DefinitionList);
|
|
59
|
+
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: '_id', title: 'Definition ID', alignment: 'left' },
|
|
69
|
+
{ name: 'title', title: 'Title', alignment: 'left' },
|
|
70
|
+
{ name: 'name', title: 'Workflow', alignment: 'left' },
|
|
71
|
+
{ name: 'tag', title: 'Tag', alignment: 'left' },
|
|
72
|
+
{ name: 'version', title: 'Version', alignment: 'right' },
|
|
73
|
+
{ name: 'stageCount', title: 'Total stages', alignment: 'right' },
|
|
74
|
+
{ name: 'inFlightCount', title: 'Instances in flight', alignment: 'right' },
|
|
75
|
+
{ name: 'totalInstances', title: 'Total instances', alignment: 'right' },
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
for (const r of rows) {
|
|
79
|
+
table.addRow(r);
|
|
80
|
+
}
|
|
81
|
+
table.printTable();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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 {
|
|
13
|
+
static description: string;
|
|
14
|
+
static args: {
|
|
15
|
+
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
16
|
+
};
|
|
17
|
+
static flags: {
|
|
18
|
+
version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
};
|
|
21
|
+
run(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The boxed summary block at the top of `definition show` output. `columns`
|
|
25
|
+
* is the terminal width; values wrap to fit it so `boxen` never re-wraps them.
|
|
26
|
+
*/
|
|
27
|
+
export declare function definitionHeader(def: WorkflowDefinition, columns?: number): string;
|
|
28
|
+
/**
|
|
29
|
+
* The body lines for `definition show`: every stage with its tasks and each
|
|
30
|
+
* task's actions. Pure so it can be asserted without driving the oclif command.
|
|
31
|
+
*/
|
|
32
|
+
export declare function describeDefinition(def: WorkflowDefinition): string[];
|
|
33
|
+
export {};
|