@sanoma-ai/workflows 0.11.3 → 0.11.5
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/KMS-SECRETS.md +3 -1
- package/README.md +33 -31
- package/dist/apply-steps.d.ts +4 -2
- package/dist/apply-steps.js +5 -3
- package/dist/apply-steps.js.map +1 -1
- package/dist/apply.d.ts +6 -1
- package/dist/apply.js +55 -20
- package/dist/apply.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +52 -5
- package/dist/config.js.map +1 -1
- package/dist/constructors.d.ts +1 -1
- package/dist/definitions.d.ts +5 -0
- package/dist/definitions.js.map +1 -1
- package/dist/describe.d.ts +1 -1
- package/dist/describe.js +16 -11
- package/dist/describe.js.map +1 -1
- package/dist/drift.d.ts +7 -0
- package/dist/drift.js +15 -3
- package/dist/drift.js.map +1 -1
- package/dist/host-derive.d.ts +145 -0
- package/dist/host-derive.js +403 -0
- package/dist/host-derive.js.map +1 -0
- package/dist/host.d.ts +70 -5
- package/dist/host.js +56 -1
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/kms-secrets.js +24 -4
- package/dist/kms-secrets.js.map +1 -1
- package/dist/resource.d.ts +10 -2
- package/dist/resource.js +19 -4
- package/dist/resource.js.map +1 -1
- package/dist/resources.js +1 -1
- package/dist/resources.js.map +1 -1
- package/dist/sites.d.ts +6 -0
- package/dist/sites.js +28 -4
- package/dist/sites.js.map +1 -1
- package/dist/source.d.ts +12 -5
- package/dist/source.js +32 -13
- package/dist/source.js.map +1 -1
- package/dist/wakeup-type.d.ts +1 -1
- package/dist/wakeup-type.js +1 -1
- package/dist/wakeup-type.js.map +1 -1
- package/package.json +1 -1
- package/src/apply-steps.ts +5 -4
- package/src/apply.ts +87 -24
- package/src/config.ts +59 -5
- package/src/definitions.ts +5 -0
- package/src/describe.ts +23 -10
- package/src/drift.ts +16 -3
- package/src/host-derive.ts +492 -0
- package/src/host.ts +101 -5
- package/src/index.ts +20 -2
- package/src/kms-secrets.ts +34 -5
- package/src/resource.ts +45 -7
- package/src/resources.ts +1 -1
- package/src/sites.ts +33 -3
- package/src/source.ts +35 -12
- package/src/wakeup-type.ts +1 -1
package/src/apply.ts
CHANGED
|
@@ -10,12 +10,13 @@ import {
|
|
|
10
10
|
importsOf,
|
|
11
11
|
observeBy,
|
|
12
12
|
observed,
|
|
13
|
+
refOf,
|
|
13
14
|
type ResolveOp,
|
|
14
15
|
type Revision,
|
|
15
16
|
sourceUrl,
|
|
16
17
|
} from "./drift.ts";
|
|
17
18
|
import { isRunControlError } from "./errors.ts";
|
|
18
|
-
import { type HostOutputs, wakeUrlOf } from "./host.ts";
|
|
19
|
+
import { detaching, type HostOutputs, wakeUrlOf } from "./host.ts";
|
|
19
20
|
import type { Binding, Bindings, Rebind } from "./lockfile.ts";
|
|
20
21
|
import type { Op } from "./op.ts";
|
|
21
22
|
import {
|
|
@@ -72,7 +73,13 @@ export type ApplyInput = z.infer<typeof ApplyInput>;
|
|
|
72
73
|
export type PlanStep = { id: string; vendor: string; type: string; connection?: ConnectionRef } & (
|
|
73
74
|
| { action: "create"; planned: Record<string, unknown>; unknown: string[] }
|
|
74
75
|
| { action: "update" | "replace"; binding: string; fields: DriftField[]; replace: string[]; unknown?: string[] }
|
|
75
|
-
| {
|
|
76
|
+
| {
|
|
77
|
+
action: "bind";
|
|
78
|
+
binding: string;
|
|
79
|
+
source: "found" | "given";
|
|
80
|
+
/** The orphaned lockfile key that held the object found, whose binding the declaration takes over. */
|
|
81
|
+
from?: string;
|
|
82
|
+
}
|
|
76
83
|
| { action: "gone"; binding: string; answer?: PlanAnswer }
|
|
77
84
|
| { action: "orphan"; key: string; binding: string; answer?: PlanAnswer }
|
|
78
85
|
| { action: "none"; binding: string }
|
|
@@ -113,6 +120,8 @@ export interface ApplyReport {
|
|
|
113
120
|
export interface ApplyDeclared extends DriftDeclared {
|
|
114
121
|
bindings: Bindings;
|
|
115
122
|
orphans: (Binding & { key: string })[];
|
|
123
|
+
/** The declarations left out with a problem (a host that derives, which apply cannot give all it needs), which an input may still name. */
|
|
124
|
+
leftOut?: string[];
|
|
116
125
|
rebinds: Rebind[];
|
|
117
126
|
}
|
|
118
127
|
|
|
@@ -133,7 +142,13 @@ export interface ApplySteps {
|
|
|
133
142
|
* another run made since this one read it stays, and commits it with `message`; a sandbox run
|
|
134
143
|
* writes nothing.
|
|
135
144
|
*/
|
|
136
|
-
|
|
145
|
+
/** Writes `key`'s binding (none: removed), and removes `moved`, the key it moved from, in the same commit. */
|
|
146
|
+
write(
|
|
147
|
+
key: string,
|
|
148
|
+
binding: Binding | undefined,
|
|
149
|
+
message: string,
|
|
150
|
+
moved?: string,
|
|
151
|
+
): Promise<{ written: boolean; revision?: string }>;
|
|
137
152
|
}
|
|
138
153
|
|
|
139
154
|
type State = Record<string, unknown>;
|
|
@@ -193,7 +208,7 @@ interface Known {
|
|
|
193
208
|
/** The lockfile's bindings, as the run has changed them so far. */
|
|
194
209
|
bindings: Bindings;
|
|
195
210
|
/** The clone URL and commit a host wanted runs. */
|
|
196
|
-
hosts: ReadonlyMap<string, { source: string; revision: string }>;
|
|
211
|
+
hosts: ReadonlyMap<string, { source: string; revision: string; ref: string }>;
|
|
197
212
|
/** Where each host's wakeup reaches it, as its state says (`wakeUrl`, else `url`). */
|
|
198
213
|
urls: ReadonlyMap<string, string>;
|
|
199
214
|
/**
|
|
@@ -232,11 +247,12 @@ function desiredOf(
|
|
|
232
247
|
const named = holderOf(out, wakeup.inputs.host);
|
|
233
248
|
if (url && named) named.holder[named.key] = url;
|
|
234
249
|
}
|
|
235
|
-
// A host is given its source's clone URL and the commit
|
|
250
|
+
// A host is given its source's clone URL, its ref (the default branch when it names none) and the commit it names now.
|
|
236
251
|
const run = hosts.get(r.id);
|
|
237
252
|
if (host && run) {
|
|
238
253
|
for (const [path, value] of [
|
|
239
254
|
[host.inputs.source, run.source],
|
|
255
|
+
[host.inputs.ref, run.ref],
|
|
240
256
|
[host.revision, run.revision],
|
|
241
257
|
] as const) {
|
|
242
258
|
const place = holderOf(out, path);
|
|
@@ -263,7 +279,7 @@ const cannotChange = (type: Pick<Resource, "vendor" | "type" | "writable"> | und
|
|
|
263
279
|
const unknown = (what: string, list: readonly string[], known: (id: string) => boolean): ResourceProblem[] =>
|
|
264
280
|
list.filter((id) => !known(id)).map((id) => ({ message: `${what}: ${id} is no declared resource` }));
|
|
265
281
|
|
|
266
|
-
/** What the input names that is neither declared nor an orphaned binding. */
|
|
282
|
+
/** What the input names that is neither declared nor an orphaned binding (nor a declaration left out with a problem). */
|
|
267
283
|
function inputProblems(
|
|
268
284
|
input: ApplyInput,
|
|
269
285
|
ids: ReadonlyMap<string, unknown>,
|
|
@@ -352,6 +368,32 @@ function fromPlan(
|
|
|
352
368
|
return { problem: problemOf(r.id, `the vendor plans to ${p.action} it, which apply does not do here`) };
|
|
353
369
|
}
|
|
354
370
|
|
|
371
|
+
/**
|
|
372
|
+
* An object found by its identity that an orphaned binding holds is that binding moved (an export
|
|
373
|
+
* renamed, a deploy key apply declares now): its `bind` step takes the binding over (`from`), and
|
|
374
|
+
* the orphan is none. The orphaned keys taken over.
|
|
375
|
+
*/
|
|
376
|
+
function takeOver(
|
|
377
|
+
planned: PlanStep[],
|
|
378
|
+
orphans: ApplyDeclared["orphans"],
|
|
379
|
+
types: ReadonlyMap<string, Resource>,
|
|
380
|
+
): Set<string> {
|
|
381
|
+
const taken = new Set<string>();
|
|
382
|
+
for (const step of planned) {
|
|
383
|
+
if (step.action !== "bind" || step.source !== "found") continue;
|
|
384
|
+
const type = `${step.vendor}.${step.type}`;
|
|
385
|
+
const held = orphans.find(
|
|
386
|
+
(o) =>
|
|
387
|
+
!taken.has(o.key) && o.id === step.binding && (o.type === type || types.get(type)?.formerly?.includes(o.type)),
|
|
388
|
+
);
|
|
389
|
+
if (held) {
|
|
390
|
+
step.from = held.key;
|
|
391
|
+
taken.add(held.key);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return taken;
|
|
395
|
+
}
|
|
396
|
+
|
|
355
397
|
/** Each orphaned binding the input wants, after the declarations, and the answers that cannot be done. */
|
|
356
398
|
function orphansOf(
|
|
357
399
|
orphans: ApplyDeclared["orphans"],
|
|
@@ -444,8 +486,9 @@ const exportOf = (key: string) => key.slice(key.indexOf("#") + 1);
|
|
|
444
486
|
function messageOf(step: PlanStep, before: Binding | undefined, after: Binding | undefined): string {
|
|
445
487
|
const key = keyOf(step);
|
|
446
488
|
if (!after) return `Apply: unbind ${key} (${before?.type} ${before?.id})`;
|
|
447
|
-
const
|
|
448
|
-
|
|
489
|
+
const from = step.action === "bind" ? step.from : undefined;
|
|
490
|
+
const what = runOf(step) === "create" ? "create" : before || from ? "rebind" : "bind";
|
|
491
|
+
return `Apply: ${what} ${key} (${after.type} ${after.id})${from ? `, from ${from}` : ""}`;
|
|
449
492
|
}
|
|
450
493
|
|
|
451
494
|
/**
|
|
@@ -518,10 +561,11 @@ export function applyWorkflow(
|
|
|
518
561
|
...(hooks.length ? (["sleep"] as const) : []),
|
|
519
562
|
],
|
|
520
563
|
run: async (ctx, input) => {
|
|
521
|
-
const { resources, problems: read, bindings: locked, orphans, rebinds } = await steps.declared();
|
|
564
|
+
const { resources, problems: read, bindings: locked, orphans, rebinds, leftOut } = await steps.declared();
|
|
522
565
|
const wanted = (id: string) => input.only === undefined || input.only.includes(id);
|
|
523
566
|
const declarations = new Map(resources.map((r) => [r.id, r]));
|
|
524
|
-
const
|
|
567
|
+
const nameable = new Set([...orphans.map((o) => o.key), ...(leftOut ?? [])]);
|
|
568
|
+
const problems = [...read, ...inputProblems(input, declarations, nameable)];
|
|
525
569
|
// Observed as drift does, every import first, by the binding given before the lockfile's.
|
|
526
570
|
const found = new Map<string, Found>();
|
|
527
571
|
for (const resource of resources) {
|
|
@@ -566,19 +610,19 @@ export function applyWorkflow(
|
|
|
566
610
|
// A host wanted runs the commit its ref names now, resolved before planning: a merge is a change
|
|
567
611
|
// to it. One bound by the input needs none.
|
|
568
612
|
const revisions = new Map<string, Revision>();
|
|
569
|
-
const hosts = new Map<string, { source: string; revision: string }>();
|
|
613
|
+
const hosts = new Map<string, { source: string; revision: string; ref: string }>();
|
|
570
614
|
for (const { id, vendor, type, desired, refs } of resources.filter(
|
|
571
615
|
(r) => wanted(r.id) && input.bind?.[r.id] === undefined,
|
|
572
616
|
)) {
|
|
573
617
|
const resource = types.get(`${vendor}.${type}`);
|
|
574
618
|
const host = resource?.host;
|
|
575
619
|
const source = host && sourceUrl(desired, refs[host.inputs.source], resource, found);
|
|
576
|
-
const ref = host &&
|
|
577
|
-
if (host && source !== undefined &&
|
|
620
|
+
const ref = host && refOf(desired, host);
|
|
621
|
+
if (host && source !== undefined && ref !== undefined) {
|
|
578
622
|
try {
|
|
579
|
-
const
|
|
580
|
-
revisions.set(id, { revision });
|
|
581
|
-
hosts.set(id, { source,
|
|
623
|
+
const resolved = await ctx.sanoma!.source!.resolve({ url: source, ...(ref ? { ref } : {}) });
|
|
624
|
+
revisions.set(id, { revision: resolved.revision });
|
|
625
|
+
hosts.set(id, { source, ...resolved });
|
|
582
626
|
} catch (err) {
|
|
583
627
|
if (isRunControlError(err)) throw err;
|
|
584
628
|
revisions.set(id, { error: errorMessage(err) });
|
|
@@ -633,10 +677,19 @@ export function applyWorkflow(
|
|
|
633
677
|
if (left.length < unfilled.length) awaited.add(resource.id);
|
|
634
678
|
const unresolved = revisions.get(resource.id);
|
|
635
679
|
// A host whose ref could not be resolved has no commit to run (compare says so only for one it found).
|
|
636
|
-
const
|
|
637
|
-
|
|
680
|
+
const now = found.get(resource.id)!;
|
|
681
|
+
const host = types.get(`${resource.vendor}.${resource.type}`)?.host;
|
|
682
|
+
const attached = host && "state" in now ? detaching(host, fields, now.state) : [];
|
|
683
|
+
const next = attached.length
|
|
684
|
+
? {
|
|
685
|
+
problem: problemOf(
|
|
686
|
+
resource.id,
|
|
687
|
+
`${attached.join(", ")} ${attached.length > 1 ? "are" : "is"} attached and no site declares a domain, so a change would detach ${attached.length > 1 ? "them" : "it"}: put \`domain\` on each site, \`defineSite({ public: true, domain })\` in sites/<site>/site.ts`,
|
|
688
|
+
),
|
|
689
|
+
}
|
|
690
|
+
: unresolved && "error" in unresolved && result.status !== "error"
|
|
638
691
|
? { problem: problemOf(resource.id, unresolved.error) }
|
|
639
|
-
: decide(resource, result,
|
|
692
|
+
: decide(resource, result, now, input, fields, types, usable);
|
|
640
693
|
if (next.ask !== undefined) {
|
|
641
694
|
try {
|
|
642
695
|
const answered = await ctx[resource.vendor]![resource.type]!.plan(next.ask, {
|
|
@@ -653,7 +706,14 @@ export function applyWorkflow(
|
|
|
653
706
|
if (next.step) add(next.step);
|
|
654
707
|
if (next.problem) problems.push(next.problem);
|
|
655
708
|
}
|
|
656
|
-
const
|
|
709
|
+
const taken = takeOver(planned, orphans, types);
|
|
710
|
+
const orphaned = orphansOf(
|
|
711
|
+
orphans.filter((o) => !taken.has(o.key)),
|
|
712
|
+
input,
|
|
713
|
+
wanted,
|
|
714
|
+
usable,
|
|
715
|
+
types,
|
|
716
|
+
);
|
|
657
717
|
const plan: ApplyPlan = {
|
|
658
718
|
problems: [...problems, ...orphaned.problems],
|
|
659
719
|
rebinds,
|
|
@@ -734,13 +794,16 @@ export function applyWorkflow(
|
|
|
734
794
|
} else if (todo === "delete") {
|
|
735
795
|
await ctx[step.vendor]![step.type]!.delete({ id: binding }, { connection: step.connection });
|
|
736
796
|
delete bindings[key];
|
|
737
|
-
} else if (todo === "bind")
|
|
797
|
+
} else if (todo === "bind") {
|
|
738
798
|
bindings[key] = { type, id: binding, ...(step.connection && { connection: step.connection }) };
|
|
739
|
-
|
|
799
|
+
if (step.action === "bind" && step.from) delete bindings[step.from];
|
|
800
|
+
} else if (todo === "forget") delete bindings[key];
|
|
740
801
|
if (state && declared) keep(declared, state);
|
|
741
802
|
const after = bindings[key];
|
|
742
|
-
|
|
743
|
-
|
|
803
|
+
// A new id, or the same object under its type's new name (`formerly`): written.
|
|
804
|
+
if (after?.id !== before?.id || (after && before && after.type !== before.type)) {
|
|
805
|
+
const moved = step.action === "bind" ? step.from : undefined;
|
|
806
|
+
const wrote = await steps.write(key, after, messageOf(step, before, after), moved);
|
|
744
807
|
written ||= wrote.written;
|
|
745
808
|
revision = wrote.revision ?? revision;
|
|
746
809
|
}
|
package/src/config.ts
CHANGED
|
@@ -406,7 +406,14 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
|
|
|
406
406
|
const connectors = connectorsOf(config);
|
|
407
407
|
// The types the data files may declare: the vendors' and the people's, whose resources are left out.
|
|
408
408
|
const declarable = resourceTypesOf(connectors);
|
|
409
|
-
const read = (
|
|
409
|
+
const read = (options: ReadOptions = {}) =>
|
|
410
|
+
readDeclared(root, connectors, declarable, types, connections, {
|
|
411
|
+
...options,
|
|
412
|
+
drivers: driversByVendor,
|
|
413
|
+
builtInStore: !config.secrets?.store,
|
|
414
|
+
signingKeyBound: config.secrets?.bindings?.SANOMA_SIGNING_KEY !== undefined,
|
|
415
|
+
...(isOp(config.login?.sender) && { sender: config.login.sender.vendor }),
|
|
416
|
+
});
|
|
410
417
|
if (types.size) {
|
|
411
418
|
names.set(
|
|
412
419
|
DRIFT_WORKFLOW,
|
|
@@ -415,8 +422,13 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
|
|
|
415
422
|
names.set(APPLY_WORKFLOW, applyWorkflow(types, ops, drivers, applySteps(appName, root, read)));
|
|
416
423
|
}
|
|
417
424
|
// Built-in, in every config: the life of a change, which deploys to the declared hosts after its merge.
|
|
418
|
-
|
|
419
|
-
|
|
425
|
+
// The declared hosts' ids, as the files say them: nothing derived, nothing minted.
|
|
426
|
+
const hosts = async () => {
|
|
427
|
+
const { loadDataFiles } = await import("./resources.ts");
|
|
428
|
+
return loadDataFiles(root, connectors, declarable)
|
|
429
|
+
.resources.filter((r) => types.get(`${r.vendor}.${r.type}`)?.host)
|
|
430
|
+
.map((r) => r.id);
|
|
431
|
+
};
|
|
420
432
|
names.set(CHANGE_WORKFLOW, changeWorkflow(changeOps, changeSteps({ remotes, hosts })));
|
|
421
433
|
const declaredObjects = () => readDeclaredObjects(root, connectors, declarable);
|
|
422
434
|
// Built-in: a direct call the policy holds for an approval, made a run, when any operation may be called directly.
|
|
@@ -706,6 +718,11 @@ async function readDeclaredObjects(
|
|
|
706
718
|
return { keys: new Map([...named, ...bound]), problems };
|
|
707
719
|
}
|
|
708
720
|
|
|
721
|
+
/** How a run reads the data files: `mint`, apply's, mints the keys a deriving host is given (`hostFacts`). */
|
|
722
|
+
export interface ReadOptions {
|
|
723
|
+
mint?: boolean;
|
|
724
|
+
}
|
|
725
|
+
|
|
709
726
|
/**
|
|
710
727
|
* The data files under `root` and the lockfile beside them, in one read, as a drift or apply run
|
|
711
728
|
* records them: each resource with its binding, without its write-only values (a secret marker
|
|
@@ -718,12 +735,48 @@ async function readDeclared(
|
|
|
718
735
|
declarable: ReadonlyMap<string, Resource>,
|
|
719
736
|
types: ReadonlyMap<string, Resource>,
|
|
720
737
|
connections: ConnectionRegistry,
|
|
738
|
+
options: ReadOptions & {
|
|
739
|
+
drivers: ReadonlyMap<string, Driver>;
|
|
740
|
+
builtInStore: boolean;
|
|
741
|
+
signingKeyBound: boolean;
|
|
742
|
+
sender?: string;
|
|
743
|
+
},
|
|
721
744
|
): Promise<Omit<ApplyDeclared, "rebinds">> {
|
|
722
745
|
// Loaded when a run reads the files, not with the worker.
|
|
723
746
|
const { loadDataFiles } = await import("./resources.ts");
|
|
724
747
|
const { resources, bindings, orphans, problems } = loadDataFiles(root, connectors, declarable);
|
|
725
|
-
// Read inside the data-file durable step: every operation of the plan uses one pinned account.
|
|
726
748
|
const run = currentRun.getStore();
|
|
749
|
+
// A host that derives is filled with what it is not told (`deriveHosts`), before it is checked
|
|
750
|
+
// or pinned: the deploy key it clones with among them. Minted only by a live apply.
|
|
751
|
+
const keep = new Set<string>();
|
|
752
|
+
const leftOut = new Set<string>();
|
|
753
|
+
if (root !== undefined) {
|
|
754
|
+
const { deriveHosts, hostFacts } = await import("./host-derive.ts");
|
|
755
|
+
const facts = await hostFacts({
|
|
756
|
+
root,
|
|
757
|
+
resources,
|
|
758
|
+
types,
|
|
759
|
+
drivers: options.drivers,
|
|
760
|
+
...(run && { secrets: run.state.secrets }),
|
|
761
|
+
builtInStore: options.builtInStore,
|
|
762
|
+
signingKeyBound: options.signingKeyBound,
|
|
763
|
+
...(options.sender !== undefined && { sender: options.sender }),
|
|
764
|
+
mint: !!options.mint && !!run && !run.sandbox,
|
|
765
|
+
});
|
|
766
|
+
if (facts) problems.push(...deriveHosts(resources, types, bindings, facts, { keep, leftOut }));
|
|
767
|
+
}
|
|
768
|
+
// A host apply cannot give all it needs is left out, with what names it (its wakeup), and what
|
|
769
|
+
// names that in turn: its problem says why, and nothing of it changes until it is fixed.
|
|
770
|
+
for (let grew = leftOut.size > 0; grew;) {
|
|
771
|
+
const before = leftOut.size;
|
|
772
|
+
for (const o of resources) if (Object.values(o.refs).some((id) => leftOut.has(id))) leftOut.add(o.id);
|
|
773
|
+
grew = leftOut.size > before;
|
|
774
|
+
}
|
|
775
|
+
for (const r of resources.filter((o) => leftOut.has(o.id))) resources.splice(resources.indexOf(r), 1);
|
|
776
|
+
// A host's deploy key apply declares, or cannot tell yet, is no orphan.
|
|
777
|
+
const declaredIds = new Set(resources.map((r) => r.id));
|
|
778
|
+
const kept = orphans.filter((o) => !declaredIds.has(o.key) && !keep.has(o.key));
|
|
779
|
+
// Read inside the data-file durable step: every operation of the plan uses one pinned account.
|
|
727
780
|
if (run && !run.sandbox) {
|
|
728
781
|
for (const resource of resources.filter((r) => types.has(`${r.vendor}.${r.type}`))) {
|
|
729
782
|
try {
|
|
@@ -760,7 +813,8 @@ async function readDeclared(
|
|
|
760
813
|
...(connection && { connection }),
|
|
761
814
|
})),
|
|
762
815
|
bindings,
|
|
763
|
-
orphans,
|
|
816
|
+
orphans: kept,
|
|
817
|
+
...(leftOut.size > 0 && { leftOut: [...leftOut] }),
|
|
764
818
|
problems,
|
|
765
819
|
};
|
|
766
820
|
}
|
package/src/definitions.ts
CHANGED
|
@@ -23,6 +23,11 @@ export interface SiteDefinition {
|
|
|
23
23
|
public?: boolean;
|
|
24
24
|
login?: string;
|
|
25
25
|
locales?: readonly [string, ...string[]];
|
|
26
|
+
/**
|
|
27
|
+
* A public site's domain (`www.acme.example`): the production host attaches it and serves the
|
|
28
|
+
* site at its root there; in a preview and locally the site stays at `/sites/<name>`.
|
|
29
|
+
*/
|
|
30
|
+
domain?: string;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/** Declares a site in its folder's `site.ts`, its default export: `defineSite({ title: "Help desk" })`. */
|
package/src/describe.ts
CHANGED
|
@@ -41,7 +41,8 @@ import {
|
|
|
41
41
|
problemLine,
|
|
42
42
|
siteAtHostname,
|
|
43
43
|
} from "./shared.ts";
|
|
44
|
-
import { readSites, type SiteEntry } from "./sites.ts";
|
|
44
|
+
import { readSites, type SiteEntry, siteDomains } from "./sites.ts";
|
|
45
|
+
import { sourceKeyIds } from "./host.ts";
|
|
45
46
|
|
|
46
47
|
// `@sanoma-ai/workflows/describe`: what a UI renders from, apart from the main entry.
|
|
47
48
|
// What `describeConfig` answers, as zod schemas and the types they infer (`./description`).
|
|
@@ -85,7 +86,7 @@ export {
|
|
|
85
86
|
// The apps under `apps/`, with what their `app.ts` declares.
|
|
86
87
|
export { crossAppImports, readApps } from "./apps.ts";
|
|
87
88
|
// What the sites call, read from their source: a workflow's Used by.
|
|
88
|
-
export { readSites, siteContentPages, siteIcons, siteLocales, sitesUnder } from "./sites.ts";
|
|
89
|
+
export { readSites, siteContentPages, siteDomains, siteIcons, siteLocales, sitesUnder } from "./sites.ts";
|
|
89
90
|
// The generated SDK, `.sanoma/api.ts`: a site's `api` typed from this description.
|
|
90
91
|
export { API_FILE, generateApi, writeApi, writeGenerated } from "./sdk.ts";
|
|
91
92
|
// The Cedar schema, `.sanoma/schema.cedarschema`: what a company's policies are validated and decided against.
|
|
@@ -193,6 +194,7 @@ export function describeConfig(config: SanomaConfig): ConfigDescription {
|
|
|
193
194
|
.toSorted((a, b) => a.id.localeCompare(b.id));
|
|
194
195
|
|
|
195
196
|
const dataFiles = loadDataFiles(resolved.root, connectorsOf(config), resources);
|
|
197
|
+
const sourceKeys = sourceKeyIds(dataFiles.resources, resources);
|
|
196
198
|
const declared = new Map(dataFiles.collections.map((d) => [d.name, d]));
|
|
197
199
|
const collections: CollectionEntry[] = [...resolved.collections.values()]
|
|
198
200
|
.map((c) => ({
|
|
@@ -289,12 +291,18 @@ export function describeConfig(config: SanomaConfig): ConfigDescription {
|
|
|
289
291
|
collections,
|
|
290
292
|
// An app's people are refused (`readPeople`), so the grid and the People page never show them.
|
|
291
293
|
resources: dataFiles.resources.filter((r) => !inAppPeople(r)),
|
|
292
|
-
|
|
294
|
+
// A host's deploy key, which apply declares, is no orphan.
|
|
295
|
+
orphans: dataFiles.orphans.filter((o) => !sourceKeys.has(o.key)),
|
|
293
296
|
problems: [
|
|
294
297
|
...dataFiles.problems,
|
|
295
298
|
...appProblems,
|
|
296
299
|
...appClashes(apps, collections, sites),
|
|
297
|
-
...domainProblems(
|
|
300
|
+
...domainProblems(
|
|
301
|
+
dataFiles.resources,
|
|
302
|
+
resources,
|
|
303
|
+
sites,
|
|
304
|
+
resolved.root === undefined ? {} : siteDomains(resolved.root),
|
|
305
|
+
),
|
|
298
306
|
...loginProblems(resolved, dataFiles.resources),
|
|
299
307
|
],
|
|
300
308
|
apps,
|
|
@@ -361,15 +369,17 @@ function appClashes(
|
|
|
361
369
|
}
|
|
362
370
|
|
|
363
371
|
/**
|
|
364
|
-
* Each site a host's `domains` (its type's `host.inputs.domains
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
372
|
+
* Each site a host's `domains` (its type's `host.inputs.domains`; for a host that derives, the
|
|
373
|
+
* public sites' own, `declaredDomains`) names that is not a public site, each domain it gives more
|
|
374
|
+
* than one site, and a domain that is the host's `hostname` too, or another host's hostname or
|
|
375
|
+
* domain, at the host's declaration: the app serves only a public site at a domain, one site at
|
|
376
|
+
* each, and itself at another, and one host holds each.
|
|
368
377
|
*/
|
|
369
378
|
function domainProblems(
|
|
370
379
|
declared: readonly DeclaredResource[],
|
|
371
380
|
types: ReadonlyMap<string, Resource>,
|
|
372
381
|
sites: readonly SiteEntry[],
|
|
382
|
+
declaredDomains: Readonly<Record<string, string>>,
|
|
373
383
|
): ResourceProblem[] {
|
|
374
384
|
const isPublic = new Set(sites.filter((site) => site.public).map((site) => site.name));
|
|
375
385
|
const problems: ResourceProblem[] = [];
|
|
@@ -379,9 +389,12 @@ function domainProblems(
|
|
|
379
389
|
const hold = (name: string, resource: DeclaredResource) =>
|
|
380
390
|
held.set(name.toLowerCase(), (held.get(name.toLowerCase()) ?? new Set()).add(resource));
|
|
381
391
|
for (const resource of declared) {
|
|
382
|
-
const
|
|
392
|
+
const host = types.get(`${resource.vendor}.${resource.type}`)?.host;
|
|
393
|
+
const inputs = host?.inputs;
|
|
383
394
|
const path = inputs?.domains;
|
|
384
|
-
const domains = (
|
|
395
|
+
const domains = (
|
|
396
|
+
path === undefined ? {} : host?.derives ? declaredDomains : (valueAt(resource.desired, path) ?? {})
|
|
397
|
+
) as Record<string, unknown>;
|
|
385
398
|
const hostname = inputs?.hostname === undefined ? undefined : valueAt(resource.desired, inputs.hostname);
|
|
386
399
|
const messages = Object.keys(domains)
|
|
387
400
|
.filter((name) => !isPublic.has(name))
|
package/src/drift.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineWorkflow, type WorkflowDefinition } from "./define.ts";
|
|
3
3
|
import { isRunControlError } from "./errors.ts";
|
|
4
|
+
import type { HostSpec } from "./host.ts";
|
|
4
5
|
import type { Op } from "./op.ts";
|
|
5
6
|
import { type DriftField, diffDeclared, fieldPathOf, type Resource, valueAt, withFrom } from "./resource.ts";
|
|
6
7
|
import type { DeclaredResource, ResourceProblem } from "./resources.ts";
|
|
@@ -240,10 +241,11 @@ export function driftWorkflow(
|
|
|
240
241
|
const resource = types.get(`${vendor}.${type}`);
|
|
241
242
|
const host = resource?.host;
|
|
242
243
|
const source = host && sourceUrl(desired, refs[host.inputs.source], resource, found);
|
|
243
|
-
const ref = host &&
|
|
244
|
-
if (host && "state" in found.get(id)! && source !== undefined &&
|
|
244
|
+
const ref = host && refOf(desired, host);
|
|
245
|
+
if (host && "state" in found.get(id)! && source !== undefined && ref !== undefined) {
|
|
245
246
|
try {
|
|
246
|
-
|
|
247
|
+
const { revision } = await ctx.sanoma!.source!.resolve({ url: source, ...(ref ? { ref } : {}) });
|
|
248
|
+
revisions.set(id, { revision });
|
|
247
249
|
} catch (err) {
|
|
248
250
|
if (isRunControlError(err)) throw err;
|
|
249
251
|
revisions.set(id, { error: errorMessage(err) });
|
|
@@ -256,6 +258,17 @@ export function driftWorkflow(
|
|
|
256
258
|
});
|
|
257
259
|
}
|
|
258
260
|
|
|
261
|
+
/**
|
|
262
|
+
* The ref a host runs: the one its data file gives, or `""` for a host that derives and names
|
|
263
|
+
* none (its repository's default branch, which `sanoma.source.resolve` tells); undefined when it
|
|
264
|
+
* has none to resolve.
|
|
265
|
+
*/
|
|
266
|
+
export function refOf(desired: State, host: HostSpec): string | undefined {
|
|
267
|
+
const ref = valueAt(desired, host.inputs.ref);
|
|
268
|
+
if (typeof ref === "string" && ref) return ref;
|
|
269
|
+
return host.derives && ref == null ? "" : undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
259
272
|
/**
|
|
260
273
|
* A host's source: the clone URL its data file gives, or, when it names a declared repository
|
|
261
274
|
* (`target`), the first of the reference's `by` fields that holds one in that repository's state,
|