@intentius/chant 0.124.23 → 0.124.25
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/ci-jobs.d.ts +41 -1
- package/dist/ci-jobs.d.ts.map +1 -1
- package/dist/ci-jobs.js +48 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +14 -0
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +9 -0
- package/dist/cli/handlers/emulator.d.ts +13 -2
- package/dist/cli/handlers/emulator.d.ts.map +1 -1
- package/dist/cli/handlers/emulator.js +63 -3
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +7 -1
- package/dist/cli/handlers/run-wave.d.ts.map +1 -1
- package/dist/cli/handlers/run-wave.js +5 -4
- package/dist/cli/handlers/run.d.ts +4 -3
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +73 -9
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +38 -11
- package/dist/cli/mcp/workspace-tools.d.ts +3 -3
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.js +27 -4
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +31 -0
- package/dist/cli/registry.d.ts +12 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +45 -6
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +97 -22
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +25 -10
- package/dist/effect-receipt.d.ts +13 -0
- package/dist/effect-receipt.d.ts.map +1 -1
- package/dist/effect-receipt.js +19 -0
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +24 -2
- package/dist/lexicon-module.d.ts +7 -1
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +7 -2
- package/dist/lifecycle/git.d.ts +11 -0
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/git.js +62 -6
- package/dist/lifecycle/no-record.d.ts +59 -0
- package/dist/lifecycle/no-record.d.ts.map +1 -0
- package/dist/lifecycle/no-record.js +88 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.js +4 -2
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +30 -9
- package/dist/op/emulator-lifecycle.d.ts +23 -0
- package/dist/op/emulator-lifecycle.d.ts.map +1 -1
- package/dist/op/emulator-lifecycle.js +39 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +3 -0
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +1 -1
- package/dist/op/local-executor.d.ts +10 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +71 -7
- package/dist/op/op-waves-notify.d.ts +2 -0
- package/dist/op/op-waves-notify.d.ts.map +1 -1
- package/dist/op/op-waves-notify.js +9 -3
- package/dist/op/op-waves-run.d.ts +8 -2
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +36 -11
- package/dist/op/op-waves.d.ts +85 -1
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +153 -1
- package/dist/op/receipt-store.d.ts +5 -0
- package/dist/op/receipt-store.d.ts.map +1 -1
- package/dist/op/receipt-store.js +4 -3
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/runtimes/local.js +6 -0
- package/dist/op/step-console.d.ts +65 -0
- package/dist/op/step-console.d.ts.map +1 -0
- package/dist/op/step-console.js +140 -0
- package/dist/op/step-output-ref.d.ts +26 -18
- package/dist/op/step-output-ref.d.ts.map +1 -1
- package/dist/op/step-output-ref.js +86 -63
- package/dist/point-decision.d.ts.map +1 -1
- package/dist/point-decision.js +3 -0
- package/dist/wave-runtime.mjs +239 -239
- package/dist/workspace/agent-cli.d.ts.map +1 -1
- package/dist/workspace/agent-cli.js +3 -0
- package/dist/workspace/compose-graph.d.ts +22 -3
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/compose-graph.js +18 -5
- package/dist/workspace/conformance/__writer_fixture__/answers/answer.schema.json +16 -1
- package/dist/workspace/conformance/writer.js +1 -1
- package/dist/workspace/decide.d.ts +51 -9
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/decide.js +125 -14
- package/dist/workspace/graph.schema.json +68 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.js +20 -11
- package/dist/workspace/nested-graph.d.ts +1 -1
- package/dist/workspace/nested-graph.d.ts.map +1 -1
- package/dist/workspace/nested-graph.js +5 -1
- package/dist/workspace/point-answer.schema.json +16 -1
- package/dist/workspace/points-cli.d.ts +5 -3
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points-cli.js +16 -7
- package/dist/workspace/points-write.schema.json +39 -4
- package/dist/workspace/points.schema.json +42 -3
- package/dist/workspace/reason-codes.d.ts +2 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +2 -1
- package/dist/workspace/records-cli.d.ts +12 -2
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-cli.js +27 -7
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records-write.js +2 -3
- package/dist/workspace/records.d.ts +10 -4
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/records.js +16 -3
- package/dist/workspace/records.schema.json +2 -1
- package/dist/workspace/runs.d.ts.map +1 -1
- package/dist/workspace/runs.js +4 -2
- package/dist/workspace/trust/verify-cli.d.ts.map +1 -1
- package/dist/workspace/trust/verify-cli.js +2 -1
- package/dist/workspace/trust/verify.d.ts +22 -0
- package/dist/workspace/trust/verify.d.ts.map +1 -1
- package/dist/workspace/trust/verify.js +94 -14
- package/dist/workspace/write-scope.d.ts +3 -1
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/workspace/write-scope.js +11 -4
- package/package.json +1 -1
- package/src/ci-jobs.ts +58 -1
- package/src/cli/commands/build.ts +18 -0
- package/src/cli/compiled-cli.e2e.test.ts +45 -1
- package/src/cli/handlers/approve.ts +9 -0
- package/src/cli/handlers/emulator.test.ts +61 -1
- package/src/cli/handlers/emulator.ts +65 -4
- package/src/cli/handlers/promote.test.ts +7 -0
- package/src/cli/handlers/promote.ts +7 -1
- package/src/cli/handlers/run-no-record.e2e.test.ts +87 -0
- package/src/cli/handlers/run-wave.ts +5 -4
- package/src/cli/handlers/run.test.ts +126 -0
- package/src/cli/handlers/run.ts +69 -9
- package/src/cli/main.test.ts +34 -0
- package/src/cli/main.ts +38 -11
- package/src/cli/mcp/workspace-tools.e2e.test.ts +1 -1
- package/src/cli/mcp/workspace-tools.ts +27 -4
- package/src/cli/parse-args.ts +21 -0
- package/src/cli/registry.ts +12 -0
- package/src/components/cli-support.test.ts +75 -0
- package/src/components/cli-support.ts +130 -25
- package/src/components/driver.test.ts +28 -0
- package/src/components/driver.ts +23 -10
- package/src/components/generate-disabled.test.ts +56 -0
- package/src/effect-receipt.ts +22 -0
- package/src/graph-ci-edges.test.ts +50 -0
- package/src/graph-detail.ts +21 -2
- package/src/lexicon-module.ts +9 -2
- package/src/lifecycle/git.ts +61 -6
- package/src/lifecycle/no-record.test.ts +175 -0
- package/src/lifecycle/no-record.ts +108 -0
- package/src/lifecycle/receipt-plan.test.ts +9 -0
- package/src/lifecycle/receipt-plan.ts +4 -1
- package/src/op/activities/shell.ts +28 -6
- package/src/op/effect-step.test.ts +95 -1
- package/src/op/emulator-lifecycle.test.ts +24 -1
- package/src/op/emulator-lifecycle.ts +58 -2
- package/src/op/gate.ts +3 -0
- package/src/op/index.ts +1 -1
- package/src/op/local-executor.test.ts +65 -0
- package/src/op/local-executor.ts +88 -8
- package/src/op/op-waves-jobs.test.ts +213 -0
- package/src/op/op-waves-notify.ts +11 -5
- package/src/op/op-waves-run.ts +42 -11
- package/src/op/op-waves.ts +202 -2
- package/src/op/receipt-store.ts +13 -3
- package/src/op/runtimes/local.ts +6 -0
- package/src/op/step-console.test.ts +150 -0
- package/src/op/step-console.ts +155 -0
- package/src/op/step-output-ref.test.ts +68 -24
- package/src/op/step-output-ref.ts +85 -65
- package/src/point-decision-bundle.test.ts +1 -0
- package/src/point-decision.ts +3 -0
- package/src/workspace/agent-cli.ts +2 -0
- package/src/workspace/compose-graph.test.ts +55 -0
- package/src/workspace/compose-graph.ts +28 -6
- package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +16 -1
- package/src/workspace/conformance/writer.ts +1 -1
- package/src/workspace/decide.test.ts +87 -3
- package/src/workspace/decide.ts +142 -17
- package/src/workspace/graph-contract.test.ts +26 -1
- package/src/workspace/graph.schema.json +68 -1
- package/src/workspace/lineage-upgrade.test.ts +21 -0
- package/src/workspace/lineage-upgrade.ts +19 -11
- package/src/workspace/nested-graph.test.ts +14 -0
- package/src/workspace/nested-graph.ts +7 -2
- package/src/workspace/point-answer.schema.json +16 -1
- package/src/workspace/points-cli.ts +16 -8
- package/src/workspace/points-write.schema.json +39 -4
- package/src/workspace/points.schema.json +42 -3
- package/src/workspace/reason-codes.ts +2 -1
- package/src/workspace/record-assets.test.ts +14 -0
- package/src/workspace/records-cli.ts +28 -7
- package/src/workspace/records-write-commands.e2e.test.ts +32 -0
- package/src/workspace/records-write.ts +2 -3
- package/src/workspace/records.schema.json +2 -1
- package/src/workspace/records.ts +17 -4
- package/src/workspace/runs.ts +4 -2
- package/src/workspace/trust/trust.test.ts +105 -0
- package/src/workspace/trust/verify-cli.ts +2 -1
- package/src/workspace/trust/verify.ts +106 -13
- package/src/workspace/write-scope.test.ts +18 -0
- package/src/workspace/write-scope.ts +16 -5
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* that touches those files must be attested by a signer the base policy
|
|
9
9
|
* trusts, and by an admin when the base policy grants the admin role.
|
|
10
10
|
*
|
|
11
|
+
* The write scope is policy too (#3079): when the workspace declaration at
|
|
12
|
+
* base has a `writeScope` or `agents` block, a commit that changes either
|
|
13
|
+
* block, or removes the declaration, is a protected write on the same terms.
|
|
14
|
+
* Other edits to the declaration are not. The declaration is found from
|
|
15
|
+
* `cwd`, as `chant workspace check --changes` finds it.
|
|
16
|
+
*
|
|
11
17
|
* Attestation is opt-in (#2525 rule 6). Without a signers file at base the
|
|
12
18
|
* check passes and says so, unless `--require attested` asks for more.
|
|
13
19
|
*
|
|
@@ -21,6 +27,11 @@
|
|
|
21
27
|
* `--require`, `runs` reports and nothing fails (studio-035 d).
|
|
22
28
|
*/
|
|
23
29
|
import { execFileSync } from "node:child_process";
|
|
30
|
+
import { posix } from "node:path";
|
|
31
|
+
import { isDeepStrictEqual } from "node:util";
|
|
32
|
+
import { DECLARATION_FILES, WorkspaceReadError } from "../declaration.js";
|
|
33
|
+
import { parseJsonText } from "../jsonc.js";
|
|
34
|
+
import { locateWorkspace } from "../which-chant.js";
|
|
24
35
|
import { policyAtBase, commitProvenance, resolveBase } from "./provenance.js";
|
|
25
36
|
import { policyWriters, protectedPaths } from "./policy.js";
|
|
26
37
|
import { fileAt, nextVersion, rotationPath, signerHistory } from "./rotation.js";
|
|
@@ -45,6 +56,47 @@ export function commitsTouching(repo, range, paths) {
|
|
|
45
56
|
}
|
|
46
57
|
return out;
|
|
47
58
|
}
|
|
59
|
+
const SCOPE_BLOCKS = ["writeScope", "agents"];
|
|
60
|
+
function scopeState(repo, rev, dir) {
|
|
61
|
+
const present = DECLARATION_FILES.map((name) => ({ name, text: fileAt(repo, rev, posix.join(dir, name)) })).filter((f) => f.text !== undefined);
|
|
62
|
+
if (present.length === 0)
|
|
63
|
+
return { blocks: {} };
|
|
64
|
+
if (present.length > 1)
|
|
65
|
+
return { unreadable: present.map((f) => f.text).join("\0") };
|
|
66
|
+
const parsed = parseJsonText(present[0].text, { jsonc: present[0].name.endsWith(".jsonc") });
|
|
67
|
+
if (!parsed.ok || typeof parsed.value !== "object" || parsed.value === null || Array.isArray(parsed.value))
|
|
68
|
+
return { unreadable: present[0].text };
|
|
69
|
+
const obj = parsed.value;
|
|
70
|
+
return { blocks: Object.fromEntries(SCOPE_BLOCKS.filter((b) => obj[b] !== undefined).map((b) => [b, obj[b]])) };
|
|
71
|
+
}
|
|
72
|
+
/** The scope blocks that differ between two states; all of them when either can't be read and the texts differ. */
|
|
73
|
+
function changedBlocks(a, b) {
|
|
74
|
+
if ("unreadable" in a || "unreadable" in b) {
|
|
75
|
+
return "unreadable" in a && "unreadable" in b && a.unreadable === b.unreadable ? [] : [...SCOPE_BLOCKS];
|
|
76
|
+
}
|
|
77
|
+
return SCOPE_BLOCKS.filter((k) => !isDeepStrictEqual(a.blocks[k], b.blocks[k]));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The directory, from the repository root, of the declaration found from
|
|
81
|
+
* `cwd` at `base`, with the scope blocks it has there. Null when there is no
|
|
82
|
+
* declaration, or one with neither block: then nothing in it is protected. A
|
|
83
|
+
* declaration that can't be read at base counts as having both.
|
|
84
|
+
*/
|
|
85
|
+
function scopedDeclarationAt(repo, cwd, base) {
|
|
86
|
+
let dir;
|
|
87
|
+
try {
|
|
88
|
+
const root = locateWorkspace(cwd, base).root;
|
|
89
|
+
dir = root === "." ? "" : root;
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (err instanceof WorkspaceReadError)
|
|
93
|
+
return null;
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
const state = scopeState(repo, base, dir);
|
|
97
|
+
const blocks = "unreadable" in state ? [...SCOPE_BLOCKS] : SCOPE_BLOCKS.filter((b) => b in state.blocks);
|
|
98
|
+
return blocks.length > 0 ? { dir, blocks } : null;
|
|
99
|
+
}
|
|
48
100
|
/** Whether a merge commit changes nothing beyond what its parents bring. */
|
|
49
101
|
function mergeWithoutOwnChanges(repo, commit, parents) {
|
|
50
102
|
if (parents < 2)
|
|
@@ -73,6 +125,7 @@ export function verifyChange(opts) {
|
|
|
73
125
|
protectedPaths: protectedPaths(policy),
|
|
74
126
|
protectedChanged: [],
|
|
75
127
|
protectedWrites: [],
|
|
128
|
+
declaration: null,
|
|
76
129
|
rotation: null,
|
|
77
130
|
notes: [],
|
|
78
131
|
failures: [],
|
|
@@ -99,7 +152,8 @@ export function verifyChange(opts) {
|
|
|
99
152
|
}
|
|
100
153
|
// Every commit in the change, oldest first, with its parents.
|
|
101
154
|
const listing = git(repo, ["rev-list", "--reverse", "--parents", `${base.commit}..${head}`]).split("\n").filter(Boolean);
|
|
102
|
-
const
|
|
155
|
+
const parentList = new Map(listing.map((l) => { const [c, ...p] = l.split(" "); return [c, p]; }));
|
|
156
|
+
const parentsOf = new Map([...parentList].map(([c, p]) => [c, p.length]));
|
|
103
157
|
report.runs = changeRuns(repo, [...parentsOf.keys()], policy.runners);
|
|
104
158
|
if (opts.require === "attested-runs") {
|
|
105
159
|
if (policy.runners.length === 0 && report.runs.commits.length > 0) {
|
|
@@ -145,24 +199,50 @@ export function verifyChange(opts) {
|
|
|
145
199
|
report.commits.push(v);
|
|
146
200
|
}
|
|
147
201
|
// Protected writes: every commit that touches the policy needs a writer's signature.
|
|
202
|
+
const writers = policyWriters(policy);
|
|
203
|
+
const judge = (commit, paths, blocks) => {
|
|
204
|
+
const v = verdicts.get(commit) ?? { ...commitProvenance(repo, policy, commit, opts.attestors), commit, subject: "" };
|
|
205
|
+
const allowed = v.level === "attested" && v.principal !== undefined && writers.has(v.principal);
|
|
206
|
+
const who = policy.roles.admin?.length ? "an admin the policy at base names" : "a signer the policy at base lists";
|
|
207
|
+
const what = blocks ? `${paths.join(", ")} (${blocks.join(", ")})` : paths.join(", ");
|
|
208
|
+
const reason = allowed
|
|
209
|
+
? `signed by ${v.principal}, ${who.replace(/^an? /, "")}`
|
|
210
|
+
: v.level === "attested"
|
|
211
|
+
? `signed by ${v.principal}, who is not ${who}`
|
|
212
|
+
: `${what} is protected and needs a signature by ${who}: ${v.reason}`;
|
|
213
|
+
report.protectedWrites.push({ commit, paths, ...(blocks ? { blocks } : {}), level: v.level, ...(v.principal ? { principal: v.principal } : {}), allowed, reason });
|
|
214
|
+
if (!allowed)
|
|
215
|
+
report.failures.push(`${commit.slice(0, 8)} changes ${what}: ${reason}`);
|
|
216
|
+
};
|
|
148
217
|
if (changedPaths.length > 0) {
|
|
149
|
-
const writers = policyWriters(policy);
|
|
150
218
|
const touching = commitsTouching(repo, `${base.commit}..${head}`, report.protectedPaths);
|
|
151
219
|
if (touching.size === 0) {
|
|
152
220
|
report.failures.push(`the policy files ${changedPaths.join(", ")} differ from base, and no commit in the change accounts for it`);
|
|
153
221
|
}
|
|
154
|
-
for (const [commit, paths] of touching)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
222
|
+
for (const [commit, paths] of touching)
|
|
223
|
+
judge(commit, paths);
|
|
224
|
+
}
|
|
225
|
+
// The write scope in the declaration at base (#3079): a commit that changes writeScope or agents.
|
|
226
|
+
const scoped = scopedDeclarationAt(repo, opts.cwd ?? repo, base.commit);
|
|
227
|
+
if (scoped) {
|
|
228
|
+
const files = DECLARATION_FILES.map((name) => posix.join(scoped.dir, name));
|
|
229
|
+
const changed = changedBlocks(scopeState(repo, mergeBase, scoped.dir), scopeState(repo, head, scoped.dir));
|
|
230
|
+
report.declaration = { path: files.find((f) => fileAt(repo, base.commit, f) !== undefined) ?? files[0], blocks: scoped.blocks, changed };
|
|
231
|
+
if (changed.length > 0) {
|
|
232
|
+
let accounted = false;
|
|
233
|
+
for (const [commit, paths] of commitsTouching(repo, `${base.commit}..${head}`, files)) {
|
|
234
|
+
const after = scopeState(repo, commit, scoped.dir);
|
|
235
|
+
// A merge changes a block only when it differs from every parent.
|
|
236
|
+
const per = (parentList.get(commit) ?? []).map((p) => new Set(changedBlocks(scopeState(repo, p, scoped.dir), after)));
|
|
237
|
+
const blocks = SCOPE_BLOCKS.filter((b) => per.length > 0 && per.every((s) => s.has(b)));
|
|
238
|
+
if (blocks.length === 0)
|
|
239
|
+
continue;
|
|
240
|
+
accounted = true;
|
|
241
|
+
judge(commit, paths, blocks);
|
|
242
|
+
}
|
|
243
|
+
if (!accounted) {
|
|
244
|
+
report.failures.push(`${changed.join(" and ")} in ${report.declaration.path} differ from base, and no commit in the change accounts for it`);
|
|
245
|
+
}
|
|
166
246
|
}
|
|
167
247
|
}
|
|
168
248
|
// A new signer set must be signed by a threshold of the set at base (#2553).
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* `write-scope-class-unknown` until the plugin is installed at the pinned
|
|
38
38
|
* version or the entry is removed.
|
|
39
39
|
*/
|
|
40
|
-
import { type AgentDeclaration, type ClassScope, type Declaration, type PrincipalClass, type ProtectedPath, type WriteVerb } from "./declaration.js";
|
|
40
|
+
import { WorkspaceReadError, type AgentDeclaration, type ClassScope, type Declaration, type PrincipalClass, type ProtectedPath, type WriteVerb } from "./declaration.js";
|
|
41
41
|
import { classesOf, roleGrants, type ClassRegistry } from "./principal-classes.js";
|
|
42
42
|
import { type TrustPolicy } from "./trust/policy.js";
|
|
43
43
|
import { AGENT_TRAILER } from "./trailers.js";
|
|
@@ -164,6 +164,8 @@ export interface ScopeSource {
|
|
|
164
164
|
policy: TrustPolicy;
|
|
165
165
|
/** The principal classes: the core four and those the declaration's pins supply, path pins read where the declaration was (#3080). */
|
|
166
166
|
classes: ClassRegistry;
|
|
167
|
+
/** Why the working tree's declaration couldn't be read when `declaration` is null: declaration-missing, or declaration-invalid with the problem (#3433). */
|
|
168
|
+
failure?: WorkspaceReadError;
|
|
167
169
|
}
|
|
168
170
|
/**
|
|
169
171
|
* The scope for a write from `cwd`: the declaration and the trust policy at
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-scope.d.ts","sourceRoot":"","sources":["../../src/workspace/write-scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAOH,OAAO,
|
|
1
|
+
{"version":3,"file":"write-scope.d.ts","sourceRoot":"","sources":["../../src/workspace/write-scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAOH,OAAO,EAEL,kBAAkB,EAClB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,SAAS,EAIT,UAAU,EAEV,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;AAO7B,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE/D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG3C,4EAA4E;AAC5E,eAAO,MAAM,SAAS,gBAAgB,CAAC;AAEvC,iHAAiH;AACjH,OAAO,EAAE,aAAa,EAAE,CAAC;AAEzB,yDAAyD;AACzD,eAAO,MAAM,iBAAiB,mGAAoI,CAAC;AACnK,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhE;;;GAGG;AACH,eAAO,MAAM,mBAAmB,4HAA2F,CAAC;AAC5H,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;AAEjC;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,GAAE,aAAmC,GAAG,cAAc,CAE1I;AAED,8CAA8C;AAC9C,MAAM,WAAW,MAAM;IACrB,iDAAiD;IACjD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,cAAc,CAAC;IACtB,yDAAyD;IACzD,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAChC;AAED,qBAAa,eAAgB,SAAQ,KAAK;IAEtC,QAAQ,CAAC,IAAI,EAAE,cAAc;IAD/B,YACW,IAAI,EAAE,cAAc,EAC7B,OAAO,EAAE,MAAM,EAIhB;CACF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,WAAW,GAAG,IAAI,EAC/B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAC3D,OAAO,GAAE,aAAmC,GAC3C,MAAM,CAsBR;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,2BAA2B,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAU7L;AAED,kIAAkI;AAClI,wBAAgB,OAAO,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAI1F;AAED,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,gBAAgB,EAAE,eAAe,CAAC,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3H,oHAAoH;AACpH,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAmB1H,8DAA8D;AAC9D,MAAM,WAAW,UAAU;IACzB,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gFAAgF;IAChF,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,8GAA8G;IAC9G,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,0GAA0G;AAC1G,wBAAgB,WAAW,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,GAAG,QAAQ,GAAG,aAAa,CA4BxI;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CASpG;AAkBD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAiB/G;AAED,uGAAuG;AACvG,MAAM,WAAW,UAAU;IACzB,MAAM,IAAI,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,IAAI,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,UAAU,GAAG,YAAY,CAqB1H;AAID,uEAAuE;AACvE,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,4HAA4H;IAC5H,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAAC;IACrC,iEAAiE;IACjE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iGAAiG;IACjG,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,WAAW,CAAC;IACpB,sIAAsI;IACtI,OAAO,EAAE,aAAa,CAAC;IACvB,4JAA4J;IAC5J,OAAO,CAAC,EAAE,kBAAkB,CAAC;CAC9B;AAoBD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,CAejE;AAID;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,UAAU,CAQlH;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAAA;CAAE,GAAG,IAAI,CASlH;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACnI,WAAW,CASb;AAID,6CAA6C;AAC7C,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,0GAA0G;IAC1G,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,cAAc,CAAC;IACtB,iFAAiF;IACjF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,oJAAoJ;IACpJ,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,cAAc,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,yFAAyF;IACzF,IAAI,EAAE,SAAS,GAAG,QAAQ,GAAG,IAAI,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,sKAAsK;IACtK,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED,8EAA8E;AAC9E,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,gFAAgF;IAChF,OAAO,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1E,MAAM,EAAE,MAAM,CAAC;CAChB;AAkCD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,WAAW,CAAC;IACzB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,sGAAsG;IACtG,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAuE9B"}
|
|
@@ -275,17 +275,22 @@ export function judgePath(declaration, writer, path, change) {
|
|
|
275
275
|
message: `${who(writer)} may not write ${path}${allowance}: writeScope.${writer.class}.protected lists ${guarded.path}`,
|
|
276
276
|
};
|
|
277
277
|
}
|
|
278
|
-
|
|
278
|
+
/** The declaration at `at` (the working tree without it), or the read error that says why there is none. */
|
|
279
|
+
function readOrFailure(cwd, at) {
|
|
279
280
|
try {
|
|
280
281
|
const located = locateWorkspace(cwd, at);
|
|
281
282
|
return { declaration: readDeclaration(located.tree), rootOnDisk: located.rootOnDisk, root: located.root, tree: located.tree };
|
|
282
283
|
}
|
|
283
284
|
catch (err) {
|
|
284
285
|
if (err instanceof WorkspaceReadError)
|
|
285
|
-
return
|
|
286
|
+
return err;
|
|
286
287
|
throw err;
|
|
287
288
|
}
|
|
288
289
|
}
|
|
290
|
+
function readAt(cwd, at) {
|
|
291
|
+
const read = readOrFailure(cwd, at);
|
|
292
|
+
return read instanceof WorkspaceReadError ? null : read;
|
|
293
|
+
}
|
|
289
294
|
/**
|
|
290
295
|
* The scope for a write from `cwd`: the declaration and the trust policy at
|
|
291
296
|
* the base revision (`origin/HEAD`, `main` or `master`), so a write can't
|
|
@@ -306,8 +311,10 @@ export function scopeSource(cwd, at) {
|
|
|
306
311
|
if (atBase)
|
|
307
312
|
return { ...withClasses(atBase), from: "base", policy };
|
|
308
313
|
}
|
|
309
|
-
const here =
|
|
310
|
-
|
|
314
|
+
const here = readOrFailure(cwd);
|
|
315
|
+
if (here instanceof WorkspaceReadError)
|
|
316
|
+
return { declaration: null, from: null, rootOnDisk: null, root: null, policy, classes: coreClassRegistry(), failure: here };
|
|
317
|
+
return { ...withClasses(here), from: "working-tree", policy };
|
|
311
318
|
}
|
|
312
319
|
const toPosix = (p) => (sep === "/" ? p : p.split(sep).join("/"));
|
|
313
320
|
/**
|
package/package.json
CHANGED
package/src/ci-jobs.ts
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
* - the `setup` steps and `environment` a job may carry, checked once per
|
|
16
16
|
* forge ({@link assertSetupSteps}, {@link assertEnvironment},
|
|
17
17
|
* {@link gitlabSetupScript}, {@link assertGitlabEnvironment});
|
|
18
|
-
* - GitLab's `rules:` expressions and `id_tokens:` declaration
|
|
18
|
+
* - GitLab's `rules:` expressions and `id_tokens:` declaration;
|
|
19
|
+
* - where a job runs and the id tokens it asks for (#3914), {@link githubRunsOn},
|
|
20
|
+
* {@link gitlabTags}, {@link gitlabIdTokensDoc} and {@link idTokenAudienceEnv}.
|
|
19
21
|
*
|
|
20
22
|
* The Forgejo dialect (action refs, dropped `permissions:` and
|
|
21
23
|
* `environment:`, the header note naming what was dropped) is the forgejo
|
|
@@ -96,11 +98,66 @@ export function dirnameOf(path: string): string {
|
|
|
96
98
|
return i <= 0 ? "." : path.slice(0, i);
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Where a generated job runs (#3914): a runner label, a list of labels the
|
|
103
|
+
* runner must all carry, or on GitHub a runner group with optional labels.
|
|
104
|
+
* `runs-on` on GitHub and Forgejo, `tags` on GitLab.
|
|
105
|
+
*/
|
|
106
|
+
export type CiRunner = string | readonly string[] | { group: string; labels?: readonly string[] };
|
|
107
|
+
|
|
108
|
+
/** True when `runner` is a GitHub runner group, which only GitHub has. */
|
|
109
|
+
export function isRunnerGroup(runner: CiRunner | undefined): runner is { group: string; labels?: readonly string[] } {
|
|
110
|
+
return runner !== undefined && typeof runner === "object" && !Array.isArray(runner);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Refuse a runner group on a forge that has none (#3914). `subject` and
|
|
115
|
+
* `field` name whose runner it is, such as `Op waves "x"` and `wave "prod"
|
|
116
|
+
* runner`.
|
|
117
|
+
*/
|
|
118
|
+
export function assertRunnerLabels(subject: string, field: string, runner: CiRunner | undefined, forge: "GitLab" | "Forgejo"): void {
|
|
119
|
+
if (!isRunnerGroup(runner)) return;
|
|
120
|
+
throw new Error(
|
|
121
|
+
`${subject}: ${field} is the runner group "${runner.group}", which only GitHub has. ` +
|
|
122
|
+
`${forge === "GitLab" ? "GitLab picks a runner by its tags" : "Forgejo picks a runner by its labels"}; give a label or a list of labels.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Each id token's audience as a variable, `<NAME>_AUDIENCE` (#3914). On
|
|
128
|
+
* GitHub and Forgejo the audience is chosen when the job requests the token,
|
|
129
|
+
* so the job reads it from there; GitLab declares it in `id_tokens:`
|
|
130
|
+
* ({@link gitlabIdTokensDoc}).
|
|
131
|
+
*/
|
|
132
|
+
export function idTokenAudienceEnv(idTokens: Readonly<Record<string, string>> | undefined): Record<string, string> {
|
|
133
|
+
return Object.fromEntries(Object.entries(idTokens ?? {}).map(([name, aud]) => [`${name}_AUDIENCE`, aud]));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A job's GitLab `id_tokens:` for `{ <variable>: <audience> }` (#3914), or undefined when there are none. */
|
|
137
|
+
export function gitlabIdTokensDoc(idTokens: Readonly<Record<string, string>> | undefined): Record<string, { aud: string }> | undefined {
|
|
138
|
+
const entries = Object.entries(idTokens ?? {});
|
|
139
|
+
return entries.length > 0 ? Object.fromEntries(entries.map(([name, aud]) => [name, { aud }])) : undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** A job's GitLab `tags:` for `runner` (#3914), or undefined when it names none. */
|
|
143
|
+
export function gitlabTags(runner: CiRunner | undefined): string[] | undefined {
|
|
144
|
+
if (runner === undefined || isRunnerGroup(runner)) return undefined;
|
|
145
|
+
return typeof runner === "string" ? [runner] : [...runner];
|
|
146
|
+
}
|
|
147
|
+
|
|
99
148
|
// ── GitHub Actions and Forgejo Actions ──────────────────────────────────────
|
|
100
149
|
|
|
101
150
|
/** The runner label a generated job asks for; the Forgejo dialect rewrites it to the project's label. */
|
|
102
151
|
export const GITHUB_RUNNER = "ubuntu-latest";
|
|
103
152
|
|
|
153
|
+
/** A job's `runs-on` for `runner` (#3914): {@link GITHUB_RUNNER} when it names none. */
|
|
154
|
+
export function githubRunsOn(runner: CiRunner | undefined): string | string[] | { group: string; labels?: string[] } {
|
|
155
|
+
if (runner === undefined) return GITHUB_RUNNER;
|
|
156
|
+
if (typeof runner === "string") return runner;
|
|
157
|
+
if (isRunnerGroup(runner)) return { group: runner.group, ...(runner.labels && runner.labels.length > 0 ? { labels: [...runner.labels] } : {}) };
|
|
158
|
+
return [...runner];
|
|
159
|
+
}
|
|
160
|
+
|
|
104
161
|
/** The job token, which GitHub and Forgejo both mint for every run. */
|
|
105
162
|
export const GITHUB_JOB_TOKEN = "${{ github.token }}";
|
|
106
163
|
|
|
@@ -32,6 +32,7 @@ import { loadPolicyChecks } from "../../lint/policy";
|
|
|
32
32
|
import { armSandboxPolicyExecution, runProjectPolicies } from "../../lint/policy-sandbox";
|
|
33
33
|
import { sortedJsonReplacer } from "../../utils";
|
|
34
34
|
import { rankFoldBlockers, toCollapsedFormat, type FoldRankResult } from "../../discovery/fold-rank";
|
|
35
|
+
import { foldExecutionCounts } from "../../discovery/fold-import";
|
|
35
36
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
36
37
|
import { writeFileSync, mkdirSync } from "fs";
|
|
37
38
|
import { resolve, dirname, join, relative } from "path";
|
|
@@ -396,6 +397,23 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
396
397
|
: (decision.reason ?? "fell back to run");
|
|
397
398
|
console.error(formatInfo(`[fold:${decision.mode}] ${rel} — ${detail}`));
|
|
398
399
|
}
|
|
400
|
+
// typescript-as-data#177 — the exact instrument beside the sampled one.
|
|
401
|
+
// `test/leftness` reports "0 MB of definition-library code on the path"
|
|
402
|
+
// from a `node --cpu-prof` recording, and a profile has a sampling
|
|
403
|
+
// floor: 0 MB is everything the profiler could see rather than nothing
|
|
404
|
+
// having run. L10.1's counters have no floor.
|
|
405
|
+
//
|
|
406
|
+
// Inside --verbose with the per-file lines, not beside them. The default
|
|
407
|
+
// path is deliberately quiet for the reason above, and the leftness
|
|
408
|
+
// capture already runs --verbose and greps this log for `[fold:fold]`.
|
|
409
|
+
const counts = foldExecutionCounts();
|
|
410
|
+
console.error(
|
|
411
|
+
formatInfo(
|
|
412
|
+
`[fold:counts] factoryInvocations=${counts.factoryInvocations} ` +
|
|
413
|
+
`projectFactoryInvocations=${counts.projectFactoryInvocations} ` +
|
|
414
|
+
`factoryInterpretations=${counts.factoryInterpretations}`,
|
|
415
|
+
),
|
|
416
|
+
);
|
|
399
417
|
} else {
|
|
400
418
|
console.error(formatInfo(summarizeFoldDecisions(result.foldDecisions)));
|
|
401
419
|
}
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* Each run is traced through a load hook that NODE_OPTIONS hands to the CLI's
|
|
8
8
|
* node, so the test can say what loaded: none of chant's TypeScript source,
|
|
9
9
|
* ever, and tsx only for a command that imports TypeScript (here the
|
|
10
|
-
* lexicon and the project source).
|
|
10
|
+
* lexicon and the project source). tsx's ESM hooks run on a thread of their
|
|
11
|
+
* own, about 200 MB of the process (#3276), so a workspace read must not
|
|
12
|
+
* register it either.
|
|
11
13
|
*
|
|
12
14
|
* End to end because it packs, installs from the registry and spawns the CLI.
|
|
13
15
|
*/
|
|
@@ -20,12 +22,14 @@ import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
|
20
22
|
|
|
21
23
|
const coreRoot = resolve(import.meta.dirname, "..", "..");
|
|
22
24
|
const renderRoot = resolve(coreRoot, "..", "..", "lexicons", "render");
|
|
25
|
+
const referenceDecisions = resolve(coreRoot, "..", "..", "reference-workspace", "decisions");
|
|
23
26
|
const version = (JSON.parse(readFileSync(join(coreRoot, "package.json"), "utf-8")) as { version: string }).version;
|
|
24
27
|
|
|
25
28
|
let scratch: string;
|
|
26
29
|
let project: string;
|
|
27
30
|
let cjsProject: string;
|
|
28
31
|
let waves: string;
|
|
32
|
+
let workspace: string;
|
|
29
33
|
let chant: string;
|
|
30
34
|
let tracer: string;
|
|
31
35
|
|
|
@@ -126,6 +130,31 @@ beforeAll(() => {
|
|
|
126
130
|
writeFileSync(join(waves, ".gitignore"), ".chant/\n");
|
|
127
131
|
git("add", "-A");
|
|
128
132
|
git("commit", "-qm", "ignore");
|
|
133
|
+
|
|
134
|
+
// A workspace whose declaration names a .mjs record kind, with a member
|
|
135
|
+
// that has a chant.config.ts, for the workspace reads below (#3276).
|
|
136
|
+
workspace = join(scratch, "workspace");
|
|
137
|
+
mkdirSync(join(workspace, "decisions"), { recursive: true });
|
|
138
|
+
mkdirSync(join(workspace, "app"));
|
|
139
|
+
for (const file of ["decision.kind.mjs", "decision.schema.json", "ref-001-how-the-app-is-deployed.md"]) {
|
|
140
|
+
writeFileSync(join(workspace, "decisions", file), readFileSync(join(referenceDecisions, file)));
|
|
141
|
+
}
|
|
142
|
+
writeFileSync(
|
|
143
|
+
join(workspace, "chant.workspace.json"),
|
|
144
|
+
JSON.stringify({
|
|
145
|
+
name: "e2e",
|
|
146
|
+
schema: 1,
|
|
147
|
+
members: [{ name: "app", dir: "app", kind: "chant" }],
|
|
148
|
+
records: [{ kind: "decisions/decision.kind.mjs" }],
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
writeFileSync(join(workspace, "app", "chant.config.ts"), `export default { lexicons: [] as string[] };\n`);
|
|
152
|
+
symlinkSync(join(project, "node_modules"), join(workspace, "node_modules"), "dir");
|
|
153
|
+
const wsGit = (...args: string[]) => execFileSync("git", ["-c", "user.email=e2e@chant", "-c", "user.name=e2e", ...args], { cwd: workspace, stdio: "ignore" });
|
|
154
|
+
wsGit("init", "-q", "-b", "main");
|
|
155
|
+
writeFileSync(join(workspace, ".gitignore"), "node_modules\n");
|
|
156
|
+
wsGit("add", "-A");
|
|
157
|
+
wsGit("commit", "-qm", "workspace");
|
|
129
158
|
}, 600_000);
|
|
130
159
|
|
|
131
160
|
afterAll(() => {
|
|
@@ -148,6 +177,21 @@ describe("the installed chant CLI (#3878)", () => {
|
|
|
148
177
|
expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0 });
|
|
149
178
|
});
|
|
150
179
|
|
|
180
|
+
test("workspace ls reads a .mjs record kind from a member with a chant.config.ts without loading tsx", () => {
|
|
181
|
+
const r = run(join(workspace, "app"), ["workspace", "ls", "--json"]);
|
|
182
|
+
expect(r.status, r.stderr).toBe(0);
|
|
183
|
+
const ls = JSON.parse(r.stdout) as { workspace: { records: Array<{ name: string; kind: string | null }> } };
|
|
184
|
+
expect(ls.workspace.records.map((k) => k.kind)).toEqual(["decision"]);
|
|
185
|
+
expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0 });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("workspace records reads the declared .mjs kind's records without loading tsx", () => {
|
|
189
|
+
const r = run(join(workspace, "app"), ["workspace", "records", "--json"]);
|
|
190
|
+
expect(r.status, r.stderr).toBe(0);
|
|
191
|
+
expect(r.stdout).toContain("ref-001");
|
|
192
|
+
expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0 });
|
|
193
|
+
});
|
|
194
|
+
|
|
151
195
|
test("build loads the TypeScript lexicon and project through tsx, against the one compiled core", () => {
|
|
152
196
|
const r = run(project, ["build", "src", "-o", "out/render.json"]);
|
|
153
197
|
expect(r.status, r.stderr).toBe(0);
|
|
@@ -24,6 +24,7 @@ import { FAN_OUT_GATE_OP, OVERRIDE_GATE_OP, WORKSPACE_UPGRADE_GATE_OP } from "..
|
|
|
24
24
|
import { PR_GATE_OP } from "../../pr-loop";
|
|
25
25
|
import { pushLifecycleReport, requireLifecycleLedger } from "../../lifecycle/git";
|
|
26
26
|
import { LIFECYCLE_LOCAL_NOTE, isLifecycleLocalNote } from "../../lifecycle/local-note";
|
|
27
|
+
import { ledgerRecordingOff, NO_RECORD_ENV } from "../../lifecycle/no-record";
|
|
27
28
|
import { formatError, formatWarning, formatSuccess, formatInfo } from "../format";
|
|
28
29
|
import type { CommandContext } from "../registry";
|
|
29
30
|
|
|
@@ -267,6 +268,14 @@ export async function recordGateApproval(
|
|
|
267
268
|
gate: string,
|
|
268
269
|
opts: GateApprovalOptions,
|
|
269
270
|
): Promise<GateApprovalOutcome> {
|
|
271
|
+
// #3955: with recording off the approval would be dropped with the process.
|
|
272
|
+
if (ledgerRecordingOff()) {
|
|
273
|
+
console.error(formatError({
|
|
274
|
+
message: `Cannot record the approval of "${gate}": ${NO_RECORD_ENV} is set, so nothing is written to chant/lifecycle`,
|
|
275
|
+
hint: `Unset ${NO_RECORD_ENV} to approve the gate.`,
|
|
276
|
+
}));
|
|
277
|
+
return { ok: false };
|
|
278
|
+
}
|
|
270
279
|
const { ops } = await discoverOps();
|
|
271
280
|
// `fan-out` is the op name every `chant components fan-out` gate is recorded
|
|
272
281
|
// under (../handlers/fan-out.ts). It is a command rather than a declaration,
|
|
@@ -42,7 +42,7 @@ vi.mock("node:child_process", async () => {
|
|
|
42
42
|
return { ...actual, exec: execWithCustom, execFile: execFileWithCustom };
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
const { runEmulator, formatEmulatorHelp, parsePortMappings } = await import("./emulator");
|
|
45
|
+
const { runEmulator, formatEmulatorHelp, parsePortMappings, parseUpOptions } = await import("./emulator");
|
|
46
46
|
|
|
47
47
|
function cap(): EmulatorCapability {
|
|
48
48
|
return {
|
|
@@ -286,3 +286,63 @@ describe("chant emulator --port and --help (#3673)", () => {
|
|
|
286
286
|
expect(formatEmulatorHelp([])).toContain("No lexicon configured for this project has a local emulator.");
|
|
287
287
|
});
|
|
288
288
|
});
|
|
289
|
+
|
|
290
|
+
describe("chant emulator up --image, --network, --setting, --loopback (#3952)", () => {
|
|
291
|
+
const floci = (): EmulatorCapability => ({ ...cap(), spec: { ...cap().spec, settingsPrefix: "FLOCI_" } });
|
|
292
|
+
const other = (): EmulatorCapability => ({
|
|
293
|
+
spec: { name: "chant-floci-az", image: "floci/floci-az:1.0.0", containerPort: 4577, healthPath: "/health" },
|
|
294
|
+
env: () => ({}),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
beforeEach(() => {
|
|
298
|
+
upMock.mockReset();
|
|
299
|
+
vi.restoreAllMocks();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("up passes each option to the lifecycle", async () => {
|
|
303
|
+
stderr();
|
|
304
|
+
upMock.mockResolvedValue({ endpoint: "http://localhost:4566" });
|
|
305
|
+
const exit = await runEmulator({
|
|
306
|
+
args: makeArgs({
|
|
307
|
+
path: "up", image: "floci/floci:1.6.0", network: "loom", loopback: true,
|
|
308
|
+
setting: ["FLOCI_STORAGE_MODE=memory", "FLOCI_X=a=b"], port: ["4567"],
|
|
309
|
+
}),
|
|
310
|
+
plugins: [plugin("aws", floci())],
|
|
311
|
+
serializers: [],
|
|
312
|
+
});
|
|
313
|
+
expect(exit).toBe(0);
|
|
314
|
+
expect(upMock).toHaveBeenCalledWith({
|
|
315
|
+
image: "floci/floci:1.6.0", network: "loom", loopback: true,
|
|
316
|
+
settings: { FLOCI_STORAGE_MODE: "memory", FLOCI_X: "a=b" }, port: 4567,
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("a setting no selected emulator takes is refused before anything starts", async () => {
|
|
321
|
+
const err = stderr();
|
|
322
|
+
const exit = await runEmulator({
|
|
323
|
+
args: makeArgs({ path: "up", setting: ["HOME=/tmp"] }),
|
|
324
|
+
plugins: [plugin("aws", floci())],
|
|
325
|
+
serializers: [],
|
|
326
|
+
});
|
|
327
|
+
expect(exit).toBe(1);
|
|
328
|
+
expect(upMock).not.toHaveBeenCalled();
|
|
329
|
+
expect(err.join("\n")).toContain("--setting HOME: no selected emulator takes it; the selected ones take FLOCI_* (chant-floci)");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("settings go only to the emulator whose prefix matches; --image needs one emulator", () => {
|
|
333
|
+
const a = floci();
|
|
334
|
+
const b = other();
|
|
335
|
+
const opts = parseUpOptions({ setting: ["FLOCI_A=1"], network: "n" }, [a, b]);
|
|
336
|
+
expect(opts.get(a)).toEqual({ network: "n", settings: { FLOCI_A: "1" } });
|
|
337
|
+
expect(opts.get(b)).toEqual({ network: "n" });
|
|
338
|
+
expect(() => parseUpOptions({ image: "x:1" }, [a, b])).toThrow(/--image x:1 is ambiguous with 2 emulators/);
|
|
339
|
+
expect(() => parseUpOptions({ setting: ["FLOCI_A"] }, [a])).toThrow(/expected <KEY>=<VALUE>/);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("the options apply to up only", async () => {
|
|
343
|
+
const err = stderr();
|
|
344
|
+
const exit = await runEmulator({ args: makeArgs({ path: "status", loopback: true }), plugins: [plugin("aws", floci())], serializers: [] });
|
|
345
|
+
expect(exit).toBe(1);
|
|
346
|
+
expect(err.join("\n")).toContain("--loopback applies to `chant emulator up` only");
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { exec, execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import { emulatorLifecycle, emulatorsOf, type EmulatorCapability } from "../../op";
|
|
3
|
+
import { checkSettings, emulatorLifecycle, emulatorsOf, type EmulatorCapability, type EmulatorUpArgs } from "../../op";
|
|
4
4
|
import type { LexiconPlugin } from "../../lexicon";
|
|
5
5
|
import { formatError, formatWarning, formatSuccess } from "../format";
|
|
6
|
-
import type { CommandContext } from "../registry";
|
|
6
|
+
import type { CommandContext, ParsedArgs } from "../registry";
|
|
7
7
|
|
|
8
8
|
const execAsync = promisify(exec);
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
@@ -116,13 +116,60 @@ export function parsePortMappings(values: readonly string[], targets: readonly E
|
|
|
116
116
|
return ports;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/** The `up` options other than the port (#3952), per selected emulator. */
|
|
120
|
+
type UpOptions = Pick<EmulatorUpArgs, "image" | "network" | "settings" | "loopback">;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `--image`, `--network`, `--setting` and `--loopback` as each selected
|
|
124
|
+
* emulator's `up` options (#3952). `--image` needs exactly one emulator
|
|
125
|
+
* selected, the way a bare `--port` does. A `--setting` goes to each emulator
|
|
126
|
+
* whose settings prefix its key starts with, and a key that no selected
|
|
127
|
+
* emulator takes is refused rather than passed to a container as plain env.
|
|
128
|
+
*/
|
|
129
|
+
export function parseUpOptions(
|
|
130
|
+
args: Pick<ParsedArgs, "image" | "network" | "setting" | "loopback">,
|
|
131
|
+
targets: readonly EmulatorCapability[],
|
|
132
|
+
): Map<EmulatorCapability, UpOptions> {
|
|
133
|
+
if (args.image !== undefined && targets.length !== 1) {
|
|
134
|
+
throw new Error(`--image ${args.image} is ambiguous with ${targets.length} emulators selected; pick one with --lexicon <name>`);
|
|
135
|
+
}
|
|
136
|
+
const settings = new Map<EmulatorCapability, Record<string, string>>();
|
|
137
|
+
for (const value of args.setting ?? []) {
|
|
138
|
+
const match = /^([^=]+)=(.*)$/s.exec(value);
|
|
139
|
+
if (!match) throw new Error(`--setting ${value}: expected <KEY>=<VALUE>, e.g. --setting FLOCI_<NAME>=<value>`);
|
|
140
|
+
const [, key, val] = match as unknown as [string, string, string];
|
|
141
|
+
const takers = targets.filter((t) => t.spec.settingsPrefix && key.startsWith(t.spec.settingsPrefix));
|
|
142
|
+
if (takers.length === 0) {
|
|
143
|
+
const prefixes = targets.filter((t) => t.spec.settingsPrefix).map((t) => `${t.spec.settingsPrefix}* (${t.spec.name})`);
|
|
144
|
+
throw new Error(`--setting ${key}: no selected emulator takes it; ${prefixes.length ? `the selected ones take ${prefixes.join(", ")}` : "the selected ones take no settings"}`);
|
|
145
|
+
}
|
|
146
|
+
for (const t of takers) {
|
|
147
|
+
const own = { ...settings.get(t), [key]: val };
|
|
148
|
+
checkSettings(t.spec, own);
|
|
149
|
+
settings.set(t, own);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const out = new Map<EmulatorCapability, UpOptions>();
|
|
153
|
+
for (const t of targets) {
|
|
154
|
+
const opts: UpOptions = {};
|
|
155
|
+
if (args.image !== undefined) opts.image = args.image;
|
|
156
|
+
if (args.network !== undefined) opts.network = args.network;
|
|
157
|
+
if (args.loopback) opts.loopback = true;
|
|
158
|
+
const own = settings.get(t);
|
|
159
|
+
if (own) opts.settings = own;
|
|
160
|
+
out.set(t, opts);
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
119
165
|
/**
|
|
120
166
|
* `chant emulator --help` (#3673): the subcommands and flags, and for each
|
|
121
167
|
* configured lexicon's emulator the port, endpoint and sign-in a client uses.
|
|
122
168
|
*/
|
|
123
169
|
export function formatEmulatorHelp(plugins: readonly LexiconPlugin[]): string {
|
|
124
170
|
const lines = [
|
|
125
|
-
"Usage: chant emulator <up|down|status> [--lexicon <name>] [--port <container-port>=<host-port>] [--
|
|
171
|
+
"Usage: chant emulator <up|down|status> [--lexicon <name>] [--port <container-port>=<host-port>] [--image <ref>]",
|
|
172
|
+
" [--network <name>] [--setting <KEY>=<VALUE>] [--loopback] [--json]",
|
|
126
173
|
"",
|
|
127
174
|
"Boot, stop or inspect the local emulators of the project's configured lexicons.",
|
|
128
175
|
"",
|
|
@@ -137,6 +184,11 @@ export function formatEmulatorHelp(plugins: readonly LexiconPlugin[]): string {
|
|
|
137
184
|
" --lexicon <name> Act on one lexicon's emulators only.",
|
|
138
185
|
" --port <c>=<h> up: publish container port <c> on host port <h>; repeatable.",
|
|
139
186
|
" A bare <h> works when one emulator is selected.",
|
|
187
|
+
" --image <ref> up: start this image instead of the pinned default; one emulator only.",
|
|
188
|
+
" --network <name> up: join each started container to this Docker network.",
|
|
189
|
+
" --setting <K>=<V> up: pass an emulator setting as -e K=V; repeatable. The key must",
|
|
190
|
+
" start with the emulator's prefix (FLOCI_ for aws).",
|
|
191
|
+
" --loopback up: publish ports on 127.0.0.1 only, not on every interface.",
|
|
140
192
|
" --json Print the machine-readable report on stdout.",
|
|
141
193
|
"",
|
|
142
194
|
];
|
|
@@ -195,9 +247,18 @@ export async function runEmulator(ctx: CommandContext): Promise<number> {
|
|
|
195
247
|
}
|
|
196
248
|
|
|
197
249
|
let ports: Map<number, number>;
|
|
250
|
+
let upOptions: Map<EmulatorCapability, UpOptions>;
|
|
198
251
|
try {
|
|
199
252
|
if (args.port?.length && action !== "up") throw new Error("--port applies to `chant emulator up` only");
|
|
253
|
+
const upOnly = [
|
|
254
|
+
args.image !== undefined && "--image",
|
|
255
|
+
args.network !== undefined && "--network",
|
|
256
|
+
args.setting?.length && "--setting",
|
|
257
|
+
args.loopback && "--loopback",
|
|
258
|
+
].filter(Boolean);
|
|
259
|
+
if (upOnly.length && action !== "up") throw new Error(`${upOnly.join(", ")} applies to \`chant emulator up\` only`);
|
|
200
260
|
ports = parsePortMappings(args.port ?? [], targets.map((t) => t.cap));
|
|
261
|
+
upOptions = parseUpOptions(args, targets.map((t) => t.cap));
|
|
201
262
|
} catch (err) {
|
|
202
263
|
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
203
264
|
return 1;
|
|
@@ -209,7 +270,7 @@ export async function runEmulator(ctx: CommandContext): Promise<number> {
|
|
|
209
270
|
if (action === "up") {
|
|
210
271
|
const port = ports.get(cap.spec.containerPort);
|
|
211
272
|
try {
|
|
212
|
-
const { endpoint } = await lc.up(port === undefined ? {} : { port });
|
|
273
|
+
const { endpoint } = await lc.up({ ...upOptions.get(cap), ...(port === undefined ? {} : { port }) });
|
|
213
274
|
reports.push({ lexicon, name: cap.spec.name, endpoint, env: cap.env(endpoint) });
|
|
214
275
|
} catch (err) {
|
|
215
276
|
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|