@intentius/chant 0.124.14 → 0.124.16
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/cli/flush.d.ts +16 -0
- package/dist/cli/flush.d.ts.map +1 -0
- package/dist/cli/flush.js +42 -0
- package/dist/cli/handlers/approve.d.ts +98 -0
- package/dist/cli/handlers/approve.d.ts.map +1 -0
- package/dist/cli/handlers/approve.js +495 -0
- package/dist/cli/handlers/operator.d.ts +1 -95
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/operator.js +4 -488
- package/dist/cli/handlers/override-approve.js +1 -1
- package/dist/cli/main.d.ts +4 -15
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +4 -1062
- package/dist/cli/parse-args.d.ts +6 -0
- package/dist/cli/parse-args.d.ts.map +1 -0
- package/dist/cli/parse-args.js +1028 -0
- package/dist/components/fan-out-record.d.ts +1 -1
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +3 -7
- package/dist/components/fan-out-run.d.ts +5 -3
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +7 -5
- 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/gated-waves.d.ts +25 -6
- package/dist/gated-waves.d.ts.map +1 -1
- package/dist/gated-waves.js +21 -6
- 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 +5 -5
- 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 +2 -2
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +8 -9
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +4 -8
- package/dist/package-version.d.ts +2 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +10 -0
- package/dist/wave-engine.d.ts +62 -0
- package/dist/wave-engine.d.ts.map +1 -0
- package/dist/wave-engine.js +59 -0
- package/dist/wave-layout.d.ts +29 -0
- package/dist/wave-layout.d.ts.map +1 -0
- package/dist/wave-layout.js +41 -0
- package/dist/wave-runtime.d.ts +29 -0
- package/dist/wave-runtime.d.ts.map +1 -0
- package/dist/wave-runtime.js +103 -0
- package/dist/wave-runtime.mjs +331 -0
- 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/ajv2020.d.ts +5 -0
- package/dist/workspace/ajv2020.d.ts.map +1 -0
- package/dist/workspace/ajv2020.js +14 -0
- 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/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +4 -8
- package/dist/workspace/kinds.d.ts +3 -7
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/kinds.js +8 -45
- package/dist/workspace/points.d.ts.map +1 -1
- package/dist/workspace/points.js +2 -5
- 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 +13 -99
- package/package.json +5 -3
- 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/cli/flush.ts +44 -0
- package/src/cli/handlers/approve.ts +618 -0
- package/src/cli/handlers/operator.ts +10 -608
- package/src/cli/handlers/override-approve.test.ts +1 -1
- package/src/cli/handlers/override-approve.ts +1 -1
- package/src/cli/main.ts +5 -816
- package/src/cli/parse-args.ts +781 -0
- package/src/components/fan-out-record.ts +3 -7
- package/src/components/fan-out-run.ts +7 -5
- 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 +6 -4
- package/src/gate-decision-identity.test.ts +59 -6
- package/src/gate-decision.ts +81 -21
- package/src/gated-waves.ts +28 -6
- 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 +9 -9
- package/src/op/op-waves.test.ts +2 -0
- package/src/op/op-waves.ts +4 -8
- package/src/op/work-lease-run.test.ts +12 -4
- package/src/package-version.ts +11 -0
- package/src/wave-engine.test.ts +107 -0
- package/src/wave-engine.ts +98 -0
- package/src/wave-layout.ts +44 -0
- package/src/wave-runtime-bundle.test.ts +70 -0
- package/src/wave-runtime.e2e.test.ts +177 -0
- package/src/wave-runtime.ts +104 -0
- package/src/workspace/__fixtures__/contract-repo.ts +17 -1
- package/src/workspace/__fixtures__/sessions.ts +3 -3
- package/src/workspace/ajv2020.ts +17 -0
- 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/declaration.ts +4 -10
- package/src/workspace/kinds.ts +8 -40
- package/src/workspace/points.ts +2 -5
- package/src/workspace/principal-classes.ts +13 -99
- 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
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wave engine (#3766): what `chant run wave` (the Op waves spec,
|
|
3
|
+
* `./op/op-waves-run.ts`) and `chant components fan-out --wave-gate`
|
|
4
|
+
* (`./components/fan-out-run.ts`) share once a wave is planned.
|
|
5
|
+
*
|
|
6
|
+
* - Gate naming: {@link waveGateName} and {@link waveGateNameFrom}.
|
|
7
|
+
* - The set digest: {@link waveSetDigest} over {@link waveDigestMembers}.
|
|
8
|
+
* - The share split of a wide wave: {@link waveShare}.
|
|
9
|
+
* - The gate decision call: {@link decideWaveGate}.
|
|
10
|
+
* - The record file a wave job hands the next one: {@link writeWaveRecordFile}.
|
|
11
|
+
*
|
|
12
|
+
* The parts that need neither the gate ledger nor the filesystem live in
|
|
13
|
+
* `./gated-waves.ts`, which terragucci bundles on its own (#3421), and
|
|
14
|
+
* `./wave-layout.ts`, which imports nothing so the Op waves spec module can
|
|
15
|
+
* use it without the change-set code; this module re-exports both so a
|
|
16
|
+
* runner imports the engine from one place.
|
|
17
|
+
*
|
|
18
|
+
* What stays with each entry point: how a wave is planned (Op runs' plan
|
|
19
|
+
* commands, or each component's capabilities), the gate policy, approval
|
|
20
|
+
* mode, decision point, hooks and spend rule (Op waves only), and the shape
|
|
21
|
+
* of the record each writes. Those shapes are read by jobs and approvals
|
|
22
|
+
* already in the wild, so the engine writes what it is handed.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { dirname } from "node:path";
|
|
26
|
+
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { evaluateGate, type GateCheck, type GateLedgerPort } from "./op/gate";
|
|
28
|
+
import type { AppliedGateRecord } from "./lifecycle/gate-applied";
|
|
29
|
+
|
|
30
|
+
export { WAVE_GATE_NAME_PATTERN, waveGateNameFrom, waveShare } from "./wave-layout";
|
|
31
|
+
export {
|
|
32
|
+
layerWaves,
|
|
33
|
+
waveGateName,
|
|
34
|
+
waveDigestMembers,
|
|
35
|
+
waveSetDigest,
|
|
36
|
+
readWaveRecords,
|
|
37
|
+
withWaveRecord,
|
|
38
|
+
describeChangedWave,
|
|
39
|
+
type WaveDigestOver,
|
|
40
|
+
type WaveMember,
|
|
41
|
+
type WaveNode,
|
|
42
|
+
type WaveRecord,
|
|
43
|
+
type WaveStatus,
|
|
44
|
+
} from "./gated-waves";
|
|
45
|
+
|
|
46
|
+
/** What a wave asks its gate. */
|
|
47
|
+
export interface WaveGateRequest {
|
|
48
|
+
/** The op the gate is recorded under: the fan-out's op, or the Op waves spec's name. */
|
|
49
|
+
op: string;
|
|
50
|
+
/** The wave's full gate name. */
|
|
51
|
+
gate: string;
|
|
52
|
+
/** The wave's set digest. */
|
|
53
|
+
digest: string;
|
|
54
|
+
/** The pending fact's description. */
|
|
55
|
+
description: string;
|
|
56
|
+
/** The pending fact's expiry, as a gate's `timeout`. */
|
|
57
|
+
timeout?: string;
|
|
58
|
+
now?: string;
|
|
59
|
+
/** Approvals a wave already applied under, which count for nothing (#3784). */
|
|
60
|
+
spent?: readonly AppliedGateRecord[];
|
|
61
|
+
/** Fields the pending fact carries (#3821). */
|
|
62
|
+
fields?: Record<string, unknown>;
|
|
63
|
+
/** The pending fact carries `neverOverMcp` (#3821). */
|
|
64
|
+
neverOverMcp?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Decide a wave's gate: an approval of `digest` under `gate` on `op`'s
|
|
69
|
+
* ledger, or the pending fact that stands for it. A field left out of the
|
|
70
|
+
* request is left out of the call, so each entry point's pending facts are
|
|
71
|
+
* the ones it always wrote.
|
|
72
|
+
*/
|
|
73
|
+
export function decideWaveGate(port: GateLedgerPort, request: WaveGateRequest): Promise<GateCheck> {
|
|
74
|
+
return evaluateGate(port, {
|
|
75
|
+
op: request.op,
|
|
76
|
+
gate: request.gate,
|
|
77
|
+
planDigest: request.digest,
|
|
78
|
+
description: request.description,
|
|
79
|
+
...(request.timeout ? { timeout: request.timeout } : {}),
|
|
80
|
+
...(request.now !== undefined ? { now: request.now } : {}),
|
|
81
|
+
...(request.spent ? { spent: request.spent } : {}),
|
|
82
|
+
...(request.fields ? { fields: request.fields } : {}),
|
|
83
|
+
...(request.neverOverMcp ? { neverOverMcp: true } : {}),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Write a wave record file as indented JSON, through a temporary file and a
|
|
89
|
+
* rename, so a job killed during the write leaves the previous record whole.
|
|
90
|
+
* The fan-out attempt record and an Op wave's decision record both go
|
|
91
|
+
* through it.
|
|
92
|
+
*/
|
|
93
|
+
export function writeWaveRecordFile(path: string, value: unknown): void {
|
|
94
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
95
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
96
|
+
writeFileSync(temporary, JSON.stringify(value, null, 2) + "\n");
|
|
97
|
+
renameSync(temporary, path);
|
|
98
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave gate names and the share split of a wide wave (#3766), the parts of
|
|
3
|
+
* the wave engine (`./wave-engine.ts`) that the Op waves spec module
|
|
4
|
+
* (`./op/op-waves.ts`) needs. That module is in the gate-decision bundle
|
|
5
|
+
* (`./gate-decision.ts` re-exports a constant from it), so this imports
|
|
6
|
+
* nothing and keeps the change-set digest code out of that bundle.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The default wave gate name pattern: `{gate}` is the base gate name, `{k}` the wave's number. */
|
|
10
|
+
export const WAVE_GATE_NAME_PATTERN = "{gate}-wave-{k}";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The gate wave `k` waits on under a name pattern (#3821). With the default
|
|
14
|
+
* pattern this is `waveGateName(gate, k)` from `./gated-waves.ts`; an Op
|
|
15
|
+
* waves spec's `gateName` names another (such as `"wave-{k}"`) so approvals
|
|
16
|
+
* already on the ledger under a product's old names keep matching.
|
|
17
|
+
*
|
|
18
|
+
* `{gate}` is filled in before `{k}`, as `opWaveGateName` always did, so a
|
|
19
|
+
* base gate name holding the text `{k}` is filled in too. That is the one
|
|
20
|
+
* input on which this and `waveGateName` differ, and each entry point keeps
|
|
21
|
+
* the one it shipped with.
|
|
22
|
+
*/
|
|
23
|
+
export function waveGateNameFrom(pattern: string | undefined, gate: string, k: number): string {
|
|
24
|
+
return (pattern ?? WAVE_GATE_NAME_PATTERN).replaceAll("{gate}", gate).replaceAll("{k}", String(k));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Share `share` (1-based) of `shares` of a wide wave (#3679): the items
|
|
29
|
+
* sorted by `keyOf`, cut into contiguous slices whose sizes differ by at most
|
|
30
|
+
* one. The deciding job and every share job cut the same way, so each item
|
|
31
|
+
* is applied by exactly one share.
|
|
32
|
+
*/
|
|
33
|
+
export function waveShare<T>(items: readonly T[], share: number, shares: number, keyOf: (item: T) => string): T[] {
|
|
34
|
+
const sorted = [...items].sort((a, b) => {
|
|
35
|
+
const x = keyOf(a);
|
|
36
|
+
const y = keyOf(b);
|
|
37
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
38
|
+
});
|
|
39
|
+
const base = Math.floor(sorted.length / shares);
|
|
40
|
+
const extra = sorted.length % shares;
|
|
41
|
+
const start = (share - 1) * base + Math.min(share - 1, extra);
|
|
42
|
+
const size = base + (share <= extra ? 1 : 0);
|
|
43
|
+
return sorted.slice(start, start + size);
|
|
44
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wave-job runtime bundles to one self-contained file (#3896). A product
|
|
3
|
+
* copies `dist/wave-runtime.mjs` into its CI image with no npm install of
|
|
4
|
+
* chant, so the file may import nothing but `node:` builtins, and it carries
|
|
5
|
+
* no TypeScript compiler, esbuild or tsx: those three are the 33 MB a wave
|
|
6
|
+
* job's image has no use for. This builds it the way `npm run build` does and
|
|
7
|
+
* reads esbuild's own list of what went in.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { builtinModules } from "node:module";
|
|
11
|
+
import { describe, expect, test } from "vitest";
|
|
12
|
+
// @ts-expect-error: a plain .mjs script with no type declarations
|
|
13
|
+
import { buildWaveRuntime, LEFT_OUT } from "../scripts/build-wave-runtime.mjs";
|
|
14
|
+
|
|
15
|
+
interface Metafile {
|
|
16
|
+
inputs: Record<string, { bytes: number; imports: { path: string; kind: string; external?: boolean }[] }>;
|
|
17
|
+
outputs: Record<string, { imports: { path: string; kind: string; external?: boolean }[]; inputs: Record<string, { bytesInOutput: number }> }>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const built = (await buildWaveRuntime({ write: false })) as { bytes: number; metafile: Metafile; text: string };
|
|
21
|
+
const output = Object.values(built.metafile.outputs)[0]!;
|
|
22
|
+
const inputs = Object.keys(output.inputs);
|
|
23
|
+
|
|
24
|
+
describe("the wave-job runtime bundle (#3896)", () => {
|
|
25
|
+
test("leaves out the TypeScript compiler, esbuild and tsx", () => {
|
|
26
|
+
expect(LEFT_OUT).toEqual(["typescript", "esbuild", "tsx"]);
|
|
27
|
+
for (const name of LEFT_OUT as string[]) {
|
|
28
|
+
expect(inputs.filter((i) => i.includes(`node_modules/${name}/`)), name).toEqual([]);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("imports nothing but node: builtins, so it runs with no node_modules", () => {
|
|
33
|
+
const builtins = new Set(builtinModules);
|
|
34
|
+
const imported = output.imports.map((i) => i.path);
|
|
35
|
+
expect(imported.filter((p) => !builtins.has(p.replace(/^node:/, "").split("/")[0]!))).toEqual([]);
|
|
36
|
+
// zod, ajv, js-yaml and picomatch are inlined; nothing reads them from disk.
|
|
37
|
+
for (const pkg of ["zod", "ajv", "js-yaml", "picomatch"]) {
|
|
38
|
+
expect(inputs.some((i) => i.includes(`node_modules/${pkg}/`)), pkg).toBe(true);
|
|
39
|
+
}
|
|
40
|
+
expect(built.text).not.toMatch(/createRequire\(import\.meta\.url\)\("\.\.\/\.\.\/package\.json"\)/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("a left-out package is reached only through import(), never on start", () => {
|
|
44
|
+
// The build refuses otherwise; this pins the four commands' own modules as the static graph.
|
|
45
|
+
const entry = Object.keys(built.metafile.inputs).find((k) => k.endsWith("src/wave-runtime.ts"))!;
|
|
46
|
+
const seen = new Set([entry]);
|
|
47
|
+
const queue = [entry];
|
|
48
|
+
while (queue.length) {
|
|
49
|
+
for (const im of built.metafile.inputs[queue.shift()!]?.imports ?? []) {
|
|
50
|
+
if (im.kind === "dynamic-import" || seen.has(im.path)) continue;
|
|
51
|
+
seen.add(im.path);
|
|
52
|
+
queue.push(im.path);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const handler of ["cli/handlers/run-wave.ts", "cli/handlers/gate-resume.ts", "cli/handlers/approve.ts", "workspace/points-cli.ts", "cli/parse-args.ts"]) {
|
|
56
|
+
expect([...seen].some((k) => k.endsWith(`src/${handler}`)), handler).toBe(true);
|
|
57
|
+
}
|
|
58
|
+
expect([...seen].filter((k) => k.startsWith("chant-wave-runtime-left-out:"))).toEqual([]);
|
|
59
|
+
// Neither the full CLI's dispatch nor the operator loop is in it, and the build and plugin loader load on no start.
|
|
60
|
+
expect(inputs.filter((i) => /src\/cli\/main\.ts$|src\/cli\/handlers\/operator\.ts$/.test(i))).toEqual([]);
|
|
61
|
+
expect([...seen].filter((i) => /src\/cli\/plugins\.ts$|src\/build\.ts$/.test(i))).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("carries its version and stays small", () => {
|
|
65
|
+
expect(built.text).toContain('"workspace points"');
|
|
66
|
+
expect(built.text.startsWith("#!/usr/bin/env node\n")).toBe(true);
|
|
67
|
+
// About 1.45 MB minified today (440 KB gzipped), zod's inlined schemas about 300 KB of it.
|
|
68
|
+
expect(built.bytes).toBeLessThan(2 * 1024 * 1024);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wave-job runtime runs alone (#3896). `dist/wave-runtime.mjs` is copied
|
|
3
|
+
* into an empty directory, the way a product's CI image carries it, and run
|
|
4
|
+
* with plain node, nothing installed beside it, through a gated wave's whole
|
|
5
|
+
* life: `run wave` waits at a ledger gate, `approve` records the resolution,
|
|
6
|
+
* `run resume --dry-run` finds it, `run wave` applies; then a wave whose gate
|
|
7
|
+
* asks a decision point waits, `workspace points` lists the open question,
|
|
8
|
+
* `workspace points answer` answers it, and `run wave` applies.
|
|
9
|
+
*
|
|
10
|
+
* End to end because it builds the bundle and spawns node for each command.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
14
|
+
import { copyFileSync, cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
19
|
+
// @ts-expect-error: a plain .mjs script with no type declarations
|
|
20
|
+
import { buildWaveRuntime } from "../scripts/build-wave-runtime.mjs";
|
|
21
|
+
import { REPO } from "./workspace/__fixtures__/decide-workspace";
|
|
22
|
+
|
|
23
|
+
const POINTS = {
|
|
24
|
+
points: {
|
|
25
|
+
"prod-apply": {
|
|
26
|
+
title: "Apply this plan to prod",
|
|
27
|
+
question: { type: "noul", instructions: "Should this plan be applied to prod?", criteria: { true: "Apply it.", false: "Do not apply it." } },
|
|
28
|
+
inputs: { "gate.component": "the Op the gate belongs to", "gate.planDigest": "the plan the gate binds" },
|
|
29
|
+
deciders: [{ kind: "quorum", count: 1 }],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const SPEC = {
|
|
35
|
+
name: "smoke",
|
|
36
|
+
op: "smoke",
|
|
37
|
+
plan: ["node", "plan.mjs", "{target}", "{plan}"],
|
|
38
|
+
apply: ["node", "apply.mjs", "{target}", "{plan}"],
|
|
39
|
+
waves: [
|
|
40
|
+
{ name: "dev", runs: [{ target: "dev" }], gate: "always" },
|
|
41
|
+
{ name: "prod", runs: [{ target: "prod" }], gate: "always", point: "prod-apply" },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let scratch: string;
|
|
46
|
+
let runtimeDir: string;
|
|
47
|
+
let runtime: string;
|
|
48
|
+
let repo: string;
|
|
49
|
+
|
|
50
|
+
/** The job's environment: no CI variables, so nothing names a CI job to resume. */
|
|
51
|
+
function jobEnv(): NodeJS.ProcessEnv {
|
|
52
|
+
const env: NodeJS.ProcessEnv = {};
|
|
53
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
54
|
+
if (/^(CI|GITHUB_|GITLAB_|FORGEJO_|GITEA_|CHANT_|NODE_OPTIONS$|VITEST)/.test(k)) continue;
|
|
55
|
+
env[k] = v;
|
|
56
|
+
}
|
|
57
|
+
return env;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function chant(...args: string[]): { status: number | null; stdout: string; stderr: string } {
|
|
61
|
+
const r = spawnSync(process.execPath, [runtime, ...args], { cwd: repo, env: jobEnv(), encoding: "utf-8" });
|
|
62
|
+
return { status: r.status, stdout: r.stdout, stderr: r.stderr };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const git = (...args: string[]) =>
|
|
66
|
+
execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false", ...args], { cwd: repo, stdio: "ignore" });
|
|
67
|
+
|
|
68
|
+
beforeAll(async () => {
|
|
69
|
+
scratch = realpathSync(mkdtempSync(join(tmpdir(), "chant-wave-runtime-")));
|
|
70
|
+
const built = join(scratch, "build", "wave-runtime.mjs");
|
|
71
|
+
mkdirSync(dirname(built), { recursive: true });
|
|
72
|
+
await buildWaveRuntime({ outfile: built });
|
|
73
|
+
runtimeDir = join(scratch, "image");
|
|
74
|
+
mkdirSync(runtimeDir);
|
|
75
|
+
runtime = join(runtimeDir, "wave-runtime.mjs");
|
|
76
|
+
copyFileSync(built, runtime);
|
|
77
|
+
|
|
78
|
+
repo = join(scratch, "repo");
|
|
79
|
+
const files: Record<string, string> = {
|
|
80
|
+
"chant.workspace.json": JSON.stringify({
|
|
81
|
+
name: "wave-runtime",
|
|
82
|
+
schema: 1,
|
|
83
|
+
members: [{ name: "app", dir: "app", kind: "other", because: "a plain Node server" }],
|
|
84
|
+
records: [{ kind: "answers/answer.kind.mjs" }],
|
|
85
|
+
}),
|
|
86
|
+
"app/server.mjs": "export const port = 8080;\n",
|
|
87
|
+
"decisions/points.json": JSON.stringify(POINTS),
|
|
88
|
+
"plan.mjs": `import { writeFileSync } from "node:fs";\nwriteFileSync(process.argv[3], JSON.stringify({ planDigest: "sha256:" + (process.argv[2] === "dev" ? "a" : "b").repeat(64) }));\n`,
|
|
89
|
+
"apply.mjs": `import { appendFileSync } from "node:fs";\nappendFileSync("applied.log", process.argv[2] + "\\n");\n`,
|
|
90
|
+
"waves.json": JSON.stringify(SPEC),
|
|
91
|
+
};
|
|
92
|
+
for (const [path, text] of Object.entries(files)) {
|
|
93
|
+
mkdirSync(dirname(join(repo, path)), { recursive: true });
|
|
94
|
+
writeFileSync(join(repo, path), text);
|
|
95
|
+
}
|
|
96
|
+
cpSync(join(REPO, "reference-workspace", "answers", "answer.kind.mjs"), join(repo, "answers", "answer.kind.mjs"));
|
|
97
|
+
cpSync(join(REPO, "reference-workspace", "answers", "answer.schema.json"), join(repo, "answers", "answer.schema.json"));
|
|
98
|
+
git("init", "-q", "-b", "main");
|
|
99
|
+
git("add", "-A");
|
|
100
|
+
git("commit", "-qm", "spec");
|
|
101
|
+
// The gate policy is read at HEAD^1, so the spec needs a commit under the head.
|
|
102
|
+
writeFileSync(join(repo, ".gitignore"), ".chant/\napplied.log\n");
|
|
103
|
+
git("add", "-A");
|
|
104
|
+
git("commit", "-qm", "ignore");
|
|
105
|
+
}, 120_000);
|
|
106
|
+
|
|
107
|
+
afterAll(() => {
|
|
108
|
+
if (scratch) rmSync(scratch, { recursive: true, force: true });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("the wave-job runtime, alone in a directory (#3896)", () => {
|
|
112
|
+
test("is one file, with no package beside it to resolve", () => {
|
|
113
|
+
expect(readdirSync(runtimeDir)).toEqual(["wave-runtime.mjs"]);
|
|
114
|
+
const resolveBeside = createRequire(runtime).resolve;
|
|
115
|
+
for (const pkg of ["@intentius/chant", "typescript", "esbuild", "tsx", "zod", "ajv"]) {
|
|
116
|
+
expect(() => resolveBeside(pkg), pkg).toThrow();
|
|
117
|
+
}
|
|
118
|
+
const version = (JSON.parse(readFileSync(join(REPO, "packages", "core", "package.json"), "utf-8")) as { version: string }).version;
|
|
119
|
+
expect(chant("--version").stdout.trim()).toBe(version);
|
|
120
|
+
const other = chant("build");
|
|
121
|
+
expect(other.status).toBe(1);
|
|
122
|
+
expect(other.stderr).toContain("is not part of chant's wave-job runtime");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("a ledger gate: run wave waits, approve, run resume --dry-run, run wave applies", () => {
|
|
126
|
+
const waiting = chant("run", "wave", "--spec", "waves.json", "--wave", "1", "--json");
|
|
127
|
+
expect(waiting.status, waiting.stderr).toBe(3);
|
|
128
|
+
const decision = (JSON.parse(waiting.stdout) as { decision: { status: string; gate: string; digest: string } }).decision;
|
|
129
|
+
expect(decision).toMatchObject({ status: "waiting", gate: "smoke-wave-1" });
|
|
130
|
+
|
|
131
|
+
const approved = chant("approve", "smoke", "smoke-wave-1", "--actor", "alice");
|
|
132
|
+
expect(approved.status, approved.stderr).toBe(0);
|
|
133
|
+
expect(approved.stderr).toContain(`Gate "smoke-wave-1" on "smoke" resolved by alice`);
|
|
134
|
+
expect(approved.stderr).toContain(`This approves the plan ${decision.digest}`);
|
|
135
|
+
|
|
136
|
+
const resume = chant("run", "resume", "--dry-run");
|
|
137
|
+
expect(resume.status, resume.stderr).toBe(0);
|
|
138
|
+
expect(resume.stderr).toContain("smoke / smoke-wave-1, approved by alice");
|
|
139
|
+
|
|
140
|
+
const applied = chant("run", "wave", "--spec", "waves.json", "--wave", "1");
|
|
141
|
+
expect(applied.status, applied.stderr).toBe(0);
|
|
142
|
+
expect(applied.stderr).toContain("approved by alice");
|
|
143
|
+
expect(readFileSync(join(repo, "applied.log"), "utf-8")).toBe("dev\n");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("a gate that asks a decision point: run wave waits, points lists and answers, run wave applies", () => {
|
|
147
|
+
const waiting = chant("run", "wave", "--spec", "waves.json", "--wave", "2", "--json");
|
|
148
|
+
expect(waiting.status, waiting.stderr).toBe(3);
|
|
149
|
+
const id = (JSON.parse(waiting.stdout) as { decision: { point: { question: { id: string } } } }).decision.point.question.id;
|
|
150
|
+
expect(id).toMatch(/^prod-apply-[0-9a-f]{12}$/);
|
|
151
|
+
|
|
152
|
+
const open = chant("workspace", "points", "--open");
|
|
153
|
+
expect(open.status, open.stderr).toBe(0);
|
|
154
|
+
expect(open.stdout).toContain(`${id} escalated`);
|
|
155
|
+
|
|
156
|
+
const answered = chant("workspace", "points", "answer", id, "--answer", "true", "--by", "alice");
|
|
157
|
+
expect(answered.status, answered.stderr).toBe(0);
|
|
158
|
+
expect(readFileSync(join(repo, "answers", `${id}.md`), "utf-8")).toMatch(/state: "answered"\nanswer: true/);
|
|
159
|
+
|
|
160
|
+
const applied = chant("run", "wave", "--spec", "waves.json", "--wave", "2");
|
|
161
|
+
expect(applied.status, applied.stderr).toBe(0);
|
|
162
|
+
expect(readFileSync(join(repo, "applied.log"), "utf-8")).toBe("dev\nprod\n");
|
|
163
|
+
|
|
164
|
+
// The ledger the runtime wrote is the full CLI's: pending, resolution, pending citing the question, resolution citing the answer.
|
|
165
|
+
const ledger = execFileSync("git", ["show", "chant/lifecycle:_gates/smoke.jsonl"], { cwd: repo, encoding: "utf-8" })
|
|
166
|
+
.trim()
|
|
167
|
+
.split("\n")
|
|
168
|
+
.map((l) => JSON.parse(l) as { kind?: string; gate: string; resolvedBy?: string; answer?: { id: string } });
|
|
169
|
+
expect(ledger.map((l) => [l.gate, l.kind ?? "resolution", l.resolvedBy ?? null])).toEqual([
|
|
170
|
+
["smoke-wave-1", "pending", null],
|
|
171
|
+
["smoke-wave-1", "resolution", "alice"],
|
|
172
|
+
["smoke-wave-2", "pending", null],
|
|
173
|
+
["smoke-wave-2", "resolution", "alice"],
|
|
174
|
+
]);
|
|
175
|
+
expect(ledger[3]!.answer?.id).toBe(id);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slim wave-job runtime (#3896): the four commands a generated Op waves
|
|
3
|
+
* pipeline runs, and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* chant run wave --spec <file> --wave <k> [--decide | --share <i> | --record-plans]
|
|
6
|
+
* chant run resume [--gate <op>/<gate>] [--dry-run]
|
|
7
|
+
* chant approve <op> <gate> [...]
|
|
8
|
+
* chant workspace points ask|answer|list|... [...]
|
|
9
|
+
*
|
|
10
|
+
* `scripts/build-runtime.mjs` bundles this file with esbuild into
|
|
11
|
+
* `dist/wave-runtime.mjs`: one self-contained ES module whose only imports are
|
|
12
|
+
* `node:` builtins, with no TypeScript compiler, esbuild, tsx or zod in it. A
|
|
13
|
+
* product copies that one file into its CI image and runs it with plain node,
|
|
14
|
+
* or runs it from an installed `@intentius/chant`
|
|
15
|
+
* (`node node_modules/@intentius/chant/dist/wave-runtime.mjs run wave ...`).
|
|
16
|
+
*
|
|
17
|
+
* The handlers are the full CLI's own (`./cli/main.ts` dispatches to the same
|
|
18
|
+
* functions), parsed by the same `parseArgs`, so flags, output, exit codes and
|
|
19
|
+
* the ledgers each command writes are the same. What the runtime does not do:
|
|
20
|
+
* evaluate `chant.config.ts` (which needs a TypeScript loader). None of the
|
|
21
|
+
* four commands reads the config; the full CLI loads it only to check `--env`
|
|
22
|
+
* against the declared `environments`, a check the runtime skips.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { parseArgs } from "./cli/parse-args";
|
|
26
|
+
import { resolveCommand, type CommandDef, type ParsedArgs } from "./cli/registry";
|
|
27
|
+
import { formatError } from "./cli/format";
|
|
28
|
+
import { flushAndExit } from "./cli/flush";
|
|
29
|
+
import { CHANT_VERSION } from "./cli/version";
|
|
30
|
+
import { isEntryPoint } from "./cli/is-entry-point";
|
|
31
|
+
import { ENV_VAR } from "./env";
|
|
32
|
+
import { initRuntime } from "./runtime-adapter";
|
|
33
|
+
import { runOpWaveCommand } from "./cli/handlers/run-wave";
|
|
34
|
+
import { runResumeCommand } from "./cli/handlers/gate-resume";
|
|
35
|
+
import { runApprove } from "./cli/handlers/approve";
|
|
36
|
+
import { runWorkspacePoints } from "./workspace/points-cli";
|
|
37
|
+
|
|
38
|
+
/** The commands the runtime carries, under the names the full CLI registers them by. */
|
|
39
|
+
export const RUNTIME_COMMANDS: CommandDef[] = [
|
|
40
|
+
{ name: "run wave", handler: runOpWaveCommand },
|
|
41
|
+
{ name: "run resume", handler: runResumeCommand },
|
|
42
|
+
{ name: "approve", handler: runApprove },
|
|
43
|
+
{ name: "workspace points", handler: runWorkspacePoints },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const HELP = `
|
|
47
|
+
chant runtime ${CHANT_VERSION} - the wave-job commands of chant, in one file
|
|
48
|
+
|
|
49
|
+
Usage: node wave-runtime.mjs <command> [options]
|
|
50
|
+
|
|
51
|
+
Commands:
|
|
52
|
+
run wave --spec <file> --wave <k> Plan, gate and apply wave k of an Op waves spec
|
|
53
|
+
(--decide, --share <i>, --record-plans, --base <ref>, --json)
|
|
54
|
+
run resume Start the CI jobs that wait at gates whose approval arrived
|
|
55
|
+
(--gate <op>/<gate>, --dry-run, --json)
|
|
56
|
+
approve <op> <gate> Record a gate's resolution on the lifecycle ledger
|
|
57
|
+
(--actor, --plan, --note, --url, --sign, --expire, --resume, ...)
|
|
58
|
+
workspace points <verb> Ask, answer and list decision points
|
|
59
|
+
|
|
60
|
+
Flags, output, exit codes and ledger formats are the full chant CLI's;
|
|
61
|
+
see "chant --help" in @intentius/chant for every flag. This runtime does
|
|
62
|
+
not evaluate chant.config.ts, so --env is not checked against the
|
|
63
|
+
declared environments.
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
/** Parse, dispatch and return the exit code. Exported for tests. */
|
|
67
|
+
export async function runRuntime(argv: string[]): Promise<number> {
|
|
68
|
+
let args: ParsedArgs;
|
|
69
|
+
try {
|
|
70
|
+
args = parseArgs(argv);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
const match = resolveCommand(args, RUNTIME_COMMANDS);
|
|
76
|
+
if (args.version && (!args.command || match)) {
|
|
77
|
+
console.log(CHANT_VERSION);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
if (args.help || !args.command) {
|
|
81
|
+
console.log(HELP);
|
|
82
|
+
return args.help ? 0 : 1;
|
|
83
|
+
}
|
|
84
|
+
if (!match) {
|
|
85
|
+
const name = args.path && args.path !== "." ? `${args.command} ${args.path}` : args.command;
|
|
86
|
+
console.error(formatError({
|
|
87
|
+
message: `"${name}" is not part of chant's wave-job runtime`,
|
|
88
|
+
hint: "It runs run wave, run resume, approve and workspace points. Every other command needs the full @intentius/chant CLI.",
|
|
89
|
+
}));
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
if (args.env) process.env[ENV_VAR] = args.env;
|
|
93
|
+
initRuntime();
|
|
94
|
+
return match.def.handler({ args, plugins: [], serializers: [] });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isEntryPoint(process.argv[1], import.meta.url)) {
|
|
98
|
+
runRuntime(process.argv.slice(2)).then(flushAndExit, async (err) => {
|
|
99
|
+
const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
|
|
100
|
+
if (verbose && err instanceof Error && err.stack) console.error(err.stack);
|
|
101
|
+
else console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
102
|
+
await flushAndExit(1);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
|
-
import { chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
import Ajv2020 from "ajv/dist/2020";
|
|
@@ -56,6 +56,22 @@ export function repo(files: Record<string, string | { text: string; mode: number
|
|
|
56
56
|
return root;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Build a fixture repository once per test file and hand each caller a copy of
|
|
61
|
+
* it. A repo costs a dozen git processes (init, add, commit, config), and a
|
|
62
|
+
* file copy none, so a file whose tests each start from the same repo builds
|
|
63
|
+
* it once (#3880). The template is rebuilt if {@link cleanScratch} removed it.
|
|
64
|
+
*/
|
|
65
|
+
export function repoFixture(build: () => string, prefix = "chant-contract-copy-"): () => string {
|
|
66
|
+
let template: string | undefined;
|
|
67
|
+
return () => {
|
|
68
|
+
if (template === undefined || !existsSync(template)) template = build();
|
|
69
|
+
const root = scratchDir(prefix);
|
|
70
|
+
cpSync(template, root, { recursive: true });
|
|
71
|
+
return root;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
export function commitAll(root: string, message = "next"): string {
|
|
60
76
|
git(root, "add", "-A");
|
|
61
77
|
git(root, "commit", "-q", "-m", message);
|
|
@@ -7,19 +7,19 @@
|
|
|
7
7
|
import { cpSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { sessionSeal } from "../record-sessions";
|
|
10
|
-
import { commitAll, git, REPO, scratchDir } from "./contract-repo";
|
|
10
|
+
import { commitAll, git, REPO, repoFixture, scratchDir } from "./contract-repo";
|
|
11
11
|
|
|
12
12
|
export const SESSIONS_KIND = "design/sessions/session.kind.mjs";
|
|
13
13
|
export const DECISIONS_KIND = "decisions/decision.kind.mjs";
|
|
14
14
|
|
|
15
15
|
/** A git repository holding a copy of the reference workspace's decisions and design member. */
|
|
16
|
-
export
|
|
16
|
+
export const sessionsRepo = repoFixture(() => {
|
|
17
17
|
const root = scratchDir("chant-sessions-");
|
|
18
18
|
for (const d of ["decisions", "design"]) cpSync(join(REPO, "reference-workspace", d), join(root, d), { recursive: true });
|
|
19
19
|
git(root, "init", "-q");
|
|
20
20
|
commitAll(root, "reference");
|
|
21
21
|
return root;
|
|
22
|
-
}
|
|
22
|
+
}, "chant-sessions-");
|
|
23
23
|
|
|
24
24
|
/** A session file's text, sealed when it is closed. */
|
|
25
25
|
export function sessionText(s: { id: string; state: "open" | "closed"; verdicts?: { record: string; principal: string; verdict: string }[]; agenda?: string[] }): string {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Ajv 2020-12 validator class, loaded on first use: ajv costs start-up
|
|
3
|
+
* time, and most commands validate nothing with it. One loader for every
|
|
4
|
+
* schema check in workspace/, so the slim wave-job runtime (#3896,
|
|
5
|
+
* `../../scripts/build-wave-runtime.mjs`) can swap in a bundled ajv here
|
|
6
|
+
* instead of a `require` resolved next to an installed chant.
|
|
7
|
+
*/
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
|
|
10
|
+
export type Ajv2020Class = new (opts: object) => { compile(schema: object): unknown };
|
|
11
|
+
|
|
12
|
+
export function loadAjv2020(): Ajv2020Class {
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
// ajv is CommonJS; its class is the default export, or that export's own default.
|
|
15
|
+
const mod = require("ajv/dist/2020") as { default?: unknown };
|
|
16
|
+
return (mod.default ?? mod) as Ajv2020Class;
|
|
17
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://intentius.io/chant/schemas/workspace/change-set/v1/change-set.schema.json",
|
|
4
4
|
"title": "change-set document",
|
|
5
|
-
"description": "One change set across planners (#3181): what a combined run's chant, terraform, tofu, choudoufu and
|
|
5
|
+
"description": "One change set across planners (#3181): what a combined run's chant, terraform, tofu, choudoufu, warden and sql members will change, a summary, and the digest a gate binds. Written by the composeChangeSet Op activity and by composeChangeSet in @intentius/chant/change-set. Version 1 of the workspace read contract. Readers ignore fields they do not know; a field is only ever added within a version.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": [
|
|
8
8
|
"$schema",
|
|
@@ -87,7 +87,8 @@
|
|
|
87
87
|
"tofu",
|
|
88
88
|
"choudoufu",
|
|
89
89
|
"chant",
|
|
90
|
-
"warden"
|
|
90
|
+
"warden",
|
|
91
|
+
"sql"
|
|
91
92
|
]
|
|
92
93
|
},
|
|
93
94
|
"scope": {
|
|
@@ -171,7 +172,8 @@
|
|
|
171
172
|
"tofu",
|
|
172
173
|
"choudoufu",
|
|
173
174
|
"chant",
|
|
174
|
-
"warden"
|
|
175
|
+
"warden",
|
|
176
|
+
"sql"
|
|
175
177
|
]
|
|
176
178
|
},
|
|
177
179
|
"address": {
|
|
@@ -124,15 +124,15 @@ describe("the token the tick pushes its tags with (#3573)", () => {
|
|
|
124
124
|
expect(steps[2].env.GITHUB_TOKEN).toBe("${{ github.token }}");
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
test("--token-secret checks out with the secret,
|
|
127
|
+
test("--token-secret checks out with the secret, or the Actions token where it is unset (#3624), and nothing else changes", () => {
|
|
128
128
|
const plain = renderCiGreenWorkflow(base);
|
|
129
129
|
const text = renderCiGreenWorkflow({ ...base, push: { secret: "CI_GREEN_TOKEN" } });
|
|
130
130
|
const steps = (parseYAMLDocument(text) as Yaml).jobs.tick.steps;
|
|
131
|
-
expect(steps[0]).toEqual({ uses: "actions/checkout@v6", with: { ref: "main", "fetch-depth": 0, token: "${{ secrets.CI_GREEN_TOKEN }}" } });
|
|
131
|
+
expect(steps[0]).toEqual({ uses: "actions/checkout@v6", with: { ref: "main", "fetch-depth": 0, token: "${{ secrets.CI_GREEN_TOKEN || github.token }}" } });
|
|
132
132
|
// Check runs are still read with the Actions token: a fine-grained token can't call the Checks API.
|
|
133
133
|
expect(steps[2].env.GITHUB_TOKEN).toBe("${{ github.token }}");
|
|
134
134
|
expect((parseYAMLDocument(text) as Yaml).permissions).toEqual({ contents: "write", checks: "read" });
|
|
135
|
-
expect(text.replace(" token: ${{ secrets.CI_GREEN_TOKEN }}\n", "")).toBe(plain);
|
|
135
|
+
expect(text.replace(" token: ${{ secrets.CI_GREEN_TOKEN || github.token }}\n", "")).toBe(plain);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
138
|
test("--app-id-var and --app-key-secret mint an App token before the checkout and check out with it", () => {
|
|
@@ -228,7 +228,7 @@ describe("chant ci workflow (#3573)", () => {
|
|
|
228
228
|
const withToken = readFileSync(file, "utf-8");
|
|
229
229
|
expect(withToken.split("\n")[0]).toBe(`# ${GENERATED_MARKER}. Regenerate with: chant ci workflow --token-secret CI_GREEN_TOKEN (in the workspace root)`);
|
|
230
230
|
const steps = (parseYAMLDocument(withToken) as Yaml).jobs.tick.steps;
|
|
231
|
-
expect(steps[0].with.token).toBe("${{ secrets.CI_GREEN_TOKEN }}");
|
|
231
|
+
expect(steps[0].with.token).toBe("${{ secrets.CI_GREEN_TOKEN || github.token }}");
|
|
232
232
|
expect(steps[2].env.GITHUB_TOKEN).toBe("${{ github.token }}");
|
|
233
233
|
|
|
234
234
|
const app = { args: { ciAppIdVar: "CI_GREEN_APP_ID", ciAppKeySecret: "CI_GREEN_APP_KEY" } } as unknown as CommandContext;
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
* commit whose workflow files differ from the default branch's as an update
|
|
28
28
|
* to those workflows, and the Actions token can never hold the `workflows`
|
|
29
29
|
* permission, so it is refused there. `--token-secret <NAME>` checks out
|
|
30
|
-
* with `secrets.<NAME>` instead,
|
|
30
|
+
* with `secrets.<NAME>` instead, falling back to the Actions token where the
|
|
31
|
+
* secret is unset (#3624), so a fork or a fresh clone still tags the commits
|
|
32
|
+
* that change no workflow file; and `--app-id-var <VAR> --app-key-secret
|
|
31
33
|
* <NAME>` mints a GitHub App token first and checks out with that. The
|
|
32
34
|
* tick's own `GITHUB_TOKEN`, which reads check runs, stays the Actions token:
|
|
33
35
|
* a fine-grained personal access token can't call the Checks API.
|
|
@@ -44,6 +46,7 @@ import { GENERATED_MARKER } from "../discovery/files";
|
|
|
44
46
|
import { parseYAMLDocument } from "../yaml";
|
|
45
47
|
import type { CiGreen } from "./declaration";
|
|
46
48
|
import { matchesCheckRun } from "./ci-green";
|
|
49
|
+
import { githubSecretOrJobToken } from "../ci-jobs";
|
|
47
50
|
|
|
48
51
|
/** Where the workflow goes, relative to the repository root. */
|
|
49
52
|
export const CI_GREEN_WORKFLOW_FILE = ".github/workflows/chant-ci-green.yml";
|
|
@@ -185,7 +188,7 @@ export function renderCiGreenWorkflow(input: WorkflowInput): string {
|
|
|
185
188
|
` private-key: \${{ secrets.${push.appKeySecret} }}`,
|
|
186
189
|
]
|
|
187
190
|
: [];
|
|
188
|
-
const token = !push ? [] : ["secret" in push ? ` token:
|
|
191
|
+
const token = !push ? [] : ["secret" in push ? ` token: ${githubSecretOrJobToken(push.secret)}` : " token: ${{ steps.app-token.outputs.token }}"];
|
|
189
192
|
const lines = [
|
|
190
193
|
`name: ${CI_GREEN_WORKFLOW_NAME}`,
|
|
191
194
|
"",
|