@fungi.computer/trellis 0.0.0-stage → 0.1.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.
@@ -0,0 +1,419 @@
1
+ import { workflowIdSchema } from "@fungi.computer/braid";
2
+ import * as z from "zod";
3
+ import * as Schema from "effect/Schema";
4
+ import { workflowDefinitionSchema } from "@fungi.computer/braid";
5
+ import { createReadCopies } from "@fungi.computer/agar/read-copy";
6
+ import { gitSha1Schema } from "@fungi.computer/puffball/git-representation";
7
+ import { createPublicationJobs } from "./publication-job.js";
8
+ import { versionRefs } from "@fungi.computer/workflows-wire";
9
+ import type { GitBinding } from "@fungi.computer/agar/git";
10
+ import {
11
+ document,
12
+ publishedDocument,
13
+ draftDocument,
14
+ version,
15
+ listings,
16
+ } from "@fungi.computer/workflows-wire";
17
+
18
+ /** The facet SQL boundary; rows are decoded rather than asserted into domain types. */
19
+ export type WorkflowSql = {
20
+ exec(
21
+ query: string,
22
+ ...bindings: (string | number | null | ArrayBuffer)[]
23
+ ): { toArray(): readonly unknown[] };
24
+ };
25
+ const issueSchema = z.object({
26
+ code: z.string(),
27
+ message: z.string(),
28
+ graph: z.string().optional(),
29
+ node: z.string().optional(),
30
+ trigger: z.string().optional(),
31
+ port: z.string().optional(),
32
+ edge: z.number().optional(),
33
+ });
34
+ const pushRowSchema = z.strictObject({
35
+ eventId: z.string(),
36
+ commit: z.string(),
37
+ workflowId: z.string(),
38
+ issues: z.string(),
39
+ });
40
+ export type WorkflowPushIssue = z.output<typeof issueSchema>;
41
+
42
+ const copiedDocument = document.transform((raw) => ({
43
+ ...raw,
44
+ definition: Schema.decodeUnknownSync(workflowDefinitionSchema)(
45
+ raw.definition,
46
+ ),
47
+ }));
48
+
49
+ const librarySnapshotSchema = z.strictObject({
50
+ refs: z.record(z.string(), gitSha1Schema),
51
+ items: listings,
52
+ });
53
+
54
+ /** Facet-local draft rows, immutable publication copies and run identities. */
55
+ export type WorkflowStorage = {
56
+ sql: WorkflowSql;
57
+ transactionSync<T>(operation: () => T): T;
58
+ };
59
+ export function createWorkflowIndexes(storage: WorkflowStorage) {
60
+ const { sql } = storage;
61
+ sql.exec(
62
+ "CREATE TABLE IF NOT EXISTS workflow_drafts (workflow_id TEXT PRIMARY KEY, revision INTEGER NOT NULL, body TEXT NOT NULL, updated_at INTEGER NOT NULL)",
63
+ );
64
+ const draftRow = z.strictObject({
65
+ revision: z.number().int().positive(),
66
+ body: z.string(),
67
+ });
68
+ const decodeDraft = (raw: unknown) => {
69
+ const row = draftRow.parse(raw);
70
+ const value = draftDocument.parse({
71
+ ...z.record(z.string(), z.json()).parse(JSON.parse(row.body)),
72
+ revision: row.revision,
73
+ });
74
+ return {
75
+ ...value,
76
+ definition: Schema.decodeUnknownSync(workflowDefinitionSchema)(
77
+ value.definition,
78
+ ),
79
+ };
80
+ };
81
+ function readDraft(workflowId: string) {
82
+ workflowIdSchema.parse(workflowId);
83
+ const raw = sql
84
+ .exec(
85
+ "SELECT revision, body FROM workflow_drafts WHERE workflow_id = ?",
86
+ workflowId,
87
+ )
88
+ .toArray()[0];
89
+ return raw === undefined ? null : decodeDraft(raw);
90
+ }
91
+ sql.exec(
92
+ 'CREATE TABLE IF NOT EXISTS workflow_push_issues (eventId TEXT NOT NULL, "commit" TEXT NOT NULL, workflowId TEXT NOT NULL, issues TEXT NOT NULL, PRIMARY KEY (eventId, "commit", workflowId))',
93
+ );
94
+ const copies = createReadCopies(sql, {
95
+ version: 2,
96
+ maxRows: 4096,
97
+ maxValueBytes: 1048576,
98
+ maxBlobBytes: 1048576,
99
+ maxBlobTotalBytes: 8388608,
100
+ });
101
+ const library = copies.open("workflow.library", librarySnapshotSchema);
102
+ const generation = copies.open(
103
+ "workflow.revision",
104
+ z.number().int().nonnegative(),
105
+ );
106
+ const documents = copies.open("workflow.document", copiedDocument.nullable());
107
+ const publications = copies.open(
108
+ "workflow.publication",
109
+ publishedDocument.transform((raw) => ({
110
+ ...raw,
111
+ definition: Schema.decodeUnknownSync(workflowDefinitionSchema)(
112
+ raw.definition,
113
+ ),
114
+ })),
115
+ );
116
+ const versionCopies = copies.open(
117
+ "workflow.versions",
118
+ z.array(version).max(1000),
119
+ );
120
+ const rebuildingRow = z.strictObject({
121
+ workflowId: z.string(),
122
+ main: copiedDocument.nullable(),
123
+ versions: z.array(version).max(1000),
124
+ versionOffset: z.number().int().nonnegative(),
125
+ });
126
+ const frontier = copies.open(
127
+ "workflow.frontier",
128
+ z.strictObject({
129
+ epoch: z.number().int().nonnegative(),
130
+ refs: z.array(z.strictObject({ name: z.string(), hash: gitSha1Schema })),
131
+ ids: z.array(z.string()).max(100),
132
+ offset: z.number().int().nonnegative(),
133
+ items: listings,
134
+ current: rebuildingRow.nullable(),
135
+ queue: z.array(rebuildingRow).max(4),
136
+ }),
137
+ );
138
+ function mergeDraftListings(items: z.output<typeof listings>) {
139
+ const saved = new Map(items.map((item) => [item.workflowId, item]));
140
+ for (const raw of sql
141
+ .exec(
142
+ "SELECT workflow_id, revision, body FROM workflow_drafts ORDER BY workflow_id LIMIT 100",
143
+ )
144
+ .toArray()) {
145
+ const row = z
146
+ .object({
147
+ workflow_id: workflowIdSchema,
148
+ revision: z.number().int().positive(),
149
+ body: z.string(),
150
+ })
151
+ .parse(raw);
152
+ const draft = decodeDraft({ revision: row.revision, body: row.body });
153
+ const previous = saved.get(row.workflow_id);
154
+ saved.set(row.workflow_id, {
155
+ workflowId: row.workflow_id,
156
+ title: draft.definition.title,
157
+ draftRevision: row.revision,
158
+ published: previous?.published ?? null,
159
+ });
160
+ }
161
+ return [...saved.values()]
162
+ .sort((a, b) => a.workflowId.localeCompare(b.workflowId))
163
+ .slice(0, 100);
164
+ }
165
+ function librarySnapshot() {
166
+ const saved = library.read("all");
167
+ const epoch = generation.read("all");
168
+ return {
169
+ revision: epoch.kind === "ready" ? epoch.value : 0,
170
+ snapshot: saved.kind === "ready" ? saved.value : null,
171
+ };
172
+ }
173
+ const indexes = {
174
+ drafts: {
175
+ read: readDraft,
176
+ save(
177
+ workflowId: string,
178
+ body: Omit<z.output<typeof draftDocument>, "revision">,
179
+ expectedRevision: number,
180
+ ) {
181
+ workflowIdSchema.parse(workflowId);
182
+ return storage.transactionSync(() => {
183
+ const current = readDraft(workflowId);
184
+ if ((current?.revision ?? 0) !== expectedRevision)
185
+ return { kind: "conflict" as const, current };
186
+ const revision = expectedRevision + 1;
187
+ const saved = draftDocument.parse({ ...body, revision });
188
+ const { revision: savedRevision, ...content } = saved;
189
+ sql.exec(
190
+ "INSERT INTO workflow_drafts (workflow_id, revision, body, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(workflow_id) DO UPDATE SET revision = excluded.revision, body = excluded.body, updated_at = excluded.updated_at",
191
+ workflowId,
192
+ savedRevision,
193
+ JSON.stringify(content),
194
+ Date.now(),
195
+ );
196
+ const currentLibrary = librarySnapshot();
197
+ const previous = currentLibrary.snapshot?.items.find(
198
+ (item) => item.workflowId === workflowId,
199
+ );
200
+ library.write(
201
+ "all",
202
+ currentLibrary.snapshot?.refs["refs/heads/main"] ?? null,
203
+ {
204
+ refs: currentLibrary.snapshot?.refs ?? {},
205
+ items: [
206
+ ...(currentLibrary.snapshot?.items ?? []).filter(
207
+ (item) => item.workflowId !== workflowId,
208
+ ),
209
+ {
210
+ workflowId,
211
+ title: Schema.decodeUnknownSync(workflowDefinitionSchema)(
212
+ content.definition,
213
+ ).title,
214
+ draftRevision: revision,
215
+ published: previous?.published ?? null,
216
+ },
217
+ ]
218
+ .sort((a, b) => a.workflowId.localeCompare(b.workflowId))
219
+ .slice(0, 100),
220
+ },
221
+ );
222
+ return { kind: "saved" as const, revision };
223
+ });
224
+ },
225
+ },
226
+ libraryList: () => library.read("all"),
227
+ documents,
228
+ versionCopies,
229
+ publications,
230
+ recordPublication(
231
+ value: Parameters<typeof publications.write>[2],
232
+ epoch: number,
233
+ ) {
234
+ storage.transactionSync(() => {
235
+ const { workflowId, version: number, commit } = value;
236
+ const saved: Pick<
237
+ typeof value,
238
+ "definition" | "layout" | "commit" | "provenance"
239
+ > = {
240
+ definition: value.definition,
241
+ layout: value.layout,
242
+ commit,
243
+ };
244
+ if (value.provenance !== undefined) saved.provenance = value.provenance;
245
+ publications.write(`${workflowId}:v${String(number)}`, commit, value);
246
+ publications.write(`${workflowId}:${commit}`, commit, value);
247
+ documents.write(`main:${workflowId}`, commit, saved);
248
+ documents.write(`at:${workflowId}:${commit}`, commit, saved);
249
+ const previous = versionCopies.read(workflowId);
250
+ versionCopies.write(
251
+ workflowId,
252
+ commit,
253
+ [
254
+ {
255
+ version: number,
256
+ commit,
257
+ committedAt: Math.floor(Date.now() / 1000),
258
+ message: `Publish ${workflowId}`,
259
+ },
260
+ ...(previous.kind === "ready" ? previous.value : []),
261
+ ].slice(0, 1000),
262
+ );
263
+ const current = librarySnapshot();
264
+ if (current.revision === epoch && current.snapshot)
265
+ library.write("all", commit, {
266
+ refs: {
267
+ ...current.snapshot.refs,
268
+ "refs/heads/main": commit,
269
+ [`refs/tags/${workflowId}@v${String(number)}`]: commit,
270
+ },
271
+ items: current.snapshot.items.map((item) =>
272
+ item.workflowId === workflowId
273
+ ? { ...item, published: { commit, version: number } }
274
+ : item,
275
+ ),
276
+ });
277
+ });
278
+ },
279
+ recordBackgroundPublication(
280
+ value: Parameters<typeof publications.write>[2],
281
+ epoch: number,
282
+ references: Awaited<ReturnType<GitBinding["refs"]>>,
283
+ ) {
284
+ storage.transactionSync(() => {
285
+ const { workflowId, version: number, commit } = value;
286
+ const saved: Pick<
287
+ typeof value,
288
+ "definition" | "layout" | "commit" | "provenance"
289
+ > = { definition: value.definition, layout: value.layout, commit };
290
+ if (value.provenance !== undefined) saved.provenance = value.provenance;
291
+ publications.write(`${workflowId}:v${String(number)}`, commit, value);
292
+ publications.write(`${workflowId}:${commit}`, commit, value);
293
+ documents.write(`at:${workflowId}:${commit}`, commit, saved);
294
+ const previous = versionCopies.read(workflowId);
295
+ const retained = previous.kind === "ready" ? previous.value : [];
296
+ const next = [
297
+ {
298
+ version: number,
299
+ commit,
300
+ committedAt: Math.floor(Date.now() / 1000),
301
+ message: `Publish ${workflowId}`,
302
+ },
303
+ ...retained.filter((version) => version.version !== number),
304
+ ]
305
+ .sort((a, b) => b.version - a.version)
306
+ .slice(0, 1000);
307
+ versionCopies.write(workflowId, next[0]?.commit ?? commit, next);
308
+ const main =
309
+ references.find((ref) => ref.name === "refs/heads/main")?.hash ??
310
+ null;
311
+ const copiedMain = documents.read(`main:${workflowId}`);
312
+ if (main === commit && (retained[0]?.version ?? 0) <= number)
313
+ documents.write(`main:${workflowId}`, commit, saved);
314
+ else if (copiedMain.kind === "ready" && copiedMain.head !== main)
315
+ documents.stale(`main:${workflowId}`);
316
+ const current = librarySnapshot();
317
+ if (current.revision === epoch && current.snapshot) {
318
+ const latest = versionRefs(workflowId, references)[0];
319
+ library.write("all", main, {
320
+ refs: Object.fromEntries(
321
+ references.map((ref) => [ref.name, ref.hash]),
322
+ ),
323
+ items: current.snapshot.items.map((item) => {
324
+ if (
325
+ item.workflowId !== workflowId ||
326
+ !latest ||
327
+ (item.published?.version ?? 0) > latest.version
328
+ )
329
+ return item;
330
+ return {
331
+ ...item,
332
+ published: { commit: latest.commit, version: latest.version },
333
+ };
334
+ }),
335
+ });
336
+ }
337
+ });
338
+ },
339
+ frontier,
340
+ librarySnapshot,
341
+ recordLibrary(
342
+ revision: number,
343
+ snapshot: {
344
+ refs: Record<string, string>;
345
+ items: z.output<typeof listings>;
346
+ },
347
+ ) {
348
+ if (librarySnapshot().revision === revision)
349
+ library.write("all", snapshot.refs["refs/heads/main"] ?? null, {
350
+ ...snapshot,
351
+ items: mergeDraftListings(snapshot.items),
352
+ });
353
+ },
354
+ invalidateLibrary(updates?: readonly { ref: string; after: string }[]) {
355
+ const current = librarySnapshot();
356
+ if (
357
+ updates &&
358
+ current.snapshot &&
359
+ updates.every(
360
+ ({ ref, after }) =>
361
+ (current.snapshot?.refs[ref] ?? "0".repeat(40)) === after,
362
+ )
363
+ )
364
+ return;
365
+ generation.write("all", null, current.revision + 1);
366
+ library.stale("all");
367
+ },
368
+ recordIssues(
369
+ eventId: string,
370
+ commit: string,
371
+ workflowId: string,
372
+ issues: readonly WorkflowPushIssue[],
373
+ ) {
374
+ sql.exec(
375
+ 'INSERT INTO workflow_push_issues (eventId, "commit", workflowId, issues) VALUES (?, ?, ?, ?) ON CONFLICT(eventId, "commit", workflowId) DO NOTHING',
376
+ eventId,
377
+ commit,
378
+ workflowId,
379
+ JSON.stringify(issues),
380
+ );
381
+ },
382
+ issues(eventId: string) {
383
+ return sql
384
+ .exec(
385
+ 'SELECT eventId, "commit", workflowId, issues FROM workflow_push_issues WHERE eventId = ? ORDER BY workflowId, "commit"',
386
+ eventId,
387
+ )
388
+ .toArray()
389
+ .map((raw) => {
390
+ const row = pushRowSchema.parse(raw);
391
+ return {
392
+ ...row,
393
+ issues: z.array(issueSchema).parse(JSON.parse(row.issues)),
394
+ };
395
+ });
396
+ },
397
+ };
398
+ const publicationJobs = createPublicationJobs(storage, {
399
+ readDraft: (workflowId) => {
400
+ const draft = readDraft(workflowId);
401
+ return draft === null ? null : draftDocument.parse(draft);
402
+ },
403
+ revision: () => librarySnapshot().revision,
404
+ publish: (value, epoch, refs) => {
405
+ indexes.recordBackgroundPublication(
406
+ {
407
+ ...value,
408
+ definition: Schema.decodeUnknownSync(workflowDefinitionSchema)(
409
+ value.definition,
410
+ ),
411
+ },
412
+ epoch,
413
+ refs,
414
+ );
415
+ },
416
+ });
417
+ return { ...indexes, publicationJobs };
418
+ }
419
+ export type WorkflowIndexes = ReturnType<typeof createWorkflowIndexes>;
@@ -0,0 +1,287 @@
1
+ import type { GraphStartRequest } from "@fungi.computer/braid/runtime";
2
+ import { workflowIdSchema } from "@fungi.computer/braid";
3
+ import * as z from "zod";
4
+ import * as S from "effect/Schema";
5
+ import { defineAppMethod } from "@fungi.computer/agar/backend-definition";
6
+ import { gitSha1Schema } from "@fungi.computer/puffball/git-representation";
7
+ import {
8
+ publishedRun,
9
+ runReply,
10
+ documentRead,
11
+ versionsRead,
12
+ rebuildCopiesInput,
13
+ rebuildCopiesReply,
14
+ saveReply,
15
+ publishReply,
16
+ publicationStatus,
17
+ publicationIdSchema,
18
+ workflow,
19
+ libraryListing,
20
+ libraryPageInput,
21
+ history,
22
+ revisionSchema,
23
+ shareExportReply,
24
+ shareCopyReply,
25
+ } from "@fungi.computer/workflows-wire";
26
+ import { readWorkflowCatalog } from "./catalog.js";
27
+ import { createWorkflowLibrary } from "./library.js";
28
+ import { createWorkflowShare } from "./share.js";
29
+ import type { WorkflowMethodContext } from "./methods.js";
30
+
31
+ async function catalog(
32
+ context: WorkflowMethodContext,
33
+ appIds: readonly string[],
34
+ ) {
35
+ const available = await readWorkflowCatalog(context.env.WORKFLOWS, appIds);
36
+ if (available === null) throw new Error("Workflow catalog unavailable");
37
+ return available;
38
+ }
39
+
40
+ function library(context: WorkflowMethodContext) {
41
+ const options: Parameters<typeof createWorkflowLibrary>[0] = {
42
+ author: { name: "Trellis", email: "trellis@app.local" },
43
+ catalog: (appIds) => catalog(context, appIds),
44
+ };
45
+ if (context.env.GIT) options.git = context.env.GIT;
46
+ if (context.env.BACKGROUND) options.background = context.env.BACKGROUND;
47
+ if (context.indexes) options.index = context.indexes;
48
+ return createWorkflowLibrary(options);
49
+ }
50
+ /** The single published-copy owner used by portable sharing and local forks. */
51
+ export function sharing(context: WorkflowMethodContext) {
52
+ return createWorkflowShare({
53
+ library: library(context),
54
+ catalog: (appIds) => catalog(context, appIds),
55
+ });
56
+ }
57
+
58
+ export const libraryMethods = {
59
+ exportWorkflow: defineAppMethod({
60
+ description:
61
+ "Export an immutable published Workflow version as a portable file.",
62
+ effect: "read",
63
+ requires: ["git.repository"],
64
+ input: z.strictObject({
65
+ workflowId: workflowIdSchema,
66
+ version: z.number().int().positive(),
67
+ }),
68
+ output: shareExportReply,
69
+ handler: (input, context: WorkflowMethodContext) =>
70
+ sharing(context).exportWorkflow(input),
71
+ }),
72
+ importWorkflow: defineAppMethod({
73
+ description:
74
+ "Check a portable Workflow file and save a new unpublished draft row.",
75
+ effect: "write",
76
+ requires: ["git.repository", "workflow.run"],
77
+ input: z.strictObject({
78
+ bytes: z.string(),
79
+ sha256: z.string(),
80
+ workflowId: workflowIdSchema,
81
+ }),
82
+ output: shareCopyReply,
83
+ handler: (input, context: WorkflowMethodContext) =>
84
+ sharing(context).importWorkflow(input),
85
+ }),
86
+ forkWorkflow: defineAppMethod({
87
+ description:
88
+ "Copy an immutable published Workflow version into a new unpublished draft row of this library.",
89
+ effect: "write",
90
+ requires: ["git.repository", "workflow.run"],
91
+ input: z.strictObject({
92
+ sourceWorkflowId: workflowIdSchema,
93
+ version: z.number().int().positive(),
94
+ workflowId: workflowIdSchema,
95
+ }),
96
+ output: shareCopyReply,
97
+ handler: (input, context: WorkflowMethodContext) =>
98
+ sharing(context).forkPublishedWorkflow({
99
+ sourceWorkflowId: input.sourceWorkflowId,
100
+ at: { version: input.version },
101
+ workflowId: input.workflowId,
102
+ }),
103
+ }),
104
+ listWorkflows: defineAppMethod({
105
+ description: "List this installation's saved Workflow library.",
106
+ effect: "read",
107
+ requires: ["git.repository"],
108
+ input: libraryPageInput,
109
+ output: libraryListing,
110
+ handler: async (input, context: WorkflowMethodContext) => {
111
+ const saved = await library(context).list();
112
+ if (!Array.isArray(saved)) return saved;
113
+ const available = saved
114
+ .filter(
115
+ (item) => input.after === undefined || item.workflowId > input.after,
116
+ )
117
+ .sort((a, b) => a.workflowId.localeCompare(b.workflowId));
118
+ const items = available.slice(0, input.limit);
119
+ const last = items.at(-1);
120
+ return available.length > input.limit && last
121
+ ? { items, next: last.workflowId }
122
+ : { items };
123
+ },
124
+ }),
125
+ rebuildCopies: defineAppMethod({
126
+ hidden: true,
127
+ description: "Rebuild saved screen copies in one bounded foreground page.",
128
+ effect: "read",
129
+ requires: ["git.repository"],
130
+ input: rebuildCopiesInput,
131
+ output: rebuildCopiesReply,
132
+ handler: async (input, context: WorkflowMethodContext) =>
133
+ rebuildCopiesReply.parse(
134
+ await library(context).rebuildCopies(input.cursor, input.target),
135
+ ),
136
+ }),
137
+ loadWorkflow: defineAppMethod({
138
+ description:
139
+ "Read a saved draft row or immutable published copy, including editor layout.",
140
+ effect: "read",
141
+ requires: ["git.repository"],
142
+ input: workflow.extend({
143
+ at: z.union([
144
+ z.literal("draft"),
145
+ z.strictObject({ commit: gitSha1Schema }),
146
+ ]),
147
+ }),
148
+ output: documentRead,
149
+ handler: async (input, context: WorkflowMethodContext) =>
150
+ documentRead.parse(
151
+ await library(context).read(input.workflowId, input.at),
152
+ ),
153
+ }),
154
+ saveDraft: defineAppMethod({
155
+ description:
156
+ "Validate and save a Workflow draft using its expected row revision.",
157
+ effect: "write",
158
+ requires: ["workflow.run"],
159
+ input: workflow.extend({
160
+ draft: z.json(),
161
+ expectedRevision: revisionSchema,
162
+ }),
163
+ output: saveReply,
164
+ handler: async (input, context: WorkflowMethodContext) =>
165
+ saveReply.parse(
166
+ await library(context).saveDraft(
167
+ input.workflowId,
168
+ input.draft,
169
+ input.expectedRevision,
170
+ ),
171
+ ),
172
+ }),
173
+ publishWorkflow: defineAppMethod({
174
+ description:
175
+ "Publish a validated draft to main and create its immutable version tag.",
176
+ effect: "write",
177
+ requires: ["git.repository", "workflow.run", "workflow.publish"],
178
+ input: workflow.extend({ expectedRevision: revisionSchema }),
179
+ output: publishReply,
180
+ handler: async (input, context: WorkflowMethodContext, caller) =>
181
+ publishReply.parse(
182
+ await library(context).publish(
183
+ input.workflowId,
184
+ input.expectedRevision,
185
+ caller,
186
+ ),
187
+ ),
188
+ }),
189
+ continuePublication: defineAppMethod({
190
+ hidden: true,
191
+ description: "Continue one freshly admitted member's publication step.",
192
+ effect: "write",
193
+ requires: [
194
+ "git.repository",
195
+ "workflow.run",
196
+ "workflow.publish",
197
+ "workflow.arm",
198
+ "media.generation",
199
+ "computer.writeFile",
200
+ ],
201
+ input: z.strictObject({}),
202
+ output: z.strictObject({ more: z.boolean() }),
203
+ handler: (_input, context: WorkflowMethodContext, caller) =>
204
+ library(context).continuePublication(caller),
205
+ }),
206
+ publicationStatus: defineAppMethod({
207
+ description: "Read publication progress without accessing Git.",
208
+ effect: "read",
209
+ requires: ["workflow.publish"],
210
+ input: workflow.extend({
211
+ publicationId: publicationIdSchema,
212
+ }),
213
+ output: publicationStatus,
214
+ handler: (input, context: WorkflowMethodContext, caller) =>
215
+ library(context).publicationStatus(
216
+ input.workflowId,
217
+ input.publicationId,
218
+ caller,
219
+ ),
220
+ }),
221
+ listVersions: defineAppMethod({
222
+ description: "List a Workflow's published version tags, newest first.",
223
+ effect: "read",
224
+ requires: ["git.repository"],
225
+ input: workflow,
226
+ output: versionsRead,
227
+ handler: async (input, context: WorkflowMethodContext) =>
228
+ versionsRead.parse(await library(context).versions(input.workflowId)),
229
+ }),
230
+ workflowHistory: defineAppMethod({
231
+ description: "Read up to fifty published Workflow versions.",
232
+ effect: "read",
233
+ requires: ["git.repository"],
234
+ input: workflow,
235
+ output: history,
236
+ handler: async (input, context: WorkflowMethodContext) =>
237
+ library(context).history(input.workflowId),
238
+ }),
239
+ revertDraft: defineAppMethod({
240
+ description: "Restore a published Workflow version as a new draft row.",
241
+ effect: "write",
242
+ requires: ["git.repository", "workflow.run"],
243
+ input: workflow.extend({
244
+ toCommit: gitSha1Schema,
245
+ expectedRevision: revisionSchema,
246
+ }),
247
+ output: saveReply,
248
+ handler: async (input, context: WorkflowMethodContext) =>
249
+ saveReply.parse(
250
+ await library(context).revert(
251
+ input.workflowId,
252
+ input.toCommit,
253
+ input.expectedRevision,
254
+ ),
255
+ ),
256
+ }),
257
+ runWorkflow: defineAppMethod({
258
+ description:
259
+ "Run an immutable published Workflow version or commit; drafts never execute. An optional delivery chat receives the run's ending.",
260
+ effect: "write",
261
+ requires: ["workflow.run"],
262
+ input: publishedRun,
263
+ output: runReply,
264
+ handler: async (input, context: WorkflowMethodContext) => {
265
+ const document = library(context).publishedRow(input.workflowId, input);
266
+ if (!document) return { error: "not_found" };
267
+ if (!context.env.WORKFLOWS)
268
+ throw new Error("Workflow access is not granted");
269
+ const base: GraphStartRequest = {
270
+ requestId: input.requestId,
271
+ definition: S.decodeUnknownSync(S.Json)(document.definition),
272
+ published: {
273
+ workflowId: input.workflowId,
274
+ commit: document.commit,
275
+ digest: document.digest,
276
+ },
277
+ bindings: input.bindings,
278
+ input: input.input,
279
+ };
280
+ const request: GraphStartRequest =
281
+ input.delivery === undefined
282
+ ? base
283
+ : { ...base, delivery: input.delivery };
284
+ return runReply.parse(await context.env.WORKFLOWS.runGraph(request));
285
+ },
286
+ }),
287
+ };