@akshatmittal/invoker 0.1.0 → 0.3.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 CHANGED
@@ -10,7 +10,91 @@ validated JSON Output in Vitest metadata for reporters and later analysis.
10
10
  pnpm add -D @akshatmittal/invoker vitest
11
11
  ```
12
12
 
13
- Invoker supports Node 24 and Vitest 4.1.10 or newer within Vitest 4.
13
+ Invoker supports Node 24.18.1 or newer within Node 24 and Vitest 4.1.10 or newer within Vitest 4.
14
+
15
+ ## Schedule GitHub Actions
16
+
17
+ `@akshatmittal/invoker/github` runs code-defined GitHub Actions schedules from
18
+ a small, long-running Node process. It is independent from the Vitest SDK, so a
19
+ scheduler-only installation does not need Vitest.
20
+
21
+ Create a GitHub App with repository **Actions: read and write**, disable its
22
+ webhook, install it on the selected repositories, and generate a private key.
23
+ No other repository, organization, user, OAuth, or webhook permissions are
24
+ needed.
25
+
26
+ Install the scheduler with t3-env and Zod in a plain ESM application:
27
+
28
+ ```sh
29
+ npm install @akshatmittal/invoker @t3-oss/env-core zod
30
+ ```
31
+
32
+ ```js
33
+ // schedule.mjs
34
+ import { createEnv } from "@t3-oss/env-core";
35
+ import { defineGitHubSchedule } from "@akshatmittal/invoker/github";
36
+ import { z } from "zod";
37
+
38
+ const env = createEnv({
39
+ server: {
40
+ GITHUB_APP_ID: z.coerce.number().int().positive().max(Number.MAX_SAFE_INTEGER),
41
+ GITHUB_APP_PRIVATE_KEY: z.string().min(1),
42
+ },
43
+ runtimeEnv: process.env,
44
+ });
45
+
46
+ await defineGitHubSchedule({
47
+ app: {
48
+ id: env.GITHUB_APP_ID,
49
+ privateKey: env.GITHUB_APP_PRIVATE_KEY,
50
+ },
51
+ schedules: [
52
+ {
53
+ cron: "0 9 * * 1",
54
+ timezone: "UTC",
55
+ repository: "acme/regressions",
56
+ workflow: "invoker.yml",
57
+ ref: "main",
58
+ inputs: { dataset: "weekly", publish: true },
59
+ },
60
+ ],
61
+ });
62
+ ```
63
+
64
+ Schedules use five-field cron expressions and default to UTC when `timezone`
65
+ is omitted. Configuration is fixed at startup. Every repository installation
66
+ and active workflow is validated before timers begin; GitHub validates the ref,
67
+ `workflow_dispatch` declaration, and input schema when a Dispatch is due.
68
+
69
+ Run exactly one replica. Dispatches may overlap, are not persisted or retried,
70
+ and failures do not stop later occurrences. The process emits events through
71
+ evlog's shared logger, so the host's filtering, sampling, redaction, and drain
72
+ configuration applies. The module does not initialize or configure evlog.
73
+ `SIGINT` and `SIGTERM` stop new Dispatches, await in-flight requests, and
74
+ resolve the long-running promise.
75
+
76
+ ### Docker
77
+
78
+ Keep `package.json`, `package-lock.json`, and `schedule.mjs` in a deployment
79
+ directory and build this image:
80
+
81
+ ```dockerfile
82
+ FROM node:24-slim
83
+
84
+ WORKDIR /app
85
+ COPY package.json package-lock.json ./
86
+ RUN npm ci --omit=dev
87
+ COPY schedule.mjs ./
88
+
89
+ USER node
90
+ CMD ["node", "schedule.mjs"]
91
+ ```
92
+
93
+ Exclude `.env`, PEM, and private-key files from the build context. Inject
94
+ `GITHUB_APP_ID` and the real multiline `GITHUB_APP_PRIVATE_KEY` through the
95
+ runtime platform's secret mechanism; never bake them into the image. Use the
96
+ platform's process-liveness check and restart policy with one replica. The
97
+ scheduler intentionally has no HTTP health endpoint.
14
98
 
15
99
  ## Define a Workflow
16
100
 
@@ -20,10 +104,10 @@ import { defineTask } from "@akshatmittal/invoker";
20
104
 
21
105
  export const evaluateModels = defineTask({
22
106
  name: "evaluate-models",
23
- matrix: {
107
+ matrix: async () => ({
24
108
  model: ["gpt-5", "gpt-5-mini"],
25
109
  dataset: ["support", "sales"],
26
- },
110
+ }),
27
111
  setup: async ({ cases }) => loadFixtures(cases),
28
112
  run: async ({ matrix, setup, vitest }) => {
29
113
  vitest.expect(setup.has(matrix.dataset)).toBe(true);
@@ -41,9 +125,9 @@ export const evaluateModels = defineTask({
41
125
  ```
42
126
 
43
127
  ```ts
44
- // regressions/model/index.test.ts
128
+ // regressions/workflows/model-regressions.test.ts
45
129
  import { defineWorkflow } from "@akshatmittal/invoker";
46
- import { evaluateModels } from "./tasks/evaluate-models.js";
130
+ import { evaluateModels } from "../tasks/evaluate-models.js";
47
131
 
48
132
  defineWorkflow({
49
133
  name: "model-regressions",
@@ -55,16 +139,18 @@ defineWorkflow({
55
139
  });
56
140
  ```
57
141
 
58
- Matrix literals determine the exact `matrix` type, `setup` determines the exact
142
+ The Matrix function runs during collection and its returned literal determines the exact
143
+ `matrix` type. `setup` determines the exact
59
144
  shared setup type, and the exact JSON return type is retained on the Task.
145
+ Axis names must be non-empty, enumerable strings that are not array indexes.
60
146
  Omitting `matrix` creates one Case with `{}`. Setup runs once per Task, Cases
61
147
  within that Task run concurrently, and teardown runs once after successful
62
148
  setup. Tasks run sequentially in their Workflow.
63
149
 
64
150
  ## Configure Vitest
65
151
 
66
- Invoker uses Vitest's built-in reporters. The JSON reporter includes each
67
- Case's data at `assertionResults[].meta.invoker`:
152
+ The JSON reporter includes each Case's data at
153
+ `assertionResults[].meta.invoker`:
68
154
 
69
155
  ```ts
70
156
  // vitest.config.ts
@@ -85,9 +171,13 @@ Run every Workflow or filter to one Task with ordinary Vitest commands:
85
171
 
86
172
  ```sh
87
173
  pnpm vitest run
88
- pnpm vitest run regressions/model/index.test.ts -t evaluate-models
174
+ pnpm vitest run regressions/workflows/model-regressions.test.ts -t evaluate-models
89
175
  ```
90
176
 
177
+ Define additional Workflows in separate `*.test.ts` files. Vitest discovers
178
+ them automatically; Invoker does not scan directories or require a central
179
+ index.
180
+
91
181
  The metadata envelope is stable and JSON-compatible:
92
182
 
93
183
  ```json
@@ -103,6 +193,48 @@ The metadata envelope is stable and JSON-compatible:
103
193
  report remains authoritative for status, failures, timing, hierarchy, and
104
194
  retries.
105
195
 
196
+ ## Notify Slack
197
+
198
+ Invoker's optional Slack reporter posts one `Invoker Report` parent message per
199
+ Vitest run. It places additional Workflow cards in the message thread so every
200
+ Task table remains within Slack's row and character limits. Each card includes
201
+ aggregate results, Workflow metadata, and a table of Task counts and durations.
202
+ A shared footer contains the elapsed span from the first Case start to the final
203
+ Case completion, a localized timestamp, and the optional run link. Final
204
+ failures are posted in the same thread with one reply per failed Task in each
205
+ Workflow. Unhandled run errors are reported once. Delivery failures are
206
+ isolated to the affected reply. Ambiguous transport failures are not retried;
207
+ an explicit Slack rate-limit rejection is reattempted only after its required
208
+ delay.
209
+
210
+ ```ts
211
+ import { slackReporter } from "@akshatmittal/invoker/slack";
212
+ import { defineConfig } from "vitest/config";
213
+
214
+ const runUrl =
215
+ process.env.GITHUB_ACTIONS === "true"
216
+ ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
217
+ : undefined;
218
+
219
+ export default defineConfig({
220
+ test: {
221
+ reporters: [
222
+ "tree",
223
+ slackReporter({
224
+ token: process.env.SLACK_BOT_TOKEN!,
225
+ channel: process.env.SLACK_CHANNEL_ID!,
226
+ runUrl,
227
+ }),
228
+ ],
229
+ },
230
+ });
231
+ ```
232
+
233
+ Create a Slack app with the `chat:write` bot scope, install it to the workspace,
234
+ and expose its bot token and target channel ID as `SLACK_BOT_TOKEN` and
235
+ `SLACK_CHANNEL_ID`. Invite the bot to private target channels. Slack delivery
236
+ failures produce a warning but do not change Vitest's exit status.
237
+
106
238
  ## GitHub Actions artifacts
107
239
 
108
240
  Create the output directory before Vitest and upload the report even when the
@@ -122,7 +254,7 @@ queries or reports. Invoker does not upload, index, or persist results itself.
122
254
 
123
255
  ## v1 scope
124
256
 
125
- Invoker does not provide a CLI, directory discovery, custom runner, custom
126
- reporter, configuration helper, Task-level parallelism, matrix include/exclude,
127
- or hosted result storage. Use Vitest configuration and your CI runner for those
128
- concerns.
257
+ Invoker does not provide a CLI, directory discovery, custom runner, general
258
+ reporter framework, configuration helper, Task-level parallelism, matrix
259
+ include/exclude, or hosted result storage. Use Vitest configuration and your CI
260
+ runner for those concerns.
@@ -0,0 +1,22 @@
1
+ //#region src/github/types.d.ts
2
+ type WorkflowInput = string | number | boolean;
3
+ type GitHubSchedule = {
4
+ readonly cron: string;
5
+ readonly timezone?: string;
6
+ readonly repository: string;
7
+ readonly workflow: string | number;
8
+ readonly ref: string;
9
+ readonly inputs?: Readonly<Record<string, WorkflowInput>>;
10
+ };
11
+ type GitHubScheduleDefinition = {
12
+ readonly app: {
13
+ readonly id: number;
14
+ readonly privateKey: string;
15
+ };
16
+ readonly schedules: readonly [GitHubSchedule, ...GitHubSchedule[]];
17
+ };
18
+ //#endregion
19
+ //#region src/github.d.ts
20
+ declare function defineGitHubSchedule(definition: GitHubScheduleDefinition): Promise<void>;
21
+ //#endregion
22
+ export { defineGitHubSchedule };
@@ -0,0 +1,353 @@
1
+ import { z } from "zod";
2
+ import { Cron } from "croner";
3
+ import { createAppAuth } from "@octokit/auth-app";
4
+ import { request } from "@octokit/request";
5
+ import { log } from "evlog";
6
+ //#region src/github/client.ts
7
+ const API_VERSION = "2026-03-10";
8
+ const dispatchResponseSchema = z.object({
9
+ workflow_run_id: z.int().positive(),
10
+ html_url: z.url()
11
+ });
12
+ const requestFailureSchema = z.object({
13
+ status: z.int().optional(),
14
+ response: z.object({ headers: z.object({ "x-github-request-id": z.string().optional() }) }).optional()
15
+ });
16
+ const githubFailures = /* @__PURE__ */ new WeakMap();
17
+ function createGitHubClient(app, signal) {
18
+ const apiRequest = request.defaults({
19
+ headers: {
20
+ accept: "application/vnd.github+json",
21
+ "x-github-api-version": API_VERSION
22
+ },
23
+ request: { signal }
24
+ });
25
+ const auth = createAppAuth({
26
+ appId: app.id,
27
+ privateKey: app.privateKey,
28
+ request: apiRequest,
29
+ log: { warn() {} }
30
+ });
31
+ const resolveInstallation = async (target) => {
32
+ try {
33
+ const authentication = await auth({ type: "app" });
34
+ return (await apiRequest("GET /repos/{owner}/{repo}/installation", {
35
+ owner: target.owner,
36
+ repo: target.repo,
37
+ headers: { authorization: `Bearer ${authentication.token}` }
38
+ })).data.id;
39
+ } catch (error) {
40
+ throw githubError(githubFailure(error, "resolve installation", target));
41
+ }
42
+ };
43
+ const installationToken = async (target, installationId) => {
44
+ try {
45
+ return (await auth({
46
+ type: "installation",
47
+ installationId,
48
+ repositoryNames: [target.repo],
49
+ permissions: { actions: "write" }
50
+ })).token;
51
+ } catch (error) {
52
+ throw githubError(githubFailure(error, "create installation token", target));
53
+ }
54
+ };
55
+ const resolveWorkflow = async (target, installationId) => {
56
+ try {
57
+ const token = await installationToken(target, installationId);
58
+ const response = await apiRequest("GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}", {
59
+ owner: target.owner,
60
+ repo: target.repo,
61
+ workflow_id: target.workflow,
62
+ headers: { authorization: `Bearer ${token}` }
63
+ });
64
+ if (response.data.state !== "active") throw new Error("workflow is not active");
65
+ return response.data.id;
66
+ } catch (error) {
67
+ throw githubError(githubFailure(error, "resolve workflow", target));
68
+ }
69
+ };
70
+ const dispatch = async (target) => {
71
+ try {
72
+ const token = await installationToken(target, target.installationId);
73
+ const response = await apiRequest("POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches", {
74
+ owner: target.owner,
75
+ repo: target.repo,
76
+ workflow_id: target.workflowId,
77
+ ref: target.ref,
78
+ return_run_details: true,
79
+ inputs: target.inputs,
80
+ headers: { authorization: `Bearer ${token}` }
81
+ });
82
+ const data = dispatchResponseSchema.parse(response.data);
83
+ return {
84
+ runId: data.workflow_run_id,
85
+ webUrl: data.html_url
86
+ };
87
+ } catch (error) {
88
+ throw githubError(githubFailure(error, "dispatch workflow", target));
89
+ }
90
+ };
91
+ return {
92
+ dispatch,
93
+ resolveInstallation,
94
+ resolveWorkflow
95
+ };
96
+ }
97
+ function githubFailure(cause, operation, target) {
98
+ if (cause instanceof Error) {
99
+ const known = githubFailures.get(cause);
100
+ if (known) return known;
101
+ }
102
+ const parsed = requestFailureSchema.safeParse(cause);
103
+ const status = parsed.success ? parsed.data.status : void 0;
104
+ const requestId = parsed.success ? parsed.data.response?.headers["x-github-request-id"] : void 0;
105
+ return {
106
+ message: `GitHub ${operation}${target ? ` for ${target.repository} workflow ${String(target.workflow)}` : ""}: ${status === 404 ? "not found or inaccessible" : "request failed"}${status === void 0 ? "" : ` (${status})`}${requestId === void 0 ? "" : ` [request ${requestId}]`}`,
107
+ operation,
108
+ repository: target?.repository,
109
+ workflow: target?.workflow,
110
+ status,
111
+ requestId
112
+ };
113
+ }
114
+ function githubError(failure) {
115
+ const error = new Error(failure.message);
116
+ githubFailures.set(error, failure);
117
+ return error;
118
+ }
119
+ //#endregion
120
+ //#region src/github/config.ts
121
+ const GITHUB_SCHEDULE_OWNER = "GitHub Schedule";
122
+ const inputSchema = z.union([
123
+ z.string(),
124
+ z.number().finite(),
125
+ z.boolean()
126
+ ]);
127
+ const timezoneSchema = z.string().refine((timezone) => {
128
+ try {
129
+ new Intl.DateTimeFormat(void 0, { timeZone: timezone });
130
+ return true;
131
+ } catch {
132
+ return false;
133
+ }
134
+ }, "expected a valid IANA timezone");
135
+ const scheduleSchema = z.strictObject({
136
+ cron: z.string().min(1),
137
+ timezone: timezoneSchema.default("UTC"),
138
+ repository: z.string().regex(/^[^/\s]+\/[^/\s]+$/),
139
+ workflow: z.union([z.int().positive(), z.string().min(1).refine((value) => value.trim() === value && !value.includes("/"))]),
140
+ ref: z.string().refine((value) => value.trim() !== ""),
141
+ inputs: z.record(z.string(), inputSchema).refine((inputs) => Object.keys(inputs).length <= 25).refine((inputs) => JSON.stringify(inputs).length <= 65535).optional()
142
+ });
143
+ const definitionSchema = z.strictObject({
144
+ app: z.strictObject({
145
+ id: z.int().positive(),
146
+ privateKey: z.string().min(1)
147
+ }),
148
+ schedules: z.array(scheduleSchema).min(1)
149
+ });
150
+ function normalizeDefinition(value) {
151
+ const result = definitionSchema.safeParse(value);
152
+ if (!result.success) throw new TypeError(`${GITHUB_SCHEDULE_OWNER}: ${z.prettifyError(result.error)}`);
153
+ const schedules = result.data.schedules.map((schedule, index) => {
154
+ let validationJob;
155
+ try {
156
+ validationJob = new Cron(schedule.cron, {
157
+ mode: "5-part",
158
+ paused: true,
159
+ timezone: schedule.timezone
160
+ });
161
+ validationJob.nextRun();
162
+ } catch {
163
+ throw new TypeError(`${GITHUB_SCHEDULE_OWNER}.schedules[${index}].cron: expected a valid five-field cron expression`);
164
+ } finally {
165
+ validationJob?.stop();
166
+ }
167
+ const separator = schedule.repository.indexOf("/");
168
+ const owner = schedule.repository.slice(0, separator);
169
+ const repo = schedule.repository.slice(separator + 1);
170
+ return {
171
+ ...schedule,
172
+ owner,
173
+ repo
174
+ };
175
+ });
176
+ return {
177
+ app: result.data.app,
178
+ schedules
179
+ };
180
+ }
181
+ function workflowKey(schedule) {
182
+ return `${schedule.repository}\0${String(schedule.workflow)}`;
183
+ }
184
+ //#endregion
185
+ //#region src/github/events.ts
186
+ const SERVICE = "github-schedule";
187
+ function logStartup(schedules, repositories, workflows, failure) {
188
+ log[failure ? "error" : "info"]({
189
+ service: SERVICE,
190
+ event: "github_schedule.startup",
191
+ schedules,
192
+ repositories,
193
+ workflows,
194
+ ...failure ? {
195
+ outcome: "failure",
196
+ failure
197
+ } : { outcome: "success" }
198
+ });
199
+ }
200
+ function logDispatch(schedule, scheduledAt, result, failure) {
201
+ log[failure ? "error" : "info"]({
202
+ service: SERVICE,
203
+ event: "github_schedule.dispatch",
204
+ repository: schedule.repository,
205
+ workflow: schedule.workflow,
206
+ ref: schedule.ref,
207
+ cron: schedule.cron,
208
+ timezone: schedule.timezone,
209
+ scheduledAt,
210
+ ...failure ? {
211
+ outcome: "failure",
212
+ failure
213
+ } : {
214
+ outcome: "success",
215
+ runId: result.runId,
216
+ runUrl: result.webUrl
217
+ }
218
+ });
219
+ }
220
+ function logShutdown(signal, drainedDispatches) {
221
+ log.info({
222
+ service: SERVICE,
223
+ event: "github_schedule.shutdown",
224
+ signal,
225
+ drainedDispatches,
226
+ outcome: "success"
227
+ });
228
+ }
229
+ //#endregion
230
+ //#region src/github/scheduler.ts
231
+ let schedulerActive = false;
232
+ async function runGitHubSchedule(definition) {
233
+ let normalized;
234
+ try {
235
+ normalized = normalizeDefinition(definition);
236
+ } catch (error) {
237
+ logStartup(0, 0, 0, localFailure(error));
238
+ throw error;
239
+ }
240
+ const repositoryCount = new Set(normalized.schedules.map(({ repository }) => repository)).size;
241
+ const workflowCount = new Set(normalized.schedules.map(workflowKey)).size;
242
+ if (schedulerActive) {
243
+ const error = /* @__PURE__ */ new TypeError(`${GITHUB_SCHEDULE_OWNER}: another scheduler is already active`);
244
+ logStartup(normalized.schedules.length, repositoryCount, workflowCount, localFailure(error));
245
+ throw error;
246
+ }
247
+ schedulerActive = true;
248
+ const startupAbort = new AbortController();
249
+ const jobs = [];
250
+ const inFlight = /* @__PURE__ */ new Set();
251
+ let state = "starting";
252
+ let stopSignal;
253
+ let drainedCount = 0;
254
+ let resolveStop;
255
+ const stopRequested = new Promise((resolve) => {
256
+ resolveStop = resolve;
257
+ });
258
+ const isStopping = () => state === "stopping";
259
+ const removeSignalHandlers = () => {
260
+ process.removeListener("SIGINT", onSigint);
261
+ process.removeListener("SIGTERM", onSigterm);
262
+ };
263
+ const requestStop = (signal) => {
264
+ if (isStopping()) return;
265
+ const duringStartup = state === "starting";
266
+ state = "stopping";
267
+ stopSignal = signal;
268
+ drainedCount = inFlight.size;
269
+ jobs.forEach((job) => job.stop());
270
+ removeSignalHandlers();
271
+ if (duringStartup) startupAbort.abort();
272
+ resolveStop();
273
+ };
274
+ function onSigint() {
275
+ requestStop("SIGINT");
276
+ }
277
+ function onSigterm() {
278
+ requestStop("SIGTERM");
279
+ }
280
+ process.on("SIGINT", onSigint);
281
+ process.on("SIGTERM", onSigterm);
282
+ try {
283
+ const github = createGitHubClient(normalized.app, startupAbort.signal);
284
+ const installations = /* @__PURE__ */ new Map();
285
+ for (const schedule of normalized.schedules) if (!installations.has(schedule.repository)) installations.set(schedule.repository, await github.resolveInstallation(schedule));
286
+ const workflows = /* @__PURE__ */ new Map();
287
+ for (const schedule of normalized.schedules) {
288
+ const key = workflowKey(schedule);
289
+ if (!workflows.has(key)) workflows.set(key, await github.resolveWorkflow(schedule, installations.get(schedule.repository)));
290
+ }
291
+ if (isStopping()) {
292
+ await finishShutdown(inFlight, stopSignal, drainedCount);
293
+ return;
294
+ }
295
+ const schedules = normalized.schedules.map((schedule) => ({
296
+ ...schedule,
297
+ installationId: installations.get(schedule.repository),
298
+ workflowId: workflows.get(workflowKey(schedule))
299
+ }));
300
+ for (const schedule of schedules) {
301
+ const job = new Cron(schedule.cron, {
302
+ catch: true,
303
+ mode: "5-part",
304
+ timezone: schedule.timezone
305
+ }, (currentJob) => {
306
+ if (state !== "running") return;
307
+ const scheduledAt = (currentJob.currentRun() ?? /* @__PURE__ */ new Date()).toISOString();
308
+ const dispatch = github.dispatch(schedule).then((result) => logDispatch(schedule, scheduledAt, result), (cause) => {
309
+ const failure = githubFailure(cause, "dispatch workflow", schedule);
310
+ logDispatch(schedule, scheduledAt, void 0, failure);
311
+ throw new Error(failure.message);
312
+ });
313
+ inFlight.add(dispatch);
314
+ dispatch.then(() => inFlight.delete(dispatch), () => inFlight.delete(dispatch));
315
+ return dispatch;
316
+ });
317
+ jobs.push(job);
318
+ }
319
+ state = "running";
320
+ logStartup(normalized.schedules.length, repositoryCount, workflowCount);
321
+ await stopRequested;
322
+ await finishShutdown(inFlight, stopSignal, drainedCount);
323
+ } catch (error) {
324
+ if (isStopping()) {
325
+ await finishShutdown(inFlight, stopSignal, drainedCount);
326
+ return;
327
+ }
328
+ jobs.forEach((job) => job.stop());
329
+ removeSignalHandlers();
330
+ schedulerActive = false;
331
+ const failure = githubFailure(error, "start scheduler");
332
+ logStartup(normalized.schedules.length, repositoryCount, workflowCount, failure);
333
+ throw githubError(failure);
334
+ }
335
+ }
336
+ async function finishShutdown(inFlight, signal, drainedCount) {
337
+ await Promise.allSettled(inFlight);
338
+ schedulerActive = false;
339
+ logShutdown(signal, drainedCount);
340
+ }
341
+ function localFailure(cause) {
342
+ return {
343
+ message: cause instanceof Error ? cause.message : `${GITHUB_SCHEDULE_OWNER}: startup validation failed`,
344
+ operation: "validate configuration"
345
+ };
346
+ }
347
+ //#endregion
348
+ //#region src/github.ts
349
+ function defineGitHubSchedule(definition) {
350
+ return runGitHubSchedule(definition);
351
+ }
352
+ //#endregion
353
+ export { defineGitHubSchedule };
package/dist/index.d.mts CHANGED
@@ -34,25 +34,22 @@ type TeardownContext<M extends Matrix, Setup> = SetupContext<M> & {
34
34
  declare const taskDefinitionBrand: unique symbol;
35
35
  interface TaskDefinition<Name extends string = string, M extends Matrix = Matrix, Setup = unknown, Output extends JsonValue = JsonValue> {
36
36
  readonly name: Name;
37
- readonly matrix: M;
38
- readonly [taskDefinitionBrand]: {
39
- readonly setup: Setup;
40
- readonly output: Output;
41
- };
37
+ readonly matrix: () => Promise<M>;
38
+ readonly [taskDefinitionBrand]: true;
42
39
  readonly setup?: (context: SetupContext<M>) => Awaitable<Setup>;
43
40
  readonly run: (context: TaskContext<CaseCoordinates<M>, Setup>) => Awaitable<Output>;
44
41
  readonly teardown?: (context: TeardownContext<M, Setup>) => Awaitable<void>;
45
42
  }
46
43
  type TaskWithSetup<Name extends string, M extends Matrix, Setup, Output extends JsonValue> = {
47
44
  readonly name: Name;
48
- readonly matrix?: M;
45
+ readonly matrix?: () => Promise<M>;
49
46
  readonly setup: (context: SetupContext<M>) => Awaitable<Setup>;
50
47
  readonly run: (context: TaskContext<CaseCoordinates<M>, Setup>) => Awaitable<Output>;
51
48
  readonly teardown?: (context: TeardownContext<M, Setup>) => Awaitable<void>;
52
49
  };
53
50
  type TaskWithoutSetup<Name extends string, M extends Matrix, Output extends JsonValue> = {
54
51
  readonly name: Name;
55
- readonly matrix?: M;
52
+ readonly matrix?: () => Promise<M>;
56
53
  readonly setup?: never;
57
54
  readonly run: (context: TaskContext<CaseCoordinates<M>, undefined>) => Awaitable<Output>;
58
55
  readonly teardown?: never;
@@ -61,11 +58,8 @@ declare function defineTask<const Name extends string, const M extends Matrix =
61
58
  declare function defineTask<const Name extends string, const M extends Matrix = Record<never, never>, const Output extends JsonValue = JsonValue>(definition: TaskWithoutSetup<Name, M, Output>): TaskDefinition<Name, M, undefined, Output>;
62
59
  type AnyTaskDefinition = {
63
60
  readonly name: string;
64
- readonly matrix: Matrix;
65
- readonly [taskDefinitionBrand]: {
66
- readonly setup: unknown;
67
- readonly output: JsonValue;
68
- };
61
+ readonly matrix: () => Promise<Matrix>;
62
+ readonly [taskDefinitionBrand]: true;
69
63
  };
70
64
  //#endregion
71
65
  //#region src/workflow.d.ts