@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,124 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { defineAppMethod } from "@fungi.computer/agar/backend-definition";
|
|
3
|
+
import { catalogReplySchema } from "@fungi.computer/workflows-wire";
|
|
4
|
+
import { runReply } from "@fungi.computer/workflows-wire";
|
|
5
|
+
import { workflowToolListing } from "../shared/tool-contracts.js";
|
|
6
|
+
import type { WorkflowIndexes } from "./indexes.js";
|
|
7
|
+
import type { WorkflowMethodContext } from "./methods.js";
|
|
8
|
+
|
|
9
|
+
const output = z.toJSONSchema(runReply, { io: "output" });
|
|
10
|
+
|
|
11
|
+
// Workflow ids have no underscores or upper-case letters. Preserve dash cases
|
|
12
|
+
// that cannot become camel case, so distinct ids cannot alias the same tool.
|
|
13
|
+
function toolName(workflowId: string): string {
|
|
14
|
+
const camel = workflowId.replace(
|
|
15
|
+
/-([a-z])|-/g,
|
|
16
|
+
(_, letter: string | undefined) =>
|
|
17
|
+
letter === undefined ? "_" : letter.toUpperCase(),
|
|
18
|
+
);
|
|
19
|
+
return /^[0-9]/.test(camel) ? `Workflow_${camel}` : camel;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A Workflow's own declared App roles: `{ alias: { appId, role? } }`. */
|
|
23
|
+
const appRoleSchema = z.record(
|
|
24
|
+
z.string(),
|
|
25
|
+
z.object({ appId: z.string(), role: z.string().optional() }),
|
|
26
|
+
);
|
|
27
|
+
type InstalledApp = Readonly<{ installationId: string; appId: string }>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve each declared App role to exactly one installed App: the same
|
|
31
|
+
* unambiguous match the Run dialog's own default picks for a person. A role
|
|
32
|
+
* with zero or more than one installed candidate has no single correct
|
|
33
|
+
* answer without a person choosing, so the caller withholds the whole row
|
|
34
|
+
* rather than guessing a binding.
|
|
35
|
+
*/
|
|
36
|
+
function resolveBindings(
|
|
37
|
+
apps: z.output<typeof appRoleSchema>,
|
|
38
|
+
installed: readonly InstalledApp[],
|
|
39
|
+
): Record<string, string> | null {
|
|
40
|
+
const bindings: Record<string, string> = {};
|
|
41
|
+
for (const [alias, spec] of Object.entries(apps)) {
|
|
42
|
+
const matches = installed.filter((app) => app.appId === spec.appId);
|
|
43
|
+
const [match] = matches;
|
|
44
|
+
if (matches.length !== 1 || match === undefined) return null;
|
|
45
|
+
bindings[alias] = match.installationId;
|
|
46
|
+
}
|
|
47
|
+
return bindings;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function listTools(
|
|
51
|
+
indexes: WorkflowIndexes | undefined,
|
|
52
|
+
workflows: WorkflowMethodContext["env"]["WORKFLOWS"],
|
|
53
|
+
): Promise<z.output<typeof workflowToolListing>> {
|
|
54
|
+
const library = indexes?.libraryList();
|
|
55
|
+
if (!indexes || library?.kind !== "ready") return { kind: "unindexed" };
|
|
56
|
+
// Read the Team's installed Apps once for every row, not once per row.
|
|
57
|
+
const catalog = workflows
|
|
58
|
+
? catalogReplySchema.parse(await workflows.catalog({}))
|
|
59
|
+
: { error: "not_granted" as const };
|
|
60
|
+
const installed: readonly InstalledApp[] =
|
|
61
|
+
"apps" in catalog
|
|
62
|
+
? catalog.apps.map((app) => ({
|
|
63
|
+
installationId: app.installationId,
|
|
64
|
+
appId: app.appId,
|
|
65
|
+
}))
|
|
66
|
+
: [];
|
|
67
|
+
const tools: unknown[] = [];
|
|
68
|
+
for (const item of [...library.value.items].sort((a, b) =>
|
|
69
|
+
a.workflowId.localeCompare(b.workflowId),
|
|
70
|
+
)) {
|
|
71
|
+
const published = item.published;
|
|
72
|
+
if (published?.version == null) continue;
|
|
73
|
+
// The immutable publication row written at publish; no Git is read here.
|
|
74
|
+
const row = indexes.publications.read(
|
|
75
|
+
`${item.workflowId}:v${String(published.version)}`,
|
|
76
|
+
);
|
|
77
|
+
if (
|
|
78
|
+
row.kind !== "ready" ||
|
|
79
|
+
row.value.workflowId !== item.workflowId ||
|
|
80
|
+
row.value.version !== published.version ||
|
|
81
|
+
row.value.commit !== published.commit
|
|
82
|
+
)
|
|
83
|
+
return { kind: "unindexed" };
|
|
84
|
+
const definition = row.value.definition;
|
|
85
|
+
const apps = appRoleSchema.safeParse(definition.apps);
|
|
86
|
+
if (!apps.success) continue;
|
|
87
|
+
const bindings = resolveBindings(apps.data, installed);
|
|
88
|
+
// No single correct binding today (a role is unmet or ambiguous): never
|
|
89
|
+
// guess one, so withhold this one Workflow's tool, not the whole family.
|
|
90
|
+
if (bindings === null) continue;
|
|
91
|
+
tools.push({
|
|
92
|
+
workflowId: item.workflowId,
|
|
93
|
+
tool: toolName(item.workflowId),
|
|
94
|
+
title: definition.title,
|
|
95
|
+
description: definition.description ?? "",
|
|
96
|
+
version: published.version,
|
|
97
|
+
commit: published.commit,
|
|
98
|
+
input: definition.input,
|
|
99
|
+
output,
|
|
100
|
+
// The exact extra arguments a generic tool-family host merges with the
|
|
101
|
+
// caller's own `input` when it calls `runWorkflow`; see tool-contracts.
|
|
102
|
+
call: {
|
|
103
|
+
workflowId: item.workflowId,
|
|
104
|
+
version: published.version,
|
|
105
|
+
bindings,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return workflowToolListing.parse({ kind: "ready", tools });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const toolFamilyMethods = {
|
|
113
|
+
listWorkflowTools: defineAppMethod({
|
|
114
|
+
description:
|
|
115
|
+
"List typed tools from saved published Workflow versions; drafts are excluded.",
|
|
116
|
+
hidden: true,
|
|
117
|
+
effect: "read",
|
|
118
|
+
requires: ["workflow.run"],
|
|
119
|
+
input: z.strictObject({}),
|
|
120
|
+
output: workflowToolListing,
|
|
121
|
+
handler: (_input, context: WorkflowMethodContext) =>
|
|
122
|
+
listTools(context.indexes, context.env.WORKFLOWS),
|
|
123
|
+
}),
|
|
124
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"types": ["@cloudflare/workers-types"],
|
|
9
|
+
"lib": ["ES2022"],
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"noUncheckedIndexedAccess": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["*.ts", "../host"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as S from "effect/Schema";
|
|
2
|
+
import { catalogSchema } from "@fungi.computer/braid";
|
|
3
|
+
import { catalogReplySchema } from "@fungi.computer/workflows-wire";
|
|
4
|
+
export { catalogReplySchema } from "@fungi.computer/workflows-wire";
|
|
5
|
+
/** The unique App ids a definition's node bindings actually name. */
|
|
6
|
+
export function definitionAppIds(definition) {
|
|
7
|
+
return [...new Set(Object.values(definition.apps).map((app) => app.appId))];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The host selects member or lifecycle authority; the App never invents
|
|
11
|
+
* grants. Reads the palette index once to resolve installations, then reads
|
|
12
|
+
* each named App's full ports only -- never the whole installed set -- so a
|
|
13
|
+
* large installed-App count never grows this reply with ports no caller asked
|
|
14
|
+
* for.
|
|
15
|
+
*/
|
|
16
|
+
export async function readWorkflowCatalog(workflows, appIds) {
|
|
17
|
+
const needed = new Set(appIds);
|
|
18
|
+
// No App is named: skip the read entirely, not just the port detail --
|
|
19
|
+
// never requiring the WORKFLOWS binding just to save an App-free draft.
|
|
20
|
+
if (needed.size === 0)
|
|
21
|
+
return {};
|
|
22
|
+
if (!workflows)
|
|
23
|
+
return null;
|
|
24
|
+
const index = catalogReplySchema.parse(await workflows.catalog({}));
|
|
25
|
+
if (!("apps" in index))
|
|
26
|
+
return null;
|
|
27
|
+
const matches = index.apps.filter((app) => needed.has(app.appId));
|
|
28
|
+
const entries = await Promise.all(matches.map(async (match) => {
|
|
29
|
+
const full = catalogReplySchema.parse(await workflows.catalog({ installationId: match.installationId }));
|
|
30
|
+
return "app" in full ? full.app : null;
|
|
31
|
+
}));
|
|
32
|
+
return S.decodeUnknownSync(catalogSchema)(Object.fromEntries(entries.flatMap((app) => app === null
|
|
33
|
+
? []
|
|
34
|
+
: [
|
|
35
|
+
[
|
|
36
|
+
app.appId,
|
|
37
|
+
app.events === undefined
|
|
38
|
+
? { methods: app.methods }
|
|
39
|
+
: { methods: app.methods, events: app.events },
|
|
40
|
+
],
|
|
41
|
+
])), { onExcessProperty: "error" });
|
|
42
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineAppGill } from "@fungi.computer/agar/gills";
|
|
2
|
+
import { libraryListing } from "@fungi.computer/workflows-wire";
|
|
3
|
+
/** The installation's saved library supplies one page, without a Git rebuild. */
|
|
4
|
+
export const workflowLibraryGill = defineAppGill({
|
|
5
|
+
id: "workflow-library",
|
|
6
|
+
title: "Workflow Library",
|
|
7
|
+
source: "listWorkflows",
|
|
8
|
+
limit: 20,
|
|
9
|
+
sizes: ["M", "L"],
|
|
10
|
+
input: ({ limit, after }) => after === undefined ? { limit } : { limit, after },
|
|
11
|
+
rows: (output) => {
|
|
12
|
+
const identity = { viewId: "workflow-library", title: "Workflow Library" };
|
|
13
|
+
const page = libraryListing.parse(output);
|
|
14
|
+
if ("kind" in page)
|
|
15
|
+
return {
|
|
16
|
+
...identity,
|
|
17
|
+
state: "error",
|
|
18
|
+
rows: [],
|
|
19
|
+
error: "The saved Workflow library isn't ready. Open Trellis to update it.",
|
|
20
|
+
recovery: { kind: "open" },
|
|
21
|
+
};
|
|
22
|
+
if (page.items.length === 0)
|
|
23
|
+
return { ...identity, state: "empty", rows: [] };
|
|
24
|
+
const rows = page.items.map((item) => {
|
|
25
|
+
const row = {
|
|
26
|
+
id: item.workflowId,
|
|
27
|
+
title: item.title.trim() || "Untitled Workflow",
|
|
28
|
+
status: item.published === null
|
|
29
|
+
? {
|
|
30
|
+
label: item.draftRevision === null ? "Not published" : "Draft",
|
|
31
|
+
tone: "neutral",
|
|
32
|
+
}
|
|
33
|
+
: {
|
|
34
|
+
label: item.published.version === null
|
|
35
|
+
? "Published"
|
|
36
|
+
: `Published v${String(item.published.version)}`,
|
|
37
|
+
tone: "success",
|
|
38
|
+
},
|
|
39
|
+
open: { params: [{ key: "workflow", value: item.workflowId }] },
|
|
40
|
+
};
|
|
41
|
+
return item.draftRevision === null
|
|
42
|
+
? row
|
|
43
|
+
: { ...row, detail: "Draft available" };
|
|
44
|
+
});
|
|
45
|
+
return page.next === undefined
|
|
46
|
+
? { ...identity, state: "full", rows }
|
|
47
|
+
: { ...identity, state: "full", rows, next: page.next };
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,294 @@
|
|
|
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 { document, publishedDocument, draftDocument, version, listings, } from "@fungi.computer/workflows-wire";
|
|
10
|
+
const issueSchema = z.object({
|
|
11
|
+
code: z.string(),
|
|
12
|
+
message: z.string(),
|
|
13
|
+
graph: z.string().optional(),
|
|
14
|
+
node: z.string().optional(),
|
|
15
|
+
trigger: z.string().optional(),
|
|
16
|
+
port: z.string().optional(),
|
|
17
|
+
edge: z.number().optional(),
|
|
18
|
+
});
|
|
19
|
+
const pushRowSchema = z.strictObject({
|
|
20
|
+
eventId: z.string(),
|
|
21
|
+
commit: z.string(),
|
|
22
|
+
workflowId: z.string(),
|
|
23
|
+
issues: z.string(),
|
|
24
|
+
});
|
|
25
|
+
const copiedDocument = document.transform((raw) => ({
|
|
26
|
+
...raw,
|
|
27
|
+
definition: Schema.decodeUnknownSync(workflowDefinitionSchema)(raw.definition),
|
|
28
|
+
}));
|
|
29
|
+
const librarySnapshotSchema = z.strictObject({
|
|
30
|
+
refs: z.record(z.string(), gitSha1Schema),
|
|
31
|
+
items: listings,
|
|
32
|
+
});
|
|
33
|
+
export function createWorkflowIndexes(storage) {
|
|
34
|
+
const { sql } = storage;
|
|
35
|
+
sql.exec("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)");
|
|
36
|
+
const draftRow = z.strictObject({
|
|
37
|
+
revision: z.number().int().positive(),
|
|
38
|
+
body: z.string(),
|
|
39
|
+
});
|
|
40
|
+
const decodeDraft = (raw) => {
|
|
41
|
+
const row = draftRow.parse(raw);
|
|
42
|
+
const value = draftDocument.parse({
|
|
43
|
+
...z.record(z.string(), z.json()).parse(JSON.parse(row.body)),
|
|
44
|
+
revision: row.revision,
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
...value,
|
|
48
|
+
definition: Schema.decodeUnknownSync(workflowDefinitionSchema)(value.definition),
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
function readDraft(workflowId) {
|
|
52
|
+
workflowIdSchema.parse(workflowId);
|
|
53
|
+
const raw = sql
|
|
54
|
+
.exec("SELECT revision, body FROM workflow_drafts WHERE workflow_id = ?", workflowId)
|
|
55
|
+
.toArray()[0];
|
|
56
|
+
return raw === undefined ? null : decodeDraft(raw);
|
|
57
|
+
}
|
|
58
|
+
sql.exec('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))');
|
|
59
|
+
const copies = createReadCopies(sql, {
|
|
60
|
+
version: 2,
|
|
61
|
+
maxRows: 4096,
|
|
62
|
+
maxValueBytes: 1048576,
|
|
63
|
+
maxBlobBytes: 1048576,
|
|
64
|
+
maxBlobTotalBytes: 8388608,
|
|
65
|
+
});
|
|
66
|
+
const library = copies.open("workflow.library", librarySnapshotSchema);
|
|
67
|
+
const generation = copies.open("workflow.revision", z.number().int().nonnegative());
|
|
68
|
+
const documents = copies.open("workflow.document", copiedDocument.nullable());
|
|
69
|
+
const publications = copies.open("workflow.publication", publishedDocument.transform((raw) => ({
|
|
70
|
+
...raw,
|
|
71
|
+
definition: Schema.decodeUnknownSync(workflowDefinitionSchema)(raw.definition),
|
|
72
|
+
})));
|
|
73
|
+
const versionCopies = copies.open("workflow.versions", z.array(version).max(1000));
|
|
74
|
+
const rebuildingRow = z.strictObject({
|
|
75
|
+
workflowId: z.string(),
|
|
76
|
+
main: copiedDocument.nullable(),
|
|
77
|
+
versions: z.array(version).max(1000),
|
|
78
|
+
versionOffset: z.number().int().nonnegative(),
|
|
79
|
+
});
|
|
80
|
+
const frontier = copies.open("workflow.frontier", z.strictObject({
|
|
81
|
+
epoch: z.number().int().nonnegative(),
|
|
82
|
+
refs: z.array(z.strictObject({ name: z.string(), hash: gitSha1Schema })),
|
|
83
|
+
ids: z.array(z.string()).max(100),
|
|
84
|
+
offset: z.number().int().nonnegative(),
|
|
85
|
+
items: listings,
|
|
86
|
+
current: rebuildingRow.nullable(),
|
|
87
|
+
queue: z.array(rebuildingRow).max(4),
|
|
88
|
+
}));
|
|
89
|
+
function mergeDraftListings(items) {
|
|
90
|
+
const saved = new Map(items.map((item) => [item.workflowId, item]));
|
|
91
|
+
for (const raw of sql
|
|
92
|
+
.exec("SELECT workflow_id, revision, body FROM workflow_drafts ORDER BY workflow_id LIMIT 100")
|
|
93
|
+
.toArray()) {
|
|
94
|
+
const row = z
|
|
95
|
+
.object({
|
|
96
|
+
workflow_id: workflowIdSchema,
|
|
97
|
+
revision: z.number().int().positive(),
|
|
98
|
+
body: z.string(),
|
|
99
|
+
})
|
|
100
|
+
.parse(raw);
|
|
101
|
+
const draft = decodeDraft({ revision: row.revision, body: row.body });
|
|
102
|
+
const previous = saved.get(row.workflow_id);
|
|
103
|
+
saved.set(row.workflow_id, {
|
|
104
|
+
workflowId: row.workflow_id,
|
|
105
|
+
title: draft.definition.title,
|
|
106
|
+
draftRevision: row.revision,
|
|
107
|
+
published: previous?.published ?? null,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return [...saved.values()]
|
|
111
|
+
.sort((a, b) => a.workflowId.localeCompare(b.workflowId))
|
|
112
|
+
.slice(0, 100);
|
|
113
|
+
}
|
|
114
|
+
function librarySnapshot() {
|
|
115
|
+
const saved = library.read("all");
|
|
116
|
+
const epoch = generation.read("all");
|
|
117
|
+
return {
|
|
118
|
+
revision: epoch.kind === "ready" ? epoch.value : 0,
|
|
119
|
+
snapshot: saved.kind === "ready" ? saved.value : null,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const indexes = {
|
|
123
|
+
drafts: {
|
|
124
|
+
read: readDraft,
|
|
125
|
+
save(workflowId, body, expectedRevision) {
|
|
126
|
+
workflowIdSchema.parse(workflowId);
|
|
127
|
+
return storage.transactionSync(() => {
|
|
128
|
+
const current = readDraft(workflowId);
|
|
129
|
+
if ((current?.revision ?? 0) !== expectedRevision)
|
|
130
|
+
return { kind: "conflict", current };
|
|
131
|
+
const revision = expectedRevision + 1;
|
|
132
|
+
const saved = draftDocument.parse({ ...body, revision });
|
|
133
|
+
const { revision: savedRevision, ...content } = saved;
|
|
134
|
+
sql.exec("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", workflowId, savedRevision, JSON.stringify(content), Date.now());
|
|
135
|
+
const currentLibrary = librarySnapshot();
|
|
136
|
+
const previous = currentLibrary.snapshot?.items.find((item) => item.workflowId === workflowId);
|
|
137
|
+
library.write("all", currentLibrary.snapshot?.refs["refs/heads/main"] ?? null, {
|
|
138
|
+
refs: currentLibrary.snapshot?.refs ?? {},
|
|
139
|
+
items: [
|
|
140
|
+
...(currentLibrary.snapshot?.items ?? []).filter((item) => item.workflowId !== workflowId),
|
|
141
|
+
{
|
|
142
|
+
workflowId,
|
|
143
|
+
title: Schema.decodeUnknownSync(workflowDefinitionSchema)(content.definition).title,
|
|
144
|
+
draftRevision: revision,
|
|
145
|
+
published: previous?.published ?? null,
|
|
146
|
+
},
|
|
147
|
+
]
|
|
148
|
+
.sort((a, b) => a.workflowId.localeCompare(b.workflowId))
|
|
149
|
+
.slice(0, 100),
|
|
150
|
+
});
|
|
151
|
+
return { kind: "saved", revision };
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
libraryList: () => library.read("all"),
|
|
156
|
+
documents,
|
|
157
|
+
versionCopies,
|
|
158
|
+
publications,
|
|
159
|
+
recordPublication(value, epoch) {
|
|
160
|
+
storage.transactionSync(() => {
|
|
161
|
+
const { workflowId, version: number, commit } = value;
|
|
162
|
+
const saved = {
|
|
163
|
+
definition: value.definition,
|
|
164
|
+
layout: value.layout,
|
|
165
|
+
commit,
|
|
166
|
+
};
|
|
167
|
+
if (value.provenance !== undefined)
|
|
168
|
+
saved.provenance = value.provenance;
|
|
169
|
+
publications.write(`${workflowId}:v${String(number)}`, commit, value);
|
|
170
|
+
publications.write(`${workflowId}:${commit}`, commit, value);
|
|
171
|
+
documents.write(`main:${workflowId}`, commit, saved);
|
|
172
|
+
documents.write(`at:${workflowId}:${commit}`, commit, saved);
|
|
173
|
+
const previous = versionCopies.read(workflowId);
|
|
174
|
+
versionCopies.write(workflowId, commit, [
|
|
175
|
+
{
|
|
176
|
+
version: number,
|
|
177
|
+
commit,
|
|
178
|
+
committedAt: Math.floor(Date.now() / 1000),
|
|
179
|
+
message: `Publish ${workflowId}`,
|
|
180
|
+
},
|
|
181
|
+
...(previous.kind === "ready" ? previous.value : []),
|
|
182
|
+
].slice(0, 1000));
|
|
183
|
+
const current = librarySnapshot();
|
|
184
|
+
if (current.revision === epoch && current.snapshot)
|
|
185
|
+
library.write("all", commit, {
|
|
186
|
+
refs: {
|
|
187
|
+
...current.snapshot.refs,
|
|
188
|
+
"refs/heads/main": commit,
|
|
189
|
+
[`refs/tags/${workflowId}@v${String(number)}`]: commit,
|
|
190
|
+
},
|
|
191
|
+
items: current.snapshot.items.map((item) => item.workflowId === workflowId
|
|
192
|
+
? { ...item, published: { commit, version: number } }
|
|
193
|
+
: item),
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
recordBackgroundPublication(value, epoch, references) {
|
|
198
|
+
storage.transactionSync(() => {
|
|
199
|
+
const { workflowId, version: number, commit } = value;
|
|
200
|
+
const saved = { definition: value.definition, layout: value.layout, commit };
|
|
201
|
+
if (value.provenance !== undefined)
|
|
202
|
+
saved.provenance = value.provenance;
|
|
203
|
+
publications.write(`${workflowId}:v${String(number)}`, commit, value);
|
|
204
|
+
publications.write(`${workflowId}:${commit}`, commit, value);
|
|
205
|
+
documents.write(`at:${workflowId}:${commit}`, commit, saved);
|
|
206
|
+
const previous = versionCopies.read(workflowId);
|
|
207
|
+
const retained = previous.kind === "ready" ? previous.value : [];
|
|
208
|
+
const next = [
|
|
209
|
+
{
|
|
210
|
+
version: number,
|
|
211
|
+
commit,
|
|
212
|
+
committedAt: Math.floor(Date.now() / 1000),
|
|
213
|
+
message: `Publish ${workflowId}`,
|
|
214
|
+
},
|
|
215
|
+
...retained.filter((version) => version.version !== number),
|
|
216
|
+
]
|
|
217
|
+
.sort((a, b) => b.version - a.version)
|
|
218
|
+
.slice(0, 1000);
|
|
219
|
+
versionCopies.write(workflowId, next[0]?.commit ?? commit, next);
|
|
220
|
+
const main = references.find((ref) => ref.name === "refs/heads/main")?.hash ??
|
|
221
|
+
null;
|
|
222
|
+
const copiedMain = documents.read(`main:${workflowId}`);
|
|
223
|
+
if (main === commit && (retained[0]?.version ?? 0) <= number)
|
|
224
|
+
documents.write(`main:${workflowId}`, commit, saved);
|
|
225
|
+
else if (copiedMain.kind === "ready" && copiedMain.head !== main)
|
|
226
|
+
documents.stale(`main:${workflowId}`);
|
|
227
|
+
const current = librarySnapshot();
|
|
228
|
+
if (current.revision === epoch && current.snapshot) {
|
|
229
|
+
const latest = versionRefs(workflowId, references)[0];
|
|
230
|
+
library.write("all", main, {
|
|
231
|
+
refs: Object.fromEntries(references.map((ref) => [ref.name, ref.hash])),
|
|
232
|
+
items: current.snapshot.items.map((item) => {
|
|
233
|
+
if (item.workflowId !== workflowId ||
|
|
234
|
+
!latest ||
|
|
235
|
+
(item.published?.version ?? 0) > latest.version)
|
|
236
|
+
return item;
|
|
237
|
+
return {
|
|
238
|
+
...item,
|
|
239
|
+
published: { commit: latest.commit, version: latest.version },
|
|
240
|
+
};
|
|
241
|
+
}),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
frontier,
|
|
247
|
+
librarySnapshot,
|
|
248
|
+
recordLibrary(revision, snapshot) {
|
|
249
|
+
if (librarySnapshot().revision === revision)
|
|
250
|
+
library.write("all", snapshot.refs["refs/heads/main"] ?? null, {
|
|
251
|
+
...snapshot,
|
|
252
|
+
items: mergeDraftListings(snapshot.items),
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
invalidateLibrary(updates) {
|
|
256
|
+
const current = librarySnapshot();
|
|
257
|
+
if (updates &&
|
|
258
|
+
current.snapshot &&
|
|
259
|
+
updates.every(({ ref, after }) => (current.snapshot?.refs[ref] ?? "0".repeat(40)) === after))
|
|
260
|
+
return;
|
|
261
|
+
generation.write("all", null, current.revision + 1);
|
|
262
|
+
library.stale("all");
|
|
263
|
+
},
|
|
264
|
+
recordIssues(eventId, commit, workflowId, issues) {
|
|
265
|
+
sql.exec('INSERT INTO workflow_push_issues (eventId, "commit", workflowId, issues) VALUES (?, ?, ?, ?) ON CONFLICT(eventId, "commit", workflowId) DO NOTHING', eventId, commit, workflowId, JSON.stringify(issues));
|
|
266
|
+
},
|
|
267
|
+
issues(eventId) {
|
|
268
|
+
return sql
|
|
269
|
+
.exec('SELECT eventId, "commit", workflowId, issues FROM workflow_push_issues WHERE eventId = ? ORDER BY workflowId, "commit"', eventId)
|
|
270
|
+
.toArray()
|
|
271
|
+
.map((raw) => {
|
|
272
|
+
const row = pushRowSchema.parse(raw);
|
|
273
|
+
return {
|
|
274
|
+
...row,
|
|
275
|
+
issues: z.array(issueSchema).parse(JSON.parse(row.issues)),
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
const publicationJobs = createPublicationJobs(storage, {
|
|
281
|
+
readDraft: (workflowId) => {
|
|
282
|
+
const draft = readDraft(workflowId);
|
|
283
|
+
return draft === null ? null : draftDocument.parse(draft);
|
|
284
|
+
},
|
|
285
|
+
revision: () => librarySnapshot().revision,
|
|
286
|
+
publish: (value, epoch, refs) => {
|
|
287
|
+
indexes.recordBackgroundPublication({
|
|
288
|
+
...value,
|
|
289
|
+
definition: Schema.decodeUnknownSync(workflowDefinitionSchema)(value.definition),
|
|
290
|
+
}, epoch, refs);
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
return { ...indexes, publicationJobs };
|
|
294
|
+
}
|