@elmundi/ship-cli 0.12.1 → 0.14.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/README.md +70 -653
- package/bin/shipctl.mjs +9 -1
- package/lib/agents/cursor.mjs +143 -0
- package/lib/agents/index.mjs +51 -0
- package/lib/commands/help.mjs +31 -23
- package/lib/commands/knowledge.mjs +18 -7
- package/lib/commands/lanes.mjs +74 -35
- package/lib/commands/process.mjs +388 -0
- package/lib/commands/run.mjs +456 -763
- package/lib/commands/sync.mjs +1 -1
- package/lib/commands/trigger.mjs +73 -40
- package/lib/config/schema.mjs +196 -10
- package/lib/process/specialist-prompt-contract.mjs +171 -0
- package/lib/runtime/routines.mjs +264 -0
- package/lib/vendor/run-agent.workflow.yml +254 -0
- package/package.json +4 -4
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { laneFanout, lanePatterns, LANE_FANOUT_DEFAULT } from "../config/schema.mjs";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SCHEDULE_WINDOW_MINUTES = 30;
|
|
4
|
+
|
|
5
|
+
export function routineEntries(config) {
|
|
6
|
+
const process = config?.process;
|
|
7
|
+
const raw = process && typeof process === "object" ? process.routines : null;
|
|
8
|
+
const out = [];
|
|
9
|
+
if (Array.isArray(raw)) {
|
|
10
|
+
for (const item of raw) {
|
|
11
|
+
if (!item || typeof item !== "object") continue;
|
|
12
|
+
const id = typeof item.id === "string" ? item.id.trim() : "";
|
|
13
|
+
if (!id) continue;
|
|
14
|
+
out.push([id, item]);
|
|
15
|
+
}
|
|
16
|
+
} else if (raw && typeof raw === "object") {
|
|
17
|
+
for (const [id, item] of Object.entries(raw)) {
|
|
18
|
+
if (!item || typeof item !== "object") continue;
|
|
19
|
+
out.push([id, { id, ...item }]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function routineMap(config) {
|
|
26
|
+
return Object.fromEntries(routineEntries(config));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveExecutable(config, id) {
|
|
30
|
+
const routines = routineMap(config);
|
|
31
|
+
if (routines[id]) {
|
|
32
|
+
return { kind: "routine", id, source: routines[id], executable: routineToExecutable(id, routines[id]) };
|
|
33
|
+
}
|
|
34
|
+
const lane = config?.lanes?.[id];
|
|
35
|
+
if (lane) {
|
|
36
|
+
return { kind: "lane", id, source: lane, executable: laneToExecutable(id, lane) };
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function executableIds(config) {
|
|
42
|
+
return {
|
|
43
|
+
routines: Object.keys(routineMap(config)).sort(),
|
|
44
|
+
lanes: Object.keys(config?.lanes || {}).sort(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function routineToExecutable(id, routine) {
|
|
49
|
+
const trigger = normalizeRoutineTrigger(routine);
|
|
50
|
+
return {
|
|
51
|
+
id,
|
|
52
|
+
type: "routine",
|
|
53
|
+
kind: trigger.kind,
|
|
54
|
+
trigger,
|
|
55
|
+
pattern: stringOrNull(routine.pattern),
|
|
56
|
+
patterns: Array.isArray(routine.patterns) ? routine.patterns : undefined,
|
|
57
|
+
pattern_version: stringOrNull(routine.pattern_version),
|
|
58
|
+
fanout: routine.fanout || LANE_FANOUT_DEFAULT,
|
|
59
|
+
idempotency: routine.idempotency || null,
|
|
60
|
+
prompt: stringOrNull(routine.prompt) || stringOrNull(routine.instructions),
|
|
61
|
+
agent_profile: stringOrNull(routine.agent_profile) || stringOrNull(routine.specialist?.agent_profile),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function laneToExecutable(id, lane) {
|
|
66
|
+
return {
|
|
67
|
+
id,
|
|
68
|
+
type: "lane",
|
|
69
|
+
kind: lane.kind,
|
|
70
|
+
trigger: laneTrigger(lane),
|
|
71
|
+
pattern: lane.pattern,
|
|
72
|
+
patterns: lane.patterns,
|
|
73
|
+
pattern_version: lane.pattern_version,
|
|
74
|
+
fanout: laneFanout(lane),
|
|
75
|
+
idempotency: lane.idempotency || null,
|
|
76
|
+
prompt: null,
|
|
77
|
+
agent_profile: null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function executablePatterns(executable) {
|
|
82
|
+
return lanePatterns(executable);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function executableFanout(executable) {
|
|
86
|
+
return executable?.fanout || LANE_FANOUT_DEFAULT;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function dueRoutines(config, { event, now = new Date() }) {
|
|
90
|
+
const due = [];
|
|
91
|
+
const skipped = [];
|
|
92
|
+
for (const [routineId, routine] of routineEntries(config)) {
|
|
93
|
+
if (routine.enabled === false) {
|
|
94
|
+
skipped.push({ routine_id: routineId, reason: "disabled" });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const executable = routineToExecutable(routineId, routine);
|
|
98
|
+
if (!routineAcceptsEvent(executable, event)) {
|
|
99
|
+
skipped.push({ routine_id: routineId, reason: "trigger_mismatch", trigger: executable.kind });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (event === "schedule") {
|
|
103
|
+
const slot = latestCronMatch(executable.trigger.cron, {
|
|
104
|
+
now,
|
|
105
|
+
windowMinutes: executable.trigger.window_minutes,
|
|
106
|
+
});
|
|
107
|
+
if (!slot) {
|
|
108
|
+
skipped.push({ routine_id: routineId, reason: "not_due", trigger: "schedule" });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const windowEnd = new Date(slot.getTime() + executable.trigger.window_minutes * 60_000);
|
|
112
|
+
due.push({
|
|
113
|
+
routine_id: routineId,
|
|
114
|
+
trigger: "schedule",
|
|
115
|
+
cron: executable.trigger.cron,
|
|
116
|
+
scheduled_for: slot.toISOString(),
|
|
117
|
+
window_start: slot.toISOString(),
|
|
118
|
+
window_end: windowEnd.toISOString(),
|
|
119
|
+
window_key: `schedule:${routineId}:${formatWindowKey(slot)}`,
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const started = new Date(now);
|
|
124
|
+
due.push({
|
|
125
|
+
routine_id: routineId,
|
|
126
|
+
trigger: event,
|
|
127
|
+
scheduled_for: started.toISOString(),
|
|
128
|
+
window_start: started.toISOString(),
|
|
129
|
+
window_end: started.toISOString(),
|
|
130
|
+
window_key: `${event}:${routineId}:${formatWindowKey(started)}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return { due, skipped };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function dueLanesFromRoutines(due) {
|
|
137
|
+
return due.map((routine) => ({
|
|
138
|
+
lane_id: routine.routine_id,
|
|
139
|
+
routine_id: routine.routine_id,
|
|
140
|
+
kind: routine.trigger,
|
|
141
|
+
reason: "due",
|
|
142
|
+
window_key: routine.window_key,
|
|
143
|
+
scheduled_for: routine.scheduled_for,
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeRoutineTrigger(routine) {
|
|
148
|
+
const trigger = routine.trigger && typeof routine.trigger === "object" ? routine.trigger : {};
|
|
149
|
+
if (routine.schedule || trigger.type === "schedule") {
|
|
150
|
+
const schedule = routine.schedule && typeof routine.schedule === "object" ? routine.schedule : {};
|
|
151
|
+
const cron =
|
|
152
|
+
stringOrNull(trigger.cron) ||
|
|
153
|
+
stringOrNull(trigger.interval) ||
|
|
154
|
+
stringOrNull(schedule.cron) ||
|
|
155
|
+
stringOrNull(schedule.interval) ||
|
|
156
|
+
(typeof routine.schedule === "string" ? routine.schedule : null);
|
|
157
|
+
return {
|
|
158
|
+
kind: "schedule",
|
|
159
|
+
cron,
|
|
160
|
+
window_minutes: parseWindowMinutes(trigger.window || schedule.window || routine.window),
|
|
161
|
+
catchup: stringOrNull(trigger.catchup) || stringOrNull(schedule.catchup) || "latest",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (trigger.type === "event" || routine.event) {
|
|
165
|
+
return {
|
|
166
|
+
kind: "event",
|
|
167
|
+
event: stringOrNull(trigger.event) || stringOrNull(routine.event),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return { kind: "once" };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function laneTrigger(lane) {
|
|
174
|
+
if (lane.kind === "schedule") {
|
|
175
|
+
return {
|
|
176
|
+
kind: "schedule",
|
|
177
|
+
cron: stringOrNull(lane.cron),
|
|
178
|
+
window_minutes: DEFAULT_SCHEDULE_WINDOW_MINUTES,
|
|
179
|
+
catchup: "latest",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (lane.kind === "event") return { kind: "event", event: stringOrNull(lane.on) };
|
|
183
|
+
return { kind: lane.kind || "once" };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function routineAcceptsEvent(executable, event) {
|
|
187
|
+
if (event === "schedule") return executable.kind === "schedule";
|
|
188
|
+
if (event === "manual") return ["schedule", "event", "once"].includes(executable.kind);
|
|
189
|
+
if (executable.kind !== "event") return false;
|
|
190
|
+
const configured = executable.trigger.event || "";
|
|
191
|
+
return configured.split(",").map((part) => part.trim()).filter(Boolean).includes(event);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function latestCronMatch(cron, { now, windowMinutes }) {
|
|
195
|
+
if (typeof cron !== "string" || !cron.trim()) return null;
|
|
196
|
+
const fields = cron.trim().split(/\s+/);
|
|
197
|
+
if (fields.length !== 5) return null;
|
|
198
|
+
const [minute, hour, dom, month, dow] = fields;
|
|
199
|
+
const cursor = new Date(now);
|
|
200
|
+
cursor.setUTCSeconds(0, 0);
|
|
201
|
+
for (let offset = 0; offset <= windowMinutes; offset += 1) {
|
|
202
|
+
const candidate = new Date(cursor.getTime() - offset * 60_000);
|
|
203
|
+
if (
|
|
204
|
+
cronFieldMatches(minute, candidate.getUTCMinutes(), 0, 59) &&
|
|
205
|
+
cronFieldMatches(hour, candidate.getUTCHours(), 0, 23) &&
|
|
206
|
+
cronFieldMatches(dom, candidate.getUTCDate(), 1, 31) &&
|
|
207
|
+
cronFieldMatches(month, candidate.getUTCMonth() + 1, 1, 12) &&
|
|
208
|
+
cronFieldMatches(dow, candidate.getUTCDay(), 0, 6)
|
|
209
|
+
) {
|
|
210
|
+
return candidate;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function cronFieldMatches(expr, value, min, max) {
|
|
217
|
+
for (const rawPart of String(expr).split(",")) {
|
|
218
|
+
const part = rawPart.trim();
|
|
219
|
+
if (!part) continue;
|
|
220
|
+
let step = 1;
|
|
221
|
+
let base = part;
|
|
222
|
+
if (part.includes("/")) {
|
|
223
|
+
const pieces = part.split("/");
|
|
224
|
+
base = pieces[0];
|
|
225
|
+
step = Number.parseInt(pieces[1], 10);
|
|
226
|
+
if (!Number.isInteger(step) || step <= 0) continue;
|
|
227
|
+
}
|
|
228
|
+
let start;
|
|
229
|
+
let end;
|
|
230
|
+
if (base === "*") {
|
|
231
|
+
start = min;
|
|
232
|
+
end = max;
|
|
233
|
+
} else if (base.includes("-")) {
|
|
234
|
+
const [a, b] = base.split("-");
|
|
235
|
+
start = Number.parseInt(a, 10);
|
|
236
|
+
end = Number.parseInt(b, 10);
|
|
237
|
+
} else {
|
|
238
|
+
start = Number.parseInt(base, 10);
|
|
239
|
+
end = start;
|
|
240
|
+
}
|
|
241
|
+
if (!Number.isInteger(start) || !Number.isInteger(end)) continue;
|
|
242
|
+
if (start <= value && value <= end && (value - start) % step === 0) return true;
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function parseWindowMinutes(value) {
|
|
248
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.floor(value);
|
|
249
|
+
if (typeof value !== "string" || !value.trim()) return DEFAULT_SCHEDULE_WINDOW_MINUTES;
|
|
250
|
+
const raw = value.trim();
|
|
251
|
+
const match = raw.match(/^(\d+)\s*(m|min|minute|minutes)?$/i);
|
|
252
|
+
if (match) return Math.max(1, Number.parseInt(match[1], 10));
|
|
253
|
+
const hours = raw.match(/^(\d+)\s*(h|hr|hour|hours)$/i);
|
|
254
|
+
if (hours) return Math.max(1, Number.parseInt(hours[1], 10) * 60);
|
|
255
|
+
return DEFAULT_SCHEDULE_WINDOW_MINUTES;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function formatWindowKey(date) {
|
|
259
|
+
return date.toISOString().slice(0, 16).replace(/[-:]/g, "").replace("T", "T");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function stringOrNull(value) {
|
|
263
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
264
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# ship-cli: run-agent v1 — generated by `shipctl lanes install`.
|
|
2
|
+
# Regenerate via: shipctl lanes install
|
|
3
|
+
# Template ships with @elmundi/ship-cli; hand edits outside this banner may be overwritten.
|
|
4
|
+
name: Ship · run-agent (reusable)
|
|
5
|
+
# RFC-0007 Phase 3 + RFC-0008 C3.2 — reusable workflow for lane wrappers.
|
|
6
|
+
# Caller repos use: uses: ./.github/workflows/run-agent.yml (same repository).
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
workflow_call:
|
|
10
|
+
inputs:
|
|
11
|
+
lane:
|
|
12
|
+
description: "Lane id as declared in .ship/config.yml"
|
|
13
|
+
type: string
|
|
14
|
+
required: true
|
|
15
|
+
trigger:
|
|
16
|
+
description: "Override trigger (once|event|schedule|manual). Defaults to github.event_name."
|
|
17
|
+
type: string
|
|
18
|
+
required: false
|
|
19
|
+
default: ""
|
|
20
|
+
shipctl_version:
|
|
21
|
+
description: "npm dist-tag or semver for @elmundi/ship-cli. Default: latest."
|
|
22
|
+
type: string
|
|
23
|
+
required: false
|
|
24
|
+
default: "latest"
|
|
25
|
+
node_version:
|
|
26
|
+
description: "Node.js major version for the runner"
|
|
27
|
+
type: string
|
|
28
|
+
required: false
|
|
29
|
+
default: "20"
|
|
30
|
+
dry_run:
|
|
31
|
+
description: "Resolve the lane but neither call the callback nor write the idempotency marker."
|
|
32
|
+
type: boolean
|
|
33
|
+
required: false
|
|
34
|
+
default: false
|
|
35
|
+
offline:
|
|
36
|
+
description: "Do not talk to the methodology API; read patterns from the local .ship/cache."
|
|
37
|
+
type: boolean
|
|
38
|
+
required: false
|
|
39
|
+
default: false
|
|
40
|
+
ship_run_id:
|
|
41
|
+
description: "Pipeline-run id the callback should report against (set by Ship dispatch)."
|
|
42
|
+
type: string
|
|
43
|
+
required: false
|
|
44
|
+
default: ""
|
|
45
|
+
ship_callback_url:
|
|
46
|
+
description: "Ship callback URL (set by Ship dispatch)."
|
|
47
|
+
type: string
|
|
48
|
+
required: false
|
|
49
|
+
default: ""
|
|
50
|
+
upload_prompt:
|
|
51
|
+
description: "Upload the rendered prompt as a workflow artifact named `ship-prompt`."
|
|
52
|
+
type: boolean
|
|
53
|
+
required: false
|
|
54
|
+
default: true
|
|
55
|
+
secrets:
|
|
56
|
+
SHIP_API_TOKEN:
|
|
57
|
+
description: "Methodology API token (optional; required for private patterns / callbacks)."
|
|
58
|
+
required: false
|
|
59
|
+
SHIP_API_BASE:
|
|
60
|
+
description: "Ship API origin for shipctl (e.g. https://api.example.com). Wizard seeds this from SHIP_PUBLIC_URL."
|
|
61
|
+
required: false
|
|
62
|
+
SHIP_RUN_TOKEN:
|
|
63
|
+
description: "Short-lived bearer token Ship issued when dispatching this run."
|
|
64
|
+
required: false
|
|
65
|
+
|
|
66
|
+
permissions:
|
|
67
|
+
contents: read
|
|
68
|
+
|
|
69
|
+
jobs:
|
|
70
|
+
plan:
|
|
71
|
+
name: "ship plan --lane ${{ inputs.lane }}"
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
timeout-minutes: 5
|
|
74
|
+
outputs:
|
|
75
|
+
matrix: ${{ steps.plan.outputs.matrix }}
|
|
76
|
+
patterns: ${{ steps.plan.outputs.patterns }}
|
|
77
|
+
fanout: ${{ steps.plan.outputs.fanout }}
|
|
78
|
+
steps:
|
|
79
|
+
- name: Checkout caller repo
|
|
80
|
+
uses: actions/checkout@v4
|
|
81
|
+
|
|
82
|
+
- name: Setup Node.js
|
|
83
|
+
uses: actions/setup-node@v4
|
|
84
|
+
with:
|
|
85
|
+
node-version: ${{ inputs.node_version }}
|
|
86
|
+
|
|
87
|
+
- name: Install shipctl
|
|
88
|
+
run: npm install -g @elmundi/ship-cli@${{ inputs.shipctl_version }}
|
|
89
|
+
|
|
90
|
+
- name: Resolve patterns + fanout
|
|
91
|
+
id: plan
|
|
92
|
+
env:
|
|
93
|
+
LANE: ${{ inputs.lane }}
|
|
94
|
+
run: |
|
|
95
|
+
set -euo pipefail
|
|
96
|
+
JSON=$(shipctl lanes list --json)
|
|
97
|
+
ROW=$(echo "$JSON" | jq -c --arg lane "$LANE" '.lanes[] | select(.lane == $lane)')
|
|
98
|
+
if [ -z "$ROW" ] || [ "$ROW" = "null" ]; then
|
|
99
|
+
echo "::error::lane '$LANE' not declared in .ship/config.yml"
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
PATTERNS=$(echo "$ROW" | jq -c '.patterns // []')
|
|
103
|
+
FANOUT=$(echo "$ROW" | jq -r '.fanout // "matrix"')
|
|
104
|
+
N=$(echo "$PATTERNS" | jq 'length')
|
|
105
|
+
if [ "$N" -lt 1 ]; then
|
|
106
|
+
echo "::error::lane '$LANE' declares no patterns"
|
|
107
|
+
exit 1
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
if [ "$FANOUT" = "matrix" ] && [ "$N" -gt 1 ]; then
|
|
111
|
+
MATRIX=$(echo "$PATTERNS" | jq -c '[.[] | {pattern: ., fanout: "matrix"}]')
|
|
112
|
+
elif [ "$N" -gt 1 ]; then
|
|
113
|
+
MATRIX=$(jq -cn --arg fanout "$FANOUT" '[{pattern: "", fanout: $fanout}]')
|
|
114
|
+
else
|
|
115
|
+
MATRIX='[{"pattern": "", "fanout": "matrix"}]'
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
echo "patterns=$PATTERNS" >> "$GITHUB_OUTPUT"
|
|
119
|
+
echo "fanout=$FANOUT" >> "$GITHUB_OUTPUT"
|
|
120
|
+
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
|
|
121
|
+
{
|
|
122
|
+
echo "### Plan"
|
|
123
|
+
echo "- lane: \`$LANE\`"
|
|
124
|
+
echo "- patterns: $(echo "$PATTERNS" | jq -r '. | join(", ")')"
|
|
125
|
+
echo "- fanout: $FANOUT"
|
|
126
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
127
|
+
|
|
128
|
+
run:
|
|
129
|
+
name: "ship run --lane ${{ inputs.lane }} (pattern=${{ matrix.entry.pattern }} fanout=${{ matrix.entry.fanout }})"
|
|
130
|
+
needs: plan
|
|
131
|
+
runs-on: ubuntu-latest
|
|
132
|
+
timeout-minutes: 20
|
|
133
|
+
strategy:
|
|
134
|
+
fail-fast: false
|
|
135
|
+
matrix:
|
|
136
|
+
entry: ${{ fromJSON(needs.plan.outputs.matrix) }}
|
|
137
|
+
steps:
|
|
138
|
+
- name: Checkout caller repo
|
|
139
|
+
uses: actions/checkout@v4
|
|
140
|
+
|
|
141
|
+
- name: Setup Node.js
|
|
142
|
+
uses: actions/setup-node@v4
|
|
143
|
+
with:
|
|
144
|
+
node-version: ${{ inputs.node_version }}
|
|
145
|
+
|
|
146
|
+
- name: Install shipctl
|
|
147
|
+
run: npm install -g @elmundi/ship-cli@${{ inputs.shipctl_version }}
|
|
148
|
+
|
|
149
|
+
- name: Resolve trigger
|
|
150
|
+
id: trigger
|
|
151
|
+
env:
|
|
152
|
+
OVERRIDE: ${{ inputs.trigger }}
|
|
153
|
+
EVENT: ${{ github.event_name }}
|
|
154
|
+
run: |
|
|
155
|
+
set -euo pipefail
|
|
156
|
+
if [ -n "${OVERRIDE:-}" ]; then
|
|
157
|
+
echo "value=$OVERRIDE" >> "$GITHUB_OUTPUT"
|
|
158
|
+
else
|
|
159
|
+
case "$EVENT" in
|
|
160
|
+
workflow_dispatch) echo "value=manual" >> "$GITHUB_OUTPUT" ;;
|
|
161
|
+
schedule) echo "value=schedule" >> "$GITHUB_OUTPUT" ;;
|
|
162
|
+
*) echo "value=event" >> "$GITHUB_OUTPUT" ;;
|
|
163
|
+
esac
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
- name: shipctl run
|
|
167
|
+
id: run
|
|
168
|
+
env:
|
|
169
|
+
SHIP_API_TOKEN: ${{ secrets.SHIP_API_TOKEN }}
|
|
170
|
+
SHIP_API_BASE: ${{ secrets.SHIP_API_BASE }}
|
|
171
|
+
SHIP_RUN_TOKEN: ${{ secrets.SHIP_RUN_TOKEN }}
|
|
172
|
+
SHIP_RUN_ID: ${{ inputs.ship_run_id }}
|
|
173
|
+
MATRIX_PATTERN: ${{ matrix.entry.pattern }}
|
|
174
|
+
MATRIX_FANOUT: ${{ matrix.entry.fanout }}
|
|
175
|
+
run: |
|
|
176
|
+
set -euo pipefail
|
|
177
|
+
mkdir -p .ship/run-output
|
|
178
|
+
ARGS=(run --lane "${{ inputs.lane }}" --trigger "${{ steps.trigger.outputs.value }}")
|
|
179
|
+
[ "${{ inputs.dry_run }}" = "true" ] && ARGS+=(--dry-run)
|
|
180
|
+
[ "${{ inputs.offline }}" = "true" ] && ARGS+=(--offline)
|
|
181
|
+
[ -n "${MATRIX_PATTERN:-}" ] && ARGS+=(--pattern "$MATRIX_PATTERN")
|
|
182
|
+
[ -n "${MATRIX_FANOUT:-}" ] && ARGS+=(--fanout "$MATRIX_FANOUT")
|
|
183
|
+
[ -n "${SHIP_RUN_ID:-}" ] && ARGS+=(--ship-run-id "$SHIP_RUN_ID")
|
|
184
|
+
|
|
185
|
+
set +e
|
|
186
|
+
shipctl "${ARGS[@]}" \
|
|
187
|
+
1> .ship/run-output/prompt.md \
|
|
188
|
+
2> .ship/run-output/shipctl.log
|
|
189
|
+
STATUS=$?
|
|
190
|
+
set -e
|
|
191
|
+
|
|
192
|
+
cat .ship/run-output/shipctl.log >&2 || true
|
|
193
|
+
echo "status=$STATUS" >> "$GITHUB_OUTPUT"
|
|
194
|
+
if [ "$STATUS" -ne 0 ]; then
|
|
195
|
+
echo "::error::shipctl run exited with $STATUS"
|
|
196
|
+
exit "$STATUS"
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
- name: Upload prompt artifact
|
|
200
|
+
if: ${{ inputs.upload_prompt && (success() || failure()) }}
|
|
201
|
+
uses: actions/upload-artifact@v4
|
|
202
|
+
with:
|
|
203
|
+
name: ship-prompt-${{ inputs.lane }}${{ matrix.entry.pattern != '' && format('-{0}', matrix.entry.pattern) || '' }}
|
|
204
|
+
path: .ship/run-output/
|
|
205
|
+
if-no-files-found: warn
|
|
206
|
+
retention-days: 14
|
|
207
|
+
|
|
208
|
+
aggregate:
|
|
209
|
+
name: "ship aggregate --lane ${{ inputs.lane }}"
|
|
210
|
+
if: ${{ always() && inputs.ship_callback_url != '' }}
|
|
211
|
+
needs: [plan, run]
|
|
212
|
+
runs-on: ubuntu-latest
|
|
213
|
+
timeout-minutes: 5
|
|
214
|
+
steps:
|
|
215
|
+
- name: Checkout caller repo
|
|
216
|
+
uses: actions/checkout@v4
|
|
217
|
+
|
|
218
|
+
- name: Setup Node.js
|
|
219
|
+
uses: actions/setup-node@v4
|
|
220
|
+
with:
|
|
221
|
+
node-version: ${{ inputs.node_version }}
|
|
222
|
+
|
|
223
|
+
- name: Install shipctl
|
|
224
|
+
run: npm install -g @elmundi/ship-cli@${{ inputs.shipctl_version }}
|
|
225
|
+
|
|
226
|
+
- name: Report rollup to Ship
|
|
227
|
+
env:
|
|
228
|
+
SHIP_RUN_ID: ${{ inputs.ship_run_id }}
|
|
229
|
+
SHIP_CALLBACK_URL: ${{ inputs.ship_callback_url }}
|
|
230
|
+
SHIP_RUN_TOKEN: ${{ secrets.SHIP_RUN_TOKEN }}
|
|
231
|
+
GH_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
232
|
+
RUN_RESULT: ${{ needs.run.result }}
|
|
233
|
+
PLAN_PATTERNS: ${{ needs.plan.outputs.patterns }}
|
|
234
|
+
PLAN_FANOUT: ${{ needs.plan.outputs.fanout }}
|
|
235
|
+
run: |
|
|
236
|
+
set -euo pipefail
|
|
237
|
+
case "$RUN_RESULT" in
|
|
238
|
+
success)
|
|
239
|
+
CB=ok
|
|
240
|
+
SUMMARY="Ship lane ${{ inputs.lane }} completed ($PLAN_FANOUT over $PLAN_PATTERNS)."
|
|
241
|
+
;;
|
|
242
|
+
failure|cancelled|skipped|*)
|
|
243
|
+
CB=fail
|
|
244
|
+
SUMMARY="Ship lane ${{ inputs.lane }} $RUN_RESULT ($PLAN_FANOUT over $PLAN_PATTERNS)."
|
|
245
|
+
;;
|
|
246
|
+
esac
|
|
247
|
+
shipctl callback --status "$CB" \
|
|
248
|
+
--summary "$SUMMARY" \
|
|
249
|
+
--metric "lane_id=${{ inputs.lane }}" \
|
|
250
|
+
--metric "fanout=$PLAN_FANOUT" \
|
|
251
|
+
--metric "patterns=$(echo "$PLAN_PATTERNS" | jq -r '. | join(",")')" \
|
|
252
|
+
--metric "gh_workflow_run_id=${{ github.run_id }}" \
|
|
253
|
+
--metric "gh_html_url=$GH_RUN_URL" \
|
|
254
|
+
--metric "gh_event=${{ github.event_name }}" || true
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elmundi/ship-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Ship CLI: bootstrap a repo, sync the
|
|
5
|
+
"description": "Ship CLI: bootstrap a repo, sync the catalog, run process routines, report outcomes.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "Denys Kuzin",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/ElMundiUA/ship.git",
|
|
10
|
+
"url": "git+https://github.com/ElMundiUA/ship.git",
|
|
11
11
|
"directory": "cli"
|
|
12
12
|
},
|
|
13
13
|
"bugs": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"bin": {
|
|
35
|
-
"shipctl": "
|
|
35
|
+
"shipctl": "bin/shipctl.mjs"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"test": "node --test tests/*.test.mjs"
|