@elmundi/ship-cli 0.12.0 → 0.12.2
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/README.md +2 -2
- package/bin/shipctl.mjs +7 -0
- package/lib/commands/help.mjs +27 -20
- package/lib/commands/knowledge.mjs +26 -11
- package/lib/commands/process.mjs +388 -0
- package/lib/commands/run.mjs +76 -52
- package/lib/commands/trigger.mjs +77 -40
- package/lib/config/schema.mjs +193 -7
- package/lib/config.mjs +3 -0
- package/lib/process/specialist-prompt-contract.mjs +171 -0
- package/lib/runtime/routines.mjs +264 -0
- package/package.json +4 -4
package/lib/commands/run.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `shipctl run` — single entry-point for executing a Ship
|
|
2
|
+
* `shipctl run` — single entry-point for executing a Ship routine.
|
|
3
3
|
*
|
|
4
4
|
* Today's scope (Phase 1):
|
|
5
|
-
* - `kind=once`
|
|
6
|
-
* - `kind=event` and `kind=schedule`
|
|
7
|
-
*
|
|
8
|
-
* runs for the same schedule window.
|
|
5
|
+
* - `kind=once` routines run with local idempotency markers.
|
|
6
|
+
* - `kind=event` and `kind=schedule` routines execute when `shipctl trigger`
|
|
7
|
+
* says they are due; Ship only claims the schedule window.
|
|
9
8
|
*
|
|
10
9
|
* The command intentionally does not fork an agent subprocess. The
|
|
11
10
|
* reusable workflow pipes shipctl's stdout into the customer's agent
|
|
@@ -16,7 +15,7 @@
|
|
|
16
15
|
* Callback behaviour: if a callback URL is available via flags or env,
|
|
17
16
|
* `shipctl run` reports `status=ok` on success and `status=fail` on any
|
|
18
17
|
* failure path. Callback errors do not override the primary exit code
|
|
19
|
-
* (a successful
|
|
18
|
+
* (a successful routine with a flaky callback still exits 0, but prints a
|
|
20
19
|
* warning to stderr).
|
|
21
20
|
*/
|
|
22
21
|
|
|
@@ -27,10 +26,14 @@ import { readConfig, findShipRoot } from "../config/io.mjs";
|
|
|
27
26
|
import {
|
|
28
27
|
validateConfig,
|
|
29
28
|
CONFIG_SCHEMA_VERSION,
|
|
30
|
-
lanePatterns,
|
|
31
|
-
laneFanout,
|
|
32
29
|
LANE_FANOUT_MODES,
|
|
33
30
|
} from "../config/schema.mjs";
|
|
31
|
+
import {
|
|
32
|
+
executableFanout,
|
|
33
|
+
executableIds,
|
|
34
|
+
executablePatterns,
|
|
35
|
+
resolveExecutable,
|
|
36
|
+
} from "../runtime/routines.mjs";
|
|
34
37
|
import { fetchArtifact } from "../http.mjs";
|
|
35
38
|
import { resolveShipRepoRootForCatalog } from "../find-ship-root.mjs";
|
|
36
39
|
import { readArtifactFile } from "../artifacts/fs-index.mjs";
|
|
@@ -46,13 +49,13 @@ const EXIT_IDEMPOTENCY = 4;
|
|
|
46
49
|
const VALID_TRIGGERS = new Set(["event", "schedule", "manual", "once"]);
|
|
47
50
|
|
|
48
51
|
function printHelp() {
|
|
49
|
-
console.log(`shipctl run — execute a Ship
|
|
52
|
+
console.log(`shipctl run — execute a Ship routine.
|
|
50
53
|
|
|
51
54
|
WHAT THIS COMMAND IS FOR
|
|
52
|
-
shipctl run is the **
|
|
53
|
-
|
|
55
|
+
shipctl run is the **Run** dispatch entry point. It resolves a
|
|
56
|
+
routine from .ship/config.yml, fetches its pattern body, checks
|
|
54
57
|
idempotency, and emits the prompt for an agent to consume. Behaviour
|
|
55
|
-
by
|
|
58
|
+
by routine trigger:
|
|
56
59
|
- kind: once — executed fully here, locally.
|
|
57
60
|
- kind: lane / event / — recognised but NOT executed locally;
|
|
58
61
|
schedule those run via the workspace's GitHub
|
|
@@ -62,14 +65,15 @@ WHAT THIS COMMAND IS FOR
|
|
|
62
65
|
wrappers can wire them safely.
|
|
63
66
|
|
|
64
67
|
USAGE
|
|
65
|
-
shipctl run --
|
|
68
|
+
shipctl run --routine <id> [--pattern <id>] [--fanout <matrix|sequential|concurrent>]
|
|
66
69
|
[--trigger <event|schedule|manual|once>]
|
|
67
70
|
[--dry-run] [--offline]
|
|
68
71
|
[--ship-run-id <uuid>] [--ship-callback-url <url>] [--ship-run-token <jwt>]
|
|
69
72
|
[--cwd <dir>] [--json]
|
|
70
73
|
|
|
71
74
|
FLAGS
|
|
72
|
-
--
|
|
75
|
+
--routine <id> Routine id declared in process.routines. Required.
|
|
76
|
+
--lane <id> Back-compat alias for --routine.
|
|
73
77
|
--pattern <id> For multi-pattern lanes: run only this pattern. This
|
|
74
78
|
is the per-entry call issued by the matrix workflow
|
|
75
79
|
(one matrix job per pattern). Must be one of the
|
|
@@ -102,7 +106,7 @@ EXIT
|
|
|
102
106
|
10 missing SHIP_RUN_TOKEN when a callback URL is configured
|
|
103
107
|
|
|
104
108
|
EXAMPLE (CI step emitted by the reusable workflow)
|
|
105
|
-
shipctl run --
|
|
109
|
+
shipctl run --routine daily_digest | feed-to-agent
|
|
106
110
|
`);
|
|
107
111
|
}
|
|
108
112
|
|
|
@@ -116,8 +120,8 @@ export async function runCommand(ctx, rest) {
|
|
|
116
120
|
printHelp();
|
|
117
121
|
process.exit(EXIT_OK);
|
|
118
122
|
}
|
|
119
|
-
if (!args.
|
|
120
|
-
die(EXIT_USAGE, "`--
|
|
123
|
+
if (!args.routine) {
|
|
124
|
+
die(EXIT_USAGE, "`--routine <id>` is required (legacy alias: `--lane <id>`).\nRun: shipctl run --help");
|
|
121
125
|
}
|
|
122
126
|
|
|
123
127
|
const cwd = args.cwd || process.cwd();
|
|
@@ -153,26 +157,29 @@ export async function runCommand(ctx, rest) {
|
|
|
153
157
|
die(EXIT_USAGE, msg);
|
|
154
158
|
}
|
|
155
159
|
|
|
156
|
-
const
|
|
157
|
-
if (!
|
|
158
|
-
const known =
|
|
160
|
+
const resolved = resolveExecutable(config, args.routine);
|
|
161
|
+
if (!resolved) {
|
|
162
|
+
const known = executableIds(config);
|
|
163
|
+
const joined = [...known.routines, ...known.lanes].sort();
|
|
159
164
|
die(
|
|
160
165
|
EXIT_USAGE,
|
|
161
|
-
`unknown lane '${args.
|
|
166
|
+
`unknown lane/routine '${args.routine}'. Known routines: ${joined.length ? joined.join(", ") : "(none)"}`,
|
|
162
167
|
);
|
|
163
168
|
}
|
|
169
|
+
const executable = resolved.executable;
|
|
164
170
|
|
|
165
|
-
const effectiveTrigger = resolveTrigger(args.trigger,
|
|
171
|
+
const effectiveTrigger = resolveTrigger(args.trigger, executable.kind);
|
|
166
172
|
if (!effectiveTrigger.fits) {
|
|
167
173
|
/* Not an error — scheduler fired us but the lane doesn't want this
|
|
168
174
|
* trigger. Exit 0 so parallel lanes in the same workflow don't all
|
|
169
175
|
* fail just because one didn't match. */
|
|
170
176
|
const summary = {
|
|
171
|
-
|
|
172
|
-
|
|
177
|
+
routine: args.routine,
|
|
178
|
+
lane: resolved.kind === "lane" ? args.routine : undefined,
|
|
179
|
+
kind: executable.kind,
|
|
173
180
|
trigger: effectiveTrigger.trigger,
|
|
174
181
|
status: "noop",
|
|
175
|
-
reason: `
|
|
182
|
+
reason: `routine.kind=${executable.kind} does not accept trigger=${effectiveTrigger.trigger}`,
|
|
176
183
|
};
|
|
177
184
|
emitSummary(ctx, args, summary);
|
|
178
185
|
process.exit(EXIT_OK);
|
|
@@ -190,31 +197,35 @@ export async function runCommand(ctx, rest) {
|
|
|
190
197
|
// the lane's fan-out mode. Matrix mode without
|
|
191
198
|
// --pattern is rejected because it requires a
|
|
192
199
|
// driving workflow (see run-agent.yml).
|
|
193
|
-
const allPatterns =
|
|
194
|
-
|
|
195
|
-
|
|
200
|
+
const allPatterns = executablePatterns(executable);
|
|
201
|
+
const promptBody = executable.prompt;
|
|
202
|
+
if (allPatterns.length === 0 && !promptBody) {
|
|
203
|
+
die(EXIT_USAGE, `routine ${JSON.stringify(args.routine)} declares no patterns or prompt.`);
|
|
196
204
|
}
|
|
197
205
|
|
|
198
|
-
const effectiveFanout = args.fanout ||
|
|
206
|
+
const effectiveFanout = args.fanout || executableFanout(executable);
|
|
199
207
|
let patternsToRun;
|
|
200
208
|
let runMode; // ``single`` | ``sequential`` | ``concurrent``
|
|
201
209
|
if (args.pattern) {
|
|
202
210
|
if (!allPatterns.includes(args.pattern)) {
|
|
203
211
|
die(
|
|
204
212
|
EXIT_USAGE,
|
|
205
|
-
`--pattern=${JSON.stringify(args.pattern)} is not declared on lane ${JSON.stringify(args.
|
|
213
|
+
`--pattern=${JSON.stringify(args.pattern)} is not declared on lane/routine ${JSON.stringify(args.routine)}. ` +
|
|
206
214
|
`Known patterns: ${allPatterns.join(", ")}.`,
|
|
207
215
|
);
|
|
208
216
|
}
|
|
209
217
|
patternsToRun = [args.pattern];
|
|
210
218
|
runMode = "single";
|
|
219
|
+
} else if (allPatterns.length === 0 && promptBody) {
|
|
220
|
+
patternsToRun = [];
|
|
221
|
+
runMode = "single";
|
|
211
222
|
} else if (allPatterns.length === 1) {
|
|
212
223
|
patternsToRun = allPatterns;
|
|
213
224
|
runMode = "single";
|
|
214
225
|
} else if (effectiveFanout === "matrix") {
|
|
215
226
|
die(
|
|
216
227
|
EXIT_USAGE,
|
|
217
|
-
`
|
|
228
|
+
`routine ${JSON.stringify(args.routine)} has fanout=matrix and ${allPatterns.length} patterns ` +
|
|
218
229
|
`but no --pattern was provided. Matrix mode dispatches one 'shipctl run --pattern <id>' per ` +
|
|
219
230
|
`pattern via the workflow (see run-agent.yml). To run them in-process instead, pass ` +
|
|
220
231
|
`--fanout sequential or --fanout concurrent.`,
|
|
@@ -228,7 +239,7 @@ export async function runCommand(ctx, rest) {
|
|
|
228
239
|
// once up front; per-pattern decisions are derived from the
|
|
229
240
|
// concatenated pattern SHA set below so a change to any member of
|
|
230
241
|
// the list re-triggers the run (expected behaviour for audit lanes).
|
|
231
|
-
const idem =
|
|
242
|
+
const idem = executable.kind === "once" ? executable.idempotency : null;
|
|
232
243
|
let marker = null;
|
|
233
244
|
if (idem) {
|
|
234
245
|
try {
|
|
@@ -246,7 +257,7 @@ export async function runCommand(ctx, rest) {
|
|
|
246
257
|
const fetchJobs = patternsToRun.map((patternId) =>
|
|
247
258
|
fetchPatternBody({
|
|
248
259
|
patternId,
|
|
249
|
-
patternVersion:
|
|
260
|
+
patternVersion: executable.pattern_version || null,
|
|
250
261
|
offline: args.offline,
|
|
251
262
|
root,
|
|
252
263
|
ctx,
|
|
@@ -264,7 +275,14 @@ export async function runCommand(ctx, rest) {
|
|
|
264
275
|
die(EXIT_USAGE, `pattern ${patternId}: ${result.error}`);
|
|
265
276
|
}
|
|
266
277
|
}
|
|
267
|
-
const runs =
|
|
278
|
+
const runs = promptBody && patternsToRun.length === 0
|
|
279
|
+
? [{
|
|
280
|
+
patternId: `${args.routine}:prompt`,
|
|
281
|
+
body: promptBody,
|
|
282
|
+
source: "routine",
|
|
283
|
+
sha256: sha256(promptBody),
|
|
284
|
+
}]
|
|
285
|
+
: fetched.map(({ patternId, result }) => ({
|
|
268
286
|
patternId,
|
|
269
287
|
body: result.body,
|
|
270
288
|
source: result.source,
|
|
@@ -281,8 +299,9 @@ export async function runCommand(ctx, rest) {
|
|
|
281
299
|
: { run: true, reason: "trigger-router-due", marker: null };
|
|
282
300
|
if (!decision.run) {
|
|
283
301
|
const summary = {
|
|
284
|
-
|
|
285
|
-
|
|
302
|
+
routine: args.routine,
|
|
303
|
+
lane: resolved.kind === "lane" ? args.routine : undefined,
|
|
304
|
+
kind: executable.kind,
|
|
286
305
|
trigger: effectiveTrigger.trigger,
|
|
287
306
|
status: "noop",
|
|
288
307
|
reason: "already-done",
|
|
@@ -292,7 +311,7 @@ export async function runCommand(ctx, rest) {
|
|
|
292
311
|
await tryCallback(
|
|
293
312
|
args,
|
|
294
313
|
"ok",
|
|
295
|
-
`
|
|
314
|
+
`routine ${args.routine}: already completed, no-op.`,
|
|
296
315
|
runMode === "single"
|
|
297
316
|
? { pattern_id: runs[0].patternId, pattern_sha256: runs[0].sha256, noop: true }
|
|
298
317
|
: { patterns: runs.map((r) => r.patternId), noop: true },
|
|
@@ -308,8 +327,9 @@ export async function runCommand(ctx, rest) {
|
|
|
308
327
|
* agent-side (or a human) can split them back apart. */
|
|
309
328
|
if (args.dryRun || ctx.dryRun) {
|
|
310
329
|
const summary = {
|
|
311
|
-
|
|
312
|
-
|
|
330
|
+
routine: args.routine,
|
|
331
|
+
lane: resolved.kind === "lane" ? args.routine : undefined,
|
|
332
|
+
kind: executable.kind,
|
|
313
333
|
trigger: effectiveTrigger.trigger,
|
|
314
334
|
status: "dry-run",
|
|
315
335
|
reason: decision.reason,
|
|
@@ -329,7 +349,7 @@ export async function runCommand(ctx, rest) {
|
|
|
329
349
|
);
|
|
330
350
|
} else {
|
|
331
351
|
console.error(
|
|
332
|
-
`# ship:
|
|
352
|
+
`# ship: routine=${args.routine} kind=${executable.kind} trigger=${effectiveTrigger.trigger} mode=${runMode} (dry-run)`,
|
|
333
353
|
);
|
|
334
354
|
emitPatternBodies(runs, { json: false });
|
|
335
355
|
}
|
|
@@ -350,8 +370,8 @@ export async function runCommand(ctx, rest) {
|
|
|
350
370
|
* failure quietly skips the prepend so local / offline runs still
|
|
351
371
|
* work. */
|
|
352
372
|
if (!(ctx.json || args.json)) {
|
|
353
|
-
const provider = resolveAgentProvider(config, args.
|
|
354
|
-
if (provider) console.error(`# ship:
|
|
373
|
+
const provider = resolveAgentProvider(config, args.routine);
|
|
374
|
+
if (provider) console.error(`# ship: routine=${args.routine} agent.provider=${provider} mode=${runMode}`);
|
|
355
375
|
const preamble = await fetchPoliciesPreamble(args);
|
|
356
376
|
if (preamble) emitPoliciesPreamble(preamble);
|
|
357
377
|
emitPatternBodies(runs, { json: false });
|
|
@@ -360,10 +380,11 @@ export async function runCommand(ctx, rest) {
|
|
|
360
380
|
if (idem) {
|
|
361
381
|
try {
|
|
362
382
|
writeMarker(cwd, idem.key, {
|
|
363
|
-
|
|
383
|
+
routine: args.routine,
|
|
384
|
+
lane: resolved.kind === "lane" ? args.routine : undefined,
|
|
364
385
|
pattern_id: runs[0].patternId,
|
|
365
386
|
pattern_sha256: sha256(compositeBody),
|
|
366
|
-
pattern_version:
|
|
387
|
+
pattern_version: executable.pattern_version || null,
|
|
367
388
|
patterns: runs.map((r) => ({ id: r.patternId, sha256: r.sha256 })),
|
|
368
389
|
});
|
|
369
390
|
} catch (err) {
|
|
@@ -380,8 +401,8 @@ export async function runCommand(ctx, rest) {
|
|
|
380
401
|
patterns: runs.map((r) => r.patternId).join(","),
|
|
381
402
|
};
|
|
382
403
|
const callbackSummary = runMode === "single"
|
|
383
|
-
? `
|
|
384
|
-
: `
|
|
404
|
+
? `routine ${args.routine} completed (pattern ${runs[0].patternId}@${runs[0].sha256.slice(0, 8)}).`
|
|
405
|
+
: `routine ${args.routine} completed (${runs.length} patterns, mode=${runMode}).`;
|
|
385
406
|
const callbackResult = await tryCallback(args, "ok", callbackSummary, callbackMetrics);
|
|
386
407
|
|
|
387
408
|
if (ctx.json || args.json) {
|
|
@@ -390,8 +411,9 @@ export async function runCommand(ctx, rest) {
|
|
|
390
411
|
// (and tests) don't break when they upgrade shipctl before
|
|
391
412
|
// starting to declare multi-pattern lanes.
|
|
392
413
|
const summaryPayload = {
|
|
393
|
-
|
|
394
|
-
|
|
414
|
+
routine: args.routine,
|
|
415
|
+
lane: resolved.kind === "lane" ? args.routine : undefined,
|
|
416
|
+
kind: executable.kind,
|
|
395
417
|
trigger: effectiveTrigger.trigger,
|
|
396
418
|
status: "completed",
|
|
397
419
|
mode: runMode,
|
|
@@ -505,7 +527,7 @@ async function fetchPoliciesPreamble(args) {
|
|
|
505
527
|
|
|
506
528
|
function parseArgs(rest) {
|
|
507
529
|
const out = {
|
|
508
|
-
|
|
530
|
+
routine: null,
|
|
509
531
|
pattern: null,
|
|
510
532
|
fanout: null,
|
|
511
533
|
trigger: null,
|
|
@@ -555,7 +577,8 @@ function parseArgs(rest) {
|
|
|
555
577
|
copy.shift();
|
|
556
578
|
continue;
|
|
557
579
|
}
|
|
558
|
-
if (str("--
|
|
580
|
+
if (str("--routine", "routine")) continue;
|
|
581
|
+
if (str("--lane", "routine")) continue;
|
|
559
582
|
if (str("--pattern", "pattern")) continue;
|
|
560
583
|
if (str("--fanout", "fanout")) continue;
|
|
561
584
|
if (str("--trigger", "trigger")) continue;
|
|
@@ -793,7 +816,8 @@ function resolveMethodologyBase(ctx, config) {
|
|
|
793
816
|
function collectCallbackMetrics(args, extra = {}) {
|
|
794
817
|
const env = process.env;
|
|
795
818
|
const out = { ...(extra || {}) };
|
|
796
|
-
if (args && args.
|
|
819
|
+
if (args && args.routine && !out.routine_id) out.routine_id = args.routine;
|
|
820
|
+
if (args && args.routine && !out.lane_id) out.lane_id = args.routine;
|
|
797
821
|
if (env.GITHUB_RUN_ID && !out.gh_workflow_run_id) {
|
|
798
822
|
out.gh_workflow_run_id = env.GITHUB_RUN_ID;
|
|
799
823
|
}
|
|
@@ -846,7 +870,7 @@ function emitSummary(ctx, args, summary) {
|
|
|
846
870
|
console.log(JSON.stringify(summary, null, 2));
|
|
847
871
|
} else {
|
|
848
872
|
console.error(
|
|
849
|
-
`# ship:
|
|
873
|
+
`# ship: routine=${summary.routine || summary.lane} status=${summary.status}${summary.reason ? ` reason="${summary.reason}"` : ""}`,
|
|
850
874
|
);
|
|
851
875
|
}
|
|
852
876
|
}
|
package/lib/commands/trigger.mjs
CHANGED
|
@@ -1,53 +1,65 @@
|
|
|
1
1
|
import { readConfig } from "../config/io.mjs";
|
|
2
|
+
import { dueLanesFromRoutines, dueRoutines } from "../runtime/routines.mjs";
|
|
2
3
|
|
|
3
|
-
const VERSION = "
|
|
4
|
+
const VERSION = "v2";
|
|
4
5
|
|
|
5
6
|
export async function triggerCommand(ctx, rest) {
|
|
6
7
|
const opts = parseArgs(rest);
|
|
7
|
-
const baseUrl = resolveBaseUrl(opts.baseUrl || ctx.baseUrl);
|
|
8
|
-
const token = requireToken();
|
|
9
|
-
let workspaceId = opts.workspace;
|
|
10
|
-
if (!workspaceId) workspaceId = await resolveSoleWorkspace(baseUrl, token);
|
|
11
|
-
const repoId = await resolveRepoId(baseUrl, token, workspaceId, opts.repo);
|
|
12
8
|
const { config } = readConfig(opts.cwd || process.cwd());
|
|
9
|
+
const local = dueRoutines(config, { event: opts.event, now: opts.now ? new Date(opts.now) : new Date() });
|
|
10
|
+
let due = local.due;
|
|
13
11
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
12
|
+
const baseUrl = resolveBaseUrl(opts.baseUrl || explicitGlobalBaseUrl(ctx));
|
|
13
|
+
const token = process.env.SHIP_API_TOKEN || "";
|
|
14
|
+
let claimStatus = "skipped:no-token";
|
|
15
|
+
if (token && due.length > 0 && !opts.noClaim) {
|
|
16
|
+
let workspaceId = opts.workspace;
|
|
17
|
+
if (!workspaceId) workspaceId = await resolveSoleWorkspace(baseUrl, token);
|
|
18
|
+
const repoId = await resolveRepoId(baseUrl, token, workspaceId, opts.repo);
|
|
19
|
+
const claimed = [];
|
|
20
|
+
for (const routine of due) {
|
|
21
|
+
const claim = await claimRoutine(baseUrl, token, workspaceId, repoId, opts.event, routine);
|
|
22
|
+
if (claim.status === "claimed" || claim.status === "unavailable") {
|
|
23
|
+
claimed.push({ ...routine, claim_status: claim.status });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
due = claimed;
|
|
27
|
+
claimStatus = "attempted";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const result = {
|
|
31
|
+
event: opts.event,
|
|
32
|
+
status: due.length ? "due" : "noop",
|
|
33
|
+
due_routines: due,
|
|
34
|
+
due_lanes: dueLanesFromRoutines(due),
|
|
35
|
+
skipped_routines: local.skipped,
|
|
36
|
+
claim_status: claimStatus,
|
|
37
|
+
};
|
|
29
38
|
|
|
30
39
|
if (ctx.json || opts.json) {
|
|
31
40
|
console.log(JSON.stringify(result, null, 2));
|
|
32
41
|
return;
|
|
33
42
|
}
|
|
34
|
-
const due = Array.isArray(result.due_lanes) ? result.due_lanes : [];
|
|
35
43
|
if (!due.length) {
|
|
36
|
-
console.log(`Ship trigger ${opts.event}: no
|
|
44
|
+
console.log(`Ship trigger ${opts.event}: no routines due.`);
|
|
37
45
|
return;
|
|
38
46
|
}
|
|
39
|
-
console.log(`Ship trigger ${opts.event}: ${due.length}
|
|
40
|
-
for (const
|
|
47
|
+
console.log(`Ship trigger ${opts.event}: ${due.length} routine(s) due`);
|
|
48
|
+
for (const routine of due) console.log(` - ${routine.routine_id}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function explicitGlobalBaseUrl(ctx) {
|
|
52
|
+
return ctx?.baseUrlSource === "flag" ? ctx.baseUrl : null;
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
function printHelp() {
|
|
44
|
-
console.log(`shipctl trigger —
|
|
56
|
+
console.log(`shipctl trigger — compute which routines are due (${VERSION})
|
|
45
57
|
|
|
46
58
|
USAGE
|
|
47
|
-
shipctl trigger --event schedule --repo <id|owner/name> [--workspace <id>] [--json]
|
|
59
|
+
shipctl trigger --event schedule [--repo <id|owner/name>] [--workspace <id>] [--json]
|
|
48
60
|
|
|
49
61
|
ENV
|
|
50
|
-
SHIP_API_TOKEN
|
|
62
|
+
SHIP_API_TOKEN Optional. When set, due routines are claimed in Ship.
|
|
51
63
|
SHIP_WORKSPACE_API_BASE Optional API base override.
|
|
52
64
|
SHIP_API_BASE Fallback API base override.
|
|
53
65
|
`);
|
|
@@ -60,6 +72,8 @@ function parseArgs(args) {
|
|
|
60
72
|
repo: null,
|
|
61
73
|
baseUrl: null,
|
|
62
74
|
cwd: null,
|
|
75
|
+
now: null,
|
|
76
|
+
noClaim: false,
|
|
63
77
|
json: false,
|
|
64
78
|
};
|
|
65
79
|
const copy = [...args];
|
|
@@ -83,10 +97,16 @@ function parseArgs(args) {
|
|
|
83
97
|
consume("--workspace", "workspace") ||
|
|
84
98
|
consume("--repo", "repo") ||
|
|
85
99
|
consume("--base-url", "baseUrl") ||
|
|
86
|
-
consume("--cwd", "cwd")
|
|
100
|
+
consume("--cwd", "cwd") ||
|
|
101
|
+
consume("--now", "now")
|
|
87
102
|
) {
|
|
88
103
|
continue;
|
|
89
104
|
}
|
|
105
|
+
if (copy[0] === "--no-claim") {
|
|
106
|
+
out.noClaim = true;
|
|
107
|
+
copy.shift();
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
90
110
|
if (copy[0] === "--json") {
|
|
91
111
|
out.json = true;
|
|
92
112
|
copy.shift();
|
|
@@ -110,15 +130,6 @@ function parseArgs(args) {
|
|
|
110
130
|
return out;
|
|
111
131
|
}
|
|
112
132
|
|
|
113
|
-
function requireToken() {
|
|
114
|
-
const token = process.env.SHIP_API_TOKEN || "";
|
|
115
|
-
if (!token) {
|
|
116
|
-
console.error("SHIP_API_TOKEN is required.");
|
|
117
|
-
process.exit(1);
|
|
118
|
-
}
|
|
119
|
-
return token;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
133
|
function resolveBaseUrl(explicit) {
|
|
123
134
|
if (explicit) return explicit.replace(/\/+$/, "");
|
|
124
135
|
if (process.env.SHIP_WORKSPACE_API_BASE) return process.env.SHIP_WORKSPACE_API_BASE.replace(/\/+$/, "");
|
|
@@ -165,6 +176,33 @@ async function apiPostJson(baseUrl, path, body, token) {
|
|
|
165
176
|
return apiRequest(baseUrl, path, "POST", token, body);
|
|
166
177
|
}
|
|
167
178
|
|
|
179
|
+
async function claimRoutine(baseUrl, token, workspaceId, repoId, event, routine) {
|
|
180
|
+
try {
|
|
181
|
+
return await apiPostJson(
|
|
182
|
+
baseUrl,
|
|
183
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/repos/${encodeURIComponent(repoId)}/routine-runs/claim`,
|
|
184
|
+
{
|
|
185
|
+
event,
|
|
186
|
+
routine_id: routine.routine_id,
|
|
187
|
+
window_key: routine.window_key,
|
|
188
|
+
scheduled_for: routine.scheduled_for,
|
|
189
|
+
window_start: routine.window_start,
|
|
190
|
+
window_end: routine.window_end,
|
|
191
|
+
github: {
|
|
192
|
+
event_name: process.env.SHIP_EVENT_NAME || process.env.GITHUB_EVENT_NAME || "",
|
|
193
|
+
ref: process.env.SHIP_REF || process.env.GITHUB_REF || "",
|
|
194
|
+
sha: process.env.SHIP_SHA || process.env.GITHUB_SHA || "",
|
|
195
|
+
run_id: process.env.GITHUB_RUN_ID || "",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
token,
|
|
199
|
+
);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
console.error(`warn: routine claim failed, running locally: ${err instanceof Error ? err.message : err}`);
|
|
202
|
+
return { status: "unavailable" };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
168
206
|
async function apiRequest(baseUrl, path, method, token, body) {
|
|
169
207
|
const url = `${baseUrl}${path}`;
|
|
170
208
|
let res;
|
|
@@ -191,6 +229,5 @@ async function apiRequest(baseUrl, path, method, token, body) {
|
|
|
191
229
|
}
|
|
192
230
|
if (res.ok) return data;
|
|
193
231
|
const msg = typeof data === "string" ? data : JSON.stringify(data);
|
|
194
|
-
|
|
195
|
-
process.exit(res.status >= 500 ? 3 : 1);
|
|
232
|
+
throw new Error(`HTTP ${res.status} ${res.statusText} on ${method} ${url}\n${msg}`);
|
|
196
233
|
}
|