@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/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/kms-secrets.ts
CHANGED
|
@@ -295,11 +295,40 @@ export function kmsPostgresStore(options: KmsPostgresOptions): SecretStore | Sec
|
|
|
295
295
|
capabilities,
|
|
296
296
|
validate: (namespace) =>
|
|
297
297
|
safe(async () => {
|
|
298
|
-
const address = { namespace, path:
|
|
299
|
-
version = randomUUID()
|
|
300
|
-
|
|
301
|
-
await
|
|
302
|
-
|
|
298
|
+
const address = { namespace, path: `__validation__/${randomUUID()}` },
|
|
299
|
+
version = randomUUID(),
|
|
300
|
+
value = randomUUID();
|
|
301
|
+
const envelope = await seal(address, value, version);
|
|
302
|
+
const db = await database();
|
|
303
|
+
const scope = [company, namespace, address.path, version];
|
|
304
|
+
let removed = false;
|
|
305
|
+
try {
|
|
306
|
+
const created = await db.query(
|
|
307
|
+
"INSERT INTO sanoma_kms.secrets(company,namespace,path,version,envelope,updated_by) VALUES($1,$2,$3,$4,$5,'validation') ON CONFLICT(company,namespace,path) DO NOTHING RETURNING version",
|
|
308
|
+
[...scope, JSON.stringify(envelope)],
|
|
309
|
+
);
|
|
310
|
+
if (created.rowCount !== 1) throw new SecretStoreError("conflict");
|
|
311
|
+
const updated = await db.query(
|
|
312
|
+
"UPDATE sanoma_kms.secrets SET envelope=$5,version=$4,updated_by='validation',updated_at=now() WHERE company=$1 AND namespace=$2 AND path=$3 AND version=$4 RETURNING version",
|
|
313
|
+
[...scope, JSON.stringify(envelope)],
|
|
314
|
+
);
|
|
315
|
+
if (updated.rowCount !== 1) throw new SecretStoreError("unavailable");
|
|
316
|
+
const stored = await db.query<Row>(
|
|
317
|
+
"SELECT version,envelope,updated_by,updated_at FROM sanoma_kms.secrets WHERE company=$1 AND namespace=$2 AND path=$3 AND version=$4",
|
|
318
|
+
scope,
|
|
319
|
+
);
|
|
320
|
+
if (!stored.rows[0] || (await open(address, stored.rows[0])).value !== value)
|
|
321
|
+
throw new SecretStoreError("invalid_value");
|
|
322
|
+
} finally {
|
|
323
|
+
// Also clean up a committed INSERT whose response was lost. The random address and
|
|
324
|
+
// exact version prevent one concurrent probe from deleting another probe or a secret.
|
|
325
|
+
const cleanup = await db.query(
|
|
326
|
+
"DELETE FROM sanoma_kms.secrets WHERE company=$1 AND namespace=$2 AND path=$3 AND version=$4",
|
|
327
|
+
scope,
|
|
328
|
+
);
|
|
329
|
+
removed = cleanup.rowCount === 1;
|
|
330
|
+
}
|
|
331
|
+
if (!removed) throw new SecretStoreError("unavailable");
|
|
303
332
|
// Own-key usability only. Cross-company authorization requires live IAM proof with another existing key.
|
|
304
333
|
}),
|
|
305
334
|
read: (address) =>
|
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`). */
|
|
@@ -209,7 +216,8 @@ export interface Declared<Id extends string = string, T = State> {
|
|
|
209
216
|
readonly connection?: ConnectionRef;
|
|
210
217
|
/**
|
|
211
218
|
* Where it names another resource: the field's dotted path (a list item's with its index,
|
|
212
|
-
* `teams.0`) to that resource's `<vendor>.<type>:<name
|
|
219
|
+
* `teams.0`) to that resource's `<vendor>.<type>:<name>`, or its `<vendor>.<type>` alone when it
|
|
220
|
+
* has no identity (then `desired` holds nothing there).
|
|
213
221
|
*/
|
|
214
222
|
readonly refs: Readonly<Record<string, string>>;
|
|
215
223
|
}
|
|
@@ -352,11 +360,20 @@ export interface Resource<
|
|
|
352
360
|
Required<
|
|
353
361
|
Omit<
|
|
354
362
|
ResourceSpec<S>,
|
|
355
|
-
|
|
363
|
+
| "identity"
|
|
364
|
+
| "find"
|
|
365
|
+
| "writable"
|
|
366
|
+
| "host"
|
|
367
|
+
| "wakeup"
|
|
368
|
+
| "database"
|
|
369
|
+
| "stateless"
|
|
370
|
+
| "afterCreate"
|
|
371
|
+
| "vendorless"
|
|
372
|
+
| "formerly"
|
|
356
373
|
>
|
|
357
374
|
>
|
|
358
375
|
>,
|
|
359
|
-
Readonly<Pick<ResourceSpec<S>, "identity" | "find" | "host" | "wakeup" | "database" | "afterCreate">> {
|
|
376
|
+
Readonly<Pick<ResourceSpec<S>, "identity" | "find" | "host" | "wakeup" | "database" | "afterCreate" | "formerly">> {
|
|
360
377
|
(
|
|
361
378
|
desired: Declaring<z.input<S>, R>,
|
|
362
379
|
options?: { connection?: ConnectionRef<Id extends `${infer V}.${string}` ? V : string> },
|
|
@@ -812,10 +829,16 @@ export function defineResource<
|
|
|
812
829
|
const types = [wants.type].flat();
|
|
813
830
|
if (!types.includes(id))
|
|
814
831
|
throw new Error(`${what}: ${at} names a resource of type ${types.join(" or ")}, not ${id} (${value.name})`);
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
832
|
+
// A resource with no identity (a Neon project) stands for nothing a vendor could read: only
|
|
833
|
+
// a field the runtime consumes by the reference itself may name it, a host's database
|
|
834
|
+
// (`refs`, `deriveHosts`) or one that takes its state (`from`); `desired` holds nothing there.
|
|
835
|
+
if (value.name === undefined && path !== host?.inputs.database && wants.from === undefined) {
|
|
836
|
+
throw new Error(
|
|
837
|
+
`${what}: ${at} names a ${id}, which has no identity to name it by: only a host's database or a \`from\` reference may`,
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
if (wants.from !== undefined || value.name === undefined) unjudged.add(at);
|
|
841
|
+
refs[at] = value.name === undefined ? id : `${id}:${value.name}`;
|
|
819
842
|
return value.name;
|
|
820
843
|
}
|
|
821
844
|
if (Array.isArray(value)) {
|
|
@@ -836,6 +859,20 @@ export function defineResource<
|
|
|
836
859
|
if (host && pathsIn(desired, new Set([host.revision])).length) {
|
|
837
860
|
throw new Error(`${what}: leave out ${host.revision}: apply sets it from ${host.inputs.ref}`);
|
|
838
861
|
}
|
|
862
|
+
// A host that derives is given its secrets and its sites' domains: a data file names neither.
|
|
863
|
+
if (host?.derives) {
|
|
864
|
+
const { secrets, domains } = host.inputs;
|
|
865
|
+
if (secrets !== undefined && pathsIn(desired, new Set([secrets])).length) {
|
|
866
|
+
throw new Error(
|
|
867
|
+
`${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`,
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
if (domains !== undefined && pathsIn(desired, new Set([domains])).length) {
|
|
871
|
+
throw new Error(
|
|
872
|
+
`${what}: leave out ${domains}: a public site's domain is declared on the site, \`defineSite({ public: true, domain })\`, and the host attaches it`,
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
839
876
|
const vendors = pathsIn(desired, owned);
|
|
840
877
|
if (vendors.length) {
|
|
841
878
|
throw new Error(
|
|
@@ -874,6 +911,7 @@ export function defineResource<
|
|
|
874
911
|
stateless: spec.stateless ?? host !== undefined,
|
|
875
912
|
vendorless: spec.vendorless ?? false,
|
|
876
913
|
...(spec.afterCreate && { afterCreate: spec.afterCreate }),
|
|
914
|
+
...(spec.formerly?.length && { formerly: Object.freeze([...spec.formerly]) }),
|
|
877
915
|
ops,
|
|
878
916
|
// A symbol, so `defineConnector` tells a resource type from a group of operations.
|
|
879
917
|
[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({
|