@intentius/chant 0.124.14 → 0.124.15
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/dist/build.d.ts +6 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +1 -0
- package/dist/change-set.d.ts +24 -2
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +50 -1
- package/dist/ci-jobs.d.ts +227 -0
- package/dist/ci-jobs.d.ts.map +1 -0
- package/dist/ci-jobs.js +448 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +3 -2
- package/dist/composite.d.ts +10 -2
- package/dist/composite.d.ts.map +1 -1
- package/dist/composite.js +8 -0
- package/dist/discovery/collect.d.ts +2 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/collect.js +16 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-import.js +2 -107
- package/dist/discovery/host-composite.d.ts +35 -0
- package/dist/discovery/host-composite.d.ts.map +1 -0
- package/dist/discovery/host-composite.js +93 -0
- package/dist/discovery/index.d.ts +9 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +1 -1
- package/dist/gate-decision.d.ts +20 -9
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +72 -19
- package/dist/lifecycle/gate-audit.d.ts +69 -2
- package/dist/lifecycle/gate-audit.d.ts.map +1 -1
- package/dist/lifecycle/gate-audit.js +76 -2
- package/dist/lifecycle/gate-ledger.d.ts +24 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.js +9 -1
- package/dist/op/activities/activity-contracts.d.ts +1 -1
- package/dist/op/activities/activity-contracts.js +1 -1
- package/dist/op/activities/change-set.d.ts +8 -3
- package/dist/op/activities/change-set.d.ts.map +1 -1
- package/dist/op/activities/change-set.js +5 -2
- package/dist/op/activities/reconcile.d.ts +28 -16
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/activities/reconcile.js +34 -20
- package/dist/op/builders.d.ts +1 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +1 -1
- package/dist/op/op-waves-run.d.ts +1 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +3 -3
- package/dist/workspace/__fixtures__/contract-repo.d.ts +7 -0
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.js +17 -1
- package/dist/workspace/__fixtures__/sessions.d.ts +1 -1
- package/dist/workspace/__fixtures__/sessions.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/sessions.js +3 -3
- package/dist/workspace/change-set.schema.json +5 -3
- package/dist/workspace/ci-green-workflow.d.ts +3 -1
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +5 -2
- package/dist/workspace/class-data.d.ts +84 -0
- package/dist/workspace/class-data.d.ts.map +1 -0
- package/dist/workspace/class-data.js +238 -0
- package/dist/workspace/kinds.d.ts +3 -7
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/kinds.js +6 -40
- package/dist/workspace/principal-classes.d.ts +4 -11
- package/dist/workspace/principal-classes.d.ts.map +1 -1
- package/dist/workspace/principal-classes.js +11 -94
- package/package.json +1 -1
- package/src/build.ts +8 -0
- package/src/change-set.test.ts +48 -0
- package/src/change-set.ts +52 -2
- package/src/ci-jobs.test.ts +49 -0
- package/src/ci-jobs.ts +515 -0
- package/src/components/pr-pipeline.ts +3 -2
- package/src/composite.ts +15 -3
- package/src/discovery/collect.ts +15 -1
- package/src/discovery/fold-build-parity.test.ts +71 -0
- package/src/discovery/fold-import.ts +2 -113
- package/src/discovery/fold-project-provenance.test.ts +21 -4
- package/src/discovery/host-composite.ts +101 -0
- package/src/discovery/index.ts +11 -1
- package/src/gate-decision-bundle.test.ts +5 -4
- package/src/gate-decision-identity.test.ts +59 -6
- package/src/gate-decision.ts +81 -21
- package/src/lifecycle/gate-audit.test.ts +55 -1
- package/src/lifecycle/gate-audit.ts +116 -3
- package/src/lifecycle/gate-ledger.test.ts +20 -1
- package/src/lifecycle/gate-ledger.ts +33 -1
- package/src/op/activities/activity-contracts.ts +1 -1
- package/src/op/activities/change-set.ts +11 -3
- package/src/op/activities/lexicon-upgrade.test.ts +2 -0
- package/src/op/activities/reconcile.test.ts +62 -2
- package/src/op/activities/reconcile.ts +46 -22
- package/src/op/builders.ts +1 -1
- package/src/op/op-waves-run.ts +4 -3
- package/src/op/op-waves.test.ts +2 -0
- package/src/op/work-lease-run.test.ts +12 -4
- package/src/workspace/__fixtures__/contract-repo.ts +17 -1
- package/src/workspace/__fixtures__/sessions.ts +3 -3
- package/src/workspace/change-set.schema.json +5 -3
- package/src/workspace/ci-green-workflow.test.ts +4 -4
- package/src/workspace/ci-green-workflow.ts +5 -2
- package/src/workspace/class-data.test.ts +64 -0
- package/src/workspace/class-data.ts +236 -0
- package/src/workspace/kinds.ts +6 -35
- package/src/workspace/principal-classes.ts +11 -94
- package/src/workspace/records-formats.test.ts +7 -2
- package/src/workspace/records-sessions-write.test.ts +3 -3
- package/src/workspace/status-contract.e2e.test.ts +37 -0
- package/src/workspace/status-contract.test.ts +23 -21
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kinds.d.ts","sourceRoot":"","sources":["../../src/workspace/kinds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"kinds.d.ts","sourceRoot":"","sources":["../../src/workspace/kinds.ts"],"names":[],"mappings":"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"}
|
package/dist/workspace/kinds.js
CHANGED
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
* kinds claim one directory, the highest precedence decides, and a tie
|
|
17
17
|
* fails ({@link resolveKind}).
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
20
|
import { createRequire } from "node:module";
|
|
21
|
-
import {
|
|
21
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
22
22
|
import schema from "./workspace-kinds.schema.json" with { type: "json" };
|
|
23
23
|
import { joinPath, skippedDir } from "./tree.js";
|
|
24
24
|
import { checkPinIntegrity } from "./pin-integrity.js";
|
|
25
|
+
import { dataExportTarget, findInstalledPackage } from "./class-data.js";
|
|
25
26
|
export const KINDS_SCHEMA_ID = schema.$id;
|
|
26
27
|
/** The subpath a package publishes its kinds at. */
|
|
27
28
|
export const KINDS_SUBPATH = "./workspace-kinds";
|
|
@@ -289,24 +290,7 @@ export function parseKindData(text, source) {
|
|
|
289
290
|
* subpath to code, which chant never runs for plugin data.
|
|
290
291
|
*/
|
|
291
292
|
export function kindsExportTarget(pkg, subpath = KINDS_SUBPATH) {
|
|
292
|
-
|
|
293
|
-
if (!exports || typeof exports !== "object" || Array.isArray(exports))
|
|
294
|
-
return undefined;
|
|
295
|
-
const entry = exports[subpath];
|
|
296
|
-
if (entry === undefined)
|
|
297
|
-
return undefined;
|
|
298
|
-
let target = entry;
|
|
299
|
-
if (target && typeof target === "object" && !Array.isArray(target)) {
|
|
300
|
-
const conditions = target;
|
|
301
|
-
target = conditions.default ?? conditions.import ?? conditions.require;
|
|
302
|
-
}
|
|
303
|
-
if (typeof target !== "string") {
|
|
304
|
-
return { problem: `exports["${subpath}"] must name one file, as a string or a "default" condition` };
|
|
305
|
-
}
|
|
306
|
-
if (!target.startsWith("./") || !target.endsWith(".json")) {
|
|
307
|
-
return { problem: `exports["${subpath}"] is ${target}; it must name a .json file inside the package, since chant reads plugin data as data and never runs a package's code` };
|
|
308
|
-
}
|
|
309
|
-
return target;
|
|
293
|
+
return dataExportTarget(pkg, subpath);
|
|
310
294
|
}
|
|
311
295
|
/** Read the kinds a package directory publishes. Reads files only; imports nothing. */
|
|
312
296
|
export function readPackageKinds(packageDir, source) {
|
|
@@ -369,26 +353,8 @@ export function substituteGraphConfig(config, at) {
|
|
|
369
353
|
};
|
|
370
354
|
return walk(config);
|
|
371
355
|
}
|
|
372
|
-
/**
|
|
373
|
-
|
|
374
|
-
* bare import: `node_modules/<name>` in `fromDir` and each directory above it.
|
|
375
|
-
* It looks for the directory, not for an entry point, so it works for a
|
|
376
|
-
* package whose `exports` don't expose `package.json`.
|
|
377
|
-
*/
|
|
378
|
-
export function findInstalledPackage(name, fromDir) {
|
|
379
|
-
for (let dir = resolve(fromDir);; dir = dirname(dir)) {
|
|
380
|
-
const candidate = join(dir, "node_modules", ...name.split("/"));
|
|
381
|
-
try {
|
|
382
|
-
if (statSync(candidate).isDirectory() && existsSync(join(candidate, "package.json")))
|
|
383
|
-
return candidate;
|
|
384
|
-
}
|
|
385
|
-
catch {
|
|
386
|
-
// Not here; keep walking.
|
|
387
|
-
}
|
|
388
|
-
if (dirname(dir) === dir)
|
|
389
|
-
return undefined;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
356
|
+
/** Where an installed package is, as Node finds a bare import (`./class-data.ts`, so the gate-decision bundle shares it). */
|
|
357
|
+
export { findInstalledPackage };
|
|
392
358
|
/**
|
|
393
359
|
* The directory a pin's plugin data is read from: an installed package at
|
|
394
360
|
* the pinned version, looked up from `workspaceRoot`, or a path pin's
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
* editing its own copy of the plugin. A package pin is read from the
|
|
22
22
|
* installed package, which must be at the version the base pins.
|
|
23
23
|
*/
|
|
24
|
+
import { type ClassData, type PackageFiles } from "./class-data.js";
|
|
24
25
|
import { type KindLoadProblem, type PinLike } from "./kinds.js";
|
|
25
26
|
import type { TrustPolicy } from "./trust/policy.js";
|
|
26
27
|
import { type WorkspaceTree } from "./tree.js";
|
|
27
28
|
import { type PrincipalClass } from "./identity-gate-rule.js";
|
|
28
29
|
export declare const PRINCIPALS_SCHEMA_ID: string;
|
|
29
|
-
|
|
30
|
-
export declare const PRINCIPALS_SUBPATH = "./workspace-principals";
|
|
30
|
+
export { PRINCIPALS_SUBPATH } from "./class-data.js";
|
|
31
31
|
export { CORE_CLASSES, CORE_PRINCIPAL_CLASSES, type CorePrincipalClass, type PrincipalClass } from "./identity-gate-rule.js";
|
|
32
32
|
/** A class name: a core class, or a domain class a pinned package supplies. */
|
|
33
33
|
export type PrincipalClassName = string;
|
|
@@ -57,11 +57,8 @@ export declare function roleGrants(policy: TrustPolicy): Record<string, string[]
|
|
|
57
57
|
export declare function classesOf(classes: ClassRegistry, policy: TrustPolicy, principal: string | null): string[];
|
|
58
58
|
/** The class write scope judges `principal` as: the first class its grants put it in, or `human`. */
|
|
59
59
|
export declare function principalClass(policy: TrustPolicy, principal: string | null, classes?: ClassRegistry): string;
|
|
60
|
-
export
|
|
61
|
-
|
|
62
|
-
/** Why some of the data can't be used, one line each. Empty when all of it can. */
|
|
63
|
-
problems: string[];
|
|
64
|
-
}
|
|
60
|
+
export type { ClassData, PackageFiles } from "./class-data.js";
|
|
61
|
+
export { diskFiles } from "./class-data.js";
|
|
65
62
|
/**
|
|
66
63
|
* Check the text of a principals file against
|
|
67
64
|
* `workspace-principals.schema.json` and the rules the schema can't say: no
|
|
@@ -69,10 +66,6 @@ export interface ClassData {
|
|
|
69
66
|
* in the classes and in the messages.
|
|
70
67
|
*/
|
|
71
68
|
export declare function parseClassData(text: string, source: string): ClassData;
|
|
72
|
-
/** A package's files, by a `/`-separated path inside it, or undefined when one is not there. */
|
|
73
|
-
export type PackageFiles = (path: string) => string | undefined;
|
|
74
|
-
/** A package directory on disk. */
|
|
75
|
-
export declare function diskFiles(dir: string): PackageFiles;
|
|
76
69
|
/** A package directory at `dir` in `tree`, such as the workspace at the base revision. */
|
|
77
70
|
export declare function treeFiles(tree: WorkspaceTree, dir: string): PackageFiles;
|
|
78
71
|
/** Read the classes a package publishes at `./workspace-principals`. Reads files only; imports nothing. A package that publishes none supplies none. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"principal-classes.d.ts","sourceRoot":"","sources":["../../src/workspace/principal-classes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;
|
|
1
|
+
{"version":3,"file":"principal-classes.d.ts","sourceRoot":"","sources":["../../src/workspace/principal-classes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EAA8D,KAAK,SAAS,EAAE,KAAK,YAAY,EAAqB,MAAM,cAAc,CAAC;AAChJ,OAAO,EAAoB,KAAK,eAAe,EAAE,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAE/E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,EAAwD,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEjH,eAAO,MAAM,oBAAoB,QAAa,CAAC;AAE/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,EAAE,YAAY,EAAE,sBAAsB,EAAE,KAAK,kBAAkB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE1H,+EAA+E;AAC/E,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAExC,uHAAuH;AACvH,eAAO,MAAM,UAAU,QAAiC,CAAC;AAEzD,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAAC;IAC9C,kEAAkE;IAClE,KAAK,IAAI,MAAM,EAAE,CAAC;IAClB,oFAAoF;IACpF,GAAG,IAAI,SAAS,cAAc,EAAE,CAAC;CAClC;AAED,wBAAgB,mBAAmB,CAAC,KAAK,GAAE,SAAS,cAAc,EAAO,GAAG,aAAa,CAQxF;AAED,+FAA+F;AAC/F,wBAAgB,iBAAiB,IAAI,aAAa,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAExE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,EAAE,CAEzG;AAED,qGAAqG;AACrG,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,GAAE,aAAmC,GAAG,MAAM,CAElI;AAqBD,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,CAatE;AAED,0FAA0F;AAC1F,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CAUxE;AAED,wJAAwJ;AACxJ,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,CAEjF;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,aAAa,CAAC;IACxB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,aAAa,CAAA;CAAO,GAAG,aAAa,CAuBrI;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,aAAa,GAAG,MAAM,EAAE,CAEhH"}
|
|
@@ -21,20 +21,17 @@
|
|
|
21
21
|
* editing its own copy of the plugin. A package pin is read from the
|
|
22
22
|
* installed package, which must be at the version the base pins.
|
|
23
23
|
*/
|
|
24
|
-
import { readFileSync } from "node:fs";
|
|
25
24
|
import { createRequire } from "node:module";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
25
|
+
import { checkClassData, diskFiles, mergePinClasses, readClassFiles } from "./class-data.js";
|
|
26
|
+
import { pinnedPackageDir } from "./kinds.js";
|
|
28
27
|
import schema from "./workspace-principals.schema.json" with { type: "json" };
|
|
29
28
|
import { joinPath } from "./tree.js";
|
|
30
|
-
import { CORE_CLASSES,
|
|
29
|
+
import { CORE_CLASSES, grantedClasses } from "./identity-gate-rule.js";
|
|
31
30
|
export const PRINCIPALS_SCHEMA_ID = schema.$id;
|
|
32
|
-
|
|
33
|
-
export const PRINCIPALS_SUBPATH = "./workspace-principals";
|
|
31
|
+
export { PRINCIPALS_SUBPATH } from "./class-data.js";
|
|
34
32
|
export { CORE_CLASSES, CORE_PRINCIPAL_CLASSES } from "./identity-gate-rule.js";
|
|
35
33
|
/** The grammar of a class name, as `workspace-principals.schema.json` and the declaration's writeScope keys say it. */
|
|
36
34
|
export const CLASS_NAME = /^(?!x-)[a-z][a-z0-9-]{0,39}$/;
|
|
37
|
-
const CORE_ROLES = new Set(CORE_CLASSES.map((c) => c.role).filter((r) => r !== null));
|
|
38
35
|
export function createClassRegistry(extra = []) {
|
|
39
36
|
const all = [...CORE_CLASSES, ...extra];
|
|
40
37
|
const byName = new Map(all.map((c) => [c.name, c]));
|
|
@@ -79,6 +76,7 @@ function validator() {
|
|
|
79
76
|
compiled = new Ajv({ allErrors: true, strict: true }).compile(schema);
|
|
80
77
|
return compiled;
|
|
81
78
|
}
|
|
79
|
+
export { diskFiles } from "./class-data.js";
|
|
82
80
|
/**
|
|
83
81
|
* Check the text of a principals file against
|
|
84
82
|
* `workspace-principals.schema.json` and the rules the schema can't say: no
|
|
@@ -98,43 +96,7 @@ export function parseClassData(text, source) {
|
|
|
98
96
|
const problems = (validate.errors ?? []).map((e) => `${source}: ${e.instancePath || "the principals file"} ${e.message ?? "is invalid"}`);
|
|
99
97
|
return { classes: [], problems: [...new Set(problems)] };
|
|
100
98
|
}
|
|
101
|
-
|
|
102
|
-
const classes = [];
|
|
103
|
-
const names = new Set();
|
|
104
|
-
const roles = new Set();
|
|
105
|
-
for (const c of raw.classes) {
|
|
106
|
-
if (CORE_PRINCIPAL_CLASSES.includes(c.name)) {
|
|
107
|
-
problems.push(`${source}: class ${c.name} is a core class and can't be supplied by a package`);
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if (CORE_ROLES.has(c.role)) {
|
|
111
|
-
problems.push(`${source}: class ${c.name} names the role ${c.role}, which puts a principal in the core ${c.role} class`);
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
if (names.has(c.name)) {
|
|
115
|
-
problems.push(`${source}: class ${c.name} is listed twice`);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (roles.has(c.role)) {
|
|
119
|
-
problems.push(`${source}: class ${c.name} names the role ${c.role}, which another class in the file names; a role puts a principal in one class`);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
names.add(c.name);
|
|
123
|
-
roles.add(c.role);
|
|
124
|
-
classes.push({ name: c.name, description: c.description, role: c.role, source });
|
|
125
|
-
}
|
|
126
|
-
return { classes, problems };
|
|
127
|
-
}
|
|
128
|
-
/** A package directory on disk. */
|
|
129
|
-
export function diskFiles(dir) {
|
|
130
|
-
return (path) => {
|
|
131
|
-
try {
|
|
132
|
-
return readFileSync(join(dir, ...path.split("/")), "utf-8");
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
};
|
|
99
|
+
return checkClassData(raw, source);
|
|
138
100
|
}
|
|
139
101
|
/** A package directory at `dir` in `tree`, such as the workspace at the base revision. */
|
|
140
102
|
export function treeFiles(tree, dir) {
|
|
@@ -152,28 +114,7 @@ export function treeFiles(tree, dir) {
|
|
|
152
114
|
}
|
|
153
115
|
/** Read the classes a package publishes at `./workspace-principals`. Reads files only; imports nothing. A package that publishes none supplies none. */
|
|
154
116
|
export function readPackageClasses(files, source) {
|
|
155
|
-
|
|
156
|
-
if (manifest === undefined)
|
|
157
|
-
return { classes: [], problems: [`${source}: no readable package.json`] };
|
|
158
|
-
let pkg;
|
|
159
|
-
try {
|
|
160
|
-
pkg = JSON.parse(manifest);
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
return { classes: [], problems: [`${source}: package.json is not JSON`] };
|
|
164
|
-
}
|
|
165
|
-
const target = kindsExportTarget(pkg, PRINCIPALS_SUBPATH);
|
|
166
|
-
if (target === undefined)
|
|
167
|
-
return { classes: [], problems: [] };
|
|
168
|
-
if (typeof target !== "string")
|
|
169
|
-
return { classes: [], problems: [`${source}: ${target.problem}`] };
|
|
170
|
-
const inside = posix.normalize(target.slice(2));
|
|
171
|
-
if (inside.startsWith("../") || inside === "..")
|
|
172
|
-
return { classes: [], problems: [`${source}: exports["${PRINCIPALS_SUBPATH}"] points outside the package`] };
|
|
173
|
-
const text = files(inside);
|
|
174
|
-
if (text === undefined)
|
|
175
|
-
return { classes: [], problems: [`${source}: exports["${PRINCIPALS_SUBPATH}"] names ${target}, which does not exist`] };
|
|
176
|
-
return parseClassData(text, source);
|
|
117
|
+
return readClassFiles(files, source, parseClassData);
|
|
177
118
|
}
|
|
178
119
|
/**
|
|
179
120
|
* The classes a workspace knows: the core four plus every class its pins
|
|
@@ -187,10 +128,7 @@ export function readPackageClasses(files, source) {
|
|
|
187
128
|
*/
|
|
188
129
|
export function loadClassRegistry(pins, workspaceRoot, opts = {}) {
|
|
189
130
|
const problems = [];
|
|
190
|
-
const
|
|
191
|
-
const byName = new Map();
|
|
192
|
-
const byRole = new Map();
|
|
193
|
-
const clashing = new Set();
|
|
131
|
+
const reads = [];
|
|
194
132
|
pins.forEach((pin, i) => {
|
|
195
133
|
let files;
|
|
196
134
|
let source;
|
|
@@ -209,31 +147,10 @@ export function loadClassRegistry(pins, workspaceRoot, opts = {}) {
|
|
|
209
147
|
files = diskFiles(found.dir);
|
|
210
148
|
source = found.source;
|
|
211
149
|
}
|
|
212
|
-
|
|
213
|
-
for (const p of read.problems)
|
|
214
|
-
problems.push({ pin: i, message: p });
|
|
215
|
-
for (const c of read.classes) {
|
|
216
|
-
const named = byName.get(c.name);
|
|
217
|
-
const roled = byRole.get(c.role);
|
|
218
|
-
if (named !== undefined || roled !== undefined) {
|
|
219
|
-
clashing.add(c.name);
|
|
220
|
-
if (named !== undefined) {
|
|
221
|
-
problems.push({ pin: i, message: `class ${c.name} is supplied by both ${named} and ${source}; a class name has one source` });
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
const other = extra.find((e) => e.role === c.role);
|
|
225
|
-
clashing.add(other.name);
|
|
226
|
-
problems.push({ pin: i, message: `class ${c.name} (${source}) names the role ${c.role}, which class ${other.name} (${roled}) names; a role puts a principal in one class` });
|
|
227
|
-
}
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
byName.set(c.name, source);
|
|
231
|
-
byRole.set(c.role, source);
|
|
232
|
-
extra.push(c);
|
|
233
|
-
}
|
|
150
|
+
reads.push({ pin: i, source, read: readPackageClasses(files, source) });
|
|
234
151
|
});
|
|
235
|
-
|
|
236
|
-
return { registry: createClassRegistry(
|
|
152
|
+
const merged = mergePinClasses(reads);
|
|
153
|
+
return { registry: createClassRegistry(merged.classes), problems: [...problems, ...merged.problems].sort((a, b) => a.pin - b.pin) };
|
|
237
154
|
}
|
|
238
155
|
/**
|
|
239
156
|
* The writeScope keys no class in `classes` has, in file order. A writer
|
package/package.json
CHANGED
package/src/build.ts
CHANGED
|
@@ -232,6 +232,13 @@ export interface BuildOptions {
|
|
|
232
232
|
*/
|
|
233
233
|
lexicons?: readonly string[];
|
|
234
234
|
|
|
235
|
+
/**
|
|
236
|
+
* chant#3612 — host package specifiers to follow a bare import into. Passed
|
|
237
|
+
* straight through to {@link DiscoveryOptions.lexiconPackages}; ignored
|
|
238
|
+
* unless {@link fold} is set. Never needed for a chant project.
|
|
239
|
+
*/
|
|
240
|
+
lexiconPackages?: readonly string[];
|
|
241
|
+
|
|
235
242
|
/**
|
|
236
243
|
* chant #1442 — lexicon name → the version of the plugin that served it.
|
|
237
244
|
* Recorded on {@link BuildResult.lexiconVersions} so a build digest can say
|
|
@@ -723,6 +730,7 @@ export async function build(
|
|
|
723
730
|
fold: options?.fold,
|
|
724
731
|
intrinsics: options?.intrinsics,
|
|
725
732
|
lexicons: options?.lexicons,
|
|
733
|
+
lexiconPackages: options?.lexiconPackages,
|
|
726
734
|
sandbox: options?.sandbox,
|
|
727
735
|
executing: options?.executing,
|
|
728
736
|
buildParams: options?.buildParams,
|
package/src/change-set.test.ts
CHANGED
|
@@ -23,11 +23,13 @@ import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
23
23
|
import { join } from "node:path";
|
|
24
24
|
import { describe, expect, test } from "vitest";
|
|
25
25
|
import {
|
|
26
|
+
CHANGE_SET_PLANNERS,
|
|
26
27
|
CHANGE_SET_SCHEMA_ID,
|
|
27
28
|
changeSetDigest,
|
|
28
29
|
changeSetDocumentDigest,
|
|
29
30
|
composeChangeSet,
|
|
30
31
|
lifecyclePlanPart,
|
|
32
|
+
readChangeSetPartFile,
|
|
31
33
|
reconcilePlanPart,
|
|
32
34
|
verifyChangeSetDigest,
|
|
33
35
|
type ChangeSetPart,
|
|
@@ -243,3 +245,49 @@ describe("the digest", () => {
|
|
|
243
245
|
expect(doc.summary.replacements.map((d) => `${d.member}:${d.address}`)).toEqual(["estate:terraform_data.worker"]);
|
|
244
246
|
});
|
|
245
247
|
});
|
|
248
|
+
|
|
249
|
+
describe("readChangeSetPartFile (#3767)", () => {
|
|
250
|
+
/** A part as the sql lexicon's adapter writes it; `lexicons/sql/src/change-set.test.ts` pins the adapter itself. */
|
|
251
|
+
const sqlPart = (): ChangeSetPart => ({
|
|
252
|
+
member: { member: "written-as", lexicon: "sql", planner: "sql", scope: "prod", status: "planned", planDigest: computePlanDigest("sql-versioned-plan", { id: "0002" }), holes: [] },
|
|
253
|
+
entries: [
|
|
254
|
+
{ member: "written-as", lexicon: "sql", planner: "sql", address: "analytics.events", type: "ClickHouse::Table", action: "update", disruption: "rolling", scope: "prod", attributes: [{ path: "sql", after: ["ALTER TABLE analytics.events ADD COLUMN region String"] }] },
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("the schema lists every planner, sql included", () => {
|
|
259
|
+
const planners = (schema as { $defs: { member: { properties: { planner: { enum: string[] } } }; entry: { properties: { planner: { enum: string[] } } } } }).$defs;
|
|
260
|
+
expect(planners.member.properties.planner.enum).toEqual([...CHANGE_SET_PLANNERS]);
|
|
261
|
+
expect(planners.entry.properties.planner.enum).toEqual([...CHANGE_SET_PLANNERS]);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("reads a written part back as the run's member, through JSON, and composes it", () => {
|
|
265
|
+
const part = readChangeSetPartFile(JSON.parse(JSON.stringify(sqlPart())), "sql", "warehouse");
|
|
266
|
+
expect(part.member.member).toBe("warehouse");
|
|
267
|
+
expect(part.entries.map((e) => e.member)).toEqual(["warehouse"]);
|
|
268
|
+
expect(part.member.planDigest).toBe(sqlPart().member.planDigest);
|
|
269
|
+
expect(readChangeSetPartFile(sqlPart(), "sql", "warehouse", "clickhouse").entries[0].lexicon).toBe("clickhouse");
|
|
270
|
+
const doc = composeChangeSet([part, reconcilePlanPart({ member: "warden", plan: wardenPlan() })]);
|
|
271
|
+
expectValid(doc);
|
|
272
|
+
expect(doc.summary.byMember.warehouse).toEqual({ update: 1 });
|
|
273
|
+
// The name the run gives it is what the set digest pairs with the writer's plan digest.
|
|
274
|
+
expect(composeChangeSet([readChangeSetPartFile(sqlPart(), "sql", "other")]).digest).not.toBe(composeChangeSet([part]).digest);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("refuses what is not a part of that planner", () => {
|
|
278
|
+
const p = sqlPart();
|
|
279
|
+
expect(() => readChangeSetPartFile([], "sql", "w")).toThrow(/not a change-set part/);
|
|
280
|
+
expect(() => readChangeSetPartFile({ ...p, member: { ...p.member, planner: "chant" } }, "sql", "w")).toThrow(/names planner chant, not sql/);
|
|
281
|
+
expect(() => readChangeSetPartFile({ ...p, member: { ...p.member, planDigest: null } }, "sql", "w")).toThrow(/planned but has no planDigest/);
|
|
282
|
+
expect(() => readChangeSetPartFile({ ...p, member: { ...p.member, holes: undefined } }, "sql", "w")).toThrow(/no holes array/);
|
|
283
|
+
expect(() => readChangeSetPartFile({ ...p, entries: [{ ...p.entries[0], planner: "terraform" }] }, "sql", "w")).toThrow(/entries\[0\] names planner terraform/);
|
|
284
|
+
expect(() => readChangeSetPartFile({ ...p, entries: [{ ...p.entries[0], action: "upsert" }] }, "sql", "w")).toThrow(/entries\[0\] has action upsert/);
|
|
285
|
+
expect(() => readChangeSetPartFile({ ...p, entries: [{ address: "x" }] }, "sql", "w")).toThrow(/entries\[0\] needs address/);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("a failed member keeps a null digest", () => {
|
|
289
|
+
const p = sqlPart();
|
|
290
|
+
const failed = readChangeSetPartFile({ ...p, member: { ...p.member, status: "failed", error: "out of order", planDigest: null } }, "sql", "w");
|
|
291
|
+
expect(failed.member).toMatchObject({ status: "failed", error: "out of order", planDigest: null });
|
|
292
|
+
});
|
|
293
|
+
});
|
package/src/change-set.ts
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* Each planner reaches it through an adapter that returns a
|
|
12
12
|
* {@link ChangeSetPart}: one member and its entries. The lifecycle and
|
|
13
13
|
* reconcile adapters are here; the terraform-family ones live in the
|
|
14
|
-
* terraform lexicon's `change-set` subpath
|
|
14
|
+
* terraform lexicon's `change-set` subpath and the SQL ones in the sql
|
|
15
|
+
* lexicon's. A part another process wrote to a file reads back through
|
|
16
|
+
* {@link readChangeSetPartFile}. {@link composeChangeSet} joins
|
|
15
17
|
* the parts.
|
|
16
18
|
*
|
|
17
19
|
* ## The digest
|
|
@@ -60,7 +62,15 @@ export const CHANGE_SET_ACTIONS: readonly ChangeSetAction[] = ["create", "update
|
|
|
60
62
|
export type ChangeSetDisruption = "in-place" | "rolling" | "replace" | "destroy" | "unknown";
|
|
61
63
|
|
|
62
64
|
/** Which tool produced a member's plan. */
|
|
63
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Which tool produced a member's plan. `sql` is a SQL schema change: a
|
|
67
|
+
* versioned plan (a product's migrations) or one of the sql lexicon's
|
|
68
|
+
* migration Ops' plans, through `@intentius/chant-lexicon-sql/change-set`.
|
|
69
|
+
*/
|
|
70
|
+
export type ChangeSetPlanner = "terraform" | "tofu" | "choudoufu" | "chant" | "warden" | "sql";
|
|
71
|
+
|
|
72
|
+
/** Every planner, in the order the schema lists them. */
|
|
73
|
+
export const CHANGE_SET_PLANNERS: readonly ChangeSetPlanner[] = ["terraform", "tofu", "choudoufu", "chant", "warden", "sql"];
|
|
64
74
|
|
|
65
75
|
/** One attribute a change writes, at top-level granularity. */
|
|
66
76
|
export interface ChangeSetAttribute {
|
|
@@ -482,3 +492,43 @@ export function reconcilePlanPart(input: ReconcilePlanPartInput): ChangeSetPart
|
|
|
482
492
|
entries,
|
|
483
493
|
};
|
|
484
494
|
}
|
|
495
|
+
|
|
496
|
+
// ── a part written to a file ───────────────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* A part an adapter in another process wrote as JSON, read back as `member`'s
|
|
502
|
+
* part. The `sql` planner's plans arrive this way: the sql lexicon's
|
|
503
|
+
* `change-set` adapters, or a product that fills them, write the part, and
|
|
504
|
+
* `readChangeSetPart` with `planner: "sql"` reads it.
|
|
505
|
+
*
|
|
506
|
+
* The part must name `planner` on its member and on every entry, and say
|
|
507
|
+
* `status`, `planDigest` and `holes`. Its member and entries are renamed to
|
|
508
|
+
* `member`, so the file need not know the name the run gives it; a `lexicon`
|
|
509
|
+
* given here replaces the part's. Nothing is recomputed: the plan digest is
|
|
510
|
+
* the one the writer took, over a plan this process never sees.
|
|
511
|
+
*/
|
|
512
|
+
export function readChangeSetPartFile(part: unknown, planner: ChangeSetPlanner, member: string, lexicon?: string): ChangeSetPart {
|
|
513
|
+
const where = `the ${planner} part for ${member}`;
|
|
514
|
+
if (!isRecord(part) || !isRecord(part.member) || !Array.isArray(part.entries)) throw new Error(`${where} is not a change-set part: it needs member and entries`);
|
|
515
|
+
const m = part.member;
|
|
516
|
+
if (m.planner !== planner) throw new Error(`${where} names planner ${String(m.planner)}, not ${planner}`);
|
|
517
|
+
if (m.status !== "planned" && m.status !== "failed") throw new Error(`${where} has no status`);
|
|
518
|
+
if (m.planDigest !== null && typeof m.planDigest !== "string") throw new Error(`${where} has no planDigest`);
|
|
519
|
+
if (m.status === "planned" && m.planDigest === null) throw new Error(`${where} planned but has no planDigest`);
|
|
520
|
+
if (!Array.isArray(m.holes)) throw new Error(`${where} has no holes array`);
|
|
521
|
+
const entries = part.entries.map((e, i) => {
|
|
522
|
+
if (!isRecord(e) || typeof e.address !== "string" || typeof e.type !== "string" || typeof e.action !== "string" || !Array.isArray(e.attributes)) {
|
|
523
|
+
throw new Error(`${where}: entries[${i}] needs address, type, action and attributes`);
|
|
524
|
+
}
|
|
525
|
+
if (!(CHANGE_SET_ACTIONS as readonly string[]).includes(e.action)) throw new Error(`${where}: entries[${i}] has action ${e.action}`);
|
|
526
|
+
if (e.planner !== planner) throw new Error(`${where}: entries[${i}] names planner ${String(e.planner)}, not ${planner}`);
|
|
527
|
+
return { ...(e as unknown as ChangeSetEntry), member, ...(lexicon !== undefined ? { lexicon } : {}) };
|
|
528
|
+
});
|
|
529
|
+
return {
|
|
530
|
+
member: { ...(m as unknown as ChangeSetMember), member, ...(lexicon !== undefined ? { lexicon } : {}) },
|
|
531
|
+
entries,
|
|
532
|
+
...(Array.isArray(part.sideEffects) ? { sideEffects: part.sideEffects as ChangeSetPart["sideEffects"] } : {}),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
assertGitlabBranch,
|
|
4
|
+
CI_IMAGE,
|
|
5
|
+
gitlabRules,
|
|
6
|
+
githubSecret,
|
|
7
|
+
githubSecretEnv,
|
|
8
|
+
githubSecretOrJobToken,
|
|
9
|
+
pushBranches,
|
|
10
|
+
triggerBranches,
|
|
11
|
+
} from "./ci-jobs";
|
|
12
|
+
import { PR_LOOP_IMAGE } from "./components/pr-pipeline";
|
|
13
|
+
|
|
14
|
+
describe("the job primitives the CI generators share (#3765)", () => {
|
|
15
|
+
test("one default image, which the pull-request pipeline uses too", () => {
|
|
16
|
+
expect(CI_IMAGE).toBe("node:22");
|
|
17
|
+
expect(PR_LOOP_IMAGE).toBe(CI_IMAGE);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("a push trigger with no branches, or an empty list, filters on main; a pull_request trigger then takes every branch", () => {
|
|
21
|
+
expect(pushBranches(undefined)).toEqual(["main"]);
|
|
22
|
+
expect(pushBranches([])).toEqual(["main"]);
|
|
23
|
+
expect(pushBranches(["trunk"])).toEqual(["trunk"]);
|
|
24
|
+
expect(triggerBranches({ kind: "push", branches: [] })).toEqual(["main"]);
|
|
25
|
+
expect(triggerBranches({ kind: "pull_request", branches: [] })).toBeUndefined();
|
|
26
|
+
expect(triggerBranches({ kind: "pull_request", branches: ["main"] })).toEqual(["main"]);
|
|
27
|
+
expect(triggerBranches({ kind: "cron", schedule: "0 * * * *" })).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("a secret, and a secret that falls back to the job token when unset (#3624)", () => {
|
|
31
|
+
expect(githubSecret("T")).toBe("${{ secrets.T }}");
|
|
32
|
+
expect(githubSecretOrJobToken("T")).toBe("${{ secrets.T || github.token }}");
|
|
33
|
+
expect(githubSecretEnv(["A", "B"])).toEqual({ A: "${{ secrets.A }}", B: "${{ secrets.B }}" });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("GitLab rules: one entry per branch, and a branch that would break the expression is refused", () => {
|
|
37
|
+
const selector = { variable: "CHANT_SCHEDULED_OP", value: "watch" };
|
|
38
|
+
expect(gitlabRules("Scheduled Op \"apply\"", { kind: "push", branches: ["main", "prod"] }, selector)).toEqual([
|
|
39
|
+
{ if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"' },
|
|
40
|
+
{ if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "prod"' },
|
|
41
|
+
]);
|
|
42
|
+
expect(gitlabRules("x", { kind: "pull_request", branches: [] }, selector)).toEqual([{ if: '$CI_PIPELINE_SOURCE == "merge_request_event"' }]);
|
|
43
|
+
expect(gitlabRules("x", { kind: "cron", schedule: "0 * * * *" }, selector)).toEqual([
|
|
44
|
+
{ if: '$CI_PIPELINE_SOURCE == "schedule" && $CHANT_SCHEDULED_OP == "watch"' },
|
|
45
|
+
]);
|
|
46
|
+
expect(() => assertGitlabBranch("Op waves \"w\"", "a$b")).toThrow(/Op waves "w" filters on branch "a\$b"/);
|
|
47
|
+
expect(() => assertGitlabBranch("Op waves \"w\"", " ")).toThrow(/empty branch filter/);
|
|
48
|
+
});
|
|
49
|
+
});
|