@intentius/chant 0.124.44 → 0.124.46
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/capabilities/aws.paths +148 -0
- package/dist/capabilities/base-mappings.json +122 -0
- package/dist/capabilities/catalog.d.ts +89 -0
- package/dist/capabilities/catalog.d.ts.map +1 -0
- package/dist/capabilities/catalog.js +204 -0
- package/dist/capabilities/language.d.ts +103 -0
- package/dist/capabilities/language.d.ts.map +1 -0
- package/dist/capabilities/language.js +335 -0
- package/dist/capabilities/matcher.d.ts +65 -0
- package/dist/capabilities/matcher.d.ts.map +1 -0
- package/dist/capabilities/matcher.js +299 -0
- package/dist/capabilities/view.d.ts +207 -0
- package/dist/capabilities/view.d.ts.map +1 -0
- package/dist/capabilities/view.js +484 -0
- package/dist/ci-identity.d.ts +183 -12
- package/dist/ci-identity.d.ts.map +1 -1
- package/dist/ci-identity.js +362 -53
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/lint.js +1 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +17 -1
- package/dist/cli/handlers/declared-graph.d.ts +44 -0
- package/dist/cli/handlers/declared-graph.d.ts.map +1 -0
- package/dist/cli/handlers/declared-graph.js +87 -0
- package/dist/cli/handlers/graph.d.ts +25 -0
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +93 -87
- package/dist/cli/handlers/pr-env.d.ts +8 -2
- package/dist/cli/handlers/pr-env.d.ts.map +1 -1
- package/dist/cli/handlers/pr-env.js +127 -225
- package/dist/cli/handlers/pr.d.ts +3 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +1 -0
- package/dist/cli/handlers/search-count.d.ts +89 -0
- package/dist/cli/handlers/search-count.d.ts.map +1 -0
- package/dist/cli/handlers/search-count.js +132 -0
- package/dist/cli/handlers/search.d.ts +358 -6
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/handlers/search.js +1065 -155
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +23 -3
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/server.js +3 -0
- package/dist/cli/mcp/tools/capabilities.d.ts +7 -0
- package/dist/cli/mcp/tools/capabilities.d.ts.map +1 -0
- package/dist/cli/mcp/tools/capabilities.js +27 -0
- package/dist/cli/mcp/workspace-tools.d.ts +3 -1
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.js +47 -2
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +64 -17
- package/dist/cli/registry.d.ts +22 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +2 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +4 -2
- package/dist/components/fan-out-support.d.ts +3 -0
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/fan-out-support.js +1 -1
- package/dist/components/pr-env-ports.d.ts +29 -0
- package/dist/components/pr-env-ports.d.ts.map +1 -0
- package/dist/components/pr-env-ports.js +77 -0
- package/dist/components/pr-env-run.d.ts +254 -0
- package/dist/components/pr-env-run.d.ts.map +1 -0
- package/dist/components/pr-env-run.js +376 -0
- package/dist/components/pr-environment.d.ts +91 -15
- package/dist/components/pr-environment.d.ts.map +1 -1
- package/dist/components/pr-environment.js +95 -9
- package/dist/components/pr-pipeline.d.ts +10 -1
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/config-environments.d.ts +92 -0
- package/dist/config-environments.d.ts.map +1 -0
- package/dist/config-environments.js +102 -0
- package/dist/config.d.ts +24 -84
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -96
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +15 -3
- package/dist/graph-effective.d.ts +33 -2
- package/dist/graph-effective.d.ts.map +1 -1
- package/dist/graph-effective.js +215 -68
- package/dist/graph-ir.d.ts +23 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ir.js +130 -0
- package/dist/graph-sub-template.d.ts +11 -0
- package/dist/graph-sub-template.d.ts.map +1 -0
- package/dist/graph-sub-template.js +19 -0
- package/dist/lexicon.d.ts +9 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +28 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/observe.js +23 -1
- package/dist/lint/rule.d.ts +6 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/live-endpoint.d.ts +1 -1
- package/dist/live-endpoint.d.ts.map +1 -1
- package/dist/live-endpoint.js +1 -1
- package/dist/observation.d.ts +48 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +21 -5
- package/dist/op/activities/activity-contracts.d.ts +4 -4
- package/dist/op/op-waves.d.ts +17 -0
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +23 -1
- package/dist/pr-forge.d.ts +15 -1
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +28 -16
- package/dist/pr-requires.d.ts +5 -1
- package/dist/pr-requires.d.ts.map +1 -1
- package/dist/wave-runtime.mjs +251 -250
- package/dist/workspace/capabilities-contract.d.ts +66 -0
- package/dist/workspace/capabilities-contract.d.ts.map +1 -0
- package/dist/workspace/capabilities-contract.js +107 -0
- package/dist/workspace/capabilities.schema.json +229 -0
- package/dist/workspace/ci-cloud-identity.d.ts +3 -1
- package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
- package/dist/workspace/ci-cloud-identity.js +16 -1
- package/dist/workspace/ci-run.d.ts +35 -8
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +95 -21
- package/dist/workspace/ci-secrets.d.ts.map +1 -1
- package/dist/workspace/ci-secrets.js +4 -3
- package/dist/workspace/compose-graph.d.ts +9 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/compose-graph.js +3 -1
- package/dist/workspace/conformance/index.d.ts +1 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/conformance/index.js +39 -2
- package/dist/workspace/declaration.d.ts +10 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +17 -1
- package/dist/workspace/declaration.schema.json +36 -1
- package/dist/workspace/graph-cache.d.ts +8 -0
- package/dist/workspace/graph-cache.d.ts.map +1 -1
- package/dist/workspace/graph-cache.js +27 -3
- package/dist/workspace/graph-cli.d.ts +39 -2
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/graph-cli.js +66 -24
- package/dist/workspace/graph.schema.json +148 -0
- package/dist/workspace/kind-readers.d.ts +3 -0
- package/dist/workspace/kind-readers.d.ts.map +1 -1
- package/dist/workspace/kind-readers.js +7 -1
- package/dist/workspace/member-commands.d.ts +10 -1
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/member-commands.js +17 -1
- package/dist/workspace/nested-graph.d.ts +2 -0
- package/dist/workspace/nested-graph.d.ts.map +1 -1
- package/dist/workspace/nested-graph.js +32 -1
- package/dist/workspace/reason-codes.d.ts +10 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +12 -0
- package/dist/workspace/search-cli.d.ts +204 -0
- package/dist/workspace/search-cli.d.ts.map +1 -0
- package/dist/workspace/search-cli.js +466 -0
- package/dist/workspace/search-contract.d.ts +56 -0
- package/dist/workspace/search-contract.d.ts.map +1 -0
- package/dist/workspace/search-contract.js +70 -0
- package/dist/workspace/search.schema.json +822 -0
- package/package.json +1 -1
- package/src/__snapshots__/okf.test.ts.snap +10 -0
- package/src/capabilities/aws.paths +148 -0
- package/src/capabilities/base-mappings.json +122 -0
- package/src/capabilities/catalog.ts +230 -0
- package/src/capabilities/language.test.ts +191 -0
- package/src/capabilities/language.ts +373 -0
- package/src/capabilities/matcher.ts +315 -0
- package/src/capabilities/view-compose.test.ts +76 -0
- package/src/capabilities/view.ts +631 -0
- package/src/ci-identity-targets.test.ts +275 -0
- package/src/ci-identity.ts +394 -61
- package/src/cli/commands/lint.ts +1 -0
- package/src/cli/handlers/build.ts +15 -1
- package/src/cli/handlers/declared-graph.ts +99 -0
- package/src/cli/handlers/graph.test.ts +94 -3
- package/src/cli/handlers/graph.ts +123 -96
- package/src/cli/handlers/pr-env.ts +135 -227
- package/src/cli/handlers/pr.ts +4 -0
- package/src/cli/handlers/search-build-roots.test.ts +144 -0
- package/src/cli/handlers/search-count.test.ts +191 -0
- package/src/cli/handlers/search-count.ts +165 -0
- package/src/cli/handlers/search-json.test.ts +333 -0
- package/src/cli/handlers/search-live.test.ts +387 -0
- package/src/cli/handlers/search-paths.test.ts +235 -0
- package/src/cli/handlers/search.ts +1312 -166
- package/src/cli/main.test.ts +28 -3
- package/src/cli/main.ts +23 -3
- package/src/cli/mcp/server.test.ts +3 -3
- package/src/cli/mcp/server.ts +3 -0
- package/src/cli/mcp/tools/capabilities.ts +32 -0
- package/src/cli/mcp/workspace-tools.e2e.test.ts +21 -1
- package/src/cli/mcp/workspace-tools.ts +47 -2
- package/src/cli/parse-args.ts +52 -13
- package/src/cli/registry.ts +22 -0
- package/src/components/cli-support.ts +6 -1
- package/src/components/fan-out-support.ts +4 -1
- package/src/components/pr-env-ports.ts +89 -0
- package/src/components/pr-env-run-bundle.test.ts +54 -0
- package/src/components/pr-env-run.test.ts +290 -0
- package/src/components/pr-env-run.ts +563 -0
- package/src/components/pr-environment.ts +161 -13
- package/src/components/pr-pipeline.ts +10 -1
- package/src/config-environments.ts +133 -0
- package/src/config.ts +37 -124
- package/src/graph-detail.ts +15 -3
- package/src/graph-effective.ts +226 -75
- package/src/graph-ir.test.ts +163 -0
- package/src/graph-ir.ts +147 -0
- package/src/graph-sub-template.ts +22 -0
- package/src/lexicon.ts +10 -0
- package/src/lifecycle/observe.test.ts +27 -1
- package/src/lifecycle/observe.ts +43 -1
- package/src/lint/rule.ts +6 -0
- package/src/live-endpoint.ts +1 -1
- package/src/observation.test.ts +25 -0
- package/src/observation.ts +69 -5
- package/src/op/op-waves.ts +38 -1
- package/src/pr-forge-bundle.test.ts +1 -1
- package/src/pr-forge.ts +44 -17
- package/src/pr-requires.test.ts +43 -6
- package/src/pr-requires.ts +5 -1
- package/src/workspace/capabilities-contract.test.ts +88 -0
- package/src/workspace/capabilities-contract.ts +132 -0
- package/src/workspace/capabilities.schema.json +229 -0
- package/src/workspace/ci-cloud-identity.test.ts +20 -0
- package/src/workspace/ci-cloud-identity.ts +14 -1
- package/src/workspace/ci-run.test.ts +74 -1
- package/src/workspace/ci-run.ts +122 -29
- package/src/workspace/ci-secrets.test.ts +1 -1
- package/src/workspace/ci-secrets.ts +4 -3
- package/src/workspace/compose-graph.ts +10 -1
- package/src/workspace/conformance/conformance.e2e.test.ts +14 -1
- package/src/workspace/conformance/index.ts +31 -2
- package/src/workspace/declaration.schema.json +36 -1
- package/src/workspace/declaration.test.ts +7 -0
- package/src/workspace/declaration.ts +24 -2
- package/src/workspace/graph-cache.ts +27 -3
- package/src/workspace/graph-cli.ts +96 -24
- package/src/workspace/graph.schema.json +148 -0
- package/src/workspace/kind-readers.e2e.test.ts +28 -0
- package/src/workspace/kind-readers.test.ts +1 -1
- package/src/workspace/kind-readers.ts +7 -1
- package/src/workspace/member-commands.ts +22 -1
- package/src/workspace/nested-graph.ts +32 -1
- package/src/workspace/reason-codes.test.ts +7 -0
- package/src/workspace/reason-codes.ts +12 -0
- package/src/workspace/search-cli.e2e.test.ts +305 -0
- package/src/workspace/search-cli.ts +591 -0
- package/src/workspace/search-contract.ts +95 -0
- package/src/workspace/search.schema.json +822 -0
|
@@ -25,8 +25,14 @@
|
|
|
25
25
|
* `down` reads the environment's record on chant/lifecycle, checks it names
|
|
26
26
|
* this pull request's environment, and tears down what `planTeardown` finds
|
|
27
27
|
* under the record's marker, after dropping any entry whose marker is not
|
|
28
|
-
* exactly that one.
|
|
29
|
-
*
|
|
28
|
+
* exactly that one. The lexicons tear down what they own, and the command
|
|
29
|
+
* `ci.prEnvironment.teardown` names tears down the rest (#4330). No gate
|
|
30
|
+
* holds it: closing the pull request is the decision, as is the TTL the base
|
|
31
|
+
* set for `sweep`.
|
|
32
|
+
*
|
|
33
|
+
* The runs themselves are ../../components/pr-env-run.ts, which a product
|
|
34
|
+
* calls with an adapter of its own (#4330). This file supplies the
|
|
35
|
+
* components adapter and the CLI's ports.
|
|
30
36
|
*/
|
|
31
37
|
|
|
32
38
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -37,39 +43,40 @@ import { isEmptyChangeSet } from "../../change-set";
|
|
|
37
43
|
import {
|
|
38
44
|
PR_ENV_EXIT,
|
|
39
45
|
PR_ENV_GATE,
|
|
40
|
-
PR_ENV_LEDGER_DIR,
|
|
41
|
-
PR_ENV_LEDGER_FILE,
|
|
42
46
|
PR_ENV_RECORD_FILE,
|
|
43
47
|
PR_ENV_REPORT_DIR,
|
|
44
48
|
acceptHandedRecord,
|
|
45
49
|
handedPrEnvReport,
|
|
46
50
|
PR_ENV_TEMPLATE_VAR,
|
|
47
51
|
derivePrEnvironment,
|
|
48
|
-
expired,
|
|
49
|
-
livePrEnvironments,
|
|
50
|
-
parsePrEnvRecords,
|
|
51
52
|
prEnvApproveCommand,
|
|
52
|
-
prEnvNoteMarker,
|
|
53
|
-
prEnvStatusContext,
|
|
54
|
-
prEnvStatusDescription,
|
|
55
53
|
prEnvStatusState,
|
|
56
54
|
prEnvironmentName,
|
|
55
|
+
prEnvironmentSite,
|
|
57
56
|
renderPrEnvNote,
|
|
58
|
-
teardownComplete,
|
|
59
|
-
teardownCounts,
|
|
60
|
-
tearDownPrEnvironment,
|
|
61
57
|
ttlMs,
|
|
62
|
-
type LivePrEnvironment,
|
|
63
|
-
type PrEnvRecord,
|
|
64
58
|
type PrEnvReport,
|
|
59
|
+
type PrEnvReportRow,
|
|
60
|
+
type PrEnvTeardownAdapter,
|
|
65
61
|
} from "../../components/pr-environment";
|
|
66
|
-
import {
|
|
67
|
-
|
|
62
|
+
import {
|
|
63
|
+
commandTeardownAdapter,
|
|
64
|
+
prEnvDown as prEnvDownRun,
|
|
65
|
+
prEnvOp,
|
|
66
|
+
prEnvSweep as prEnvSweepRun,
|
|
67
|
+
prEnvUp as prEnvUpRun,
|
|
68
|
+
type PrEnvAdapter,
|
|
69
|
+
type PrEnvPorts,
|
|
70
|
+
type PrEnvTarget,
|
|
71
|
+
} from "../../components/pr-env-run";
|
|
72
|
+
import { forgePrEnvPublisher, lifecyclePrEnvPorts } from "../../components/pr-env-ports";
|
|
73
|
+
import { applyPrSet, type PrPlanSet } from "../../components/pr-run";
|
|
68
74
|
import { prOp } from "../../pr-loop";
|
|
69
75
|
import { FORGE_KINDS, forgeFromEnv, forgePrincipalOf, type ForgeKind, type PrForge } from "../../pr-forge";
|
|
70
76
|
import { formatError, formatInfo, formatWarning } from "../format";
|
|
71
77
|
import { withComponentEndpoint } from "../component-endpoint";
|
|
72
78
|
import type { CommandContext } from "../registry";
|
|
79
|
+
import type { Prepared } from "./pr";
|
|
73
80
|
|
|
74
81
|
const USAGE = "chant components pr-env up --base <ref> --pr <n> [--no-push] | post --pr <n> --head <sha> | down --pr <n> [--reason closed|expired] | sweep";
|
|
75
82
|
|
|
@@ -107,36 +114,30 @@ function memberOf(ctx: CommandContext): string | undefined {
|
|
|
107
114
|
return ctx.args.members?.length === 1 ? ctx.args.members[0] : undefined;
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
// ── the
|
|
117
|
+
// ── the ports ──────────────────────────────────────────────────────────────
|
|
111
118
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
await fetchLifecycle().catch(() => false);
|
|
115
|
-
return parsePrEnvRecords(await readBlobFromPath(PR_ENV_LEDGER_DIR, PR_ENV_LEDGER_FILE));
|
|
116
|
-
}
|
|
119
|
+
const log = (line: string): void => console.error(formatInfo(line));
|
|
120
|
+
const warn = (message: string): void => console.error(formatWarning({ message }));
|
|
117
121
|
|
|
118
122
|
/**
|
|
119
|
-
*
|
|
120
|
-
* the
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
+
* The ports `pr-env` runs with (../../components/pr-env-run.ts): the records
|
|
124
|
+
* and the gate ledger on chant/lifecycle here, and the report written under
|
|
125
|
+
* `--output` and posted through `forge`. With `--no-push` (#4295) the record
|
|
126
|
+
* is written to `pr-env-record.json` under `--output` instead, for `pr-env
|
|
127
|
+
* post` to append from a job that runs none of the change's code.
|
|
123
128
|
*/
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (pushed.pushWarning) console.error(formatWarning({ message: pushed.pushWarning }));
|
|
137
|
-
} catch (err) {
|
|
138
|
-
console.error(formatWarning({ message: `could not record ${record.env}'s ${record.kind} on chant/lifecycle: ${err instanceof Error ? err.message : String(err)}` }));
|
|
139
|
-
}
|
|
129
|
+
function cliPorts(ctx: CommandContext, forge: PrForge | undefined): PrEnvPorts {
|
|
130
|
+
const lifecycle = lifecyclePrEnvPorts({ ...(forge ? { forge } : {}), log, warn });
|
|
131
|
+
return {
|
|
132
|
+
...lifecycle,
|
|
133
|
+
async appendRecord(record) {
|
|
134
|
+
if (!ctx.args.noPush) return lifecycle.appendRecord(record);
|
|
135
|
+
const dir = resolve(ctx.args.output ?? PR_ENV_REPORT_DIR);
|
|
136
|
+
mkdirSync(dir, { recursive: true });
|
|
137
|
+
writeFileSync(join(dir, PR_ENV_RECORD_FILE), JSON.stringify(record, null, 2) + "\n");
|
|
138
|
+
},
|
|
139
|
+
publish: (report) => finish(ctx, forge, report),
|
|
140
|
+
};
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
// ── the report and the note ────────────────────────────────────────────────
|
|
@@ -148,24 +149,7 @@ async function finish(ctx: CommandContext, forge: PrForge | undefined, report: P
|
|
|
148
149
|
writeFileSync(join(dir, `pr-env-${report.stage}.json`), JSON.stringify(report, null, 2) + "\n");
|
|
149
150
|
writeFileSync(join(dir, "pr-env-note.md"), note);
|
|
150
151
|
if (ctx.args.json) console.log(JSON.stringify(report, null, 2));
|
|
151
|
-
if (
|
|
152
|
-
if (report.head) {
|
|
153
|
-
try {
|
|
154
|
-
await forge.setStatus(report.head, {
|
|
155
|
-
context: prEnvStatusContext(report.member),
|
|
156
|
-
state: prEnvStatusState(report),
|
|
157
|
-
description: prEnvStatusDescription(report),
|
|
158
|
-
...(forge.runUrl ? { url: forge.runUrl } : {}),
|
|
159
|
-
});
|
|
160
|
-
} catch (err) {
|
|
161
|
-
console.error(formatWarning({ message: `could not set the ${prEnvStatusContext(report.member)} status: ${err instanceof Error ? err.message : String(err)}` }));
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
try {
|
|
165
|
-
await forge.upsertNote(report.pr, prEnvNoteMarker(report.env, report.member), note);
|
|
166
|
-
} catch (err) {
|
|
167
|
-
console.error(formatWarning({ message: `could not post the note on #${report.pr}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
168
|
-
}
|
|
152
|
+
if (forge) await forgePrEnvPublisher(forge, warn)(report);
|
|
169
153
|
}
|
|
170
154
|
|
|
171
155
|
// ── up ─────────────────────────────────────────────────────────────────────
|
|
@@ -190,7 +174,6 @@ async function prEnvUp(ctx: CommandContext): Promise<number> {
|
|
|
190
174
|
const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} as ChantConfig }));
|
|
191
175
|
const derived = derivePrEnvironment(config, setting.template, pr);
|
|
192
176
|
if ("refusal" in derived) return fail(derived.refusal);
|
|
193
|
-
const ttl = ttlMs(setting.ttl) ?? ttlMs("72h")!;
|
|
194
177
|
|
|
195
178
|
// The build sees pr-<n> as its environment and the template's name beside it.
|
|
196
179
|
process.env[ENV_VAR] = derived.env;
|
|
@@ -200,111 +183,76 @@ async function prEnvUp(ctx: CommandContext): Promise<number> {
|
|
|
200
183
|
args: { ...args, env: derived.env, param: [...(args.param ?? []).filter((p) => !p.startsWith(`${derived.param}=`)), `${derived.param}=${derived.env}`] },
|
|
201
184
|
};
|
|
202
185
|
console.error(formatInfo(`pr-env up: ${derived.env}, derived from ${derived.template}${derived.endpoint ? ` at ${derived.endpoint}` : ""}, marker ${derived.stack}/${derived.env}`));
|
|
203
|
-
return withComponentEndpoint(envCtx, () => up(envCtx, pr, derived, ttl), { config: { ...config, environments: derived.environments }, environment: derived.env });
|
|
186
|
+
return withComponentEndpoint(envCtx, () => up(envCtx, pr, derived, setting.ttl), { config: { ...config, environments: derived.environments }, environment: derived.env });
|
|
204
187
|
}
|
|
205
188
|
|
|
206
|
-
async function up(ctx: CommandContext, pr: number, derived: Exclude<ReturnType<typeof derivePrEnvironment>, { refusal: string }>, ttl:
|
|
189
|
+
async function up(ctx: CommandContext, pr: number, derived: Exclude<ReturnType<typeof derivePrEnvironment>, { refusal: string }>, ttl: string): Promise<number> {
|
|
207
190
|
const { prepare, planAll, placeHooks, runStageHook } = await import("./pr");
|
|
208
191
|
const p = await prepare(ctx, "plan");
|
|
209
192
|
if (typeof p === "number") return p;
|
|
210
|
-
const forge = p.forge;
|
|
211
|
-
const op = prOp(pr, p.member);
|
|
212
193
|
// ci.hooks (#4288), read at base as the other pull-request jobs read them, around the plan and the apply.
|
|
213
|
-
placeHooks(ctx, p, "apply",
|
|
214
|
-
const
|
|
215
|
-
stage: "up",
|
|
194
|
+
placeHooks(ctx, p, "apply", prEnvOp(pr, p.member));
|
|
195
|
+
const result = await prEnvUpRun({
|
|
216
196
|
pr,
|
|
217
|
-
env: derived.env,
|
|
218
|
-
...(p.member ? { member: p.member } : {}),
|
|
219
|
-
template: derived.template,
|
|
220
197
|
head: p.head,
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
198
|
+
template: derived.template,
|
|
199
|
+
ttl,
|
|
200
|
+
stack: derived.stack,
|
|
201
|
+
...(p.member ? { member: p.member } : {}),
|
|
225
202
|
...(derived.endpoint ? { endpoint: derived.endpoint } : {}),
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
203
|
+
adapter: componentsPrEnvAdapter(p, { planAll, runStageHook }),
|
|
204
|
+
ports: cliPorts(ctx, p.forge),
|
|
205
|
+
actor: actor(),
|
|
206
|
+
...(p.forge?.runUrl ? { runUrl: p.forge.runUrl } : {}),
|
|
207
|
+
});
|
|
208
|
+
if (result.report.status === "waiting" || result.report.status === "changed") console.error(formatInfo(`approve : ${prEnvApproveCommand(result.report.op, result.report.digest)}`));
|
|
209
|
+
return result.exit;
|
|
210
|
+
}
|
|
233
211
|
|
|
234
|
-
|
|
235
|
-
|
|
212
|
+
/**
|
|
213
|
+
* The components as a pull request environment's adapter: the plan is
|
|
214
|
+
* `pr-plan`'s, with the plan-side hooks around it, and the apply is the
|
|
215
|
+
* fan-out's, with the apply-side hooks around it. Each row carries the
|
|
216
|
+
* component's site in `pr-<n>`, carried over from the template's (#4330,
|
|
217
|
+
* `prEnvironmentSite`).
|
|
218
|
+
*/
|
|
219
|
+
function componentsPrEnvAdapter(p: Prepared, run: { planAll: typeof import("./pr").planAll; runStageHook: typeof import("./pr").runStageHook }): PrEnvAdapter {
|
|
220
|
+
let set: PrPlanSet | undefined;
|
|
221
|
+
const rows = (target: PrEnvTarget, status: (component: string) => string): PrEnvReportRow[] =>
|
|
236
222
|
p.plan.order.map((component) => {
|
|
237
|
-
const ms = set
|
|
223
|
+
const ms = (set?.members ?? []).filter((m) => m.component === component);
|
|
238
224
|
const sum = (k: "create" | "update" | "replace" | "delete") => ms.reduce((n, m) => n + m.counts[k], 0);
|
|
239
|
-
|
|
225
|
+
const found = prEnvironmentSite(p.sites?.[component], target.template, target.env);
|
|
226
|
+
const site = found ? { ...(found.runtime ? { runtime: found.runtime } : {}), ...(found.url ? { url: found.url } : {}), ...(found.domain ? { domain: found.domain } : {}) } : undefined;
|
|
227
|
+
return { component, status: status(component), create: sum("create"), update: sum("update"), replace: sum("replace"), delete: sum("delete"), ...(site && Object.keys(site).length > 0 ? { site } : {}) };
|
|
240
228
|
});
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
await
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
await finish(ctx, forge, { ...base, digest, status: "failed", message: hookStop, components: rows(() => "planned"), ...(approvedBy ? { approvedBy } : {}) });
|
|
269
|
-
return PR_ENV_EXIT.failed;
|
|
270
|
-
}
|
|
271
|
-
const applied = await applyPrSet({ components: p.components, plan: p.plan, registry: p.registry, env: derived.env, set, gate: PR_ENV_GATE });
|
|
272
|
-
const afterHook = runStageHook(p, "after-apply", p.plan.order, set, "the environment stands as applied");
|
|
273
|
-
const status = (component: string) => applied.members.find((m) => m.component === component)?.status ?? "planned";
|
|
274
|
-
const ok = applied.status === "applied";
|
|
275
|
-
const now = new Date();
|
|
276
|
-
const expiresAt = new Date(now.getTime() + ttl).toISOString();
|
|
277
|
-
// A copy that failed part way is live too: what applied is torn down on close or at expiry like the rest.
|
|
278
|
-
await appendRecord(ctx, {
|
|
279
|
-
version: 1,
|
|
280
|
-
kind: "up",
|
|
281
|
-
pr,
|
|
282
|
-
env: derived.env,
|
|
283
|
-
...(p.member ? { member: p.member } : {}),
|
|
284
|
-
template: derived.template,
|
|
285
|
-
marker: { stack: derived.stack, env: derived.env },
|
|
286
|
-
commit: p.head,
|
|
287
|
-
components: p.plan.order,
|
|
288
|
-
planDigest: digest,
|
|
289
|
-
result: ok ? "applied" : "failed",
|
|
290
|
-
by: actor(),
|
|
291
|
-
timestamp: now.toISOString(),
|
|
292
|
-
expiresAt,
|
|
293
|
-
...(approvedBy ? { approvedBy } : {}),
|
|
294
|
-
releaseTarget: false,
|
|
295
|
-
...(forge?.runUrl ? { runUrl: forge.runUrl } : {}),
|
|
296
|
-
});
|
|
297
|
-
await finish(ctx, forge, {
|
|
298
|
-
...base,
|
|
299
|
-
digest,
|
|
300
|
-
status: ok ? "applied" : "failed",
|
|
301
|
-
components: rows(status),
|
|
302
|
-
...(approvedBy ? { approvedBy } : {}),
|
|
303
|
-
expiresAt,
|
|
304
|
-
...(ok ? (afterHook ? { message: afterHook } : {}) : { message: applied.status === "waiting" ? "A component read an upstream output that moved when it applied; run the job again to plan it against the new value." : "A component failed to apply; what applied stays in the environment and is torn down with it." }),
|
|
305
|
-
});
|
|
306
|
-
console.error((ok ? formatInfo : (m: string) => formatWarning({ message: m }))(`pr-env up: ${derived.env} ${ok ? "applied" : applied.status}; it expires ${expiresAt}`));
|
|
307
|
-
return ok && !afterHook ? PR_ENV_EXIT.done : PR_ENV_EXIT.failed;
|
|
229
|
+
return {
|
|
230
|
+
async plan(target) {
|
|
231
|
+
if (p.plan.order.length === 0) return { digest: "", empty: true, rows: [], ...(p.routedAway ? { message: p.routedAway } : {}) };
|
|
232
|
+
set = await run.planAll(p);
|
|
233
|
+
const planned = set;
|
|
234
|
+
return {
|
|
235
|
+
digest: planned.doc.digest,
|
|
236
|
+
empty: isEmptyChangeSet(planned.doc),
|
|
237
|
+
...(planned.failed ? { failed: true } : {}),
|
|
238
|
+
rows: rows(target, (c) => (planned.failed && planned.members.some((m) => m.component === c && m.status === "plan-failed") ? "plan failed" : "planned")),
|
|
239
|
+
...(p.hookFailed ? { message: p.hookFailed } : {}),
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
async apply(target) {
|
|
243
|
+
const planned = set!;
|
|
244
|
+
const hookStop = run.runStageHook(p, "before-apply", p.plan.order, planned);
|
|
245
|
+
if (hookStop) return { status: "stopped", rows: rows(target, () => "planned"), message: hookStop };
|
|
246
|
+
const applied = await applyPrSet({ components: p.components, plan: p.plan, registry: p.registry, env: target.env, set: planned, gate: PR_ENV_GATE });
|
|
247
|
+
const afterHook = run.runStageHook(p, "after-apply", p.plan.order, planned, "the environment stands as applied");
|
|
248
|
+
const status = (component: string) => applied.members.find((m) => m.component === component)?.status ?? "planned";
|
|
249
|
+
return {
|
|
250
|
+
status: applied.status === "applied" ? "applied" : applied.status === "waiting" ? "waiting" : "failed",
|
|
251
|
+
rows: rows(target, status),
|
|
252
|
+
...(afterHook ? { afterFailure: afterHook } : {}),
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
};
|
|
308
256
|
}
|
|
309
257
|
|
|
310
258
|
// ── post ───────────────────────────────────────────────────────────────────
|
|
@@ -352,7 +300,7 @@ async function prEnvPost(ctx: CommandContext): Promise<number> {
|
|
|
352
300
|
report.message = `${report.message ? `${report.message} ` : ""}${accepted.refusal}`;
|
|
353
301
|
code = PR_ENV_EXIT.failed;
|
|
354
302
|
} else {
|
|
355
|
-
await
|
|
303
|
+
await lifecyclePrEnvPorts({ warn }).appendRecord(accepted.record);
|
|
356
304
|
if (accepted.record.expiresAt) report.expiresAt = accepted.record.expiresAt;
|
|
357
305
|
}
|
|
358
306
|
}
|
|
@@ -363,6 +311,19 @@ async function prEnvPost(ctx: CommandContext): Promise<number> {
|
|
|
363
311
|
|
|
364
312
|
// ── down ───────────────────────────────────────────────────────────────────
|
|
365
313
|
|
|
314
|
+
/**
|
|
315
|
+
* What tears the environment down: the lexicons, and the command
|
|
316
|
+
* `ci.prEnvironment.teardown` names in the declaration at this commit
|
|
317
|
+
* (#4330), which the down and sweep jobs check out from the base branch.
|
|
318
|
+
*/
|
|
319
|
+
async function teardownSources(): Promise<{ config: ChantConfig; adapters: PrEnvTeardownAdapter[] }> {
|
|
320
|
+
const projectPath = resolve(".");
|
|
321
|
+
const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} as ChantConfig }));
|
|
322
|
+
const { scopeSource } = await import("../../workspace/write-scope");
|
|
323
|
+
const hook = scopeSource(projectPath, "HEAD").declaration?.ci?.prEnvironment?.teardown;
|
|
324
|
+
return { config, adapters: hook ? [commandTeardownAdapter(hook, { cwd: projectPath })] : [] };
|
|
325
|
+
}
|
|
326
|
+
|
|
366
327
|
async function prEnvDown(ctx: CommandContext): Promise<number> {
|
|
367
328
|
const { args } = ctx;
|
|
368
329
|
if (args.pr === undefined) return fail("pr-env down needs the pull or merge request: --pr <n>");
|
|
@@ -370,57 +331,20 @@ async function prEnvDown(ctx: CommandContext): Promise<number> {
|
|
|
370
331
|
if (reason !== "closed" && reason !== "expired") return fail(`--reason ${reason} is not closed or expired`);
|
|
371
332
|
const forge = forgeOf(ctx);
|
|
372
333
|
if (typeof forge === "number") return forge;
|
|
373
|
-
const live = livePrEnvironments(await readRecords()).get(prEnvironmentName(args.pr));
|
|
374
|
-
return teardown(ctx, forge, args.pr, live, reason);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
async function teardown(ctx: CommandContext, forge: PrForge | undefined, pr: number, live: LivePrEnvironment | undefined, reason: "closed" | "expired"): Promise<number> {
|
|
378
|
-
const env = prEnvironmentName(pr);
|
|
379
334
|
const member = memberOf(ctx);
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
console.error(formatInfo(`pr-env down: ${env} has no live record, so there is nothing to tear down`));
|
|
383
|
-
if (forge) await finish(ctx, forge, report);
|
|
384
|
-
return PR_ENV_EXIT.done;
|
|
385
|
-
}
|
|
386
|
-
const { config } = await loadChantConfig(resolve(".")).catch(() => ({ config: {} as ChantConfig }));
|
|
387
|
-
const torn = await tearDownPrEnvironment({ pr, live, config, plugins: ctx.plugins, warn: (message) => console.error(formatWarning({ message })) });
|
|
388
|
-
if ("refusal" in torn) {
|
|
389
|
-
await finish(ctx, forge, { ...report, template: live.template, status: "refused", message: torn.refusal });
|
|
390
|
-
return fail(torn.refusal);
|
|
391
|
-
}
|
|
392
|
-
const { report: result, dropped, marker } = torn;
|
|
393
|
-
const counts = teardownCounts(result);
|
|
394
|
-
const complete = teardownComplete(result);
|
|
395
|
-
const lexicons = [...new Set(result.outcomes.map((o) => o.lexicon))].sort();
|
|
396
|
-
const head = await headCommit();
|
|
397
|
-
await appendRecord(ctx, {
|
|
398
|
-
version: 1,
|
|
399
|
-
kind: "down",
|
|
400
|
-
pr,
|
|
401
|
-
env,
|
|
402
|
-
...(live.member ? { member: live.member } : {}),
|
|
403
|
-
template: live.template,
|
|
404
|
-
marker,
|
|
405
|
-
commit: head,
|
|
406
|
-
components: lexicons,
|
|
407
|
-
planDigest: "",
|
|
408
|
-
result: complete ? "destroyed" : "incomplete",
|
|
409
|
-
by: actor(),
|
|
410
|
-
timestamp: new Date().toISOString(),
|
|
335
|
+
const result = await prEnvDownRun({
|
|
336
|
+
pr: args.pr,
|
|
411
337
|
reason,
|
|
412
|
-
|
|
413
|
-
|
|
338
|
+
...(member ? { member } : {}),
|
|
339
|
+
ports: cliPorts(ctx, forge),
|
|
340
|
+
...(await teardownSources()),
|
|
341
|
+
plugins: ctx.plugins,
|
|
342
|
+
commit: await headCommit(),
|
|
343
|
+
actor: actor(),
|
|
414
344
|
...(forge?.runUrl ? { runUrl: forge.runUrl } : {}),
|
|
415
345
|
});
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (result.unimplemented.length > 0) why.push(`no teardown execution in ${result.unimplemented.join(", ")}`);
|
|
419
|
-
if (result.plan.holes.length > 0) why.push(`${result.plan.holes.length} part${result.plan.holes.length === 1 ? "" : "s"} could not be read`);
|
|
420
|
-
for (const o of result.outcomes.filter((x) => x.outcome === "failed")) why.push(`${o.lexicon} ${o.name}: ${o.detail ?? "failed"}`);
|
|
421
|
-
await finish(ctx, forge, { ...report, template: live.template, status: complete ? "destroyed" : "incomplete", outcomes: counts, ...(why.length > 0 ? { message: why.join("; ") } : {}) });
|
|
422
|
-
console.error((complete ? formatInfo : (m: string) => formatWarning({ message: m }))(`pr-env down: ${env} ${complete ? "torn down" : "not wholly torn down; it stays live and the sweep tries again"} (${counts.deleted} deleted, ${counts.failed} failed, ${counts.skipped} skipped)`));
|
|
423
|
-
return complete ? PR_ENV_EXIT.done : PR_ENV_EXIT.failed;
|
|
346
|
+
if (result.report.status === "refused") return fail(result.report.message ?? "refused");
|
|
347
|
+
return result.exit;
|
|
424
348
|
}
|
|
425
349
|
|
|
426
350
|
async function headCommit(): Promise<string> {
|
|
@@ -434,29 +358,13 @@ async function headCommit(): Promise<string> {
|
|
|
434
358
|
async function prEnvSweep(ctx: CommandContext): Promise<number> {
|
|
435
359
|
const forge = forgeOf(ctx);
|
|
436
360
|
if (typeof forge === "number") return forge;
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (!reason && forge?.pullRequestState) {
|
|
447
|
-
try {
|
|
448
|
-
const state = await forge.pullRequestState(e.pr);
|
|
449
|
-
if (state !== "open") reason = "closed";
|
|
450
|
-
} catch (err) {
|
|
451
|
-
console.error(formatWarning({ message: `could not read whether #${e.pr} is open (${err instanceof Error ? err.message : String(err)}); its TTL still holds` }));
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
if (!reason) {
|
|
455
|
-
console.error(formatInfo(`pr-env sweep: ${e.env} stays; it expires ${e.expiresAt}`));
|
|
456
|
-
continue;
|
|
457
|
-
}
|
|
458
|
-
const c = await teardown(ctx, forge, e.pr, e, reason);
|
|
459
|
-
if (c !== PR_ENV_EXIT.done) code = PR_ENV_EXIT.failed;
|
|
460
|
-
}
|
|
461
|
-
return code;
|
|
361
|
+
const result = await prEnvSweepRun({
|
|
362
|
+
ports: cliPorts(ctx, forge),
|
|
363
|
+
...(await teardownSources()),
|
|
364
|
+
plugins: ctx.plugins,
|
|
365
|
+
commit: await headCommit(),
|
|
366
|
+
actor: actor(),
|
|
367
|
+
...(forge?.runUrl ? { runUrl: forge.runUrl } : {}),
|
|
368
|
+
});
|
|
369
|
+
return result.exit;
|
|
462
370
|
}
|
package/src/cli/handlers/pr.ts
CHANGED
|
@@ -129,6 +129,7 @@ import { withComponentEndpoint } from "../component-endpoint";
|
|
|
129
129
|
import type { ComponentChangeSignal, FanOutPlan } from "../../components/fan-out";
|
|
130
130
|
import { failedRunExitCode, type DriverComponent } from "../../components/driver";
|
|
131
131
|
import type { CapabilityRegistry } from "../../components/capability";
|
|
132
|
+
import type { ComponentEnvironmentDeclaration } from "../../components/component";
|
|
132
133
|
import { prLoopHooksAt, runPrHooks, type PrHookPlace, type PrLoopHooks } from "../../components/pr-hooks";
|
|
133
134
|
import { decidePrPolicyOverride, prLoopPolicyAt, prPolicyReport, runPrPolicy, spendPrPolicyOverride, type PrLoopPolicy } from "../../components/pr-policy";
|
|
134
135
|
import type { OpWaveHookMoment } from "../../op/op-waves";
|
|
@@ -157,6 +158,8 @@ export interface Prepared {
|
|
|
157
158
|
signal: ComponentChangeSignal;
|
|
158
159
|
registry: CapabilityRegistry;
|
|
159
160
|
seededOutputs: Record<string, Record<string, unknown>>;
|
|
161
|
+
/** Component name to the environments it declares (#3153), whose sites `pr-env up` carries over (#4330). */
|
|
162
|
+
sites?: Record<string, ComponentEnvironmentDeclaration[]>;
|
|
160
163
|
/** `ci.hooks` from the declaration at base (#4288). */
|
|
161
164
|
hooks: PrLoopHooks;
|
|
162
165
|
/** Every hook the stage ran so far, for the report's `hooks`. */
|
|
@@ -296,6 +299,7 @@ export async function prepare(ctx: CommandContext, stage: "plan" | "apply" | "re
|
|
|
296
299
|
components: derived.components,
|
|
297
300
|
plan: routedAway ? { ...derived.plan, order: [], waves: [], seeds: [], indeterminate: [] } : derived.plan,
|
|
298
301
|
signal: derived.signal,
|
|
302
|
+
...(derived.sites ? { sites: derived.sites } : {}),
|
|
299
303
|
registry,
|
|
300
304
|
seededOutputs,
|
|
301
305
|
hooks,
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import type { ParsedArgs } from "../registry";
|
|
3
|
+
import { DECLARABLE_MARKER, type Declarable } from "../../declarable";
|
|
4
|
+
import type { LexiconPlugin } from "../../lexicon";
|
|
5
|
+
import { createMockPlugin } from "../../../../test-utils/src/mock-plugin";
|
|
6
|
+
|
|
7
|
+
// #4365 / #4368: search answers over the graph `chant graph` draws. That is
|
|
8
|
+
// discovery plus the build-root contributions (a terraform root, a kustomize
|
|
9
|
+
// root) plus the lexicons' `graphEdges`, on the declared path AND on the
|
|
10
|
+
// `--live`/`--at` canvas, and a contributor that throws fails the search
|
|
11
|
+
// instead of leaving it to answer `(no matches)` over a graph with a hole in it.
|
|
12
|
+
|
|
13
|
+
const discoverMock = vi.fn();
|
|
14
|
+
vi.mock("../../discovery/index", () => ({
|
|
15
|
+
discover: (...a: unknown[]) => discoverMock(...a),
|
|
16
|
+
}));
|
|
17
|
+
const loadPluginsMock = vi.fn();
|
|
18
|
+
const resolveLexMock = vi.fn();
|
|
19
|
+
vi.mock("../plugins", async () => {
|
|
20
|
+
const actual = await vi.importActual<typeof import("../plugins")>("../plugins");
|
|
21
|
+
return {
|
|
22
|
+
...actual,
|
|
23
|
+
loadPlugins: (...a: unknown[]) => loadPluginsMock(...a),
|
|
24
|
+
resolveProjectLexicons: (...a: unknown[]) => resolveLexMock(...a),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
const replaySnapshotsMock = vi.fn();
|
|
28
|
+
vi.mock("../../lifecycle/replay", () => ({
|
|
29
|
+
replaySnapshots: (...a: unknown[]) => replaySnapshotsMock(...a),
|
|
30
|
+
hasSnapshot: () => Promise.resolve(false),
|
|
31
|
+
}));
|
|
32
|
+
const loadChantConfigMock = vi.fn();
|
|
33
|
+
vi.mock("../../config", async () => {
|
|
34
|
+
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
35
|
+
return {
|
|
36
|
+
...actual,
|
|
37
|
+
loadChantConfig: (...a: unknown[]) => loadChantConfigMock(...a),
|
|
38
|
+
loadChantConfigUpward: (...a: unknown[]) => loadChantConfigMock(...a),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
const buildResultMock = vi.fn();
|
|
42
|
+
vi.mock("../../build", async () => {
|
|
43
|
+
const actual = await vi.importActual<typeof import("../../build")>("../../build");
|
|
44
|
+
return { ...actual, build: (...a: unknown[]) => Promise.resolve(buildResultMock(...a)) };
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const { runSearch } = await import("./search");
|
|
48
|
+
|
|
49
|
+
function decl<T extends object>(base: T): Declarable & T {
|
|
50
|
+
return { [DECLARABLE_MARKER]: true, ...base } as Declarable & T;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A lexicon whose only entities come from a build root, and whose only edge comes from `graphEdges`. */
|
|
54
|
+
function rootLexicon(overrides: Partial<LexiconPlugin> = {}): LexiconPlugin {
|
|
55
|
+
return {
|
|
56
|
+
...createMockPlugin({ name: "tf" }),
|
|
57
|
+
buildRoots: async () => ({
|
|
58
|
+
entities: new Map<string, Declarable>([
|
|
59
|
+
["net/aws_vpc.main", decl({ lexicon: "tf", entityType: "Tf::Resource", props: { address: "aws_vpc.main" } })],
|
|
60
|
+
["net/aws_subnet.a", decl({ lexicon: "tf", entityType: "Tf::Resource", props: { address: "aws_subnet.a" } })],
|
|
61
|
+
]),
|
|
62
|
+
warnings: [],
|
|
63
|
+
}),
|
|
64
|
+
graphEdges: () => ({ references: [{ from: "net/aws_subnet.a", to: "net/aws_vpc.main", kind: "ref", viaAttr: "vpc_id" }] }),
|
|
65
|
+
...overrides,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function makeArgs(overrides: Partial<ParsedArgs> = {}): ParsedArgs {
|
|
70
|
+
return {
|
|
71
|
+
command: "search", path: "attr:address=aws_subnet ->attr:address=aws_vpc",
|
|
72
|
+
format: "", fix: false, watch: false, verbose: false, help: false, live: false, json: true,
|
|
73
|
+
...overrides,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe("search over build-root entities and lexicon edges (#4365, #4368)", () => {
|
|
78
|
+
let out: string[];
|
|
79
|
+
let err: string[];
|
|
80
|
+
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
out = [];
|
|
83
|
+
err = [];
|
|
84
|
+
vi.spyOn(console, "log").mockImplementation((s: string) => { out.push(s); });
|
|
85
|
+
vi.spyOn(console, "error").mockImplementation((s: string) => { err.push(s); });
|
|
86
|
+
discoverMock.mockReset();
|
|
87
|
+
// A fresh map per call: the merge writes into it.
|
|
88
|
+
discoverMock.mockImplementation(async () => ({ entities: new Map(), errors: [], sourceFiles: [] }));
|
|
89
|
+
buildResultMock.mockReset();
|
|
90
|
+
buildResultMock.mockReturnValue({ errors: [], entities: new Map(), outputs: new Map(), warnings: [] });
|
|
91
|
+
loadChantConfigMock.mockReset();
|
|
92
|
+
loadChantConfigMock.mockResolvedValue({ config: {} });
|
|
93
|
+
resolveLexMock.mockReset();
|
|
94
|
+
resolveLexMock.mockResolvedValue(["tf"]);
|
|
95
|
+
replaySnapshotsMock.mockReset();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("declared: an edge term follows a lexicon edge between two build-root entities", async () => {
|
|
99
|
+
loadPluginsMock.mockResolvedValue([rootLexicon()]);
|
|
100
|
+
const exit = await runSearch({ args: makeArgs(), plugins: [], serializers: [] });
|
|
101
|
+
expect(exit).toBe(0);
|
|
102
|
+
const json = JSON.parse(out.join("\n"));
|
|
103
|
+
expect(json.matches.map((m: { id: string }) => m.id)).toEqual(["net/aws_subnet.a"]);
|
|
104
|
+
expect(json.matches[0].paths[0].hops).toEqual([{ from: "net/aws_subnet.a", to: "net/aws_vpc.main", fromAttr: "vpc_id" }]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("--at: the snapshot overlays onto a canvas that carries the build roots and their lexicon edges", async () => {
|
|
108
|
+
loadPluginsMock.mockResolvedValue([rootLexicon({ describeResources: async () => ({}) })]);
|
|
109
|
+
replaySnapshotsMock.mockResolvedValue({
|
|
110
|
+
observations: [{
|
|
111
|
+
lexicon: "tf",
|
|
112
|
+
resources: {
|
|
113
|
+
"net/aws_vpc.main": { type: "Tf::Resource", physicalId: "vpc-1", status: "OK" },
|
|
114
|
+
"net/aws_subnet.a": { type: "Tf::Resource", physicalId: "subnet-1", status: "OK" },
|
|
115
|
+
},
|
|
116
|
+
}],
|
|
117
|
+
commit: "a1b2c3d4e5f6", timestamp: "2026-10-01T00:00:00.000Z", depth: "identity",
|
|
118
|
+
});
|
|
119
|
+
const exit = await runSearch({ args: makeArgs({ at: "latest", env: "dev" }), plugins: [], serializers: [] });
|
|
120
|
+
expect(exit).toBe(0);
|
|
121
|
+
const json = JSON.parse(out.join("\n"));
|
|
122
|
+
expect(json.source.kind).toBe("snapshot");
|
|
123
|
+
expect(json.matches.map((m: { id: string; physicalId?: string }) => [m.id, m.physicalId])).toEqual([["net/aws_subnet.a", "subnet-1"]]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a contributor that throws fails the declared search with its error", async () => {
|
|
127
|
+
loadPluginsMock.mockResolvedValue([rootLexicon({ buildRoots: async () => { throw new Error("roots.net: main.tf does not parse"); } })]);
|
|
128
|
+
const exit = await runSearch({ args: makeArgs({ json: false }), plugins: [], serializers: [] });
|
|
129
|
+
expect(exit).toBe(1);
|
|
130
|
+
expect(out.join("\n")).not.toContain("(no matches)");
|
|
131
|
+
expect(err.join("\n")).toContain("roots.net: main.tf does not parse");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("a contributor that throws fails the --at search too", async () => {
|
|
135
|
+
loadPluginsMock.mockResolvedValue([rootLexicon({
|
|
136
|
+
describeResources: async () => ({}),
|
|
137
|
+
buildRoots: async () => { throw new Error("roots.net: main.tf does not parse"); },
|
|
138
|
+
})]);
|
|
139
|
+
replaySnapshotsMock.mockResolvedValue({ observations: [], commit: "a1b2c3d4e5f6", timestamp: "2026-10-01T00:00:00.000Z", depth: "identity" });
|
|
140
|
+
const exit = await runSearch({ args: makeArgs({ json: false, at: "latest", env: "dev" }), plugins: [], serializers: [] });
|
|
141
|
+
expect(exit).toBe(1);
|
|
142
|
+
expect(err.join("\n")).toContain("roots.net: main.tf does not parse");
|
|
143
|
+
});
|
|
144
|
+
});
|