@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.
@@ -1,11 +1,10 @@
1
1
  /**
2
- * `shipctl run` — single entry-point for executing a Ship lane (RFC-0007).
2
+ * `shipctl run` — single entry-point for executing a Ship routine.
3
3
  *
4
4
  * Today's scope (Phase 1):
5
- * - `kind=once` lanes run with local idempotency markers.
6
- * - `kind=event` and `kind=schedule` lanes execute when Ship's trigger
7
- * router says they are due; router-side audit state prevents duplicate
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 lane with a flaky callback still exits 0, but prints a
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 lane (Automation in the operator console).
52
+ console.log(`shipctl run — execute a Ship routine.
50
53
 
51
54
  WHAT THIS COMMAND IS FOR
52
- shipctl run is the **one-shot dispatch** entry point. It resolves a
53
- lane from .ship/config.yml, fetches its pattern body, checks
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 lane kind:
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 --lane <id> [--pattern <id>] [--fanout <matrix|sequential|concurrent>]
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
- --lane <id> Lane id declared in .ship/config.yml. Required.
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 --lane seed_knowledge_starters | feed-to-agent
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.lane) {
120
- die(EXIT_USAGE, "`--lane <id>` is required.\nRun: shipctl run --help");
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 lane = config.lanes?.[args.lane];
157
- if (!lane) {
158
- const known = Object.keys(config.lanes || {}).sort();
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.lane}'. Known lanes: ${known.length ? known.join(", ") : "(none)"}`,
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, lane.kind);
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
- lane: args.lane,
172
- kind: lane.kind,
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: `lane.kind=${lane.kind} does not accept trigger=${effectiveTrigger.trigger}`,
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 = lanePatterns(lane);
194
- if (allPatterns.length === 0) {
195
- die(EXIT_USAGE, `lane ${JSON.stringify(args.lane)} declares no patterns.`);
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 || laneFanout(lane);
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.lane)}. ` +
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
- `lane ${JSON.stringify(args.lane)} has fanout=matrix and ${allPatterns.length} patterns ` +
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 = lane.kind === "once" ? lane.idempotency : null;
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: lane.pattern_version || null,
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 = fetched.map(({ patternId, result }) => ({
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
- lane: args.lane,
285
- kind: lane.kind,
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
- `lane ${args.lane}: already completed, no-op.`,
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
- lane: args.lane,
312
- kind: lane.kind,
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: lane=${args.lane} kind=${lane.kind} trigger=${effectiveTrigger.trigger} mode=${runMode} (dry-run)`,
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.lane);
354
- if (provider) console.error(`# ship: lane=${args.lane} agent.provider=${provider} mode=${runMode}`);
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
- lane: args.lane,
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: lane.pattern_version || null,
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
- ? `lane ${args.lane} completed (pattern ${runs[0].patternId}@${runs[0].sha256.slice(0, 8)}).`
384
- : `lane ${args.lane} completed (${runs.length} patterns, mode=${runMode}).`;
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
- lane: args.lane,
394
- kind: lane.kind,
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
- lane: null,
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("--lane", "lane")) continue;
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.lane && !out.lane_id) out.lane_id = args.lane;
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: lane=${summary.lane} status=${summary.status}${summary.reason ? ` reason="${summary.reason}"` : ""}`,
873
+ `# ship: routine=${summary.routine || summary.lane} status=${summary.status}${summary.reason ? ` reason="${summary.reason}"` : ""}`,
850
874
  );
851
875
  }
852
876
  }
@@ -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 = "v1";
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 result = await apiPostJson(
15
- baseUrl,
16
- `/v1/workspaces/${encodeURIComponent(workspaceId)}/repos/${encodeURIComponent(repoId)}/trigger`,
17
- {
18
- event: opts.event,
19
- config,
20
- github: {
21
- event_name: process.env.SHIP_EVENT_NAME || process.env.GITHUB_EVENT_NAME || "",
22
- ref: process.env.SHIP_REF || process.env.GITHUB_REF || "",
23
- sha: process.env.SHIP_SHA || process.env.GITHUB_SHA || "",
24
- run_id: process.env.GITHUB_RUN_ID || "",
25
- },
26
- },
27
- token,
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 lanes due.`);
44
+ console.log(`Ship trigger ${opts.event}: no routines due.`);
37
45
  return;
38
46
  }
39
- console.log(`Ship trigger ${opts.event}: ${due.length} lane(s) due`);
40
- for (const lane of due) console.log(` - ${lane.lane_id}`);
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 — ask Ship which lanes are due (${VERSION})
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 Required.
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
- console.error(`HTTP ${res.status} ${res.statusText} on ${method} ${url}\n${msg}`);
195
- process.exit(res.status >= 500 ? 3 : 1);
232
+ throw new Error(`HTTP ${res.status} ${res.statusText} on ${method} ${url}\n${msg}`);
196
233
  }