@sanoma-ai/workflows 0.11.3 → 0.11.4
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 +32 -30
- 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/resource.d.ts +8 -1
- package/dist/resource.js +11 -0
- 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/resource.ts +33 -2
- 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/index.ts
CHANGED
|
@@ -59,6 +59,7 @@ export {
|
|
|
59
59
|
type ResourceFields,
|
|
60
60
|
type ResourceOps,
|
|
61
61
|
type ResourceSpec,
|
|
62
|
+
valueAt,
|
|
62
63
|
withoutWriteOnly,
|
|
63
64
|
} from "./resource.ts";
|
|
64
65
|
export { type DnsRecord, dnsRecord } from "./dns.ts";
|
|
@@ -190,6 +191,7 @@ export {
|
|
|
190
191
|
type Draft,
|
|
191
192
|
draftBranch,
|
|
192
193
|
type DraftDiff,
|
|
194
|
+
defaultBranch,
|
|
193
195
|
draftOf,
|
|
194
196
|
type HostEnforcement,
|
|
195
197
|
type PullRequest,
|
|
@@ -204,7 +206,23 @@ export {
|
|
|
204
206
|
type SourceHost,
|
|
205
207
|
} from "./source.ts";
|
|
206
208
|
export type { HostCapabilities, HostLifetime, HostOutputs, HostSpec } from "./host.ts";
|
|
207
|
-
export { wakeUrlOf } from "./host.ts";
|
|
209
|
+
export { RUNTIME_IMAGE, RUNTIME_PORT, runtimeImage, sourceKeyId, UNNAMED, wakeUrlOf } from "./host.ts";
|
|
210
|
+
export {
|
|
211
|
+
cloneUrl,
|
|
212
|
+
deriveHosts,
|
|
213
|
+
type HostFacts,
|
|
214
|
+
hostFacts,
|
|
215
|
+
DEPLOY_KEY_TITLE,
|
|
216
|
+
DEPLOY_KEY_TYPE,
|
|
217
|
+
GITHUB_APP,
|
|
218
|
+
githubRepository,
|
|
219
|
+
HOST_SIGNING_KEY,
|
|
220
|
+
installedSanoma,
|
|
221
|
+
originUrl,
|
|
222
|
+
SOURCE_KEY,
|
|
223
|
+
sourceRepository,
|
|
224
|
+
wokenBy,
|
|
225
|
+
} from "./host-derive.ts";
|
|
208
226
|
export {
|
|
209
227
|
checkDatabase,
|
|
210
228
|
type DatabaseCapabilities,
|
|
@@ -326,7 +344,7 @@ export {
|
|
|
326
344
|
export { checkSecretStore, type SecretStoreCheck } from "./secret-store-check.ts";
|
|
327
345
|
|
|
328
346
|
export { postgresSecretStore } from "./credentials.ts";
|
|
329
|
-
export { deploymentKey, signingKeyFor } from "./deployment-key.ts";
|
|
347
|
+
export { deploymentKey, keyBytes, signingKeyFor } from "./deployment-key.ts";
|
|
330
348
|
|
|
331
349
|
export {
|
|
332
350
|
defineConnection,
|
package/src/resource.ts
CHANGED
|
@@ -131,6 +131,13 @@ export interface ResourceSpec<S extends z.ZodObject = z.ZodObject> {
|
|
|
131
131
|
* Supabase project's health, until it is healthy, then its database URL stored.
|
|
132
132
|
*/
|
|
133
133
|
afterCreate?: readonly AfterCreate[];
|
|
134
|
+
/**
|
|
135
|
+
* The ids (`<vendor>.<type>`) this type was known by before a rename: a lockfile binding under
|
|
136
|
+
* one of them binds a declaration of this type to the same object, so a rename recreates nothing
|
|
137
|
+
* (`cloudflare.host`, formerly `cloudflare.container`). Apply writes the new id with its next
|
|
138
|
+
* change to that binding.
|
|
139
|
+
*/
|
|
140
|
+
formerly?: readonly string[];
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
/** One operation apply calls after a create (`ResourceSpec.afterCreate`). */
|
|
@@ -352,11 +359,20 @@ export interface Resource<
|
|
|
352
359
|
Required<
|
|
353
360
|
Omit<
|
|
354
361
|
ResourceSpec<S>,
|
|
355
|
-
|
|
362
|
+
| "identity"
|
|
363
|
+
| "find"
|
|
364
|
+
| "writable"
|
|
365
|
+
| "host"
|
|
366
|
+
| "wakeup"
|
|
367
|
+
| "database"
|
|
368
|
+
| "stateless"
|
|
369
|
+
| "afterCreate"
|
|
370
|
+
| "vendorless"
|
|
371
|
+
| "formerly"
|
|
356
372
|
>
|
|
357
373
|
>
|
|
358
374
|
>,
|
|
359
|
-
Readonly<Pick<ResourceSpec<S>, "identity" | "find" | "host" | "wakeup" | "database" | "afterCreate">> {
|
|
375
|
+
Readonly<Pick<ResourceSpec<S>, "identity" | "find" | "host" | "wakeup" | "database" | "afterCreate" | "formerly">> {
|
|
360
376
|
(
|
|
361
377
|
desired: Declaring<z.input<S>, R>,
|
|
362
378
|
options?: { connection?: ConnectionRef<Id extends `${infer V}.${string}` ? V : string> },
|
|
@@ -836,6 +852,20 @@ export function defineResource<
|
|
|
836
852
|
if (host && pathsIn(desired, new Set([host.revision])).length) {
|
|
837
853
|
throw new Error(`${what}: leave out ${host.revision}: apply sets it from ${host.inputs.ref}`);
|
|
838
854
|
}
|
|
855
|
+
// A host that derives is given its secrets and its sites' domains: a data file names neither.
|
|
856
|
+
if (host?.derives) {
|
|
857
|
+
const { secrets, domains } = host.inputs;
|
|
858
|
+
if (secrets !== undefined && pathsIn(desired, new Set([secrets])).length) {
|
|
859
|
+
throw new Error(
|
|
860
|
+
`${what}: leave out ${secrets}: apply gives the host its database's URL, its keys and the connections it deploys with, and no secret is written in a file`,
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
if (domains !== undefined && pathsIn(desired, new Set([domains])).length) {
|
|
864
|
+
throw new Error(
|
|
865
|
+
`${what}: leave out ${domains}: a public site's domain is declared on the site, \`defineSite({ public: true, domain })\`, and the host attaches it`,
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
839
869
|
const vendors = pathsIn(desired, owned);
|
|
840
870
|
if (vendors.length) {
|
|
841
871
|
throw new Error(
|
|
@@ -874,6 +904,7 @@ export function defineResource<
|
|
|
874
904
|
stateless: spec.stateless ?? host !== undefined,
|
|
875
905
|
vendorless: spec.vendorless ?? false,
|
|
876
906
|
...(spec.afterCreate && { afterCreate: spec.afterCreate }),
|
|
907
|
+
...(spec.formerly?.length && { formerly: Object.freeze([...spec.formerly]) }),
|
|
877
908
|
ops,
|
|
878
909
|
// A symbol, so `defineConnector` tells a resource type from a group of operations.
|
|
879
910
|
[RESOURCE]: true as const,
|
package/src/resources.ts
CHANGED
|
@@ -359,7 +359,7 @@ function declarationsOf(
|
|
|
359
359
|
const why =
|
|
360
360
|
value.connection !== undefined && binding.connection?.$connection !== value.connection.$connection
|
|
361
361
|
? `${id} changes the connection of a bound resource: explicitly unbind it before selecting another account`
|
|
362
|
-
: binding.type !== value[DECLARED]
|
|
362
|
+
: binding.type !== value[DECLARED] && !types.get(value[DECLARED])?.formerly?.includes(binding.type)
|
|
363
363
|
? `${id} is bound to a ${binding.type} in ${LOCKFILE}, but declares a ${value[DECLARED]}`
|
|
364
364
|
: first !== undefined
|
|
365
365
|
? `${id} is bound to the same ${object} as ${first} in ${LOCKFILE}: bind each object once`
|
package/src/sites.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
apiNameOf,
|
|
16
16
|
BUILT_IN_WORKFLOWS,
|
|
17
17
|
DATA_VENDOR,
|
|
18
|
+
DOMAIN_NAME,
|
|
18
19
|
LOCALE,
|
|
19
20
|
SITE_NAME,
|
|
20
21
|
siteKey,
|
|
@@ -129,7 +130,9 @@ export function readSites(
|
|
|
129
130
|
const read = readSiteFiles(root, dir);
|
|
130
131
|
const resolved = read.flatMap(({ uses }) => uses.filter((use) => !isAppCollection(use)).map(resolve));
|
|
131
132
|
const pages = pagesOf(read);
|
|
132
|
-
|
|
133
|
+
// Its declared domain is the production host's to attach (`siteDomains`): where the site answers
|
|
134
|
+
// is `domain`, which only the app sets, from the `SANOMA_DOMAINS` that host hands it.
|
|
135
|
+
const { problems: optionProblems, domain: _declared, ...options } = siteDefinitionOf(root, dir, connectors);
|
|
133
136
|
const calls = resolved.map((r) => r.call);
|
|
134
137
|
const problems = [
|
|
135
138
|
...resolved.flatMap(({ call, problem }) => {
|
|
@@ -225,6 +228,21 @@ export function siteIcons(root: string): Record<string, string> {
|
|
|
225
228
|
);
|
|
226
229
|
}
|
|
227
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Each public site's domain (`defineSite({ public: true, domain })`) under `root`, by name, for
|
|
233
|
+
* the sites whose `site.ts` gives one without a problem there: what the production host attaches
|
|
234
|
+
* and hands the app as `SANOMA_DOMAINS` (`deriveHosts`). Loaded as `readSites` loads it.
|
|
235
|
+
*/
|
|
236
|
+
export function siteDomains(root: string): Record<string, string> {
|
|
237
|
+
const none = new Set<string>();
|
|
238
|
+
return Object.fromEntries(
|
|
239
|
+
sitesUnder(root).flatMap(({ name, dir }) => {
|
|
240
|
+
const { domain } = siteDefinitionOf(root, dir, { vendors: none, logins: none });
|
|
241
|
+
return domain === undefined ? [] : [[name, domain]];
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
228
246
|
/**
|
|
229
247
|
* The sites under `root`, by key (`siteKey`): each folder of `sites/` with a `routes/` whose name
|
|
230
248
|
* is a URL segment, then each app's `apps/<app>/sites/`, with its folder relative to `root`.
|
|
@@ -427,10 +445,14 @@ const SiteDefinitionSchema = z.strictObject({
|
|
|
427
445
|
public: z.boolean().optional(),
|
|
428
446
|
login: z.string().optional(),
|
|
429
447
|
locales: z.array(z.string()).optional(),
|
|
448
|
+
domain: z
|
|
449
|
+
.string()
|
|
450
|
+
.regex(DOMAIN_NAME, "a host name of two labels or more, lower case, such as `www.acme.example`")
|
|
451
|
+
.optional(),
|
|
430
452
|
});
|
|
431
453
|
|
|
432
454
|
/** What `site.ts` says of who may see a site: read together or not at all. */
|
|
433
|
-
const ACCESS = ["public", "login", "locales"];
|
|
455
|
+
const ACCESS = ["public", "login", "locales", "domain"];
|
|
434
456
|
|
|
435
457
|
/**
|
|
436
458
|
* What the site in `dir` (relative to `root`) says of itself in its `site.ts`, loaded
|
|
@@ -445,6 +467,8 @@ function siteDefinitionOf(
|
|
|
445
467
|
dir: string,
|
|
446
468
|
connectors: SiteConnectors,
|
|
447
469
|
): Pick<SiteEntry, "at" | "title" | "icon" | "public" | "login" | "locales"> & {
|
|
470
|
+
/** `defineSite({ domain })`, kept only on a public site. */
|
|
471
|
+
domain?: string;
|
|
448
472
|
problems: (ResourceProblem & { file: string })[];
|
|
449
473
|
} {
|
|
450
474
|
const file = `${dir}/site.ts`;
|
|
@@ -474,7 +498,7 @@ function siteDefinitionOf(
|
|
|
474
498
|
);
|
|
475
499
|
if (!parsed.success) return { problems };
|
|
476
500
|
}
|
|
477
|
-
const { title, icon, public: isPublic, login, locales } = parsed.data;
|
|
501
|
+
const { title, icon, public: isPublic, login, locales, domain } = parsed.data;
|
|
478
502
|
const loginProblem = (): string | undefined => {
|
|
479
503
|
if (!isPublic) return "a login is for a public site: add `public: true`, or remove `login`";
|
|
480
504
|
if (!connectors.vendors.has(login!)) return `login names "${login}", which is not a connector in sanoma.config.ts`;
|
|
@@ -496,6 +520,11 @@ function siteDefinitionOf(
|
|
|
496
520
|
};
|
|
497
521
|
const localesMessage = locales && localesProblem(locales);
|
|
498
522
|
if (localesMessage) problems.push(problem(localesMessage));
|
|
523
|
+
const domainMessage =
|
|
524
|
+
domain !== undefined && !isPublic
|
|
525
|
+
? "a domain is for a public site: add `public: true`, or remove `domain`"
|
|
526
|
+
: undefined;
|
|
527
|
+
if (domainMessage) problems.push(problem(domainMessage));
|
|
499
528
|
return {
|
|
500
529
|
at: { file, export: "default" },
|
|
501
530
|
...(title !== undefined && { title }),
|
|
@@ -503,6 +532,7 @@ function siteDefinitionOf(
|
|
|
503
532
|
...(isPublic && { public: true as const }),
|
|
504
533
|
...(login !== undefined && { login }),
|
|
505
534
|
...(locales && !localesMessage && { locales }),
|
|
535
|
+
...(domain !== undefined && !domainMessage && { domain }),
|
|
506
536
|
problems,
|
|
507
537
|
};
|
|
508
538
|
}
|
package/src/source.ts
CHANGED
|
@@ -130,7 +130,8 @@ export interface CheckResult {
|
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
132
|
* The built-in operation `sanoma.source.resolve`: the commit `ref` names in the repository at
|
|
133
|
-
* `url` now, by `git ls-remote` (a branch first, then a tag, peeled; a full commit id is itself)
|
|
133
|
+
* `url` now, by `git ls-remote` (a branch first, then a tag, peeled; a full commit id is itself),
|
|
134
|
+
* and the ref; with no `ref`, the repository's default branch (its `HEAD`) and the commit it names.
|
|
134
135
|
* Apply calls it to fill a host's revision before it plans, and drift to see a merge. `resolveConfig`
|
|
135
136
|
* registers it when a connector declares a host type.
|
|
136
137
|
*/
|
|
@@ -139,10 +140,11 @@ export const sourceOps = defineConnector("sanoma", {
|
|
|
139
140
|
resolve: {
|
|
140
141
|
effect: "read",
|
|
141
142
|
idempotent: true,
|
|
142
|
-
description:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
description:
|
|
144
|
+
"Resolve a branch or tag of a git repository, its default branch by default, to the commit it names now",
|
|
145
|
+
input: z.object({ url: z.string().min(1), ref: z.string().min(1).optional() }),
|
|
146
|
+
output: z.object({ revision: z.string(), ref: z.string() }),
|
|
147
|
+
target: ({ url, ref }: { url: string; ref?: string }) => `${url}#${ref ?? "HEAD"}`,
|
|
146
148
|
},
|
|
147
149
|
},
|
|
148
150
|
});
|
|
@@ -151,7 +153,10 @@ export const SOURCE_RESOLVE = "sanoma.source.resolve" satisfies typeof sourceOps
|
|
|
151
153
|
|
|
152
154
|
/** The driver of `sanoma.source.resolve`: git on this machine, with its own credential helpers. */
|
|
153
155
|
export const sourceDriver = defineDriver(sourceOps, {
|
|
154
|
-
source: {
|
|
156
|
+
source: {
|
|
157
|
+
resolve: async ({ url, ref }) =>
|
|
158
|
+
ref === undefined ? defaultBranch(url) : { revision: await resolveRevision(url, ref), ref },
|
|
159
|
+
},
|
|
155
160
|
});
|
|
156
161
|
|
|
157
162
|
const COMMIT = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
@@ -159,20 +164,20 @@ const COMMIT = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
|
159
164
|
/**
|
|
160
165
|
* The fake of `sanoma.source.resolve`, which a sandbox run calls (`sandboxDrivers`): no git and no
|
|
161
166
|
* network, a full commit id is itself as the driver has it, and any other ref the SHA-1 of
|
|
162
|
-
* `<url>#<ref>`, so a scenario's revisions are the same on every run
|
|
167
|
+
* `<url>#<ref>`, so a scenario's revisions are the same on every run; the default branch is `main`.
|
|
163
168
|
*/
|
|
164
169
|
export const fakeSourceDriver = defineDriver(sourceOps, {
|
|
165
170
|
source: {
|
|
166
|
-
resolve: async ({ url, ref }) => ({
|
|
171
|
+
resolve: async ({ url, ref = "main" }) => ({
|
|
167
172
|
revision: COMMIT.test(ref) ? ref : createHash("sha1").update(`${url}#${ref}`).digest("hex"),
|
|
173
|
+
ref,
|
|
168
174
|
}),
|
|
169
175
|
},
|
|
170
176
|
});
|
|
171
177
|
|
|
172
|
-
/**
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const { stdout } = await promisify(execFile)("git", ["ls-remote", "--", url, ref, `${ref}^{}`], {
|
|
178
|
+
/** `git ls-remote` of `patterns` in the repository at `url`, with `options`, never prompting for credentials. */
|
|
179
|
+
async function lsRemote(url: string, patterns: string[], options: string[] = []): Promise<string> {
|
|
180
|
+
const { stdout } = await promisify(execFile)("git", ["ls-remote", ...options, "--", url, ...patterns], {
|
|
176
181
|
// Never a prompt for credentials: a repository that needs them and has none fails.
|
|
177
182
|
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
178
183
|
timeout: 30_000,
|
|
@@ -182,6 +187,24 @@ export async function resolveRevision(url: string, ref: string): Promise<string>
|
|
|
182
187
|
cause: err,
|
|
183
188
|
});
|
|
184
189
|
});
|
|
190
|
+
return stdout;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The default branch of the repository at `url` (the branch its `HEAD` names) and the commit it names now. */
|
|
194
|
+
export async function defaultBranch(url: string): Promise<{ revision: string; ref: string }> {
|
|
195
|
+
const stdout = await lsRemote(url, ["HEAD"], ["--symref"]);
|
|
196
|
+
const branch = /^ref: refs\/heads\/(\S+)\tHEAD$/m.exec(stdout)?.[1];
|
|
197
|
+
const revision = /^([0-9a-f]{40,64})\tHEAD$/m.exec(stdout)?.[1];
|
|
198
|
+
if (!branch || !revision) {
|
|
199
|
+
throw new DriverError(`${url} names no default branch`, { retryable: false, vendorCode: "no_such_ref" });
|
|
200
|
+
}
|
|
201
|
+
return { revision, ref: branch };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The commit `ref` (a branch, a tag, `HEAD`, a full ref or a commit id) names in the repository at `url`. */
|
|
205
|
+
export async function resolveRevision(url: string, ref: string): Promise<string> {
|
|
206
|
+
if (COMMIT.test(ref)) return ref;
|
|
207
|
+
const stdout = await lsRemote(url, [ref, `${ref}^{}`]);
|
|
185
208
|
const refs = new Map(
|
|
186
209
|
stdout.split("\n").flatMap((line) => {
|
|
187
210
|
const [sha, name] = line.split("\t");
|
package/src/wakeup-type.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { defineResource } from "./resource.ts";
|
|
|
15
15
|
* The host types a wakeup may name: each one a data file declares a wakeup for. A new host type
|
|
16
16
|
* is added here. `testhost.app` is `@sanoma-ai/testing`'s, for the conformance kits.
|
|
17
17
|
*/
|
|
18
|
-
const HOSTS = ["cloudflare.container", "docker.container", "testhost.app"] as const;
|
|
18
|
+
const HOSTS = ["cloudflare.host", "cloudflare.container", "docker.container", "testhost.app"] as const;
|
|
19
19
|
|
|
20
20
|
/** An instance of the wakeup service, as `GET /wakeup/instances/{id}` shows it. */
|
|
21
21
|
const wakeupSchema = z.object({
|