@skippercorp/skipper 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/package.json +48 -0
- package/src/app/cli.ts +31 -0
- package/src/app/register-commands.ts +37 -0
- package/src/command/a.ts +213 -0
- package/src/command/aws/bootstrap.ts +508 -0
- package/src/command/aws/cloudformation.ts +243 -0
- package/src/command/aws/defaults.ts +103 -0
- package/src/command/aws/deploy-template.ts +308 -0
- package/src/command/aws/deploy.ts +593 -0
- package/src/command/aws/github.ts +358 -0
- package/src/command/aws/index.ts +17 -0
- package/src/command/aws/lambda/eventbridge-handler.ts +83 -0
- package/src/command/aws/lambda/handler.ts +521 -0
- package/src/command/aws/lambda/types.ts +86 -0
- package/src/command/aws/network.ts +51 -0
- package/src/command/aws/run.ts +566 -0
- package/src/command/aws/template.ts +406 -0
- package/src/command/aws/verify-issue-subscription.ts +782 -0
- package/src/command/clone.ts +67 -0
- package/src/command/rm.ts +126 -0
- package/src/command/run.ts +43 -0
- package/src/index.ts +16 -0
- package/src/shared/command/interactive.ts +120 -0
- package/src/shared/validation/parse-json.ts +59 -0
- package/src/worker/aws-params.ts +54 -0
- package/src/worker/contract.ts +324 -0
- package/src/worker/github-events.ts +57 -0
- package/src/worker/load.ts +86 -0
- package/src/worker/route.ts +91 -0
- package/src/worker/serialize.ts +175 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
type WorkerProvider = "github";
|
|
2
|
+
type WorkerAgent = "claude" | "opencode";
|
|
3
|
+
type WorkerMode = "comment-only" | "apply";
|
|
4
|
+
|
|
5
|
+
export type WorkerMetadata = {
|
|
6
|
+
id: string;
|
|
7
|
+
type: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
version?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type WorkerTriggerFilter = {
|
|
14
|
+
repository?: string[];
|
|
15
|
+
baseBranches?: string[];
|
|
16
|
+
headBranches?: string[];
|
|
17
|
+
draft?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type WorkerTrigger = {
|
|
21
|
+
provider: WorkerProvider;
|
|
22
|
+
event: string;
|
|
23
|
+
actions?: string[];
|
|
24
|
+
if?: WorkerTriggerFilter;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type WorkerRuntime = {
|
|
28
|
+
agent?: WorkerAgent;
|
|
29
|
+
mode?: WorkerMode;
|
|
30
|
+
prompt: string;
|
|
31
|
+
allowPush?: boolean;
|
|
32
|
+
maxDurationMinutes?: number;
|
|
33
|
+
env?: Record<string, string>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type WorkerDefinition = {
|
|
37
|
+
metadata: WorkerMetadata;
|
|
38
|
+
triggers: WorkerTrigger[];
|
|
39
|
+
runtime: WorkerRuntime;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type WorkerManifest = {
|
|
43
|
+
workers: WorkerDefinition[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse worker module export into typed worker definition.
|
|
48
|
+
*
|
|
49
|
+
* @since 1.0.0
|
|
50
|
+
* @category Shared
|
|
51
|
+
*/
|
|
52
|
+
export function parseWorkerDefinition(value: unknown, label: string): WorkerDefinition {
|
|
53
|
+
if (!isRecord(value)) {
|
|
54
|
+
throw new Error(`invalid worker definition in ${label}`);
|
|
55
|
+
}
|
|
56
|
+
const metadata = parseMetadata(value.metadata, label);
|
|
57
|
+
const triggers = parseTriggers(value.triggers, label);
|
|
58
|
+
const runtime = parseRuntime(value.runtime, label);
|
|
59
|
+
return {
|
|
60
|
+
metadata: {
|
|
61
|
+
...metadata,
|
|
62
|
+
enabled: metadata.enabled ?? true,
|
|
63
|
+
},
|
|
64
|
+
triggers,
|
|
65
|
+
runtime,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse worker metadata section.
|
|
71
|
+
*
|
|
72
|
+
* @since 1.0.0
|
|
73
|
+
* @category Shared
|
|
74
|
+
*/
|
|
75
|
+
function parseMetadata(value: unknown, label: string): WorkerMetadata {
|
|
76
|
+
if (!isRecord(value)) throw new Error(`missing metadata in ${label}`);
|
|
77
|
+
const id = parseWorkerId(value.id, label);
|
|
78
|
+
const type = readNonEmptyString(value.type, `metadata.type in ${label}`);
|
|
79
|
+
const description = readOptionalString(value.description, `metadata.description in ${label}`);
|
|
80
|
+
const enabled = readOptionalBoolean(value.enabled, `metadata.enabled in ${label}`);
|
|
81
|
+
const version = readOptionalString(value.version, `metadata.version in ${label}`);
|
|
82
|
+
return { id, type, description, enabled, version };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse worker trigger list.
|
|
87
|
+
*
|
|
88
|
+
* @since 1.0.0
|
|
89
|
+
* @category Shared
|
|
90
|
+
*/
|
|
91
|
+
function parseTriggers(value: unknown, label: string): WorkerTrigger[] {
|
|
92
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
93
|
+
throw new Error(`missing triggers in ${label}`);
|
|
94
|
+
}
|
|
95
|
+
return value.map((entry, index) => parseTrigger(entry, `${label} triggers[${index}]`));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse one worker trigger.
|
|
100
|
+
*
|
|
101
|
+
* @since 1.0.0
|
|
102
|
+
* @category Shared
|
|
103
|
+
*/
|
|
104
|
+
function parseTrigger(value: unknown, label: string): WorkerTrigger {
|
|
105
|
+
if (!isRecord(value)) {
|
|
106
|
+
throw new Error(`invalid ${label}`);
|
|
107
|
+
}
|
|
108
|
+
const provider = readNonEmptyString(value.provider, `${label} provider`);
|
|
109
|
+
if (provider !== "github") {
|
|
110
|
+
throw new Error(`invalid provider in ${label}`);
|
|
111
|
+
}
|
|
112
|
+
const event = readNonEmptyString(value.event, `${label} event`);
|
|
113
|
+
const actions = readOptionalStringArray(value.actions, `${label} actions`);
|
|
114
|
+
const filter = parseTriggerFilter(value.if, label);
|
|
115
|
+
return {
|
|
116
|
+
provider,
|
|
117
|
+
event,
|
|
118
|
+
actions,
|
|
119
|
+
if: filter,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parse trigger filter block.
|
|
125
|
+
*
|
|
126
|
+
* @since 1.0.0
|
|
127
|
+
* @category Shared
|
|
128
|
+
*/
|
|
129
|
+
function parseTriggerFilter(value: unknown, label: string): WorkerTriggerFilter | undefined {
|
|
130
|
+
if (value === undefined) return undefined;
|
|
131
|
+
if (!isRecord(value)) {
|
|
132
|
+
throw new Error(`invalid if block in ${label}`);
|
|
133
|
+
}
|
|
134
|
+
const repository = readOptionalStringArray(value.repository, `${label} if.repository`);
|
|
135
|
+
const baseBranches = readOptionalStringArray(value.baseBranches, `${label} if.baseBranches`);
|
|
136
|
+
const headBranches = readOptionalStringArray(value.headBranches, `${label} if.headBranches`);
|
|
137
|
+
const draft = readOptionalBoolean(value.draft, `${label} if.draft`);
|
|
138
|
+
return {
|
|
139
|
+
repository,
|
|
140
|
+
baseBranches,
|
|
141
|
+
headBranches,
|
|
142
|
+
draft,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Parse worker runtime section.
|
|
148
|
+
*
|
|
149
|
+
* @since 1.0.0
|
|
150
|
+
* @category Shared
|
|
151
|
+
*/
|
|
152
|
+
function parseRuntime(value: unknown, label: string): WorkerRuntime {
|
|
153
|
+
if (!isRecord(value)) throw new Error(`missing runtime in ${label}`);
|
|
154
|
+
const prompt = readNonEmptyString(value.prompt, `runtime.prompt in ${label}`);
|
|
155
|
+
const agentValue = readOptionalString(value.agent, `runtime.agent in ${label}`);
|
|
156
|
+
const modeValue = readOptionalString(value.mode, `runtime.mode in ${label}`);
|
|
157
|
+
const maxDurationMinutes = readOptionalNumber(
|
|
158
|
+
value.maxDurationMinutes,
|
|
159
|
+
`runtime.maxDurationMinutes in ${label}`,
|
|
160
|
+
);
|
|
161
|
+
const env = readOptionalStringRecord(value.env, `runtime.env in ${label}`);
|
|
162
|
+
const allowPush = readOptionalBoolean(value.allowPush, `runtime.allowPush in ${label}`);
|
|
163
|
+
const agent = parseRuntimeAgent(agentValue, label);
|
|
164
|
+
const mode = parseRuntimeMode(modeValue, label);
|
|
165
|
+
return {
|
|
166
|
+
prompt,
|
|
167
|
+
agent,
|
|
168
|
+
mode,
|
|
169
|
+
allowPush,
|
|
170
|
+
maxDurationMinutes,
|
|
171
|
+
env,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Parse runtime agent value.
|
|
177
|
+
*
|
|
178
|
+
* @since 1.0.0
|
|
179
|
+
* @category Shared
|
|
180
|
+
*/
|
|
181
|
+
function parseRuntimeAgent(value: string | undefined, label: string): WorkerAgent | undefined {
|
|
182
|
+
if (value === undefined) return undefined;
|
|
183
|
+
if (value !== "claude" && value !== "opencode") {
|
|
184
|
+
throw new Error(`invalid runtime.agent in ${label}`);
|
|
185
|
+
}
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Parse runtime mode value.
|
|
191
|
+
*
|
|
192
|
+
* @since 1.0.0
|
|
193
|
+
* @category Shared
|
|
194
|
+
*/
|
|
195
|
+
function parseRuntimeMode(value: string | undefined, label: string): WorkerMode | undefined {
|
|
196
|
+
if (value === undefined) return undefined;
|
|
197
|
+
if (value !== "comment-only" && value !== "apply") {
|
|
198
|
+
throw new Error(`invalid runtime.mode in ${label}`);
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Parse worker id.
|
|
205
|
+
*
|
|
206
|
+
* @since 1.0.0
|
|
207
|
+
* @category Shared
|
|
208
|
+
*/
|
|
209
|
+
function parseWorkerId(value: unknown, label: string): string {
|
|
210
|
+
const workerId = readNonEmptyString(value, `metadata.id in ${label}`);
|
|
211
|
+
if (!/^[a-z0-9][a-z0-9-]*$/i.test(workerId)) {
|
|
212
|
+
throw new Error(`invalid metadata.id in ${label}`);
|
|
213
|
+
}
|
|
214
|
+
return workerId;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Read optional string value.
|
|
219
|
+
*
|
|
220
|
+
* @since 1.0.0
|
|
221
|
+
* @category Shared
|
|
222
|
+
*/
|
|
223
|
+
function readOptionalString(value: unknown, label: string): string | undefined {
|
|
224
|
+
if (value === undefined) return undefined;
|
|
225
|
+
if (typeof value !== "string") {
|
|
226
|
+
throw new Error(`${label} must be string`);
|
|
227
|
+
}
|
|
228
|
+
const normalized = value.trim();
|
|
229
|
+
if (normalized.length === 0) {
|
|
230
|
+
throw new Error(`${label} must be non-empty`);
|
|
231
|
+
}
|
|
232
|
+
return normalized;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Read required non-empty string value.
|
|
237
|
+
*
|
|
238
|
+
* @since 1.0.0
|
|
239
|
+
* @category Shared
|
|
240
|
+
*/
|
|
241
|
+
function readNonEmptyString(value: unknown, label: string): string {
|
|
242
|
+
const normalized = readOptionalString(value, label);
|
|
243
|
+
if (!normalized) {
|
|
244
|
+
throw new Error(`${label} is required`);
|
|
245
|
+
}
|
|
246
|
+
return normalized;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Read optional number value.
|
|
251
|
+
*
|
|
252
|
+
* @since 1.0.0
|
|
253
|
+
* @category Shared
|
|
254
|
+
*/
|
|
255
|
+
function readOptionalNumber(value: unknown, label: string): number | undefined {
|
|
256
|
+
if (value === undefined) return undefined;
|
|
257
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
258
|
+
throw new Error(`${label} must be a positive number`);
|
|
259
|
+
}
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Read optional boolean value.
|
|
265
|
+
*
|
|
266
|
+
* @since 1.0.0
|
|
267
|
+
* @category Shared
|
|
268
|
+
*/
|
|
269
|
+
function readOptionalBoolean(value: unknown, label: string): boolean | undefined {
|
|
270
|
+
if (value === undefined) return undefined;
|
|
271
|
+
if (typeof value !== "boolean") {
|
|
272
|
+
throw new Error(`${label} must be boolean`);
|
|
273
|
+
}
|
|
274
|
+
return value;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Read optional string array value.
|
|
279
|
+
*
|
|
280
|
+
* @since 1.0.0
|
|
281
|
+
* @category Shared
|
|
282
|
+
*/
|
|
283
|
+
function readOptionalStringArray(value: unknown, label: string): string[] | undefined {
|
|
284
|
+
if (value === undefined) return undefined;
|
|
285
|
+
if (!Array.isArray(value)) {
|
|
286
|
+
throw new Error(`${label} must be string[]`);
|
|
287
|
+
}
|
|
288
|
+
const result = value.map((entry, index) =>
|
|
289
|
+
readNonEmptyString(entry, `${label}[${index}]`),
|
|
290
|
+
);
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Read optional string record value.
|
|
296
|
+
*
|
|
297
|
+
* @since 1.0.0
|
|
298
|
+
* @category Shared
|
|
299
|
+
*/
|
|
300
|
+
function readOptionalStringRecord(
|
|
301
|
+
value: unknown,
|
|
302
|
+
label: string,
|
|
303
|
+
): Record<string, string> | undefined {
|
|
304
|
+
if (value === undefined) return undefined;
|
|
305
|
+
if (!isRecord(value)) throw new Error(`${label} must be object`);
|
|
306
|
+
const result: Record<string, string> = {};
|
|
307
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
308
|
+
if (typeof entry !== "string") {
|
|
309
|
+
throw new Error(`${label}.${key} must be string`);
|
|
310
|
+
}
|
|
311
|
+
result[key] = entry;
|
|
312
|
+
}
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Check plain object shape.
|
|
318
|
+
*
|
|
319
|
+
* @since 1.0.0
|
|
320
|
+
* @category Shared
|
|
321
|
+
*/
|
|
322
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
323
|
+
return typeof value === "object" && value !== null;
|
|
324
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { WorkerDefinition } from "./contract.js";
|
|
2
|
+
|
|
3
|
+
export type WorkerGithubEventSubscription = {
|
|
4
|
+
workerId: string;
|
|
5
|
+
events: string[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Collect unique GitHub webhook events from enabled workers.
|
|
10
|
+
*
|
|
11
|
+
* @since 1.0.0
|
|
12
|
+
* @category Shared
|
|
13
|
+
*/
|
|
14
|
+
export function collectGithubEventsFromWorkers(workers: WorkerDefinition[]): string[] {
|
|
15
|
+
const events = new Set<string>();
|
|
16
|
+
for (const subscription of collectGithubEventSubscriptions(workers)) {
|
|
17
|
+
for (const event of subscription.events) {
|
|
18
|
+
events.add(event);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return [...events].sort((left, right) => left.localeCompare(right));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Collect per-worker GitHub event subscriptions for enabled workers.
|
|
26
|
+
*
|
|
27
|
+
* @since 1.0.0
|
|
28
|
+
* @category Shared
|
|
29
|
+
*/
|
|
30
|
+
export function collectGithubEventSubscriptions(
|
|
31
|
+
workers: WorkerDefinition[],
|
|
32
|
+
): WorkerGithubEventSubscription[] {
|
|
33
|
+
const subscriptions: WorkerGithubEventSubscription[] = [];
|
|
34
|
+
for (const worker of workers) {
|
|
35
|
+
if (worker.metadata.enabled === false) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const events = new Set<string>();
|
|
39
|
+
for (const trigger of worker.triggers) {
|
|
40
|
+
if (trigger.provider !== "github") {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const event = trigger.event.trim();
|
|
44
|
+
if (event.length > 0) {
|
|
45
|
+
events.add(event);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (events.size === 0) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
subscriptions.push({
|
|
52
|
+
workerId: worker.metadata.id,
|
|
53
|
+
events: [...events].sort((left, right) => left.localeCompare(right)),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return subscriptions.sort((left, right) => left.workerId.localeCompare(right.workerId));
|
|
57
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Glob } from "bun";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { parseWorkerDefinition, type WorkerDefinition } from "./contract.js";
|
|
5
|
+
|
|
6
|
+
const WORKER_GLOB = new Glob(".skipper/worker/*.ts");
|
|
7
|
+
|
|
8
|
+
type WorkerModuleExport = {
|
|
9
|
+
default?: unknown;
|
|
10
|
+
metadata?: unknown;
|
|
11
|
+
triggers?: unknown;
|
|
12
|
+
runtime?: unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Discover and load worker definitions from repository.
|
|
17
|
+
*
|
|
18
|
+
* @since 1.0.0
|
|
19
|
+
* @category Shared
|
|
20
|
+
*/
|
|
21
|
+
export async function loadWorkers(rootDir: string): Promise<WorkerDefinition[]> {
|
|
22
|
+
const files = await discoverWorkerFiles(rootDir);
|
|
23
|
+
const workers: WorkerDefinition[] = [];
|
|
24
|
+
const seenIds = new Set<string>();
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const worker = await loadWorkerFile(rootDir, file);
|
|
27
|
+
if (seenIds.has(worker.metadata.id)) {
|
|
28
|
+
throw new Error(`duplicate worker id: ${worker.metadata.id}`);
|
|
29
|
+
}
|
|
30
|
+
seenIds.add(worker.metadata.id);
|
|
31
|
+
workers.push(worker);
|
|
32
|
+
}
|
|
33
|
+
return workers;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Discover worker file paths from root directory.
|
|
38
|
+
*
|
|
39
|
+
* @since 1.0.0
|
|
40
|
+
* @category Shared
|
|
41
|
+
*/
|
|
42
|
+
export async function discoverWorkerFiles(rootDir: string): Promise<string[]> {
|
|
43
|
+
const files: string[] = [];
|
|
44
|
+
for await (const file of WORKER_GLOB.scan({ cwd: rootDir, dot: true })) {
|
|
45
|
+
files.push(file);
|
|
46
|
+
}
|
|
47
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Load and validate one worker module file.
|
|
52
|
+
*
|
|
53
|
+
* @since 1.0.0
|
|
54
|
+
* @category Shared
|
|
55
|
+
*/
|
|
56
|
+
async function loadWorkerFile(rootDir: string, relativeFile: string): Promise<WorkerDefinition> {
|
|
57
|
+
const absoluteFile = resolve(rootDir, relativeFile);
|
|
58
|
+
const url = pathToFileURL(absoluteFile).href;
|
|
59
|
+
const module = (await import(url)) as WorkerModuleExport;
|
|
60
|
+
const value = readWorkerValue(module);
|
|
61
|
+
return parseWorkerDefinition(value, relativeFile);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve worker data from module shape.
|
|
66
|
+
*
|
|
67
|
+
* @since 1.0.0
|
|
68
|
+
* @category Shared
|
|
69
|
+
*/
|
|
70
|
+
function readWorkerValue(module: WorkerModuleExport): unknown {
|
|
71
|
+
if (module.default !== undefined) {
|
|
72
|
+
return module.default;
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
module.metadata !== undefined ||
|
|
76
|
+
module.triggers !== undefined ||
|
|
77
|
+
module.runtime !== undefined
|
|
78
|
+
) {
|
|
79
|
+
return {
|
|
80
|
+
metadata: module.metadata,
|
|
81
|
+
triggers: module.triggers,
|
|
82
|
+
runtime: module.runtime,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
throw new Error("worker module must export default or metadata/triggers/runtime");
|
|
86
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { WorkerDefinition, WorkerManifest, WorkerTrigger } from "./contract.js";
|
|
2
|
+
|
|
3
|
+
export type WorkerRouteContext = {
|
|
4
|
+
provider: "github";
|
|
5
|
+
event: string;
|
|
6
|
+
action?: string;
|
|
7
|
+
repository?: string;
|
|
8
|
+
baseBranch?: string;
|
|
9
|
+
headBranch?: string;
|
|
10
|
+
draft?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Route workers matching incoming event context.
|
|
15
|
+
*
|
|
16
|
+
* @since 1.0.0
|
|
17
|
+
* @category Shared
|
|
18
|
+
*/
|
|
19
|
+
export function routeWorkers(
|
|
20
|
+
manifest: WorkerManifest,
|
|
21
|
+
context: WorkerRouteContext,
|
|
22
|
+
): WorkerDefinition[] {
|
|
23
|
+
const matched: WorkerDefinition[] = [];
|
|
24
|
+
for (const worker of manifest.workers) {
|
|
25
|
+
if (worker.metadata.enabled === false) continue;
|
|
26
|
+
if (matchesWorker(worker, context)) {
|
|
27
|
+
matched.push(worker);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return matched;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check whether worker has at least one matching trigger.
|
|
35
|
+
*
|
|
36
|
+
* @since 1.0.0
|
|
37
|
+
* @category Shared
|
|
38
|
+
*/
|
|
39
|
+
function matchesWorker(worker: WorkerDefinition, context: WorkerRouteContext): boolean {
|
|
40
|
+
for (const trigger of worker.triggers) {
|
|
41
|
+
if (matchesTrigger(trigger, context)) return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check whether trigger matches context values.
|
|
48
|
+
*
|
|
49
|
+
* @since 1.0.0
|
|
50
|
+
* @category Shared
|
|
51
|
+
*/
|
|
52
|
+
function matchesTrigger(trigger: WorkerTrigger, context: WorkerRouteContext): boolean {
|
|
53
|
+
if (trigger.provider !== context.provider) return false;
|
|
54
|
+
if (trigger.event !== context.event) return false;
|
|
55
|
+
if (trigger.actions && !matchesAction(trigger.actions, context.action)) return false;
|
|
56
|
+
if (!trigger.if) return true;
|
|
57
|
+
if (!matchesOptionalFilter(trigger.if.repository, context.repository)) return false;
|
|
58
|
+
if (!matchesOptionalFilter(trigger.if.baseBranches, context.baseBranch)) return false;
|
|
59
|
+
if (!matchesOptionalFilter(trigger.if.headBranches, context.headBranch)) return false;
|
|
60
|
+
if (
|
|
61
|
+
trigger.if.draft !== undefined &&
|
|
62
|
+
context.draft !== undefined &&
|
|
63
|
+
trigger.if.draft !== context.draft
|
|
64
|
+
) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check action value against trigger actions list.
|
|
72
|
+
*
|
|
73
|
+
* @since 1.0.0
|
|
74
|
+
* @category Shared
|
|
75
|
+
*/
|
|
76
|
+
function matchesAction(actions: string[], action: string | undefined): boolean {
|
|
77
|
+
if (!action) return false;
|
|
78
|
+
return actions.some((candidate) => candidate === action);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Match optional string filter list.
|
|
83
|
+
*
|
|
84
|
+
* @since 1.0.0
|
|
85
|
+
* @category Shared
|
|
86
|
+
*/
|
|
87
|
+
function matchesOptionalFilter(filters: string[] | undefined, value: string | undefined): boolean {
|
|
88
|
+
if (!filters || filters.length === 0) return true;
|
|
89
|
+
if (!value) return false;
|
|
90
|
+
return filters.some((candidate) => candidate === value);
|
|
91
|
+
}
|