@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
package/src/workspace/ci-run.ts
CHANGED
|
@@ -84,7 +84,7 @@ import { execFileSync } from "node:child_process";
|
|
|
84
84
|
import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
85
85
|
import { tmpdir } from "node:os";
|
|
86
86
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
87
|
-
import { PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prLoopJobs, prLoopSetup, PR_LOOP_IMAGE, type PrLoopJob, type PrLoopPipelineOptions } from "../components/pr-pipeline";
|
|
87
|
+
import { PR_APPLY_RECORD, PR_ENV_POST_JOB, PR_ENV_REPORT_DIR, PR_LOOP_REPORT_DIR, prEnvJobs, prLoopJobs, prLoopSetup, PR_LOOP_IMAGE, type PrEnvJob, type PrLoopJob, type PrLoopPipelineOptions } from "../components/pr-pipeline";
|
|
88
88
|
import { detectForge, ForgeEnvironmentError, forgeFromEnv, FORGE_KINDS, type CommitStatus, type ForgeKind, type PrForge } from "../pr-forge";
|
|
89
89
|
import { holdsForgeToken, withoutForgeTokens } from "../ci-handover";
|
|
90
90
|
import { postPlanReport } from "../pr-post";
|
|
@@ -114,20 +114,42 @@ import { hostRunnerKey } from "./ci-runner-key";
|
|
|
114
114
|
import { requireRunnerStart } from "./ci-runner-start";
|
|
115
115
|
import { securityGroupsAt } from "./security";
|
|
116
116
|
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
/**
|
|
118
|
+
* The jobs a pull-request pipeline has: plan, and on merge record (#4168) or,
|
|
119
|
+
* opted in, apply; and with `--pr-env` (#4298, #4330) the jobs of a pull
|
|
120
|
+
* request's environment.
|
|
121
|
+
*/
|
|
122
|
+
export type CiJobName = PrLoopJob["jobName"] | PrEnvJob["jobName"];
|
|
123
|
+
export const CI_JOB_NAMES: readonly CiJobName[] = ["plan", "apply", "record", "env-up", "env-post", "env-down", "env-sweep"];
|
|
124
|
+
|
|
125
|
+
/** The jobs of a pull request's environment (#4330). */
|
|
126
|
+
export const CI_ENV_JOB_NAMES: readonly PrEnvJob["jobName"][] = ["env-up", "env-post", "env-down", "env-sweep"];
|
|
120
127
|
|
|
121
|
-
/**
|
|
122
|
-
export const
|
|
128
|
+
/** Whether `job` is one of a pull request environment's jobs. */
|
|
129
|
+
export const isCiEnvJob = (job: CiJobName): job is PrEnvJob["jobName"] => (CI_ENV_JOB_NAMES as readonly string[]).includes(job);
|
|
130
|
+
|
|
131
|
+
/** The Op each job is recorded as, which `ci.green` phases match. An environment job's is its report's stem too. */
|
|
132
|
+
export const CI_JOB_OPS: Record<CiJobName, string> = { plan: "pr-plan", apply: "pr-apply", record: "pr-record", "env-up": "pr-env-up", "env-post": "pr-env-post", "env-down": "pr-env-down", "env-sweep": "pr-env-sweep" };
|
|
123
133
|
|
|
124
134
|
/** What each job's Op says it does. */
|
|
125
135
|
const CI_JOB_OVERVIEWS: Record<CiJobName, string> = {
|
|
126
136
|
plan: "Plan the members this pull request reaches",
|
|
127
137
|
apply: "Apply the plan a reviewer approved",
|
|
128
138
|
record: "Record the plan on the merged commit; nothing applies",
|
|
139
|
+
"env-up": "Plan this pull request's environment and apply it once its plan is approved",
|
|
140
|
+
"env-post": "Keep the environment's record and post its note",
|
|
141
|
+
"env-down": "Tear down what this pull request's environment owns",
|
|
142
|
+
"env-sweep": "Tear down the environments whose TTL passed or whose pull request closed",
|
|
129
143
|
};
|
|
130
144
|
|
|
145
|
+
/** One job as `ci run` runs it: a pull-request job, or an environment job (#4330). */
|
|
146
|
+
export type CiJob = Pick<PrLoopJob, "command" | "env" | "setup" | "build" | "wave"> & { jobName: CiJobName };
|
|
147
|
+
|
|
148
|
+
/** Where a job writes its report, inside its checkout. */
|
|
149
|
+
export function ciJobReportDir(job: CiJobName): string {
|
|
150
|
+
return isCiEnvJob(job) ? PR_ENV_REPORT_DIR : PR_LOOP_REPORT_DIR;
|
|
151
|
+
}
|
|
152
|
+
|
|
131
153
|
/** Where a job the chant runner ran says it ran (#4094). */
|
|
132
154
|
export const CHANT_RUNNER_ORIGIN: RunOrigin = { forge: "chant" };
|
|
133
155
|
|
|
@@ -167,6 +189,10 @@ export interface CiRunValues {
|
|
|
167
189
|
pr?: number;
|
|
168
190
|
/** The full commit the job runs at. */
|
|
169
191
|
commit: string;
|
|
192
|
+
/** `--head`: the pull request's head, `HEAD_SHA`, which `env-post` reads (#4330). */
|
|
193
|
+
head?: string;
|
|
194
|
+
/** `--result`: the env-up job's result, `UP_RESULT`, which `env-post` reads on GitHub and Forgejo (#4330). */
|
|
195
|
+
upResult?: string;
|
|
170
196
|
}
|
|
171
197
|
|
|
172
198
|
export class CiRunError extends Error {
|
|
@@ -188,7 +214,7 @@ export class CiRunError extends Error {
|
|
|
188
214
|
* runner sets for the job. `CHANT_RUNNER_KEY` is never in it.
|
|
189
215
|
*/
|
|
190
216
|
export function ciJobEnv(
|
|
191
|
-
job:
|
|
217
|
+
job: CiJob,
|
|
192
218
|
input: {
|
|
193
219
|
forge: ForgeKind;
|
|
194
220
|
env: string;
|
|
@@ -211,12 +237,18 @@ export function ciJobEnv(
|
|
|
211
237
|
const flags: Record<string, { value: string | undefined; flag: string }> = {
|
|
212
238
|
BASE_SHA: { value: input.values.base, flag: "--base <sha>" },
|
|
213
239
|
PR_NUMBER: { value: input.values.pr !== undefined ? String(input.values.pr) : undefined, flag: "--pr <number>" },
|
|
240
|
+
HEAD_SHA: { value: input.values.head, flag: "--head <sha>" },
|
|
241
|
+
UP_RESULT: { value: input.values.upResult, flag: "--result success|failure" },
|
|
214
242
|
};
|
|
243
|
+
// #4330: an environment job's CHANT_ENV is pr-<n>, which a forge fills from the event.
|
|
244
|
+
const prEnv = isCiEnvJob(job.jobName) && input.values.pr !== undefined ? `pr-${input.values.pr}` : undefined;
|
|
215
245
|
for (const [name, value] of Object.entries(job.env)) {
|
|
216
246
|
const given = flags[name];
|
|
217
247
|
if (given) {
|
|
218
248
|
if (given.value === undefined) throw new CiRunError(`the ${job.jobName} job reads ${name}, which a forge fills from the event; pass ${given.flag}`);
|
|
219
249
|
out[name] = given.value;
|
|
250
|
+
} else if (name === "CHANT_ENV" && prEnv) {
|
|
251
|
+
out[name] = prEnv;
|
|
220
252
|
} else if (!value.includes("$")) {
|
|
221
253
|
out[name] = value;
|
|
222
254
|
} else if (input.secrets?.[name] !== undefined) {
|
|
@@ -231,7 +263,7 @@ export function ciJobEnv(
|
|
|
231
263
|
|
|
232
264
|
/** What {@link ciJobOp} builds the Op from. */
|
|
233
265
|
export interface CiJobOpInput {
|
|
234
|
-
job:
|
|
266
|
+
job: CiJob;
|
|
235
267
|
env: string;
|
|
236
268
|
/** The directory the steps run in, inside the fresh checkout. */
|
|
237
269
|
cwd: string;
|
|
@@ -251,7 +283,7 @@ export function withChant(line: string, chant?: string): string {
|
|
|
251
283
|
}
|
|
252
284
|
|
|
253
285
|
/** The job's own setup lines and its command, as one shell script, with `chant` at its start replaced by the command the caller names. */
|
|
254
|
-
export function ciJobCommand(job:
|
|
286
|
+
export function ciJobCommand(job: CiJob, chant?: string): string {
|
|
255
287
|
return [...(job.setup ?? []), withChant(job.command, chant)].join("\n");
|
|
256
288
|
}
|
|
257
289
|
|
|
@@ -283,7 +315,7 @@ export function ciJobOp(input: CiJobOpInput): OpConfig {
|
|
|
283
315
|
}
|
|
284
316
|
|
|
285
317
|
/** The pull-request report statuses that mean the job did not do its work, which a failed job's `failure.code` takes. */
|
|
286
|
-
const FAILED_REPORT_STATUSES = new Set(["plan-failed", "failed", "refused"]);
|
|
318
|
+
const FAILED_REPORT_STATUSES = new Set(["plan-failed", "failed", "refused", "incomplete"]);
|
|
287
319
|
|
|
288
320
|
/** The exit code a failed shell step's error names (`command exited <n>`), when it names one. */
|
|
289
321
|
function exitCodeOf(error: string | undefined): number | undefined {
|
|
@@ -441,10 +473,14 @@ export interface CiRunRequest {
|
|
|
441
473
|
* `forge` then only shapes the job's variables.
|
|
442
474
|
*/
|
|
443
475
|
noForge?: boolean;
|
|
444
|
-
/** `--base`, `--pr` and `--commit` (default `HEAD`). */
|
|
476
|
+
/** `--base`, `--pr` and `--commit` (default `HEAD`; for `env-post`, `--base`). */
|
|
445
477
|
base?: string;
|
|
446
478
|
pr?: number;
|
|
447
479
|
commit?: string;
|
|
480
|
+
/** `--head`: for `env-post` (#4330), the pull request's head its environment was applied from. */
|
|
481
|
+
head?: string;
|
|
482
|
+
/** `--result`: for `env-post` (#4330), the env-up job's result. */
|
|
483
|
+
upResult?: string;
|
|
448
484
|
/**
|
|
449
485
|
* The generator's options: `--gate`, `--branch`, `--events-json`,
|
|
450
486
|
* `--report-url`, `--pr-build` and `--pr-build-command`, and `--wave-gate`
|
|
@@ -553,6 +589,11 @@ export interface CiRunResult {
|
|
|
553
589
|
checkout: string;
|
|
554
590
|
/** The job ran with `local` (#4161): its record stays on this machine. */
|
|
555
591
|
local?: true;
|
|
592
|
+
/**
|
|
593
|
+
* For `env-up` (#4330): the `env-post` job the runner ran after it at
|
|
594
|
+
* `--base`, with its own token, which kept the record and posted the note.
|
|
595
|
+
*/
|
|
596
|
+
post?: CiRunResult;
|
|
556
597
|
}
|
|
557
598
|
|
|
558
599
|
const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
@@ -619,7 +660,7 @@ function freshCheckout(top: string, commit: string): { dir: string; remove(): vo
|
|
|
619
660
|
*/
|
|
620
661
|
async function recordInIndex(input: {
|
|
621
662
|
url: string;
|
|
622
|
-
job:
|
|
663
|
+
job: PrLoopJob["jobName"];
|
|
623
664
|
commit: string;
|
|
624
665
|
member?: string;
|
|
625
666
|
pr?: number;
|
|
@@ -679,11 +720,14 @@ export async function ciRun(req: CiRunRequest): Promise<CiRunResult> {
|
|
|
679
720
|
const cwd = realpathSync(resolve(req.cwd));
|
|
680
721
|
const top = gitTop(cwd);
|
|
681
722
|
if (!top) throw new CiRunError("a job runs in a fresh checkout of a commit, and this directory is not in a git repository");
|
|
682
|
-
|
|
723
|
+
// #4330: env-post runs at the target branch's commit, none of the change's code.
|
|
724
|
+
const commit = resolveJobCommit(top, req.commit ?? (req.job === PR_ENV_POST_JOB && req.base ? req.base : "HEAD"));
|
|
683
725
|
if (req.inPlace && commit !== resolveJobCommit(top, "HEAD")) throw new CiRunError(`--in-place runs the checkout's HEAD, and --commit names ${commit.slice(0, 12)}`);
|
|
684
726
|
if (req.local && req.inPlace) throw new CiRunError("--in-place runs a forge job in the forge's checkout, and --local runs one on this machine; pass one of them");
|
|
685
727
|
const place = ciJobPlace(cwd, top);
|
|
686
|
-
const
|
|
728
|
+
const loop = req.local ? localLoop(req.loop) : (req.loop ?? {});
|
|
729
|
+
// #4330: the jobs of a pull request's environment, as the generators wrap them with --pr-env.
|
|
730
|
+
const jobs: CiJob[] = isCiEnvJob(req.job) ? prEnvJobs(req.forge, loop, place.member) : prLoopJobs(req.forge, req.env, loop, place.member);
|
|
687
731
|
const found = jobs.find((j) => j.jobName === req.job);
|
|
688
732
|
if (!found) throw new CiRunError(`the pull-request pipeline has no ${req.job} job; it has ${jobs.map((j) => j.jobName).join(" and ")}`);
|
|
689
733
|
if (req.wave !== undefined && (req.job !== "apply" || !req.loop?.waveGate)) throw new CiRunError("--wave names the one wave an apply runs, and takes --wave-gate: ci run apply --wave-gate <gate> --wave <k>");
|
|
@@ -699,15 +743,15 @@ export async function ciRun(req: CiRunRequest): Promise<CiRunResult> {
|
|
|
699
743
|
const own = ciJobEnv(job, {
|
|
700
744
|
forge: req.forge,
|
|
701
745
|
env: req.env,
|
|
702
|
-
values: { base: req.base, pr: req.pr, commit },
|
|
746
|
+
values: { base: req.base, pr: req.pr, commit, ...(req.head ? { head: req.head } : {}), ...(req.upResult ? { upResult: req.upResult } : {}) },
|
|
703
747
|
hostEnv,
|
|
704
748
|
passEnv: req.passEnv,
|
|
705
749
|
secrets: ciSecretsFor(req.secrets, req.job),
|
|
706
750
|
});
|
|
707
751
|
// #4156: in place, the forge job's environment is the job's, less the runner key.
|
|
708
|
-
// #4295: the plan
|
|
752
|
+
// #4295: the plan and env-up run the pull request's code, so no step of them gets a forge token, whatever gave it one.
|
|
709
753
|
const merged = req.inPlace ? { ...definedEnv(hostEnv), ...own } : own;
|
|
710
|
-
const jobEnv = req.job === "plan" ? withoutForgeTokens(merged) : merged;
|
|
754
|
+
const jobEnv = req.job === "plan" || req.job === "env-up" ? withoutForgeTokens(merged) : merged;
|
|
711
755
|
delete jobEnv[RUNNER_KEY_ENV];
|
|
712
756
|
const poster = req.job === "plan" ? planPoster(req, hostEnv) : undefined;
|
|
713
757
|
// #4096: the secrets and the job's secret-looking variables, redacted from everything the run writes and returns.
|
|
@@ -716,7 +760,9 @@ export async function ciRun(req: CiRunRequest): Promise<CiRunResult> {
|
|
|
716
760
|
try {
|
|
717
761
|
const ran = await runCiJob(req, { cwd, top, commit, place, job, jobEnv, ...(redact ? { redact } : {}) });
|
|
718
762
|
const posted = poster ? await postFromReport(req, poster, ran, { commit, ...(place.member ? { member: place.member } : {}) }) : [];
|
|
719
|
-
const
|
|
763
|
+
const envPost = req.job === "env-up" ? await envPostAfterUp(req, { cwd, top, head: commit, place, jobs, hostEnv, ran }) : undefined;
|
|
764
|
+
const withPost = envPost ? ("result" in envPost ? { ...ran, post: envPost.result } : { ...ran, warnings: [...ran.warnings, envPost.warning] }) : ran;
|
|
765
|
+
const withPosts = posted.length > 0 ? { ...withPost, warnings: [...withPost.warnings, ...posted] } : withPost;
|
|
720
766
|
const result = started ? { ...withPosts, notes: [started, ...withPosts.notes] } : withPosts;
|
|
721
767
|
return redact ? { ...result, warnings: result.warnings.map(redact), notes: result.notes.map(redact), ...(result.pushWarning ? { pushWarning: redact(result.pushWarning) } : {}) } : result;
|
|
722
768
|
} catch (err) {
|
|
@@ -725,6 +771,40 @@ export async function ciRun(req: CiRunRequest): Promise<CiRunResult> {
|
|
|
725
771
|
}
|
|
726
772
|
}
|
|
727
773
|
|
|
774
|
+
/**
|
|
775
|
+
* The env-post job after an env-up (#4330), as a generated pipeline runs it
|
|
776
|
+
* in a job of its own: at `--base`, the target branch's commit, with the
|
|
777
|
+
* forge token env-up's steps never got, taking the report and record env-up
|
|
778
|
+
* handed on. None on `--local`, in place (the forge's env-post job posts),
|
|
779
|
+
* for a plain git server, without `--base` and `--pr`, or without a token.
|
|
780
|
+
*/
|
|
781
|
+
async function envPostAfterUp(
|
|
782
|
+
req: CiRunRequest,
|
|
783
|
+
at: { cwd: string; top: string; head: string; place: { member?: string; dir: string }; jobs: CiJob[]; hostEnv: Record<string, string | undefined>; ran: CiRunResult },
|
|
784
|
+
): Promise<{ result: CiRunResult } | { warning: string } | undefined> {
|
|
785
|
+
if (req.local || req.inPlace || req.noForge || req.pr === undefined) return undefined;
|
|
786
|
+
if (!req.base) return { warning: "env-post did not run: it runs at the target branch's commit, so pass --base <sha>" };
|
|
787
|
+
if (!holdsForgeToken(at.hostEnv)) return { warning: "env-post did not run: there is no forge token, so the environment's record is in the report and nothing kept it" };
|
|
788
|
+
const found = at.jobs.find((j) => j.jobName === PR_ENV_POST_JOB);
|
|
789
|
+
if (!found) return undefined;
|
|
790
|
+
const base = resolveJobCommit(at.top, req.base);
|
|
791
|
+
const job = { ...found, command: req.noForge ? found.command.replace(` --forge ${req.forge}`, "") : found.command };
|
|
792
|
+
const postReq: CiRunRequest = { ...req, job: PR_ENV_POST_JOB, commit: base, head: at.head, upResult: at.ran.status === "ok" ? "success" : "failure" };
|
|
793
|
+
delete postReq.status;
|
|
794
|
+
delete postReq.resumeRecord;
|
|
795
|
+
delete postReq.wave;
|
|
796
|
+
const jobEnv = ciJobEnv(job, { forge: req.forge, env: req.env, values: { base: req.base, pr: req.pr, commit: base, head: at.head, upResult: postReq.upResult! }, hostEnv: at.hostEnv, passEnv: req.passEnv, secrets: ciSecretsFor(req.secrets, PR_ENV_POST_JOB) });
|
|
797
|
+
delete jobEnv[RUNNER_KEY_ENV];
|
|
798
|
+
// Its own redactor: env-post holds the token env-up's steps never got.
|
|
799
|
+
const redact = ciRedactor(jobEnv, req.secrets);
|
|
800
|
+
try {
|
|
801
|
+
const result = await runCiJob(postReq, { cwd: at.cwd, top: at.top, commit: base, place: at.place, job, jobEnv, ...(redact ? { redact } : {}), ...(at.ran.report ? { seed: at.ran.report } : {}) });
|
|
802
|
+
return { result: redact ? { ...result, warnings: result.warnings.map(redact), notes: result.notes.map(redact), ...(result.pushWarning ? { pushWarning: redact(result.pushWarning) } : {}) } : result };
|
|
803
|
+
} catch (err) {
|
|
804
|
+
return { warning: `env-post did not run: ${message(err)}` };
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
728
808
|
/**
|
|
729
809
|
* Who posts the plan's note and `chant/plan` status (#4295): the runner,
|
|
730
810
|
* from the report, once the plan's steps settled, with the forge token the
|
|
@@ -854,7 +934,7 @@ function jobCheckout(top: string, commit: string, inPlace?: boolean): { dir: str
|
|
|
854
934
|
/** {@link ciRun} once the job and its environment are known. */
|
|
855
935
|
async function runCiJob(
|
|
856
936
|
req: CiRunRequest,
|
|
857
|
-
at: { cwd: string; top: string; commit: string; place: { member?: string; dir: string }; job:
|
|
937
|
+
at: { cwd: string; top: string; commit: string; place: { member?: string; dir: string }; job: CiJob; jobEnv: Record<string, string>; redact?: Redact; seed?: string },
|
|
858
938
|
): Promise<CiRunResult> {
|
|
859
939
|
const { cwd, top, commit, place, job, jobEnv, redact } = at;
|
|
860
940
|
const local = req.local === true;
|
|
@@ -884,6 +964,8 @@ async function runCiJob(
|
|
|
884
964
|
...(req.chant ? { chant: req.chant } : {}),
|
|
885
965
|
});
|
|
886
966
|
const warnings: string[] = [...early];
|
|
967
|
+
// #4330: env-post takes the report env-up wrote, as a forge job takes it from the up job's outputs.
|
|
968
|
+
if (at.seed && existsSync(at.seed)) cpSync(at.seed, join(jobDir, ciJobReportDir(req.job)), { recursive: true });
|
|
887
969
|
const resume = req.job === "apply" && req.resumeRecord ? { host: resolve(req.resumeRecord), job: join(jobDir, PR_APPLY_RECORD) } : undefined;
|
|
888
970
|
if (resume && existsSync(resume.host)) {
|
|
889
971
|
mkdirSync(dirname(resume.job), { recursive: true });
|
|
@@ -913,7 +995,7 @@ async function runCiJob(
|
|
|
913
995
|
commit,
|
|
914
996
|
ledger: { cwd: jobDir },
|
|
915
997
|
runner: { kind: "pipeline", runtime: local ? "local" : "ci", runOrigin: origin },
|
|
916
|
-
describeFailure: (records) => ciJobFailure({ job: req.job, op, records, reportDir: join(jobDir,
|
|
998
|
+
describeFailure: (records) => ciJobFailure({ job: req.job, op, records, reportDir: join(jobDir, ciJobReportDir(req.job)) }),
|
|
917
999
|
...(req.runEvents ? { runEvents: { write: req.runEvents } } : {}),
|
|
918
1000
|
...(req.stepOutput ? { stepOutput: req.stepOutput } : {}),
|
|
919
1001
|
...(signer ? { signRecord: (record: OpRunRecord) => signOpRunRecord(record, signer!) } : {}),
|
|
@@ -946,10 +1028,10 @@ async function runCiJob(
|
|
|
946
1028
|
warnings.push(`the resume record could not be kept at ${resume.host}: ${message(err)}`);
|
|
947
1029
|
}
|
|
948
1030
|
}
|
|
949
|
-
const written = join(jobDir,
|
|
1031
|
+
const written = join(jobDir, ciJobReportDir(req.job));
|
|
950
1032
|
let report: string | null = null;
|
|
951
1033
|
if (existsSync(written)) {
|
|
952
|
-
report = resolve(cwd, req.output ??
|
|
1034
|
+
report = resolve(cwd, req.output ?? ciJobReportDir(req.job));
|
|
953
1035
|
// In place the report is already where the forge keeps it, and stays as the command wrote it.
|
|
954
1036
|
if (!(existsSync(report) && realpathSync(report) === realpathSync(written))) {
|
|
955
1037
|
cpSync(written, report, { recursive: true });
|
|
@@ -958,7 +1040,8 @@ async function runCiJob(
|
|
|
958
1040
|
}
|
|
959
1041
|
const failed = result.records.find((r) => r.status === "fail");
|
|
960
1042
|
const exitCode = failed?.phase === req.job ? exitCodeOf(failed.error) : undefined;
|
|
961
|
-
|
|
1043
|
+
// The run index lists the pull-request jobs; an environment job's run is in its record only (#4330).
|
|
1044
|
+
if (reportUrl && !isCiEnvJob(req.job)) {
|
|
962
1045
|
await recordInIndex({ url: reportUrl, job: req.job, commit, member: place.member, pr: req.pr, record: result.record, runOrigin: origin, signer: result.record.statement && signer ? signer.principal : null, exitCode: ciJobExitCode({ status: result.status, ...(exitCode !== undefined ? { exitCode } : {}) }), reportDir: written, limit: req.runIndexLimit, ...(redact ? { redact } : {}), warn: (w) => warnings.push(w) });
|
|
963
1046
|
}
|
|
964
1047
|
// In place the forge job's outcome is the command's, so a record that could not be written is a warning there.
|
|
@@ -1045,7 +1128,7 @@ export function ciLoopOptions(args: CommandContext["args"]): PrLoopPipelineOptio
|
|
|
1045
1128
|
}
|
|
1046
1129
|
|
|
1047
1130
|
const USAGE =
|
|
1048
|
-
"chant ci run plan|record|apply --env <env> --base <sha> [--pr <n>] [--commit <sha>] [--forge github|gitlab|forgejo] " +
|
|
1131
|
+
"chant ci run plan|record|apply|env-up|env-post|env-down|env-sweep --env <env> --base <sha> [--pr <n>] [--commit <sha>] [--head <sha>] [--result <r>] [--forge github|gitlab|forgejo] " +
|
|
1049
1132
|
"[--gate <name>] [--wave-gate <name>] [--wave <k>] [--branch <name>] [--events-json] [--report-url <url>] [--run-index-limit <n>] [--image <image>] [--before-script <line>]... " +
|
|
1050
1133
|
"[--pr-build] [--pr-build-command <line>] [--require-review] [--apply-before-merge] [--no-status] [--status-url <url template>] " +
|
|
1051
1134
|
"[--chant <command>] [--pass-env <NAME>]... [--secrets <file>] [--key <runner key.pem>] [-o <dir>] [--in-place | --local] [--no-push] [--verbose] | chant ci run --at <sha> | chant ci run --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>]";
|
|
@@ -1101,9 +1184,11 @@ export async function runCiRun(ctx: CommandContext): Promise<number> {
|
|
|
1101
1184
|
try {
|
|
1102
1185
|
if (args.setupKey) return (await import("./ci-serve")).runnerKeySetupCommand(ctx);
|
|
1103
1186
|
if (args.at !== undefined && args.extraPositional === undefined) return await requestBuildCommand(args.at);
|
|
1104
|
-
const job = args.extraPositional;
|
|
1105
|
-
if (job
|
|
1106
|
-
|
|
1187
|
+
const job = args.extraPositional as CiJobName | undefined;
|
|
1188
|
+
if (!job || !CI_JOB_NAMES.includes(job)) throw new CiRunError(`ci run takes the job to run, ${CI_JOB_NAMES.join(", ")}${job ? `, not ${JSON.stringify(job)}` : ""}`);
|
|
1189
|
+
// #4330: an environment job's environment is pr-<n>; the sweep's run is recorded under --env, else local.
|
|
1190
|
+
const env = args.env ?? (isCiEnvJob(job) ? (args.pr !== undefined && job !== "env-sweep" ? `pr-${args.pr}` : "local") : undefined);
|
|
1191
|
+
if (!env) throw new CiRunError("--env names the environment the pipeline is for, as chant build --components --generate <lexicon> --pr-loop took it: --env <env>");
|
|
1107
1192
|
const forge = args.forge ?? detectForge(process.env);
|
|
1108
1193
|
if (!forge) throw new CiRunError("--forge names the forge the job posts to: --forge github|gitlab|forgejo");
|
|
1109
1194
|
if (!(FORGE_KINDS as readonly string[]).includes(forge)) throw new CiRunError(`--forge takes github, gitlab or forgejo, not ${JSON.stringify(forge)}`);
|
|
@@ -1124,11 +1209,13 @@ export async function runCiRun(ctx: CommandContext): Promise<number> {
|
|
|
1124
1209
|
cwd: process.cwd(),
|
|
1125
1210
|
...(status ? { status } : {}),
|
|
1126
1211
|
job,
|
|
1127
|
-
env
|
|
1212
|
+
env,
|
|
1128
1213
|
forge: forge as ForgeKind,
|
|
1129
1214
|
...(args.base ? { base: args.base } : {}),
|
|
1130
1215
|
...(args.pr !== undefined ? { pr: args.pr } : {}),
|
|
1131
1216
|
...(args.commit ? { commit: args.commit } : {}),
|
|
1217
|
+
...(args.head ? { head: args.head } : {}),
|
|
1218
|
+
...(args.planResult ? { upResult: args.planResult } : {}),
|
|
1132
1219
|
loop: ciLoopOptions(reportUrl ? { ...args, reportUrl } : args),
|
|
1133
1220
|
...(args.ciInPlace ? { inPlace: true } : {}),
|
|
1134
1221
|
...(args.local ? { local: true } : {}),
|
|
@@ -1149,8 +1236,14 @@ export async function runCiRun(ctx: CommandContext): Promise<number> {
|
|
|
1149
1236
|
for (const w of result.warnings) console.error(formatWarning({ message: w }));
|
|
1150
1237
|
if (args.verbose) for (const n of result.notes) console.error(`ci run: ${n}`);
|
|
1151
1238
|
console.error(formatCiRun(result));
|
|
1239
|
+
if (result.post) {
|
|
1240
|
+
for (const w of result.post.warnings) console.error(formatWarning({ message: w }));
|
|
1241
|
+
console.error(formatCiRun(result.post));
|
|
1242
|
+
}
|
|
1152
1243
|
// #4156: a failed command's own exit code, such as 3 for a moved plan, is the job's, as it was before the job ran through ci run.
|
|
1153
|
-
|
|
1244
|
+
// #4330: an env-up whose env-post failed fails too, since its record was not kept.
|
|
1245
|
+
const code = ciJobExitCode(result);
|
|
1246
|
+
return code === 0 && result.post ? ciJobExitCode(result.post) : code;
|
|
1154
1247
|
} catch (err) {
|
|
1155
1248
|
console.error(formatError({ message: `chant ci run: ${message(err)}`, hint: USAGE }));
|
|
1156
1249
|
return 1;
|
|
@@ -69,7 +69,7 @@ describe("the secrets file (#4096)", () => {
|
|
|
69
69
|
expect(ciSecretsFor(undefined, "plan")).toEqual({});
|
|
70
70
|
expect(ciSecretsFor({ ...s, jobs: { plan: [] } }, "plan")).toEqual({});
|
|
71
71
|
expect(() => parseCiSecrets(JSON.stringify({ secrets: { A: "aaaa" }, jobs: { plan: ["C"] } }), "s.json")).toThrow(/jobs\.plan names C, which the file does not set/);
|
|
72
|
-
expect(() => parseCiSecrets(JSON.stringify({ secrets: { A: "aaaa" }, jobs: { deploy: [] } }), "s.json")).toThrow(/the jobs are plan
|
|
72
|
+
expect(() => parseCiSecrets(JSON.stringify({ secrets: { A: "aaaa" }, jobs: { deploy: [] } }), "s.json")).toThrow(/the jobs are plan, apply, env-up/);
|
|
73
73
|
expect(() => parseCiSecrets('{ "A": 1 }', "s.json")).toThrow(/must be a string/);
|
|
74
74
|
// A parse error does not quote the text, which may hold a secret.
|
|
75
75
|
expect(() => parseCiSecrets('{ "A": "hunter2', "s.json")).toThrow(/^s\.json starts with \{, and is not valid JSON$/);
|
|
@@ -37,7 +37,8 @@ export const SECRET_NAME = /TOKEN|SECRET|KEY|PASSWORD/i;
|
|
|
37
37
|
|
|
38
38
|
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
/** The jobs a secrets file can give a list of its own: the pull-request jobs, and a pull request environment's (#4330). */
|
|
41
|
+
const JOBS: readonly CiJobName[] = ["plan", "apply", "env-up", "env-post", "env-down", "env-sweep"];
|
|
41
42
|
|
|
42
43
|
/** The secrets file, read. */
|
|
43
44
|
export interface CiSecrets {
|
|
@@ -176,9 +177,9 @@ export function parseCiSecrets(text: string, file: string): Omit<CiSecrets, "fil
|
|
|
176
177
|
if (structured) {
|
|
177
178
|
for (const key of Object.keys(doc)) if (key !== "secrets" && key !== "jobs") throw new CiSecretsError(`${file} has ${JSON.stringify(key)} beside secrets and jobs`);
|
|
178
179
|
const given = doc.jobs ?? {};
|
|
179
|
-
if (given === null || typeof given !== "object" || Array.isArray(given)) throw new CiSecretsError(`${file}: jobs must map
|
|
180
|
+
if (given === null || typeof given !== "object" || Array.isArray(given)) throw new CiSecretsError(`${file}: jobs must map ${JOBS.join(", ")} to lists of secret names`);
|
|
180
181
|
for (const [job, names] of Object.entries(given as Record<string, unknown>)) {
|
|
181
|
-
if (!(JOBS as readonly string[]).includes(job)) throw new CiSecretsError(`${file}: jobs names ${JSON.stringify(job)}, and the jobs are
|
|
182
|
+
if (!(JOBS as readonly string[]).includes(job)) throw new CiSecretsError(`${file}: jobs names ${JSON.stringify(job)}, and the jobs are ${JOBS.join(", ")}`);
|
|
182
183
|
if (!Array.isArray(names) || names.some((n) => typeof n !== "string")) throw new CiSecretsError(`${file}: jobs.${job} must be a list of secret names`);
|
|
183
184
|
for (const n of names as string[]) if (!Object.prototype.hasOwnProperty.call(values, n)) throw new CiSecretsError(`${file}: jobs.${job} names ${n}, which the file does not set`);
|
|
184
185
|
jobs[job as CiJobName] = [...(names as string[])];
|
|
@@ -41,6 +41,7 @@ import type { KindLinkRead } from "./kind-link-readers";
|
|
|
41
41
|
import type { RecordLinkRow } from "./record-assets";
|
|
42
42
|
import type { ReasonCode } from "./reason-codes";
|
|
43
43
|
import { GRAPH_IR_VERSION, type GraphIR, type IRExport, type IRDanglingRef, type IRGroups, type IRImport, type IREdge, type IRNode } from "../graph-ir";
|
|
44
|
+
import type { ViewDocument } from "../capabilities/view";
|
|
44
45
|
|
|
45
46
|
/** The version of the composed document `chant workspace graph` writes. */
|
|
46
47
|
export const WORKSPACE_GRAPH_VERSION = 1;
|
|
@@ -105,6 +106,12 @@ export interface ComposedMember {
|
|
|
105
106
|
* `chant workspace graph` on every member; null when the box declares neither.
|
|
106
107
|
*/
|
|
107
108
|
box?: { factory: FactoryView | null; listing: ListingView | null } | null;
|
|
109
|
+
/**
|
|
110
|
+
* With `--capability-view` (#4378): the lexicon-neutral view the member's
|
|
111
|
+
* own chant mapped through its own lexicons, with `<member>/<id>` ids. null
|
|
112
|
+
* when the member's chant predates the view and printed none.
|
|
113
|
+
*/
|
|
114
|
+
capabilityView?: ViewDocument | null;
|
|
108
115
|
/** Whole-read facts the member's IR carried (`meta`, `pipeline`), kept apart from the composed sections. */
|
|
109
116
|
meta?: Record<string, unknown>;
|
|
110
117
|
pipeline?: unknown;
|
|
@@ -249,7 +256,9 @@ export interface ComposeInput {
|
|
|
249
256
|
ir?: GraphIR;
|
|
250
257
|
}
|
|
251
258
|
|
|
252
|
-
|
|
259
|
+
/** A member's node id as the composed workspace documents give it: `<member>/<id>`. `workspace search` uses the same rule (#4374). */
|
|
260
|
+
export const composedId = (member: string, id: string): string => `${member}/${id}`;
|
|
261
|
+
const prefix = composedId;
|
|
253
262
|
|
|
254
263
|
const strings = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : []);
|
|
255
264
|
const records = (v: unknown): Record<string, unknown>[] =>
|
|
@@ -39,6 +39,8 @@ const JSON_FLAG: Record<ReadContractCommand, string[]> = {
|
|
|
39
39
|
"graph --composites": ["--json"],
|
|
40
40
|
runs: ["--json"],
|
|
41
41
|
wip: ["--json"],
|
|
42
|
+
search: ["--json"],
|
|
43
|
+
"search --capabilities": ["--json"],
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
/** The smallest reader that conforms, with a hook to misbehave. */
|
|
@@ -65,6 +67,17 @@ describe("the generated conformance workspace (#2679)", () => {
|
|
|
65
67
|
expect(report.skipped).toEqual([]);
|
|
66
68
|
}, 300_000);
|
|
67
69
|
|
|
70
|
+
test("search answers over the chant member with workspace graph's ids, and lists the others (#4374)", async () => {
|
|
71
|
+
const docs: Record<string, unknown>[] = [];
|
|
72
|
+
const report = await runWorkspaceReaderConformance(reader((doc) => (docs.push(doc), doc)), { workspaceDir: ws.dir, commands: ["search"] });
|
|
73
|
+
expect(report.problems).toEqual([]);
|
|
74
|
+
const doc = docs[0] as { at: null; matches: { id: string; member: string }[]; members: { name: string; status: string; alone?: boolean }[] };
|
|
75
|
+
expect(doc.at).toBeNull();
|
|
76
|
+
expect(doc.matches.length).toBeGreaterThan(0);
|
|
77
|
+
for (const m of doc.matches) expect(m.id.startsWith(`${m.member}/`), m.id).toBe(true);
|
|
78
|
+
expect(doc.members.find((m) => m.name === "delivery")).toMatchObject({ status: "searched", alone: true });
|
|
79
|
+
}, 300_000);
|
|
80
|
+
|
|
68
81
|
test("graph --composites joins the fixture's composite to its component", async () => {
|
|
69
82
|
const docs: Record<string, unknown>[] = [];
|
|
70
83
|
await runWorkspaceReaderConformance(reader((doc) => (docs.push(doc), doc)), { workspaceDir: ws.dir, commands: ["graph --composites"] });
|
|
@@ -120,7 +133,7 @@ describe("commands (#2679)", () => {
|
|
|
120
133
|
expect(report.problems).toEqual([]);
|
|
121
134
|
expect(read).toEqual(["ls", "status", "graph --composites"]);
|
|
122
135
|
expect(report.checked).toEqual(["ls", "status", "graph --composites"]);
|
|
123
|
-
expect(report.skipped).toEqual(["graph", "check", "records", "records --uncommitted", "graph --intent", "runs", "wip"]);
|
|
136
|
+
expect(report.skipped).toEqual(["graph", "check", "records", "records --uncommitted", "graph --intent", "runs", "wip", "search", "search --capabilities"]);
|
|
124
137
|
}, 300_000);
|
|
125
138
|
|
|
126
139
|
test("a name that is not a contract command, or an empty list, is refused", () => {
|
|
@@ -56,7 +56,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
56
56
|
import { READ_CONTRACT_VERSION } from "../reason-codes";
|
|
57
57
|
|
|
58
58
|
/** The read-contract commands, as a reader names them. */
|
|
59
|
-
export const READ_CONTRACT_COMMANDS = ["ls", "graph", "check", "status", "records", "records --uncommitted", "graph --intent", "graph --composites", "runs", "wip"] as const;
|
|
59
|
+
export const READ_CONTRACT_COMMANDS = ["ls", "graph", "check", "status", "records", "records --uncommitted", "graph --intent", "graph --composites", "runs", "wip", "search", "search --capabilities"] as const;
|
|
60
60
|
export type ReadContractCommand = (typeof READ_CONTRACT_COMMANDS)[number];
|
|
61
61
|
|
|
62
62
|
/**
|
|
@@ -75,6 +75,10 @@ export const READ_CONTRACT_SCHEMAS: Record<ReadContractCommand, string> = {
|
|
|
75
75
|
"graph --composites": "composites.schema.json",
|
|
76
76
|
runs: "runs.schema.json",
|
|
77
77
|
wip: "wip.schema.json",
|
|
78
|
+
// `workspace search` (#4374). Plain `chant search --json` prints the same document for one project.
|
|
79
|
+
search: "search.schema.json",
|
|
80
|
+
// The capability catalog (#4375), from the workspace's chant.
|
|
81
|
+
"search --capabilities": "capabilities.schema.json",
|
|
78
82
|
};
|
|
79
83
|
|
|
80
84
|
/** The flags that ask each command for its JSON document. A reader adds one of these and nothing else. */
|
|
@@ -89,6 +93,8 @@ export const READ_CONTRACT_JSON_FLAGS: Record<ReadContractCommand, readonly (rea
|
|
|
89
93
|
"graph --composites": [[], ["--json"]],
|
|
90
94
|
runs: [["--json"]],
|
|
91
95
|
wip: [["--json"]],
|
|
96
|
+
search: [["--json"]],
|
|
97
|
+
"search --capabilities": [["--json"]],
|
|
92
98
|
};
|
|
93
99
|
|
|
94
100
|
/** The arguments the suite passes each read, on the reference workspace or the generated one. */
|
|
@@ -103,6 +109,9 @@ export const REFERENCE_READS: Record<ReadContractCommand, string[]> = {
|
|
|
103
109
|
"graph --composites": [],
|
|
104
110
|
runs: [],
|
|
105
111
|
wip: [],
|
|
112
|
+
// A bare word: on either workspace it searches the chant member, delivery, and lists the others.
|
|
113
|
+
search: ["app"],
|
|
114
|
+
"search --capabilities": [],
|
|
106
115
|
};
|
|
107
116
|
|
|
108
117
|
/** One chant run: the arguments after `chant`, what it was given on stdin, and what it printed. */
|
|
@@ -344,6 +353,8 @@ export const MCP_READ_TOOLS: Partial<Record<ReadContractCommand, string>> = {
|
|
|
344
353
|
"graph --intent": "workspace-graph",
|
|
345
354
|
"graph --composites": "workspace-graph",
|
|
346
355
|
runs: "workspace-runs",
|
|
356
|
+
search: "workspace-search",
|
|
357
|
+
"search --capabilities": "capabilities",
|
|
347
358
|
};
|
|
348
359
|
|
|
349
360
|
/**
|
|
@@ -359,9 +370,14 @@ export function mcpToolCall(argv: readonly string[]): { name: string; arguments:
|
|
|
359
370
|
const args: Record<string, unknown> = {};
|
|
360
371
|
const positional: string[] = [];
|
|
361
372
|
const kinds: string[] = [];
|
|
373
|
+
const members: string[] = [];
|
|
362
374
|
for (let i = 0; i < rest.length; i++) {
|
|
363
375
|
const a = rest[i];
|
|
364
376
|
const value = (): string | undefined => rest[++i];
|
|
377
|
+
if (a === "--") {
|
|
378
|
+
positional.push(...rest.slice(i + 1));
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
365
381
|
if (a === "--json") continue;
|
|
366
382
|
if (a === "--format" && rest[i + 1] === "json") {
|
|
367
383
|
i++;
|
|
@@ -381,7 +397,14 @@ export function mcpToolCall(argv: readonly string[]): { name: string; arguments:
|
|
|
381
397
|
else if (a === "--by") args.by = value();
|
|
382
398
|
else if (a === "--uncommitted") args.uncommitted = true;
|
|
383
399
|
else if (a === "--composites") args.composites = true;
|
|
384
|
-
else if (a
|
|
400
|
+
else if (verb === "search" && a === "--member") members.push(value() ?? "");
|
|
401
|
+
else if (verb === "search" && a === "--env") args.env = value();
|
|
402
|
+
else if (verb === "search" && a === "--snapshot") args.snapshot = value();
|
|
403
|
+
else if (verb === "search" && a === "--count-by") args.countBy = value();
|
|
404
|
+
else if (verb === "search" && a === "--show") args.show = value();
|
|
405
|
+
else if (verb === "search" && a === "--explain") args.explain = true;
|
|
406
|
+
else if (verb === "search" && a === "--capabilities") args.capabilities = true;
|
|
407
|
+
else if (/^-[A-Za-z-]/.test(a)) return undefined;
|
|
385
408
|
else positional.push(a);
|
|
386
409
|
}
|
|
387
410
|
switch (verb) {
|
|
@@ -397,6 +420,12 @@ export function mcpToolCall(argv: readonly string[]): { name: string; arguments:
|
|
|
397
420
|
return positional.length === 0 ? { name: "workspace-graph", arguments: { ...args, ...(kinds.length ? { kind: kinds } : {}) } } : undefined;
|
|
398
421
|
case "runs":
|
|
399
422
|
return positional.length === 0 && kinds.length === 0 ? { name: "workspace-runs", arguments: args } : undefined;
|
|
423
|
+
case "search":
|
|
424
|
+
// The catalog (#4375) is its own tool, and takes nothing.
|
|
425
|
+
if (args.capabilities) return positional.length === 0 && Object.keys(args).length === 1 && kinds.length === 0 && members.length === 0 ? { name: "capabilities", arguments: {} } : undefined;
|
|
426
|
+
return positional.length <= 1 && kinds.length === 0
|
|
427
|
+
? { name: "workspace-search", arguments: { ...args, query: positional[0] ?? "", ...(members.length ? { member: members } : {}) } }
|
|
428
|
+
: undefined;
|
|
400
429
|
default:
|
|
401
430
|
return undefined;
|
|
402
431
|
}
|
|
@@ -289,6 +289,33 @@
|
|
|
289
289
|
"type": "string",
|
|
290
290
|
"pattern": "^[1-9][0-9]*(m|h|d)$",
|
|
291
291
|
"description": "How long a pull request's environment lives after its last apply, such as 72h, 90m or 7d. chant components pr-env sweep tears down one whose TTL passed. 72h when absent."
|
|
292
|
+
},
|
|
293
|
+
"teardown": {
|
|
294
|
+
"type": "object",
|
|
295
|
+
"description": "A command that tears down what no lexicon does, such as a Terraform root's copy of the environment (#4330). pr-env down and pr-env sweep run it after the ownership check, twice: CHANT_PR_ENV_STAGE=owned writes the candidates with their read-back markers to CHANT_PR_ENV_RESULT, and CHANT_PR_ENV_STAGE=teardown deletes the candidates in CHANT_PR_ENV_CANDIDATES and writes their outcomes. It runs without the forge token variables. Added in schema 1 by chant 0.124.45, so a declaration that uses it sets minReader to 0.124.45 or newer.",
|
|
296
|
+
"required": ["command"],
|
|
297
|
+
"additionalProperties": false,
|
|
298
|
+
"patternProperties": {
|
|
299
|
+
"^x-": true
|
|
300
|
+
},
|
|
301
|
+
"properties": {
|
|
302
|
+
"name": {
|
|
303
|
+
"type": "string",
|
|
304
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$",
|
|
305
|
+
"description": "The name the teardown's outcomes are attributed to, as a lexicon's are. teardown when absent."
|
|
306
|
+
},
|
|
307
|
+
"command": {
|
|
308
|
+
"type": "array",
|
|
309
|
+
"minItems": 1,
|
|
310
|
+
"items": { "type": "string", "minLength": 1 },
|
|
311
|
+
"description": "Argv, run in the member's directory at the commit the job runs at. {env}, {pr} and {stage} are filled in."
|
|
312
|
+
},
|
|
313
|
+
"lexicons": {
|
|
314
|
+
"type": "array",
|
|
315
|
+
"items": { "type": "string", "minLength": 1 },
|
|
316
|
+
"description": "The lexicons the command tears down in place of, such as terraform. Their own teardown is left out, so one with no teardown execution does not leave the environment incomplete."
|
|
317
|
+
}
|
|
318
|
+
}
|
|
292
319
|
}
|
|
293
320
|
}
|
|
294
321
|
}
|
|
@@ -363,7 +390,7 @@
|
|
|
363
390
|
"ciCloudIdentityRules": {
|
|
364
391
|
"type": "array",
|
|
365
392
|
"minItems": 1,
|
|
366
|
-
"description": "Rules, first match wins: a rule matches a pipeline when its environments match the pipeline's environment and its members match the pipeline's member. A rule with no environments matches every environment,
|
|
393
|
+
"description": "Rules, first match wins: a rule matches a pipeline when its environments match the pipeline's environment and its members match the pipeline's member, and a target inside the member when its targets match the target too. A rule with no environments matches every environment, one with no members every member and a pipeline outside a member, and one with no targets every target.",
|
|
367
394
|
"items": {
|
|
368
395
|
"type": "object",
|
|
369
396
|
"additionalProperties": false,
|
|
@@ -378,6 +405,14 @@
|
|
|
378
405
|
"description": "Environment-name globs, such as prod or staging-*."
|
|
379
406
|
},
|
|
380
407
|
"members": { "$ref": "#/$defs/ciMemberGlobs" },
|
|
408
|
+
"targets": {
|
|
409
|
+
"type": "array",
|
|
410
|
+
"minItems": 1,
|
|
411
|
+
"uniqueItems": true,
|
|
412
|
+
"items": { "type": "string", "minLength": 1 },
|
|
413
|
+
"description": "Target globs inside the member (#4332), such as Terraform roots or Op wave run targets: * within one / segment, ** across segments, ? one character. The plan job trades for every plan identity the member's targets take, and an apply splits into one job per apply identity. Added in schema 1 by chant 0.124.45, so a declaration that uses it sets minReader to 0.124.45 or newer."
|
|
414
|
+
},
|
|
415
|
+
"name": { "type": "string", "pattern": "^[a-z0-9_-]+$", "description": "The name of the apply job group the rule's targets form (#4332). Default: from its first target glob. Added by chant 0.124.45." },
|
|
381
416
|
"plan": { "type": "string", "minLength": 1, "description": "The identity the plan and record jobs take: a role ARN, a service account email or a client id." },
|
|
382
417
|
"apply": { "type": "string", "minLength": 1, "description": "The identity the apply jobs take." }
|
|
383
418
|
}
|
|
@@ -564,4 +564,11 @@ describe("ci.prEnvironment (#4298)", () => {
|
|
|
564
564
|
expect(failure(base(members, { ci: { prEnvironment: {} } })).code).toBe("declaration-invalid");
|
|
565
565
|
expect(failure(base(members, { ci: { prEnvironment: { template: "staging", ttl: "1w" } } })).code).toBe("declaration-invalid");
|
|
566
566
|
});
|
|
567
|
+
|
|
568
|
+
test("a teardown command is read with its argv and the lexicons it covers (#4330)", () => {
|
|
569
|
+
const teardown = { name: "terragucci", command: ["terragucci", "pr-env", "teardown", "{stage}"], lexicons: ["terraform"] };
|
|
570
|
+
expect(parse(base(members, { ci: { prEnvironment: { template: "staging", teardown } } })).ci?.prEnvironment).toEqual({ template: "staging", ttl: "72h", teardown });
|
|
571
|
+
expect(failure(base(members, { ci: { prEnvironment: { template: "staging", teardown: { command: [] } } } })).code).toBe("declaration-invalid");
|
|
572
|
+
expect(failure(base(members, { ci: { prEnvironment: { template: "staging", teardown: { command: ["x"], runs: "y" } } } })).code).toBe("declaration-invalid");
|
|
573
|
+
});
|
|
567
574
|
});
|