@sanoma-ai/workflows 0.7.1 → 0.8.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 +34 -7
- package/dist/apps-list.d.ts +90 -0
- package/dist/apps-list.js +109 -0
- package/dist/apps-list.js.map +1 -0
- package/dist/apps.d.ts +42 -0
- package/dist/apps.js +137 -0
- package/dist/apps.js.map +1 -0
- package/dist/call-workflow.js +5 -6
- package/dist/call-workflow.js.map +1 -1
- package/dist/call.js +6 -3
- package/dist/call.js.map +1 -1
- package/dist/cedar.d.ts +1 -1
- package/dist/cedar.js +6 -4
- package/dist/cedar.js.map +1 -1
- package/dist/change-steps.d.ts +10 -0
- package/dist/change-steps.js +204 -0
- package/dist/change-steps.js.map +1 -0
- package/dist/change-workflow.d.ts +54 -0
- package/dist/change-workflow.js +161 -0
- package/dist/change-workflow.js.map +1 -0
- package/dist/change.d.ts +121 -24
- package/dist/change.js +196 -17
- package/dist/change.js.map +1 -1
- package/dist/claims.d.ts +58 -0
- package/dist/claims.js +27 -0
- package/dist/claims.js.map +1 -0
- package/dist/client.d.ts +92 -0
- package/dist/client.js +268 -6
- package/dist/client.js.map +1 -1
- package/dist/collection.d.ts +6 -1
- package/dist/collection.js +11 -3
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +25 -2
- package/dist/config.js +36 -20
- package/dist/config.js.map +1 -1
- package/dist/db.d.ts +12 -0
- package/dist/db.js +15 -0
- package/dist/db.js.map +1 -1
- package/dist/deadlines.d.ts +5 -2
- package/dist/deadlines.js.map +1 -1
- package/dist/define.d.ts +30 -14
- package/dist/define.js +9 -1
- package/dist/define.js.map +1 -1
- package/dist/demo.d.ts +15 -4
- package/dist/demo.js +27 -12
- package/dist/demo.js.map +1 -1
- package/dist/describe.d.ts +16 -2
- package/dist/describe.js +39 -4
- package/dist/describe.js.map +1 -1
- package/dist/direct.d.ts +4 -3
- package/dist/direct.js +7 -4
- package/dist/direct.js.map +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/lint.js +19 -5
- package/dist/lint.js.map +1 -1
- package/dist/outline.js +13 -6
- package/dist/outline.js.map +1 -1
- package/dist/paths.d.ts +16 -0
- package/dist/paths.js +54 -2
- package/dist/paths.js.map +1 -1
- package/dist/resources.d.ts +7 -2
- package/dist/resources.js +23 -8
- package/dist/resources.js.map +1 -1
- package/dist/run.d.ts +7 -0
- package/dist/run.js.map +1 -1
- package/dist/scenario.d.ts +2 -2
- package/dist/scenario.js +10 -4
- package/dist/scenario.js.map +1 -1
- package/dist/scenarios.d.ts +1 -1
- package/dist/scenarios.js.map +1 -1
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +19 -5
- package/dist/sdk.js.map +1 -1
- package/dist/shared.d.ts +43 -2
- package/dist/shared.js +46 -4
- package/dist/shared.js.map +1 -1
- package/dist/sites.d.ts +26 -5
- package/dist/sites.js +56 -20
- package/dist/sites.js.map +1 -1
- package/dist/source.d.ts +55 -5
- package/dist/source.js +4 -0
- package/dist/source.js.map +1 -1
- package/dist/version.d.ts +8 -0
- package/dist/version.js +21 -13
- package/dist/version.js.map +1 -1
- package/dist/worker.js +50 -3
- package/dist/worker.js.map +1 -1
- package/oxlint.json +2 -2
- package/package.json +1 -1
- package/src/apps-list.ts +119 -0
- package/src/apps.ts +162 -0
- package/src/call-workflow.ts +10 -2
- package/src/call.ts +6 -3
- package/src/cedar.ts +9 -7
- package/src/change-steps.ts +274 -0
- package/src/change-workflow.ts +198 -0
- package/src/change.ts +257 -27
- package/src/claims.ts +79 -0
- package/src/client.ts +320 -4
- package/src/collection.ts +22 -3
- package/src/config.ts +60 -22
- package/src/db.ts +23 -0
- package/src/deadlines.ts +5 -2
- package/src/define.ts +31 -12
- package/src/demo.ts +55 -24
- package/src/describe.ts +67 -4
- package/src/direct.ts +9 -5
- package/src/errors.ts +3 -0
- package/src/index.ts +14 -0
- package/src/lint.ts +20 -5
- package/src/outline.ts +15 -6
- package/src/paths.ts +57 -2
- package/src/resources.ts +27 -9
- package/src/run.ts +5 -0
- package/src/scenario.ts +14 -4
- package/src/scenarios.ts +1 -1
- package/src/sdk.ts +21 -7
- package/src/shared.ts +60 -5
- package/src/sites.ts +70 -24
- package/src/source.ts +50 -5
- package/src/version.ts +25 -16
- package/src/worker.ts +46 -3
package/src/source.ts
CHANGED
|
@@ -28,8 +28,9 @@ export interface Source {
|
|
|
28
28
|
/** Pushes `ref` to the repository at `url`, signed in with `token` (a draft by its `branch`). */
|
|
29
29
|
push?(target: { url: string; token: string; ref: string }): void | Promise<void>;
|
|
30
30
|
/**
|
|
31
|
-
* The draft `id` (letters, digits, `_` and `-`), opened
|
|
32
|
-
* first time, then the same one, edits and all, until its branch is
|
|
31
|
+
* The draft `id` (letters, digits, `_` and `-`), opened on its branch (`draftBranch`): made from
|
|
32
|
+
* the checked-out revision the first time, then the same one, edits and all, until its branch is
|
|
33
|
+
* deleted.
|
|
33
34
|
*/
|
|
34
35
|
draft?(id: string): Promise<Draft>;
|
|
35
36
|
/** The open drafts' ids, sorted. */
|
|
@@ -47,13 +48,27 @@ export interface Source {
|
|
|
47
48
|
/** The URL of the repository this one was cloned from (`origin`); undefined when it has none. */
|
|
48
49
|
remoteUrl?(): Promise<string | undefined>;
|
|
49
50
|
/**
|
|
50
|
-
* Fast-forwards the
|
|
51
|
-
* `
|
|
52
|
-
* `
|
|
51
|
+
* Fast-forwards the branch `ref` to `ref` in the repository at `url`, signed in with `token`, as
|
|
52
|
+
* `push` is, where it is checked out: the root's checked-out branch, or an open draft's (by its
|
|
53
|
+
* `branch`), whose new commits on the host it takes so. Only when nothing is uncommitted there
|
|
54
|
+
* and the remote's `ref` contains it; otherwise `skipped`, saying why. The revision after.
|
|
53
55
|
*/
|
|
54
56
|
follow?(target: { url: string; token: string; ref: string }): Promise<{ revision: string } | { skipped: string }>;
|
|
57
|
+
/**
|
|
58
|
+
* How many commits the source repository of the app `folder` (the URL its `apps/<folder>/app.ts`
|
|
59
|
+
* names) has on its default branch that the checked-out one lacks, of those changing what an
|
|
60
|
+
* update merges (`apps/<folder>/` and `package.json`): fetched now. An error when the app names
|
|
61
|
+
* no source.
|
|
62
|
+
*/
|
|
63
|
+
behind?(folder: string): Promise<number>;
|
|
55
64
|
}
|
|
56
65
|
|
|
66
|
+
/** The branch of the draft `id`, as a source names it: `draft/<id>`. */
|
|
67
|
+
export const draftBranch = (id: string) => `draft/${id}`;
|
|
68
|
+
|
|
69
|
+
/** The draft whose branch `branch` is (`draftBranch`), or undefined for a branch that is no draft's. */
|
|
70
|
+
export const draftOf = (branch: string) => (branch.startsWith("draft/") ? branch.slice("draft/".length) : undefined);
|
|
71
|
+
|
|
57
72
|
/**
|
|
58
73
|
* A draft: a checkout of its own, at `root`, on `branch`, which `commit` records in as
|
|
59
74
|
* `Source.commit` does in its root. Its `node_modules` may be the root's own install, linked in:
|
|
@@ -63,6 +78,36 @@ export interface Draft {
|
|
|
63
78
|
branch: string;
|
|
64
79
|
root: string;
|
|
65
80
|
commit: Source["commit"];
|
|
81
|
+
/**
|
|
82
|
+
* Takes an app into the draft: fetches `ref` (the default branch by default) of the repository
|
|
83
|
+
* at `url`, a source (a public company repo with one `apps/<folder>/`), and merges it in with its
|
|
84
|
+
* history (`--allow-unrelated-histories`), as one merge commit by `author`. What it merges is
|
|
85
|
+
* `apps/<folder>/` and `package.json`; every other file stays the draft's. `folder` may be left
|
|
86
|
+
* out when the source has one app. Refused when the draft has `apps/<folder>/` already.
|
|
87
|
+
*/
|
|
88
|
+
take?(app: { url: string; ref?: string; folder?: string; author: string }): Promise<AppMerge>;
|
|
89
|
+
/**
|
|
90
|
+
* Merges the new commits of the app `folder`'s source (the URL its `app.ts` names, its default
|
|
91
|
+
* branch) into the draft, by `author`, as `take` merges: `apps/<folder>/` and `package.json`.
|
|
92
|
+
*/
|
|
93
|
+
update?(app: { folder: string; author: string }): Promise<AppMerge>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* What taking or updating an app merged into a draft. A line both sides changed is committed with
|
|
98
|
+
* git's markers in its file (ours, the merge base and theirs), so the draft's checks fail until the
|
|
99
|
+
* agent resolves it; a file one side deleted stays as the draft had it. The merge commit keeps the
|
|
100
|
+
* source's history either way.
|
|
101
|
+
*/
|
|
102
|
+
export interface AppMerge {
|
|
103
|
+
/** The app's folder: `apps/<folder>/`. */
|
|
104
|
+
folder: string;
|
|
105
|
+
/** The draft's commit after the merge. */
|
|
106
|
+
revision: string;
|
|
107
|
+
/** The source's commits the merge brought in that change the app's folder or `package.json`; 0 when the draft had them all, and nothing was merged. */
|
|
108
|
+
merged: number;
|
|
109
|
+
/** The files git could not merge, relative to the root, committed with conflict markers; empty when it merged clean. */
|
|
110
|
+
conflicts: string[];
|
|
66
111
|
}
|
|
67
112
|
|
|
68
113
|
/** A draft's change: the files it touches, relative to the root, and git's `--stat` of it. */
|
package/src/version.ts
CHANGED
|
@@ -63,24 +63,33 @@ export function computeVersion({
|
|
|
63
63
|
}): string {
|
|
64
64
|
const named = process.env.DBOS__APPVERSION?.trim();
|
|
65
65
|
if (named) return `${appName}@${named}`;
|
|
66
|
-
const workflows = definitions
|
|
67
|
-
|
|
68
|
-
(wf) =>
|
|
69
|
-
[
|
|
70
|
-
wf.name,
|
|
71
|
-
wf.run.toString(),
|
|
72
|
-
opIdsOf(wf),
|
|
73
|
-
// The input schema is parsed outside steps on replay, so a change to it must change the version.
|
|
74
|
-
inputSchemaOf(wf),
|
|
75
|
-
] as const,
|
|
76
|
-
)
|
|
77
|
-
.toSorted(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
78
|
-
const hash = createHash("sha256")
|
|
79
|
-
.update(JSON.stringify([appName, RUNTIME_VERSION, STEP_LAYOUT, installedDbosVersion(), ...workflows]))
|
|
80
|
-
.digest("hex");
|
|
81
|
-
return `${appName}@${hash}`;
|
|
66
|
+
const workflows = definitions.map(codeOf).toSorted(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
67
|
+
return `${appName}@${sha256([appName, RUNTIME_VERSION, STEP_LAYOUT, installedDbosVersion(), ...workflows])}`;
|
|
82
68
|
}
|
|
83
69
|
|
|
70
|
+
/**
|
|
71
|
+
* A hash of the built-in `change` workflow's own code, which each change run carries (its
|
|
72
|
+
* `changeCode` attribute) so a worker on another version of the app may take it over
|
|
73
|
+
* (`carryChangeRuns` in worker.ts): what `computeVersion` hashes of that one workflow, with
|
|
74
|
+
* `RUNTIME_VERSION`, `STEP_LAYOUT` and the installed DBOS version. Never the company's workflows
|
|
75
|
+
* nor `DBOS__APPVERSION`, so a merge that changes only the company's code leaves it as it is.
|
|
76
|
+
*/
|
|
77
|
+
export function changeCodeOf(change: WorkflowDefinition<any, any>): string {
|
|
78
|
+
return sha256([RUNTIME_VERSION, STEP_LAYOUT, installedDbosVersion(), codeOf(change)]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const sha256 = (parts: unknown[]) => createHash("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
82
|
+
|
|
83
|
+
/** What the hashes read of a workflow: its name, the source of its `run`, its operations and its input schema. */
|
|
84
|
+
const codeOf = (wf: WorkflowDefinition<any, any>) =>
|
|
85
|
+
[
|
|
86
|
+
wf.name,
|
|
87
|
+
wf.run.toString(),
|
|
88
|
+
opIdsOf(wf),
|
|
89
|
+
// The input schema is parsed outside steps on replay, so a change to it must change the version.
|
|
90
|
+
inputSchemaOf(wf),
|
|
91
|
+
] as const;
|
|
92
|
+
|
|
84
93
|
/** The workflow's input schema as the hash sees it: its JSON Schema, or its text when zod can't convert it. */
|
|
85
94
|
function inputSchemaOf(wf: WorkflowDefinition<any, any>): unknown {
|
|
86
95
|
try {
|
package/src/worker.ts
CHANGED
|
@@ -4,19 +4,19 @@ import { buildCtx, decideStart, isInfrastructureError, startHoldOf } from "./cal
|
|
|
4
4
|
import { dbosStatusesOf } from "./client.ts";
|
|
5
5
|
import { declaredWakeup, type ResolvedConfig, resolveConfig, type SanomaConfig } from "./config.ts";
|
|
6
6
|
import { storedSecrets, watchCredentials } from "./credentials.ts";
|
|
7
|
-
import { DBOS_DATABASE_OPTIONS, lazyPool, prepareDatabase } from "./db.ts";
|
|
7
|
+
import { type Db, DBOS_DATABASE_OPTIONS, lazyPool, prepareDatabase, stampVersion } from "./db.ts";
|
|
8
8
|
import { deadlineRegistry, type Deadlines, idle, onOwnQueue, type SelfStop, stopWhenIdle } from "./deadlines.ts";
|
|
9
9
|
import type { WorkflowDefinition } from "./define.ts";
|
|
10
10
|
import { credentialsOf, unconfigured, unconfiguredSummary } from "./env.ts";
|
|
11
11
|
import { errorInfo, parseOrThrow, PolicyDeniedError } from "./errors.ts";
|
|
12
12
|
import { entry, ownedLedger, skipped, write, writeFailure } from "./ledger.ts";
|
|
13
|
-
import { dbosLogger, quietIfSandbox, setLogLevel, warn } from "./log.ts";
|
|
13
|
+
import { dbosLogger, info, quietIfSandbox, setLogLevel, warn } from "./log.ts";
|
|
14
14
|
import { lineStartsOf, readSource } from "./ast.ts";
|
|
15
15
|
import { callsOf, outlineWithSource } from "./outline.ts";
|
|
16
16
|
import { currentRun, type Run, type RunArgs, type WorkerState, type WorkflowOutline } from "./run.ts";
|
|
17
17
|
import { applyScheduled, nextScheduledFire, type ScheduledStarter, scheduledStarter } from "./schedules.ts";
|
|
18
18
|
import type { Source } from "./source.ts";
|
|
19
|
-
import { approverLabel, errorMessage, WORKFLOW_ACTION } from "./shared.ts";
|
|
19
|
+
import { approverLabel, CHANGE_WORKFLOW, errorMessage, WORKFLOW_ACTION } from "./shared.ts";
|
|
20
20
|
|
|
21
21
|
export interface Worker {
|
|
22
22
|
/**
|
|
@@ -144,6 +144,12 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
144
144
|
secrets: storedSecrets(() => secrets.db(), "sanoma"),
|
|
145
145
|
workflows: resolved.workflows,
|
|
146
146
|
outlines: new Map([...resolved.workflows].map(([name, wf]) => [name, outlineOf(wf)])),
|
|
147
|
+
// `registered` is filled below, before any run starts: every name of this config's is registered then.
|
|
148
|
+
startRun: async (name, runId, args) => {
|
|
149
|
+
const fn = registered.get(name) as unknown as ((args: RunArgs) => Promise<unknown>) | undefined;
|
|
150
|
+
if (!fn) throw new Error(`The worker has no workflow "${name}" to start`);
|
|
151
|
+
return DBOS.startWorkflow(fn, { workflowID: runId })(args);
|
|
152
|
+
},
|
|
147
153
|
stopped: false,
|
|
148
154
|
deadlines: deadlineRegistry(),
|
|
149
155
|
};
|
|
@@ -216,6 +222,8 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
216
222
|
);
|
|
217
223
|
}
|
|
218
224
|
}
|
|
225
|
+
// Change runs move to the latest version only: a rollback's worker, unless promoted, leaves them be.
|
|
226
|
+
if (isLatest) await carryChangeRuns(resolved, () => secrets.db());
|
|
219
227
|
await DBOS.registerQueue(resolved.queueName);
|
|
220
228
|
// The schedules are the latest version's: an older worker leaves them as the newer set them.
|
|
221
229
|
if (isLatest && options.schedules !== false) {
|
|
@@ -282,6 +290,41 @@ function outlineOf(wf: WorkflowDefinition<any, any>): WorkflowOutline {
|
|
|
282
290
|
return { file: outline.file, lineStarts: lineStartsOf(source!), calls: callsOf(outline.nodes) };
|
|
283
291
|
}
|
|
284
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Takes over the unfinished change runs of other versions whose change code is this worker's,
|
|
295
|
+
* keeping each run's id, history and unread messages: a change's own deploy restarts the worker on
|
|
296
|
+
* the merged code, a new version, which DBOS would never give the run to. Each is stamped with this
|
|
297
|
+
* version (`stampVersion`), and a PENDING one queued again on the app's queue (`resumeWorkflows`);
|
|
298
|
+
* one stopped between the two is PENDING on this version, which DBOS's recovery takes. Not
|
|
299
|
+
* `rewindWorkflow`, which takes only a cancelled run and deletes its unread messages. It assumes one
|
|
300
|
+
* live worker per database, as a deploy does: an older one still running such a run runs it on
|
|
301
|
+
* until it stops. A failure is warned about, and the worker starts.
|
|
302
|
+
*/
|
|
303
|
+
async function carryChangeRuns({ appName, version, changeCode, queueName }: ResolvedConfig, db: () => Promise<Db>) {
|
|
304
|
+
try {
|
|
305
|
+
const runs = await DBOS.listWorkflows({
|
|
306
|
+
workflowName: CHANGE_WORKFLOW,
|
|
307
|
+
status: ["PENDING", "ENQUEUED"],
|
|
308
|
+
applicationName: appName,
|
|
309
|
+
attributes: { changeCode },
|
|
310
|
+
loadInput: false,
|
|
311
|
+
loadOutput: false,
|
|
312
|
+
});
|
|
313
|
+
const theirs = runs
|
|
314
|
+
.filter((r) => r.applicationVersion && r.applicationVersion !== version)
|
|
315
|
+
.map((r) => r.workflowID);
|
|
316
|
+
if (!theirs.length) return;
|
|
317
|
+
const carried = await stampVersion(await db(), theirs, version);
|
|
318
|
+
const pending = carried.filter((r) => r.pending).map((r) => r.id);
|
|
319
|
+
if (pending.length) await DBOS.resumeWorkflows(pending, { queueName });
|
|
320
|
+
if (carried.length) {
|
|
321
|
+
info(`carried ${carried.length} change run(s) onto version ${version}: ${carried.map((r) => r.id).join(", ")}`);
|
|
322
|
+
}
|
|
323
|
+
} catch (err) {
|
|
324
|
+
warn(`carrying the change runs of other versions onto ${version} failed, so they stay there: ${errorMessage(err)}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
285
328
|
/**
|
|
286
329
|
* Warns about unfinished runs this worker will never pick up: started on another version of
|
|
287
330
|
* the app, or queued on another queue (such as the single "sanoma" queue before queues were
|