@fungi.computer/trellis 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +125 -2
- package/backend/catalog.ts +58 -0
- package/backend/gills.ts +56 -0
- package/backend/index.ts +40 -0
- package/backend/indexes.ts +419 -0
- package/backend/library-methods.ts +287 -0
- package/backend/library.ts +594 -0
- package/backend/methods.ts +221 -0
- package/backend/publication-job.ts +625 -0
- package/backend/push.ts +287 -0
- package/backend/schedule-methods.ts +120 -0
- package/backend/share.ts +221 -0
- package/backend/tool-family.ts +124 -0
- package/backend/tsconfig.build.json +11 -0
- package/backend/tsconfig.json +15 -0
- package/dist/backend/catalog.js +42 -0
- package/dist/backend/gills.js +49 -0
- package/dist/backend/indexes.js +294 -0
- package/dist/backend/library-methods.js +219 -0
- package/dist/backend/library.js +454 -0
- package/dist/backend/methods.js +141 -0
- package/dist/backend/publication-job.js +451 -0
- package/dist/backend/schedule-methods.js +90 -0
- package/dist/backend/share.js +140 -0
- package/dist/backend/tool-family.js +98 -0
- package/dist/shared/tool-contracts.js +32 -0
- package/package.json +166 -3
- package/shared/tool-contracts.ts +35 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { workflowIdSchema } from "@fungi.computer/braid";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import * as S from "effect/Schema";
|
|
4
|
+
import { defineAppMethod } from "@fungi.computer/agar/backend-definition";
|
|
5
|
+
import { gitSha1Schema } from "@fungi.computer/puffball/git-representation";
|
|
6
|
+
import { publishedRun, runReply, documentRead, versionsRead, rebuildCopiesInput, rebuildCopiesReply, saveReply, publishReply, publicationStatus, publicationIdSchema, workflow, libraryListing, libraryPageInput, history, revisionSchema, shareExportReply, shareCopyReply, } from "@fungi.computer/workflows-wire";
|
|
7
|
+
import { readWorkflowCatalog } from "./catalog.js";
|
|
8
|
+
import { createWorkflowLibrary } from "./library.js";
|
|
9
|
+
import { createWorkflowShare } from "./share.js";
|
|
10
|
+
async function catalog(context, appIds) {
|
|
11
|
+
const available = await readWorkflowCatalog(context.env.WORKFLOWS, appIds);
|
|
12
|
+
if (available === null)
|
|
13
|
+
throw new Error("Workflow catalog unavailable");
|
|
14
|
+
return available;
|
|
15
|
+
}
|
|
16
|
+
function library(context) {
|
|
17
|
+
const options = {
|
|
18
|
+
author: { name: "Trellis", email: "trellis@app.local" },
|
|
19
|
+
catalog: (appIds) => catalog(context, appIds),
|
|
20
|
+
};
|
|
21
|
+
if (context.env.GIT)
|
|
22
|
+
options.git = context.env.GIT;
|
|
23
|
+
if (context.env.BACKGROUND)
|
|
24
|
+
options.background = context.env.BACKGROUND;
|
|
25
|
+
if (context.indexes)
|
|
26
|
+
options.index = context.indexes;
|
|
27
|
+
return createWorkflowLibrary(options);
|
|
28
|
+
}
|
|
29
|
+
/** The single published-copy owner used by portable sharing and local forks. */
|
|
30
|
+
export function sharing(context) {
|
|
31
|
+
return createWorkflowShare({
|
|
32
|
+
library: library(context),
|
|
33
|
+
catalog: (appIds) => catalog(context, appIds),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export const libraryMethods = {
|
|
37
|
+
exportWorkflow: defineAppMethod({
|
|
38
|
+
description: "Export an immutable published Workflow version as a portable file.",
|
|
39
|
+
effect: "read",
|
|
40
|
+
requires: ["git.repository"],
|
|
41
|
+
input: z.strictObject({
|
|
42
|
+
workflowId: workflowIdSchema,
|
|
43
|
+
version: z.number().int().positive(),
|
|
44
|
+
}),
|
|
45
|
+
output: shareExportReply,
|
|
46
|
+
handler: (input, context) => sharing(context).exportWorkflow(input),
|
|
47
|
+
}),
|
|
48
|
+
importWorkflow: defineAppMethod({
|
|
49
|
+
description: "Check a portable Workflow file and save a new unpublished draft row.",
|
|
50
|
+
effect: "write",
|
|
51
|
+
requires: ["git.repository", "workflow.run"],
|
|
52
|
+
input: z.strictObject({
|
|
53
|
+
bytes: z.string(),
|
|
54
|
+
sha256: z.string(),
|
|
55
|
+
workflowId: workflowIdSchema,
|
|
56
|
+
}),
|
|
57
|
+
output: shareCopyReply,
|
|
58
|
+
handler: (input, context) => sharing(context).importWorkflow(input),
|
|
59
|
+
}),
|
|
60
|
+
forkWorkflow: defineAppMethod({
|
|
61
|
+
description: "Copy an immutable published Workflow version into a new unpublished draft row of this library.",
|
|
62
|
+
effect: "write",
|
|
63
|
+
requires: ["git.repository", "workflow.run"],
|
|
64
|
+
input: z.strictObject({
|
|
65
|
+
sourceWorkflowId: workflowIdSchema,
|
|
66
|
+
version: z.number().int().positive(),
|
|
67
|
+
workflowId: workflowIdSchema,
|
|
68
|
+
}),
|
|
69
|
+
output: shareCopyReply,
|
|
70
|
+
handler: (input, context) => sharing(context).forkPublishedWorkflow({
|
|
71
|
+
sourceWorkflowId: input.sourceWorkflowId,
|
|
72
|
+
at: { version: input.version },
|
|
73
|
+
workflowId: input.workflowId,
|
|
74
|
+
}),
|
|
75
|
+
}),
|
|
76
|
+
listWorkflows: defineAppMethod({
|
|
77
|
+
description: "List this installation's saved Workflow library.",
|
|
78
|
+
effect: "read",
|
|
79
|
+
requires: ["git.repository"],
|
|
80
|
+
input: libraryPageInput,
|
|
81
|
+
output: libraryListing,
|
|
82
|
+
handler: async (input, context) => {
|
|
83
|
+
const saved = await library(context).list();
|
|
84
|
+
if (!Array.isArray(saved))
|
|
85
|
+
return saved;
|
|
86
|
+
const available = saved
|
|
87
|
+
.filter((item) => input.after === undefined || item.workflowId > input.after)
|
|
88
|
+
.sort((a, b) => a.workflowId.localeCompare(b.workflowId));
|
|
89
|
+
const items = available.slice(0, input.limit);
|
|
90
|
+
const last = items.at(-1);
|
|
91
|
+
return available.length > input.limit && last
|
|
92
|
+
? { items, next: last.workflowId }
|
|
93
|
+
: { items };
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
rebuildCopies: defineAppMethod({
|
|
97
|
+
hidden: true,
|
|
98
|
+
description: "Rebuild saved screen copies in one bounded foreground page.",
|
|
99
|
+
effect: "read",
|
|
100
|
+
requires: ["git.repository"],
|
|
101
|
+
input: rebuildCopiesInput,
|
|
102
|
+
output: rebuildCopiesReply,
|
|
103
|
+
handler: async (input, context) => rebuildCopiesReply.parse(await library(context).rebuildCopies(input.cursor, input.target)),
|
|
104
|
+
}),
|
|
105
|
+
loadWorkflow: defineAppMethod({
|
|
106
|
+
description: "Read a saved draft row or immutable published copy, including editor layout.",
|
|
107
|
+
effect: "read",
|
|
108
|
+
requires: ["git.repository"],
|
|
109
|
+
input: workflow.extend({
|
|
110
|
+
at: z.union([
|
|
111
|
+
z.literal("draft"),
|
|
112
|
+
z.strictObject({ commit: gitSha1Schema }),
|
|
113
|
+
]),
|
|
114
|
+
}),
|
|
115
|
+
output: documentRead,
|
|
116
|
+
handler: async (input, context) => documentRead.parse(await library(context).read(input.workflowId, input.at)),
|
|
117
|
+
}),
|
|
118
|
+
saveDraft: defineAppMethod({
|
|
119
|
+
description: "Validate and save a Workflow draft using its expected row revision.",
|
|
120
|
+
effect: "write",
|
|
121
|
+
requires: ["workflow.run"],
|
|
122
|
+
input: workflow.extend({
|
|
123
|
+
draft: z.json(),
|
|
124
|
+
expectedRevision: revisionSchema,
|
|
125
|
+
}),
|
|
126
|
+
output: saveReply,
|
|
127
|
+
handler: async (input, context) => saveReply.parse(await library(context).saveDraft(input.workflowId, input.draft, input.expectedRevision)),
|
|
128
|
+
}),
|
|
129
|
+
publishWorkflow: defineAppMethod({
|
|
130
|
+
description: "Publish a validated draft to main and create its immutable version tag.",
|
|
131
|
+
effect: "write",
|
|
132
|
+
requires: ["git.repository", "workflow.run", "workflow.publish"],
|
|
133
|
+
input: workflow.extend({ expectedRevision: revisionSchema }),
|
|
134
|
+
output: publishReply,
|
|
135
|
+
handler: async (input, context, caller) => publishReply.parse(await library(context).publish(input.workflowId, input.expectedRevision, caller)),
|
|
136
|
+
}),
|
|
137
|
+
continuePublication: defineAppMethod({
|
|
138
|
+
hidden: true,
|
|
139
|
+
description: "Continue one freshly admitted member's publication step.",
|
|
140
|
+
effect: "write",
|
|
141
|
+
requires: [
|
|
142
|
+
"git.repository",
|
|
143
|
+
"workflow.run",
|
|
144
|
+
"workflow.publish",
|
|
145
|
+
"workflow.arm",
|
|
146
|
+
"media.generation",
|
|
147
|
+
"computer.writeFile",
|
|
148
|
+
],
|
|
149
|
+
input: z.strictObject({}),
|
|
150
|
+
output: z.strictObject({ more: z.boolean() }),
|
|
151
|
+
handler: (_input, context, caller) => library(context).continuePublication(caller),
|
|
152
|
+
}),
|
|
153
|
+
publicationStatus: defineAppMethod({
|
|
154
|
+
description: "Read publication progress without accessing Git.",
|
|
155
|
+
effect: "read",
|
|
156
|
+
requires: ["workflow.publish"],
|
|
157
|
+
input: workflow.extend({
|
|
158
|
+
publicationId: publicationIdSchema,
|
|
159
|
+
}),
|
|
160
|
+
output: publicationStatus,
|
|
161
|
+
handler: (input, context, caller) => library(context).publicationStatus(input.workflowId, input.publicationId, caller),
|
|
162
|
+
}),
|
|
163
|
+
listVersions: defineAppMethod({
|
|
164
|
+
description: "List a Workflow's published version tags, newest first.",
|
|
165
|
+
effect: "read",
|
|
166
|
+
requires: ["git.repository"],
|
|
167
|
+
input: workflow,
|
|
168
|
+
output: versionsRead,
|
|
169
|
+
handler: async (input, context) => versionsRead.parse(await library(context).versions(input.workflowId)),
|
|
170
|
+
}),
|
|
171
|
+
workflowHistory: defineAppMethod({
|
|
172
|
+
description: "Read up to fifty published Workflow versions.",
|
|
173
|
+
effect: "read",
|
|
174
|
+
requires: ["git.repository"],
|
|
175
|
+
input: workflow,
|
|
176
|
+
output: history,
|
|
177
|
+
handler: async (input, context) => library(context).history(input.workflowId),
|
|
178
|
+
}),
|
|
179
|
+
revertDraft: defineAppMethod({
|
|
180
|
+
description: "Restore a published Workflow version as a new draft row.",
|
|
181
|
+
effect: "write",
|
|
182
|
+
requires: ["git.repository", "workflow.run"],
|
|
183
|
+
input: workflow.extend({
|
|
184
|
+
toCommit: gitSha1Schema,
|
|
185
|
+
expectedRevision: revisionSchema,
|
|
186
|
+
}),
|
|
187
|
+
output: saveReply,
|
|
188
|
+
handler: async (input, context) => saveReply.parse(await library(context).revert(input.workflowId, input.toCommit, input.expectedRevision)),
|
|
189
|
+
}),
|
|
190
|
+
runWorkflow: defineAppMethod({
|
|
191
|
+
description: "Run an immutable published Workflow version or commit; drafts never execute. An optional delivery chat receives the run's ending.",
|
|
192
|
+
effect: "write",
|
|
193
|
+
requires: ["workflow.run"],
|
|
194
|
+
input: publishedRun,
|
|
195
|
+
output: runReply,
|
|
196
|
+
handler: async (input, context) => {
|
|
197
|
+
const document = library(context).publishedRow(input.workflowId, input);
|
|
198
|
+
if (!document)
|
|
199
|
+
return { error: "not_found" };
|
|
200
|
+
if (!context.env.WORKFLOWS)
|
|
201
|
+
throw new Error("Workflow access is not granted");
|
|
202
|
+
const base = {
|
|
203
|
+
requestId: input.requestId,
|
|
204
|
+
definition: S.decodeUnknownSync(S.Json)(document.definition),
|
|
205
|
+
published: {
|
|
206
|
+
workflowId: input.workflowId,
|
|
207
|
+
commit: document.commit,
|
|
208
|
+
digest: document.digest,
|
|
209
|
+
},
|
|
210
|
+
bindings: input.bindings,
|
|
211
|
+
input: input.input,
|
|
212
|
+
};
|
|
213
|
+
const request = input.delivery === undefined
|
|
214
|
+
? base
|
|
215
|
+
: { ...base, delivery: input.delivery };
|
|
216
|
+
return runReply.parse(await context.env.WORKFLOWS.runGraph(request));
|
|
217
|
+
},
|
|
218
|
+
}),
|
|
219
|
+
};
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { workflowIdSchema } from "@fungi.computer/braid";
|
|
2
|
+
import * as S from "effect/Schema";
|
|
3
|
+
import { countingGit } from "@fungi.computer/agar/read-copy";
|
|
4
|
+
import { sha256DigestSchema } from "@fungi.computer/puffball/effect";
|
|
5
|
+
import { mayEnqueueBackground, } from "@fungi.computer/puffball/background-replay";
|
|
6
|
+
import { commitFiles, moveRefs, } from "@fungi.computer/agar/git-commit";
|
|
7
|
+
import { validate, contentDigest } from "@fungi.computer/braid";
|
|
8
|
+
import { definitionAppIds } from "./catalog.js";
|
|
9
|
+
import { draftDocument, listingGit, readPublishedWorkflow, readWorkflowAt as readAt, versionRefs, workflowDraftSchema as draftSchema, workflowFilePath, workflowFiles, workflowLibraryRepository, } from "@fungi.computer/workflows-wire";
|
|
10
|
+
export function retainedDraft(draft) {
|
|
11
|
+
const content = { definition: draft.definition, layout: draft.layout };
|
|
12
|
+
if (draft.provenance !== undefined)
|
|
13
|
+
return { ...content, provenance: draft.provenance };
|
|
14
|
+
return content;
|
|
15
|
+
}
|
|
16
|
+
export function serializedDraft(draft) {
|
|
17
|
+
const content = {
|
|
18
|
+
...draft.definition,
|
|
19
|
+
layout: draft.layout,
|
|
20
|
+
};
|
|
21
|
+
if (draft.provenance !== undefined)
|
|
22
|
+
return JSON.stringify({ ...content, provenance: draft.provenance });
|
|
23
|
+
return JSON.stringify(content);
|
|
24
|
+
}
|
|
25
|
+
const repository = workflowLibraryRepository;
|
|
26
|
+
const filePath = workflowFilePath;
|
|
27
|
+
/** Installation-local git owns documents; the catalog owns method validation. */
|
|
28
|
+
export function createWorkflowLibrary({ git: gitAccess, author, catalog, background, index, }) {
|
|
29
|
+
function requireGit() {
|
|
30
|
+
if (!gitAccess)
|
|
31
|
+
throw new Error("Git access is not granted");
|
|
32
|
+
return gitAccess;
|
|
33
|
+
}
|
|
34
|
+
async function refs() {
|
|
35
|
+
return (await requireGit().info(repository))
|
|
36
|
+
? requireGit().refs(repository)
|
|
37
|
+
: [];
|
|
38
|
+
}
|
|
39
|
+
async function head(ref) {
|
|
40
|
+
return (await refs()).find((entry) => entry.name === ref)?.hash ?? null;
|
|
41
|
+
}
|
|
42
|
+
async function move(request, next = []) {
|
|
43
|
+
try {
|
|
44
|
+
return await moveRefs(requireGit(), {
|
|
45
|
+
repository: request.repository,
|
|
46
|
+
source: request.source,
|
|
47
|
+
commit: request.commit,
|
|
48
|
+
updates: [{ ref: request.ref, expected: request.expected }, ...next],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (cause) {
|
|
52
|
+
if (!(cause instanceof RangeError))
|
|
53
|
+
throw cause;
|
|
54
|
+
const current = await head(`refs/heads/${request.source}`);
|
|
55
|
+
if (current === request.commit)
|
|
56
|
+
throw cause;
|
|
57
|
+
return { kind: "stale", ref: request.ref, head: current };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function read(workflowId, at) {
|
|
61
|
+
workflowIdSchema.parse(workflowId);
|
|
62
|
+
if (at === "draft")
|
|
63
|
+
return Promise.resolve(index?.drafts.read(workflowId) ?? null);
|
|
64
|
+
const key = `at:${workflowId}:${at.commit}`;
|
|
65
|
+
const copied = index?.documents.read(key);
|
|
66
|
+
if (copied?.kind !== "ready")
|
|
67
|
+
return Promise.resolve({ kind: "rebuilding" });
|
|
68
|
+
return Promise.resolve(copied.value === null
|
|
69
|
+
? null
|
|
70
|
+
: {
|
|
71
|
+
...S.decodeUnknownSync(draftSchema)(retainedDraft(copied.value)),
|
|
72
|
+
commit: copied.value.commit,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async function saveDraft(workflowId, raw, expectedRevision) {
|
|
76
|
+
workflowIdSchema.parse(workflowId);
|
|
77
|
+
let draft;
|
|
78
|
+
try {
|
|
79
|
+
draft = S.decodeUnknownSync(draftSchema)(raw, {
|
|
80
|
+
onExcessProperty: "error",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (cause) {
|
|
84
|
+
if (!S.isSchemaError(cause))
|
|
85
|
+
throw cause;
|
|
86
|
+
return {
|
|
87
|
+
kind: "invalid",
|
|
88
|
+
issues: [
|
|
89
|
+
{
|
|
90
|
+
code: "schema",
|
|
91
|
+
message: "Workflow definition or layout failed decoding",
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const available = await catalog(definitionAppIds(draft.definition));
|
|
97
|
+
const issues = validate(draft.definition, available);
|
|
98
|
+
if (issues.length)
|
|
99
|
+
return { kind: "invalid", issues };
|
|
100
|
+
if (!index)
|
|
101
|
+
throw new Error("Workflow row storage unavailable");
|
|
102
|
+
const body = draftDocument.omit({ revision: true }).parse(draft);
|
|
103
|
+
return index.drafts.save(workflowId, body, expectedRevision);
|
|
104
|
+
}
|
|
105
|
+
function versions(workflowId) {
|
|
106
|
+
workflowIdSchema.parse(workflowId);
|
|
107
|
+
const copied = index?.versionCopies.read(workflowId);
|
|
108
|
+
if (copied?.kind === "ready")
|
|
109
|
+
return Promise.resolve(copied.value);
|
|
110
|
+
const snapshot = index?.librarySnapshot().snapshot;
|
|
111
|
+
if (snapshot &&
|
|
112
|
+
versionRefs(workflowId, Object.entries(snapshot.refs).map(([name, hash]) => ({ name, hash }))).length === 0)
|
|
113
|
+
return Promise.resolve([]);
|
|
114
|
+
return Promise.resolve({ kind: "rebuilding" });
|
|
115
|
+
}
|
|
116
|
+
const fileContent = (draft) => S.decodeUnknownSync(S.Json)(retainedDraft(draft));
|
|
117
|
+
async function sameFile(left, right) {
|
|
118
|
+
return ((await contentDigest(fileContent(left))) ===
|
|
119
|
+
(await contentDigest(fileContent(right))));
|
|
120
|
+
}
|
|
121
|
+
async function publish(workflowId, expectedRevision, caller) {
|
|
122
|
+
if (!index)
|
|
123
|
+
throw new Error("Workflow row storage unavailable");
|
|
124
|
+
const draft = index.drafts.read(workflowId);
|
|
125
|
+
if (draft?.revision !== expectedRevision)
|
|
126
|
+
return { kind: "conflict", head: null };
|
|
127
|
+
const retainedPublication = index.publicationJobs.existing(workflowId, expectedRevision, caller);
|
|
128
|
+
if (retainedPublication !== null)
|
|
129
|
+
return retainedPublication;
|
|
130
|
+
const issues = validate(draft.definition, await catalog(definitionAppIds(draft.definition)));
|
|
131
|
+
if (issues.length)
|
|
132
|
+
return { kind: "invalid", issues };
|
|
133
|
+
const snapshot = index.librarySnapshot();
|
|
134
|
+
const mainCopy = index.documents.read(`main:${workflowId}`);
|
|
135
|
+
const versionsCopy = index.versionCopies.read(workflowId);
|
|
136
|
+
if (mainCopy.kind === "ready" &&
|
|
137
|
+
mainCopy.value &&
|
|
138
|
+
versionsCopy.kind === "ready" &&
|
|
139
|
+
(await sameFile(retainedDraft(draft), retainedDraft(mainCopy.value)))) {
|
|
140
|
+
for (const previous of versionsCopy.value) {
|
|
141
|
+
const publication = index.publications.read(`${workflowId}:v${String(previous.version)}`);
|
|
142
|
+
if (publication.kind === "ready" &&
|
|
143
|
+
(await sameFile(retainedDraft(draft), retainedDraft(publication.value))))
|
|
144
|
+
return {
|
|
145
|
+
kind: "published",
|
|
146
|
+
commit: publication.value.commit,
|
|
147
|
+
version: publication.value.version,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (caller && mayEnqueueBackground(caller))
|
|
152
|
+
return index.publicationJobs.enqueue(workflowId, expectedRevision, caller);
|
|
153
|
+
if (!(await requireGit().info(repository)))
|
|
154
|
+
await requireGit().create(repository, { defaultBranch: "main" });
|
|
155
|
+
const references = await requireGit().refs(repository);
|
|
156
|
+
const main = references.find((ref) => ref.name === "refs/heads/main")?.hash ?? null;
|
|
157
|
+
const existing = versionRefs(workflowId, references);
|
|
158
|
+
const result = await commitFiles(requireGit(), {
|
|
159
|
+
repository,
|
|
160
|
+
branch: "main",
|
|
161
|
+
expectedHead: main,
|
|
162
|
+
changes: [
|
|
163
|
+
{
|
|
164
|
+
path: filePath(workflowId),
|
|
165
|
+
content: new TextEncoder().encode(serializedDraft(retainedDraft(draft))),
|
|
166
|
+
mode: "100644",
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
message: `Publish ${workflowId}`,
|
|
170
|
+
author,
|
|
171
|
+
});
|
|
172
|
+
if (result.kind === "stale")
|
|
173
|
+
return { kind: "conflict", head: result.head };
|
|
174
|
+
if (result.kind === "updated")
|
|
175
|
+
throw new Error("Unexpected ref-only publication");
|
|
176
|
+
const version = (existing[0]?.version ?? 0) + 1;
|
|
177
|
+
const tagRef = `refs/tags/${workflowId}@v${String(version)}`;
|
|
178
|
+
const tag = await move({
|
|
179
|
+
repository,
|
|
180
|
+
ref: tagRef,
|
|
181
|
+
expected: null,
|
|
182
|
+
commit: result.commit,
|
|
183
|
+
source: "main",
|
|
184
|
+
});
|
|
185
|
+
if (tag.kind === "stale" && tag.head !== result.commit)
|
|
186
|
+
return { kind: "conflict", head: tag.head };
|
|
187
|
+
const saved = { ...retainedDraft(draft), commit: result.commit };
|
|
188
|
+
const publication = {
|
|
189
|
+
...saved,
|
|
190
|
+
workflowId,
|
|
191
|
+
version,
|
|
192
|
+
digest: S.decodeUnknownSync(sha256DigestSchema)(await contentDigest(S.decodeUnknownSync(S.Json)(draft.definition))),
|
|
193
|
+
};
|
|
194
|
+
index.recordPublication(publication, snapshot.revision);
|
|
195
|
+
return { kind: "published", commit: result.commit, version };
|
|
196
|
+
}
|
|
197
|
+
async function rebuildCopies(cursor, target) {
|
|
198
|
+
if (!index)
|
|
199
|
+
throw new Error("Workflow copy storage unavailable");
|
|
200
|
+
const controller = new AbortController();
|
|
201
|
+
const counted = countingGit(requireGit(), {
|
|
202
|
+
limit: 40,
|
|
203
|
+
signal: controller.signal,
|
|
204
|
+
});
|
|
205
|
+
const source = listingGit(counted.git);
|
|
206
|
+
try {
|
|
207
|
+
if (target) {
|
|
208
|
+
workflowIdSchema.parse(target.workflowId);
|
|
209
|
+
const at = target.at;
|
|
210
|
+
const key = `at:${target.workflowId}:${at.commit}`;
|
|
211
|
+
const copied = await index.documents.load(key, {
|
|
212
|
+
git: requireGit(),
|
|
213
|
+
signal: controller.signal,
|
|
214
|
+
async rebuild(currentGit) {
|
|
215
|
+
const available = await currentGit.info(repository);
|
|
216
|
+
const commit = available ? at.commit : null;
|
|
217
|
+
const value = available
|
|
218
|
+
? await readAt(listingGit(currentGit), target.workflowId, commit)
|
|
219
|
+
: null;
|
|
220
|
+
return { head: commit, value };
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
if (copied.kind !== "ready")
|
|
224
|
+
return { done: false, cursor: null };
|
|
225
|
+
const doc = copied.value;
|
|
226
|
+
if (doc)
|
|
227
|
+
index.documents.write(`at:${target.workflowId}:${doc.commit}`, doc.commit, doc);
|
|
228
|
+
return { done: true, cursor: null };
|
|
229
|
+
}
|
|
230
|
+
const epoch = index.librarySnapshot().revision;
|
|
231
|
+
const progress = cursor === null ? null : index.frontier.read(cursor);
|
|
232
|
+
let state = progress?.kind === "ready" && progress.value.epoch === epoch
|
|
233
|
+
? progress.value
|
|
234
|
+
: null;
|
|
235
|
+
const key = String(epoch);
|
|
236
|
+
if (!state) {
|
|
237
|
+
const references = (await counted.git.info(repository))
|
|
238
|
+
? await counted.git.refs(repository)
|
|
239
|
+
: [];
|
|
240
|
+
const main = references.find((ref) => ref.name === "refs/heads/main")?.hash ??
|
|
241
|
+
null;
|
|
242
|
+
const ids = new Set();
|
|
243
|
+
for (const file of await workflowFiles(source, main)) {
|
|
244
|
+
const id = file.name.endsWith(".json") ? file.name.slice(0, -5) : "";
|
|
245
|
+
if (workflowIdSchema.safeParse(id).success)
|
|
246
|
+
ids.add(id);
|
|
247
|
+
}
|
|
248
|
+
for (const ref of references)
|
|
249
|
+
if (ref.name.startsWith("refs/tags/")) {
|
|
250
|
+
const id = ref.name.slice("refs/tags/".length).split("@v")[0];
|
|
251
|
+
if (id !== undefined &&
|
|
252
|
+
workflowIdSchema.safeParse(id).success &&
|
|
253
|
+
versionRefs(id, [ref]).length)
|
|
254
|
+
ids.add(id);
|
|
255
|
+
}
|
|
256
|
+
state = {
|
|
257
|
+
epoch,
|
|
258
|
+
refs: references,
|
|
259
|
+
ids: [...ids].sort().slice(0, 100),
|
|
260
|
+
offset: 0,
|
|
261
|
+
items: [],
|
|
262
|
+
current: null,
|
|
263
|
+
queue: [],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const main = state.refs.find((ref) => ref.name === "refs/heads/main")?.hash ?? null;
|
|
267
|
+
while (state.offset < state.ids.length) {
|
|
268
|
+
const workflowId = state.ids[state.offset];
|
|
269
|
+
if (workflowId === undefined)
|
|
270
|
+
throw new Error("Workflow frontier is incomplete");
|
|
271
|
+
if (state.current === null) {
|
|
272
|
+
if (state.queue.length === 0) {
|
|
273
|
+
const width = Math.min(4, Math.floor((40 - counted.total()) / 8));
|
|
274
|
+
if (width === 0)
|
|
275
|
+
break;
|
|
276
|
+
state.queue = await Promise.all(state.ids
|
|
277
|
+
.slice(state.offset, state.offset + width)
|
|
278
|
+
.map(async (id) => {
|
|
279
|
+
const published = await readAt(source, id, main);
|
|
280
|
+
return {
|
|
281
|
+
workflowId: id,
|
|
282
|
+
main: published,
|
|
283
|
+
versions: [],
|
|
284
|
+
versionOffset: 0,
|
|
285
|
+
};
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
const prepared = state.queue.shift();
|
|
289
|
+
if (!prepared)
|
|
290
|
+
throw new Error("Workflow cursor has no pending key");
|
|
291
|
+
state.current = prepared;
|
|
292
|
+
}
|
|
293
|
+
const current = state.current;
|
|
294
|
+
const tags = versionRefs(workflowId, state.refs).slice(0, 1000);
|
|
295
|
+
while (current.versionOffset < tags.length && counted.total() < 36) {
|
|
296
|
+
const tag = tags[current.versionOffset];
|
|
297
|
+
if (tag === undefined)
|
|
298
|
+
throw new Error("Workflow version frontier is incomplete");
|
|
299
|
+
const metadata = await source.readCommit(repository, tag.commit);
|
|
300
|
+
const historical = await readAt(source, workflowId, tag.commit);
|
|
301
|
+
if (metadata)
|
|
302
|
+
current.versions.push({
|
|
303
|
+
...tag,
|
|
304
|
+
committedAt: metadata.committedAt,
|
|
305
|
+
message: metadata.message,
|
|
306
|
+
});
|
|
307
|
+
if (historical) {
|
|
308
|
+
index.documents.write(`at:${workflowId}:${tag.commit}`, tag.commit, historical);
|
|
309
|
+
const stored = {
|
|
310
|
+
...historical,
|
|
311
|
+
workflowId,
|
|
312
|
+
version: tag.version,
|
|
313
|
+
digest: S.decodeUnknownSync(sha256DigestSchema)(await contentDigest(S.decodeUnknownSync(S.Json)(historical.definition))),
|
|
314
|
+
};
|
|
315
|
+
index.publications.write(`${workflowId}:v${String(tag.version)}`, tag.commit, stored);
|
|
316
|
+
index.publications.write(`${workflowId}:${tag.commit}`, tag.commit, stored);
|
|
317
|
+
}
|
|
318
|
+
current.versionOffset++;
|
|
319
|
+
}
|
|
320
|
+
if (index.librarySnapshot().revision !== epoch)
|
|
321
|
+
return { done: false, cursor: null };
|
|
322
|
+
if (current.versionOffset < tags.length)
|
|
323
|
+
break;
|
|
324
|
+
const doc = current.main;
|
|
325
|
+
index.documents.write(`main:${workflowId}`, main, current.main);
|
|
326
|
+
for (const selected of [current.main])
|
|
327
|
+
if (selected)
|
|
328
|
+
index.documents.write(`at:${workflowId}:${selected.commit}`, selected.commit, selected);
|
|
329
|
+
index.versionCopies.write(workflowId, main, current.versions);
|
|
330
|
+
if (doc) {
|
|
331
|
+
let latest = null;
|
|
332
|
+
if (current.main)
|
|
333
|
+
for (const version of current.versions.slice(0, 1)) {
|
|
334
|
+
const historical = index.documents.read(`at:${workflowId}:${version.commit}`);
|
|
335
|
+
if (historical.kind === "ready" &&
|
|
336
|
+
historical.value &&
|
|
337
|
+
(await sameFile(retainedDraft(current.main), retainedDraft(historical.value)))) {
|
|
338
|
+
latest = version;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
state.items.push({
|
|
343
|
+
workflowId,
|
|
344
|
+
title: doc.definition.title,
|
|
345
|
+
draftRevision: index.drafts.read(workflowId)?.revision ?? null,
|
|
346
|
+
published: current.main
|
|
347
|
+
? {
|
|
348
|
+
commit: latest?.commit ?? current.main.commit,
|
|
349
|
+
version: latest?.version ?? null,
|
|
350
|
+
}
|
|
351
|
+
: null,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
state.offset++;
|
|
355
|
+
state.current = null;
|
|
356
|
+
}
|
|
357
|
+
if (index.librarySnapshot().revision !== epoch)
|
|
358
|
+
return { done: false, cursor: null };
|
|
359
|
+
if (state.offset === state.ids.length) {
|
|
360
|
+
index.recordLibrary(epoch, {
|
|
361
|
+
refs: Object.fromEntries(state.refs.map((ref) => [ref.name, ref.hash])),
|
|
362
|
+
items: state.items,
|
|
363
|
+
});
|
|
364
|
+
index.frontier.stale(key);
|
|
365
|
+
return { done: true, cursor: null };
|
|
366
|
+
}
|
|
367
|
+
index.frontier.write(key, main, state);
|
|
368
|
+
return { done: false, cursor: key };
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
controller.abort();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function list() {
|
|
375
|
+
const cached = index?.libraryList();
|
|
376
|
+
return Promise.resolve(cached?.kind === "ready"
|
|
377
|
+
? cached.value.items
|
|
378
|
+
: { kind: "unindexed" });
|
|
379
|
+
}
|
|
380
|
+
async function history(workflowId) {
|
|
381
|
+
const saved = await versions(workflowId);
|
|
382
|
+
return Array.isArray(saved)
|
|
383
|
+
? saved.slice(0, 50).map(({ commit, committedAt, message }) => ({
|
|
384
|
+
commit,
|
|
385
|
+
committedAt,
|
|
386
|
+
message,
|
|
387
|
+
}))
|
|
388
|
+
: [];
|
|
389
|
+
}
|
|
390
|
+
async function revert(workflowId, toCommit, expectedRevision) {
|
|
391
|
+
const previous = await read(workflowId, { commit: toCommit });
|
|
392
|
+
return previous && !("kind" in previous)
|
|
393
|
+
? saveDraft(workflowId, retainedDraft(previous), expectedRevision)
|
|
394
|
+
: {
|
|
395
|
+
kind: "invalid",
|
|
396
|
+
issues: [
|
|
397
|
+
{ code: "schema", message: "Workflow file not found at commit" },
|
|
398
|
+
],
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
async function published(workflowId, at) {
|
|
402
|
+
return readPublishedWorkflow(requireGit(), workflowId, at, (commit) => {
|
|
403
|
+
const copied = index?.documents.read(`at:${workflowId}:${commit}`);
|
|
404
|
+
return copied?.kind === "ready" &&
|
|
405
|
+
copied.head === commit &&
|
|
406
|
+
copied.value?.commit === commit
|
|
407
|
+
? { ...retainedDraft(copied.value), commit }
|
|
408
|
+
: null;
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
function publishedRow(workflowId, at) {
|
|
412
|
+
workflowIdSchema.parse(workflowId);
|
|
413
|
+
if (!index)
|
|
414
|
+
throw new Error("Workflow row storage unavailable");
|
|
415
|
+
const key = "version" in at
|
|
416
|
+
? `${workflowId}:v${String(at.version)}`
|
|
417
|
+
: `${workflowId}:${at.commit}`;
|
|
418
|
+
const row = index.publications.read(key);
|
|
419
|
+
if (row.kind !== "ready" || row.value.workflowId !== workflowId)
|
|
420
|
+
return null;
|
|
421
|
+
if ("version" in at
|
|
422
|
+
? row.value.version !== at.version
|
|
423
|
+
: row.value.commit !== at.commit)
|
|
424
|
+
return null;
|
|
425
|
+
return row.value;
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
publishedRow,
|
|
429
|
+
read,
|
|
430
|
+
saveDraft,
|
|
431
|
+
publish,
|
|
432
|
+
versions,
|
|
433
|
+
list,
|
|
434
|
+
rebuildCopies,
|
|
435
|
+
history,
|
|
436
|
+
revert,
|
|
437
|
+
published,
|
|
438
|
+
continuePublication: (caller) => {
|
|
439
|
+
if (!index)
|
|
440
|
+
throw new Error("Workflow row storage unavailable");
|
|
441
|
+
return index.publicationJobs.continue(caller, {
|
|
442
|
+
git: requireGit(),
|
|
443
|
+
author,
|
|
444
|
+
background,
|
|
445
|
+
catalog,
|
|
446
|
+
});
|
|
447
|
+
},
|
|
448
|
+
publicationStatus: (workflowId, publicationId, caller) => {
|
|
449
|
+
if (!index)
|
|
450
|
+
throw new Error("Workflow row storage unavailable");
|
|
451
|
+
return index.publicationJobs.status(workflowId, publicationId, caller);
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|