@sanity/workflow-cli 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +516 -0
- package/README.md +71 -19
- package/bin/run.js +0 -4
- package/dist/commands/abort.d.ts +4 -6
- package/dist/commands/abort.js +13 -24
- package/dist/commands/definition/delete.d.ts +2 -2
- package/dist/commands/definition/delete.js +14 -27
- package/dist/commands/definition/diff.d.ts +2 -2
- package/dist/commands/definition/diff.js +3 -12
- package/dist/commands/definition/list.d.ts +3 -4
- package/dist/commands/definition/list.js +41 -47
- package/dist/commands/definition/show.d.ts +35 -3
- package/dist/commands/definition/show.js +44 -24
- package/dist/commands/deploy.d.ts +11 -5
- package/dist/commands/deploy.js +54 -57
- package/dist/commands/diagnose.d.ts +22 -4
- package/dist/commands/diagnose.js +60 -56
- package/dist/commands/fire-action.d.ts +2 -4
- package/dist/commands/fire-action.js +39 -62
- package/dist/commands/list.d.ts +18 -10
- package/dist/commands/list.js +77 -69
- package/dist/commands/reset-activity.js +0 -1
- package/dist/commands/set-stage.d.ts +2 -2
- package/dist/commands/set-stage.js +14 -30
- package/dist/commands/show.d.ts +2 -2
- package/dist/commands/show.js +12 -24
- package/dist/commands/start.d.ts +4 -28
- package/dist/commands/start.js +19 -80
- package/dist/commands/tail.d.ts +2 -2
- package/dist/commands/tail.js +34 -36
- package/dist/hooks/finally/telemetry.d.ts +8 -0
- package/dist/hooks/finally/telemetry.js +13 -0
- package/dist/hooks/prerun/telemetry.d.ts +8 -0
- package/dist/hooks/prerun/telemetry.js +9 -0
- package/dist/index.js +0 -3
- package/dist/lib/base-command.d.ts +14 -0
- package/dist/lib/base-command.js +10 -0
- package/dist/lib/client.js +13 -30
- package/dist/lib/context.d.ts +49 -13
- package/dist/lib/context.js +49 -48
- package/dist/lib/definitions.d.ts +10 -0
- package/dist/lib/definitions.js +11 -24
- package/dist/lib/diff.d.ts +5 -2
- package/dist/lib/diff.js +61 -27
- package/dist/lib/env.d.ts +4 -0
- package/dist/lib/env.js +4 -3
- package/dist/lib/fail.d.ts +19 -6
- package/dist/lib/fail.js +26 -26
- package/dist/lib/flags.d.ts +0 -7
- package/dist/lib/flags.js +0 -17
- package/dist/lib/load-config.d.ts +11 -7
- package/dist/lib/load-config.js +40 -20
- package/dist/lib/operation-args.d.ts +14 -23
- package/dist/lib/operation-args.js +6 -38
- package/dist/lib/ops-report.d.ts +18 -10
- package/dist/lib/ops-report.js +20 -16
- package/dist/lib/params.js +0 -8
- package/dist/lib/read-fanout.d.ts +22 -0
- package/dist/lib/read-fanout.js +28 -0
- package/dist/lib/select-deployment.js +0 -21
- package/dist/lib/share-definitions.d.ts +120 -0
- package/dist/lib/share-definitions.js +187 -0
- package/dist/lib/stub.js +0 -7
- package/dist/lib/telemetry-setup.d.ts +70 -0
- package/dist/lib/telemetry-setup.js +99 -0
- package/dist/lib/telemetry.d.ts +131 -0
- package/dist/lib/telemetry.js +94 -0
- package/dist/lib/ui.d.ts +33 -1
- package/dist/lib/ui.js +32 -22
- package/oclif.manifest.json +21 -52
- package/package.json +14 -8
package/dist/commands/tail.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
|
-
import { Args
|
|
2
|
+
import { Args } from '@oclif/core';
|
|
3
|
+
import { displayTitle, errorMessage } from '@sanity/workflow-engine';
|
|
3
4
|
import logSymbols from 'log-symbols';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { WorkflowCommand } from "../lib/base-command.js";
|
|
6
|
+
import { findInstance, resolveReadTargets } from "../lib/context.js";
|
|
7
|
+
import { fail, failureDetail } from "../lib/fail.js";
|
|
6
8
|
import { tagFlags } from "../lib/flags.js";
|
|
7
9
|
import { formatTimestamp } from "../lib/ui.js";
|
|
8
|
-
|
|
10
|
+
const TAIL_TAG = 'tail';
|
|
11
|
+
export default class Tail extends WorkflowCommand {
|
|
9
12
|
static description = 'Stream new history entries on a workflow instance as they land in the dataset.';
|
|
10
13
|
static examples = ['<%= config.bin %> tail wf-instance.abc123'];
|
|
11
14
|
static args = {
|
|
@@ -19,35 +22,34 @@ export default class Tail extends Command {
|
|
|
19
22
|
};
|
|
20
23
|
async run() {
|
|
21
24
|
const { args, flags } = await this.parse(Tail);
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
if (!
|
|
25
|
+
const targets = await resolveReadTargets(flags);
|
|
26
|
+
const hit = await findInstance({ targets, instanceId: args.instanceId, requestTag: TAIL_TAG });
|
|
27
|
+
if (!hit) {
|
|
25
28
|
fail(`No instance with id "${args.instanceId}"`);
|
|
26
29
|
}
|
|
30
|
+
const { client, instance: initial } = hit;
|
|
27
31
|
this.log(styleText('dim', `tailing ${styleText('bold', args.instanceId)} (${initial.definition} v${initial.pinnedVersion}, ${initial.history.length} prior entries, currently in ${initial.currentStage})`));
|
|
28
32
|
this.log(styleText('dim', 'Ctrl+C to stop'));
|
|
29
33
|
this.log('');
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let
|
|
34
|
+
let rejectListen;
|
|
35
|
+
const listenFailed = new Promise((_, reject) => {
|
|
36
|
+
rejectListen = reject;
|
|
37
|
+
});
|
|
38
|
+
let printChain = Promise.resolve(initial.history.length);
|
|
35
39
|
const subscription = client
|
|
36
|
-
.listen(`*[_id == $id]`, { id: args.instanceId }, {
|
|
40
|
+
.listen(`*[_id == $id]`, { id: args.instanceId }, {
|
|
41
|
+
includeResult: false,
|
|
42
|
+
visibility: 'query',
|
|
43
|
+
tag: TAIL_TAG,
|
|
44
|
+
})
|
|
37
45
|
.subscribe({
|
|
38
|
-
next:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
44
|
-
process.stderr.write(`${styleText('red', `${logSymbols.error} fetch failed:`)} ${message}\n`);
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
error: (err) => {
|
|
48
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
49
|
-
fail('Listen subscription error:', message);
|
|
46
|
+
next: () => {
|
|
47
|
+
printChain = printChain.then((seen) => this.printNewEntries({ client, instanceId: args.instanceId, seen }).catch((err) => {
|
|
48
|
+
process.stderr.write(`${styleText('red', `${logSymbols.error} fetch failed:`)} ${errorMessage(err)}\n`);
|
|
49
|
+
return seen;
|
|
50
|
+
}));
|
|
50
51
|
},
|
|
52
|
+
error: rejectListen,
|
|
51
53
|
});
|
|
52
54
|
const shutdown = () => {
|
|
53
55
|
subscription.unsubscribe();
|
|
@@ -56,23 +58,19 @@ export default class Tail extends Command {
|
|
|
56
58
|
};
|
|
57
59
|
process.on('SIGINT', shutdown);
|
|
58
60
|
process.on('SIGTERM', shutdown);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
await listenFailed.catch((err) => {
|
|
62
|
+
subscription.unsubscribe();
|
|
63
|
+
fail('Listen subscription error:', failureDetail(err));
|
|
64
|
+
});
|
|
62
65
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Fetch the current instance state, print history entries past the
|
|
65
|
-
* given high-water mark, return the new mark. Pulled out so the
|
|
66
|
-
* async work in the subscription's `next` handler stays tidy.
|
|
67
|
-
*/
|
|
68
66
|
async printNewEntries({ client, instanceId, seen, }) {
|
|
69
|
-
const next = await client.getDocument(instanceId);
|
|
67
|
+
const next = await client.getDocument(instanceId, { tag: TAIL_TAG });
|
|
70
68
|
if (!next)
|
|
71
69
|
return seen;
|
|
72
70
|
const newEntries = next.history.slice(seen);
|
|
73
71
|
for (const entry of newEntries) {
|
|
74
|
-
this.log(`${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${styleText('cyan', entry._type)}`);
|
|
72
|
+
this.log(`${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${styleText('cyan', displayTitle(entry._type))}`);
|
|
75
73
|
}
|
|
76
|
-
return next.history.length;
|
|
74
|
+
return Math.max(seen, next.history.length);
|
|
77
75
|
}
|
|
78
76
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Hook } from '@oclif/core';
|
|
2
|
+
/**
|
|
3
|
+
* Completes the command trace — command id, declared-flag names, success —
|
|
4
|
+
* and flushes the final batch. Runs on success and on error alike (oclif's
|
|
5
|
+
* `finally`); an invocation the prerun hook left with telemetry off no-ops.
|
|
6
|
+
*/
|
|
7
|
+
declare const hook: Hook<'finally'>;
|
|
8
|
+
export default hook;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { finishCliTelemetry, usedFlagNames } from "../../lib/telemetry.js";
|
|
2
|
+
const hook = async function (options) {
|
|
3
|
+
try {
|
|
4
|
+
await finishCliTelemetry({
|
|
5
|
+
command: options.id,
|
|
6
|
+
flags: usedFlagNames(options.argv, options.Command?.flags),
|
|
7
|
+
success: options.error === undefined,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export default hook;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Hook } from '@oclif/core';
|
|
2
|
+
/** Builds and installs the invocation's telemetry shell before the command
|
|
3
|
+
* runs; the `finally` hook completes the trace and flushes. Never throws.
|
|
4
|
+
* An explicit `--share-defs` forces the store to send despite CI /
|
|
5
|
+
* `DO_NOT_TRACK` — resolved from raw argv here, since the store is built
|
|
6
|
+
* before oclif parses flags. */
|
|
7
|
+
declare const hook: Hook<'prerun'>;
|
|
8
|
+
export default hook;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { shouldForceShareTelemetry } from "../../lib/share-definitions.js";
|
|
2
|
+
import { setupCliTelemetry } from "../../lib/telemetry-setup.js";
|
|
3
|
+
const hook = async function ({ config, Command, argv }) {
|
|
4
|
+
await setupCliTelemetry({
|
|
5
|
+
cliVersion: config.version,
|
|
6
|
+
forceSend: shouldForceShareTelemetry({ commandId: Command?.id, argv }),
|
|
7
|
+
});
|
|
8
|
+
};
|
|
9
|
+
export default hook;
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
/**
|
|
3
|
+
* Base for the API-backed workflow commands: the backstop that renders an
|
|
4
|
+
* auth rejection (HTTP 401) escaping a command's `run` through the clean
|
|
5
|
+
* {@link fail} path — with {@link failureDetail}'s recovery hint — instead
|
|
6
|
+
* of an oclif error dump. Errors caught inside commands render the same
|
|
7
|
+
* detail at their own `fail` sites; every other error propagates to oclif
|
|
8
|
+
* unchanged.
|
|
9
|
+
*/
|
|
10
|
+
export declare abstract class WorkflowCommand extends Command {
|
|
11
|
+
protected catch(err: Error & {
|
|
12
|
+
exitCode?: number;
|
|
13
|
+
}): Promise<unknown>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { fail, failureDetail, isAuthRejection } from "./fail.js";
|
|
3
|
+
export class WorkflowCommand extends Command {
|
|
4
|
+
async catch(err) {
|
|
5
|
+
if (isAuthRejection(err)) {
|
|
6
|
+
fail('Authentication failed:', failureDetail(err));
|
|
7
|
+
}
|
|
8
|
+
return super.catch(err);
|
|
9
|
+
}
|
|
10
|
+
}
|
package/dist/lib/client.js
CHANGED
|
@@ -1,28 +1,16 @@
|
|
|
1
1
|
import { getCliToken, isStaging } from '@sanity/cli-core';
|
|
2
2
|
import { createClient } from '@sanity/client';
|
|
3
|
-
import {
|
|
4
|
-
import { ENV } from "./env.js";
|
|
3
|
+
import { clientConfigFromResource, ENGINE_API_VERSION, errorMessage, } from '@sanity/workflow-engine';
|
|
4
|
+
import { ENV, envAuthToken } from "./env.js";
|
|
5
5
|
import { fail } from "./fail.js";
|
|
6
6
|
const API_VERSION = ENGINE_API_VERSION;
|
|
7
7
|
const STAGING_API_HOST = 'https://api.sanity.work';
|
|
8
|
-
/**
|
|
9
|
-
* The API host to hit: an explicit `SANITY_API_HOST` wins; otherwise mirror
|
|
10
|
-
* the Sanity CLI — staging routes to the staging API, prod falls through to
|
|
11
|
-
* `@sanity/client`'s default. Pairs with {@link resolveToken}, which reads the
|
|
12
|
-
* matching session token.
|
|
13
|
-
*/
|
|
14
8
|
function resolveApiHost() {
|
|
15
9
|
return process.env[ENV.SANITY_API_HOST] ?? (isStaging() ? STAGING_API_HOST : undefined);
|
|
16
10
|
}
|
|
17
|
-
/**
|
|
18
|
-
* Resolve the Sanity auth token. An explicit `SANITY_AUTH_TOKEN` wins (CI /
|
|
19
|
-
* scripted use); otherwise fall back to the `sanity login` session token.
|
|
20
|
-
* Throws when neither exists rather than building an unauthenticated client
|
|
21
|
-
* that only 401s once it hits the API.
|
|
22
|
-
*/
|
|
23
11
|
export async function resolveToken() {
|
|
24
|
-
const fromEnv =
|
|
25
|
-
if (fromEnv) {
|
|
12
|
+
const fromEnv = envAuthToken();
|
|
13
|
+
if (fromEnv !== undefined) {
|
|
26
14
|
return fromEnv;
|
|
27
15
|
}
|
|
28
16
|
const fromSession = await getCliToken();
|
|
@@ -31,22 +19,17 @@ export async function resolveToken() {
|
|
|
31
19
|
}
|
|
32
20
|
throw new Error('No Sanity token found — run `sanity login`, or set SANITY_AUTH_TOKEN.');
|
|
33
21
|
}
|
|
34
|
-
/** {@link resolveToken}, funnelling the no-token case through the clean
|
|
35
|
-
* {@link fail} path rather than letting it surface as an oclif stack. */
|
|
36
22
|
export async function resolveTokenOrFail() {
|
|
37
|
-
return resolveToken().catch((err) => fail('Authentication required:',
|
|
23
|
+
return resolveToken().catch((err) => fail('Authentication required:', errorMessage(err)));
|
|
38
24
|
}
|
|
39
|
-
/** Build a client for a workflow resource, authenticated with `token`. */
|
|
40
25
|
export function clientFor(resource, token) {
|
|
41
26
|
const apiHost = resolveApiHost();
|
|
42
|
-
const baseParams = {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
return createClient({ ...baseParams, resource });
|
|
27
|
+
const baseParams = {
|
|
28
|
+
token,
|
|
29
|
+
apiVersion: API_VERSION,
|
|
30
|
+
useCdn: false,
|
|
31
|
+
requestTagPrefix: 'sanity.workflows.cli',
|
|
32
|
+
...(apiHost ? { apiHost } : {}),
|
|
33
|
+
};
|
|
34
|
+
return createClient({ ...baseParams, ...clientConfigFromResource(resource) });
|
|
52
35
|
}
|
package/dist/lib/context.d.ts
CHANGED
|
@@ -20,24 +20,34 @@ export interface InstanceContext {
|
|
|
20
20
|
export declare function resolveContext(flags: {
|
|
21
21
|
tag?: string | undefined;
|
|
22
22
|
}): Promise<DeploymentContext>;
|
|
23
|
+
/** One resource a read inspects, with its authenticated client. */
|
|
24
|
+
export interface ReadTarget {
|
|
25
|
+
resource: WorkflowResource;
|
|
26
|
+
client: SanityClient;
|
|
27
|
+
}
|
|
23
28
|
/**
|
|
24
|
-
* The resolution
|
|
25
|
-
* inspect — and nothing more. A read lists/inspects whatever is in the
|
|
26
|
-
* Lake, so it must NOT inherit a deployment's tag or definition set
|
|
27
|
-
* filter. The config only locates the
|
|
28
|
-
* user's explicit `--tag` (if any) as a query filter itself.
|
|
29
|
+
* The resolution every READ shares: an authenticated client per resource to
|
|
30
|
+
* inspect — and nothing more. A read lists/inspects whatever is in the
|
|
31
|
+
* Content Lake, so it must NOT inherit a deployment's tag or definition set
|
|
32
|
+
* as a filter. The config only locates the resources here; the command
|
|
33
|
+
* applies the user's explicit `--tag` (if any) as a query filter itself.
|
|
29
34
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
35
|
+
* Untagged, this is every distinct resource the config mentions — a read is
|
|
36
|
+
* 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.
|
|
33
39
|
*/
|
|
34
|
-
export declare function
|
|
40
|
+
export declare function resolveReadTargets(flags: {
|
|
35
41
|
tag?: string | undefined;
|
|
36
|
-
}): Promise<
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
}): Promise<ReadTarget[]>;
|
|
43
|
+
/** The distinct resources a read targets: the tagged deployment's sole
|
|
44
|
+
* resource, or — untagged — every distinct one the config's deployments
|
|
45
|
+
* mention (deduped by `type:id`). */
|
|
46
|
+
export declare function resolveReadResources(config: WorkflowConfig, tag: string | undefined): WorkflowResource[];
|
|
39
47
|
/** The single resource a read should target, or fail asking which when the
|
|
40
|
-
* config spans more than one.
|
|
48
|
+
* config spans more than one. For the paths that need one definite dataset
|
|
49
|
+
* (the instance-targeted verbs); listings fan out via
|
|
50
|
+
* {@link resolveReadResources} instead. */
|
|
41
51
|
export declare function resolveReadResource(config: WorkflowConfig, tag: string | undefined): WorkflowResource;
|
|
42
52
|
/**
|
|
43
53
|
* The resolution an instance-id-targeted command shares, read or write: an
|
|
@@ -60,3 +70,29 @@ export declare function resolveInstanceContext(flags: {
|
|
|
60
70
|
* {@link resolveInstanceContext} so the not-found path is unit-testable
|
|
61
71
|
* without a live config/token/client. */
|
|
62
72
|
export declare function loadInstanceOrFail(client: Pick<SanityClient, 'getDocument'>, instanceId: string): Promise<WorkflowInstance>;
|
|
73
|
+
/** A read target that turned out to hold the instance being looked up. */
|
|
74
|
+
export interface InstanceHit extends ReadTarget {
|
|
75
|
+
instance: WorkflowInstance;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The sole cross-resource hit, or fail listing every holding resource —
|
|
79
|
+
* ambiguity across datasets must error, never pick. `subject` names the
|
|
80
|
+
* thing being looked up in the diagnostic (`Instance <id>`, `Definition "x"`).
|
|
81
|
+
*/
|
|
82
|
+
export declare function soleHitOrFail<T extends {
|
|
83
|
+
resource: WorkflowResource;
|
|
84
|
+
}>(hits: T[], subject: string): T | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Look an instance id up across every read target. Exactly one hit wins —
|
|
87
|
+
* instance ids are random and collision-free, so a single match IS the
|
|
88
|
+
* instance. Several hits (id reuse across datasets) fail listing the
|
|
89
|
+
* resources rather than silently picking one. `undefined` means confidently
|
|
90
|
+
* not found: when a probe throws and no other target has the id, that error
|
|
91
|
+
* propagates instead — an unreadable dataset must not masquerade as "no such
|
|
92
|
+
* instance".
|
|
93
|
+
*/
|
|
94
|
+
export declare function findInstance({ targets, instanceId, requestTag, }: {
|
|
95
|
+
targets: ReadTarget[];
|
|
96
|
+
instanceId: string;
|
|
97
|
+
requestTag: string;
|
|
98
|
+
}): Promise<InstanceHit | undefined>;
|
package/dist/lib/context.js
CHANGED
|
@@ -2,66 +2,41 @@ import { clientFor, resolveTokenOrFail } from "./client.js";
|
|
|
2
2
|
import { fail } from "./fail.js";
|
|
3
3
|
import { loadWorkflowConfig } from "./load-config.js";
|
|
4
4
|
import { selectDeployment } from "./select-deployment.js";
|
|
5
|
-
|
|
6
|
-
* The resolution a WRITE shares: discover the config, pick the deployment for
|
|
7
|
-
* `--tag` (or the sole one), and build an authenticated client for that
|
|
8
|
-
* deployment's resource. A write acts on one specific deployment, so a
|
|
9
|
-
* single, definite target is exactly what it needs.
|
|
10
|
-
*/
|
|
5
|
+
import { resourceLabel } from "./ui.js";
|
|
11
6
|
export async function resolveContext(flags) {
|
|
12
7
|
const config = await loadWorkflowConfig();
|
|
13
8
|
const deployment = selectDeployment(config, { tag: flags.tag });
|
|
14
9
|
return { deployment, client: clientFor(deployment.workflowResource, await resolveTokenOrFail()) };
|
|
15
10
|
}
|
|
16
|
-
|
|
17
|
-
* The resolution a READ shares: an authenticated client for the resource to
|
|
18
|
-
* inspect — and nothing more. A read lists/inspects whatever is in the Content
|
|
19
|
-
* Lake, so it must NOT inherit a deployment's tag or definition set as a
|
|
20
|
-
* filter. The config only locates the resource here; the command applies the
|
|
21
|
-
* user's explicit `--tag` (if any) as a query filter itself.
|
|
22
|
-
*
|
|
23
|
-
* Resource selection: when every deployment targets the same resource (the
|
|
24
|
-
* common case) it's used with no `--tag` needed; only a config that spans
|
|
25
|
-
* *different* resources needs `--tag` to say which dataset to read.
|
|
26
|
-
*/
|
|
27
|
-
export async function resolveReadContext(flags) {
|
|
11
|
+
export async function resolveReadTargets(flags) {
|
|
28
12
|
const config = await loadWorkflowConfig();
|
|
29
|
-
|
|
13
|
+
const resources = resolveReadResources(config, flags.tag);
|
|
14
|
+
const token = await resolveTokenOrFail();
|
|
15
|
+
return resources.map((resource) => ({ resource, client: clientFor(resource, token) }));
|
|
30
16
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
17
|
+
export function resolveReadResources(config, tag) {
|
|
18
|
+
if (tag !== undefined) {
|
|
19
|
+
return [selectDeployment(config, { tag }).workflowResource];
|
|
20
|
+
}
|
|
34
21
|
const byKey = new Map(config.deployments.map((d) => [
|
|
35
22
|
`${d.workflowResource.type}:${d.workflowResource.id}`,
|
|
36
23
|
d.workflowResource,
|
|
37
24
|
]));
|
|
38
25
|
const resources = [...byKey.values()];
|
|
39
|
-
|
|
40
|
-
if (sole === undefined) {
|
|
26
|
+
if (resources.length === 0) {
|
|
41
27
|
fail('No deployments configured.');
|
|
42
28
|
}
|
|
43
|
-
|
|
29
|
+
return resources;
|
|
30
|
+
}
|
|
31
|
+
export function resolveReadResource(config, tag) {
|
|
32
|
+
const resources = resolveReadResources(config, tag);
|
|
33
|
+
const [sole] = resources;
|
|
34
|
+
if (resources.length === 1 && sole !== undefined) {
|
|
44
35
|
return sole;
|
|
45
36
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
fail('Config spans multiple resources — pass --tag to choose one.', `Available tags: ${tags}`);
|
|
49
|
-
}
|
|
50
|
-
return selectDeployment(config, { tag }).workflowResource;
|
|
37
|
+
const tags = config.deployments.map((d) => d.tag).join(', ');
|
|
38
|
+
fail('Config spans multiple resources — pass --tag to choose one.', `Available tags: ${tags}`);
|
|
51
39
|
}
|
|
52
|
-
/**
|
|
53
|
-
* The resolution an instance-id-targeted command shares, read or write: an
|
|
54
|
-
* authenticated client for the resource, plus the instance itself — so the
|
|
55
|
-
* command derives its partition (`tag`) from the instance, never from the
|
|
56
|
-
* config's declared deployments. An instance id is globally unique and carries
|
|
57
|
-
* its own `tag`, so a command that names one acts on that instance regardless
|
|
58
|
-
* of which tags the config happens to deploy; the config only locates the
|
|
59
|
-
* resource (via {@link resolveReadResource}, no tag filter).
|
|
60
|
-
*
|
|
61
|
-
* Contrast {@link resolveContext}: the deploy/diff/delete path IS scoped to a
|
|
62
|
-
* declared deployment because it acts on the authored definition set (or a
|
|
63
|
-
* definition name, which — unlike an instance id — isn't unique across tags).
|
|
64
|
-
*/
|
|
65
40
|
export async function resolveInstanceContext(flags, instanceId) {
|
|
66
41
|
const config = await loadWorkflowConfig();
|
|
67
42
|
const workflowResource = resolveReadResource(config, flags.tag);
|
|
@@ -69,14 +44,40 @@ export async function resolveInstanceContext(flags, instanceId) {
|
|
|
69
44
|
const instance = await loadInstanceOrFail(client, instanceId);
|
|
70
45
|
return { client, scope: { tag: instance.tag, workflowResource } };
|
|
71
46
|
}
|
|
72
|
-
/** Fetch an instance by id, exiting cleanly when the resource has no such
|
|
73
|
-
* document — the diagnostic an operator sees on a mistyped id. Split out from
|
|
74
|
-
* {@link resolveInstanceContext} so the not-found path is unit-testable
|
|
75
|
-
* without a live config/token/client. */
|
|
76
47
|
export async function loadInstanceOrFail(client, instanceId) {
|
|
77
|
-
const instance = await client.getDocument(instanceId
|
|
48
|
+
const instance = await client.getDocument(instanceId, {
|
|
49
|
+
tag: 'instance.load',
|
|
50
|
+
});
|
|
78
51
|
if (!instance) {
|
|
79
52
|
fail(`Workflow instance ${instanceId} not found`);
|
|
80
53
|
}
|
|
81
54
|
return instance;
|
|
82
55
|
}
|
|
56
|
+
export function soleHitOrFail(hits, subject) {
|
|
57
|
+
const [sole, ...rest] = hits;
|
|
58
|
+
if (rest.length > 0) {
|
|
59
|
+
fail(`${subject} exists in several resources — pass --tag to choose one.`, hits.map((hit) => resourceLabel(hit.resource)).join('\n'));
|
|
60
|
+
}
|
|
61
|
+
return sole;
|
|
62
|
+
}
|
|
63
|
+
export async function findInstance({ targets, instanceId, requestTag, }) {
|
|
64
|
+
const probes = await Promise.allSettled(targets.map(async (target) => {
|
|
65
|
+
const instance = await target.client.getDocument(instanceId, {
|
|
66
|
+
tag: requestTag,
|
|
67
|
+
});
|
|
68
|
+
return instance === undefined ? undefined : { ...target, instance };
|
|
69
|
+
}));
|
|
70
|
+
const hits = probes
|
|
71
|
+
.filter((probe) => probe.status === 'fulfilled')
|
|
72
|
+
.map((probe) => probe.value)
|
|
73
|
+
.filter((hit) => hit !== undefined);
|
|
74
|
+
const sole = soleHitOrFail(hits, `Instance ${instanceId}`);
|
|
75
|
+
if (sole !== undefined) {
|
|
76
|
+
return sole;
|
|
77
|
+
}
|
|
78
|
+
const failedProbe = probes.find((probe) => probe.status === 'rejected');
|
|
79
|
+
if (failedProbe !== undefined) {
|
|
80
|
+
throw failedProbe.reason;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
@@ -9,6 +9,16 @@ export declare function buildDefinitionShowQuery(flags: DefinitionShowQueryArgs)
|
|
|
9
9
|
groq: string;
|
|
10
10
|
params: Record<string, unknown>;
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* The tag partitions holding any version of a definition name within one
|
|
14
|
+
* dataset. An untagged lookup that would span several partitions must error
|
|
15
|
+
* (the highest version ACROSS partitions is meaningless — it could show a
|
|
16
|
+
* prod operator the dev definition), the same way a cross-dataset hit does.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildDefinitionTagsQuery(name: string): {
|
|
19
|
+
groq: string;
|
|
20
|
+
params: Record<string, unknown>;
|
|
21
|
+
};
|
|
12
22
|
/**
|
|
13
23
|
* Fetch the deployed definition a command targets — latest unless pinned via
|
|
14
24
|
* `version`. An absent pinned version always fails (the caller asked for
|
package/dist/lib/definitions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { WORKFLOW_DEFINITION_TYPE, tagScopeFilter, validateDefinition, } from '@sanity/workflow-engine';
|
|
2
|
-
import {
|
|
1
|
+
import { WORKFLOW_DEFINITION_TYPE, errorMessage, tagScopeFilter, validateDefinition, } from '@sanity/workflow-engine';
|
|
2
|
+
import { fail } from "./fail.js";
|
|
3
3
|
export function buildDefinitionShowQuery(flags) {
|
|
4
4
|
const params = { name: flags.name };
|
|
5
5
|
const filters = [`_type == "${WORKFLOW_DEFINITION_TYPE}"`, 'name == $name'];
|
|
@@ -14,26 +14,22 @@ export function buildDefinitionShowQuery(flags) {
|
|
|
14
14
|
const groq = `*[${filters.join(' && ')}] | order(version desc) [0]`;
|
|
15
15
|
return { groq, params };
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
export function buildDefinitionTagsQuery(name) {
|
|
18
|
+
return {
|
|
19
|
+
groq: `array::unique(*[_type == "${WORKFLOW_DEFINITION_TYPE}" && name == $name].tag)`,
|
|
20
|
+
params: { name },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
23
|
export async function fetchDeployedDefinition({ client, name, tag, version, }) {
|
|
24
24
|
const { groq, params } = buildDefinitionShowQuery({ name, tag, version });
|
|
25
|
-
const deployed = await client.fetch(groq, params
|
|
25
|
+
const deployed = await client.fetch(groq, params, {
|
|
26
|
+
tag: 'definition.load',
|
|
27
|
+
});
|
|
26
28
|
if (deployed === null && version !== undefined) {
|
|
27
29
|
fail(`No deployed definition "${name}" v${version}.`);
|
|
28
30
|
}
|
|
29
31
|
return deployed ?? undefined;
|
|
30
32
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Narrow a batch to one definition name (`only`), failing with the available
|
|
33
|
-
* names when there's no match. With `only` unset the whole batch passes through
|
|
34
|
-
* (a fresh array, so callers can mutate without aliasing). `context` prefixes
|
|
35
|
-
* the failure so callers whose run spans several deployments can attribute it.
|
|
36
|
-
*/
|
|
37
33
|
export function selectDefinitions(allDefinitions, { only, context = '' }) {
|
|
38
34
|
if (!only) {
|
|
39
35
|
return [...allDefinitions];
|
|
@@ -45,11 +41,6 @@ export function selectDefinitions(allDefinitions, { only, context = '' }) {
|
|
|
45
41
|
}
|
|
46
42
|
return filtered;
|
|
47
43
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Per-definition structural/GROQ checks plus one batch-level check the engine
|
|
50
|
-
* doesn't do for us: duplicate `name` within the set. Returns the
|
|
51
|
-
* human-readable error lines; empty means the batch is valid.
|
|
52
|
-
*/
|
|
53
44
|
export function collectValidationErrors(defs) {
|
|
54
45
|
const errors = [];
|
|
55
46
|
const seen = new Map();
|
|
@@ -70,10 +61,6 @@ export function collectValidationErrors(defs) {
|
|
|
70
61
|
}
|
|
71
62
|
return errors;
|
|
72
63
|
}
|
|
73
|
-
/** Validate a set of definitions, exiting with the collected errors on any
|
|
74
|
-
* failure. The single validate-then-fail gate `deploy` and `definition diff`
|
|
75
|
-
* share, so the two can't drift into divergent failure messages. `context`
|
|
76
|
-
* prefixes the failure with the deployment it belongs to in multi-target runs. */
|
|
77
64
|
export function validateOrFail(defs, context = '') {
|
|
78
65
|
const errors = collectValidationErrors(defs);
|
|
79
66
|
if (errors.length > 0) {
|
package/dist/lib/diff.d.ts
CHANGED
|
@@ -2,7 +2,10 @@ import type { DiffEntry } from '@sanity/workflow-engine';
|
|
|
2
2
|
/** One summary line per diff entry, tagged create / unchanged. Definitions are
|
|
3
3
|
* immutable, so a content change is a new version (`create`), never an update. */
|
|
4
4
|
export declare function summaryLines(entries: DiffEntry[]): string[];
|
|
5
|
-
/** Render a JSON diff of `oldObj` → `newObj` as `+`/`-`/context lines
|
|
5
|
+
/** Render a JSON diff of `oldObj` → `newObj` as `+`/`-`/context lines, long
|
|
6
|
+
* unchanged runs collapsed to a dim `… N unchanged lines` marker. */
|
|
6
7
|
export declare function diffLines(oldObj: object, newObj: object): string[];
|
|
7
|
-
/** The full diff report: the summary block plus a per-change diff
|
|
8
|
+
/** The full diff report: the summary block plus a per-change diff of the
|
|
9
|
+
* authored content — the deploy envelope is stripped from both sides (the
|
|
10
|
+
* version and docId already render on the summary line). */
|
|
8
11
|
export declare function diffReport(entries: DiffEntry[]): string[];
|