@intentius/chant 0.124.30 → 0.124.32
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/bin/chant +7 -2
- package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
- package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-plugin.js +10 -7
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.js +18 -12
- package/dist/cli/commands/import.d.ts +10 -0
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/import.js +37 -0
- package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/onboard.js +8 -4
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +2 -1
- package/dist/cli/dependencies.d.ts +18 -0
- package/dist/cli/dependencies.d.ts.map +1 -0
- package/dist/cli/dependencies.js +120 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +23 -4
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +12 -4
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +16 -6
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lint.js +6 -0
- package/dist/cli/handlers/operator.d.ts +2 -2
- package/dist/cli/handlers/operator.js +3 -3
- package/dist/cli/handlers/pr.d.ts +14 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +488 -32
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +3 -1
- package/dist/cli/handlers/run.d.ts +13 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +38 -15
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +74 -12
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +65 -0
- package/dist/cli/registry.d.ts +29 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/registry.js +6 -2
- package/dist/codegen/generate-post-synth-barrel.js +2 -2
- package/dist/codegen/lexicon-regen.d.ts +4 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.js +1 -1
- package/dist/codegen/pinned-upgrade.js +2 -2
- package/dist/components/capability.d.ts +44 -3
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +21 -0
- package/dist/components/changed-files.d.ts +7 -1
- package/dist/components/changed-files.d.ts.map +1 -1
- package/dist/components/changed-files.js +9 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +47 -6
- package/dist/components/fan-out-record.d.ts +6 -1
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +24 -0
- package/dist/components/fan-out-run.d.ts +20 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +21 -1
- package/dist/components/pr-pipeline.d.ts +65 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +107 -15
- package/dist/components/pr-run.d.ts +127 -29
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +290 -70
- package/dist/components/wave-plan.d.ts +5 -2
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +12 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-import.js +16 -6
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.js +6 -0
- package/dist/discovery/specifier-resolve.d.ts.map +1 -1
- package/dist/discovery/specifier-resolve.js +8 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/fold.js +68 -1
- package/dist/fold/subset.d.ts +37 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/fold/subset.js +88 -0
- package/dist/lexicon-module.d.ts +10 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +53 -8
- package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
- package/dist/lexicon-plugin-helpers.js +6 -4
- package/dist/lexicon.d.ts +2 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/affected.d.ts.map +1 -1
- package/dist/lifecycle/affected.js +7 -1
- package/dist/lifecycle/release-ledger.d.ts +21 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +24 -0
- package/dist/lifecycle/work-dir.d.ts +45 -0
- package/dist/lifecycle/work-dir.d.ts.map +1 -0
- package/dist/lifecycle/work-dir.js +78 -0
- package/dist/lint/discover.d.ts +17 -9
- package/dist/lint/discover.d.ts.map +1 -1
- package/dist/lint/discover.js +62 -54
- package/dist/module-source.d.ts +57 -0
- package/dist/module-source.d.ts.map +1 -0
- package/dist/module-source.js +104 -0
- package/dist/op/activities/factory.js +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +13 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +3 -0
- package/dist/op/local-executor.d.ts +12 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +22 -3
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +20 -5
- package/dist/op/operator.d.ts +2 -2
- package/dist/op/operator.js +2 -2
- package/dist/op/redact.d.ts +34 -0
- package/dist/op/redact.d.ts.map +1 -0
- package/dist/op/redact.js +94 -0
- package/dist/op/step-console.d.ts +2 -0
- package/dist/op/step-console.d.ts.map +1 -1
- package/dist/op/step-console.js +5 -2
- package/dist/op/steward.d.ts +6 -4
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/steward.js +5 -3
- package/dist/op/work-lease-run.d.ts +20 -7
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/op/work-lease-run.js +56 -12
- package/dist/pr-forge.d.ts +27 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +51 -6
- package/dist/pr-loop.d.ts +141 -4
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +122 -6
- package/dist/pr-report-url.d.ts +10 -2
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/pr-report-url.js +52 -6
- package/dist/ts-loader.d.ts +38 -4
- package/dist/ts-loader.d.ts.map +1 -1
- package/dist/ts-loader.js +190 -16
- package/dist/wave-runtime.mjs +208 -208
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -1
- package/dist/workspace/chant-migrations.js +2 -0
- package/dist/workspace/checks/boxes.d.ts +6 -6
- package/dist/workspace/checks/boxes.js +9 -9
- package/dist/workspace/ci-green-forge.d.ts +6 -4
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green.d.ts +28 -12
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +49 -21
- package/dist/workspace/ci-list.schema.json +12 -6
- package/dist/workspace/ci-report-url.d.ts +18 -0
- package/dist/workspace/ci-report-url.d.ts.map +1 -0
- package/dist/workspace/ci-report-url.js +33 -0
- package/dist/workspace/ci-run-index.d.ts +108 -0
- package/dist/workspace/ci-run-index.d.ts.map +1 -0
- package/dist/workspace/ci-run-index.js +164 -0
- package/dist/workspace/ci-run-index.schema.json +52 -0
- package/dist/workspace/ci-run.d.ts +159 -9
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +419 -50
- package/dist/workspace/ci-runner-key.d.ts +133 -0
- package/dist/workspace/ci-runner-key.d.ts.map +1 -0
- package/dist/workspace/ci-runner-key.js +237 -0
- package/dist/workspace/ci-secrets.d.ts +65 -0
- package/dist/workspace/ci-secrets.d.ts.map +1 -0
- package/dist/workspace/ci-secrets.js +211 -0
- package/dist/workspace/ci-serve.d.ts +67 -4
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +240 -35
- package/dist/workspace/declaration.d.ts +8 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +11 -2
- package/dist/workspace/declaration.schema.json +7 -2
- package/dist/workspace/export.d.ts +1 -1
- package/dist/workspace/export.d.ts.map +1 -1
- package/dist/workspace/export.js +7 -3
- package/dist/workspace/ls.d.ts +6 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +1 -0
- package/dist/workspace/ls.schema.json +7 -1
- package/dist/workspace/pin-integrity.d.ts.map +1 -1
- package/dist/workspace/pin-integrity.js +3 -2
- package/dist/workspace/pr-report.schema.json +74 -7
- package/dist/workspace/reason-codes.d.ts +7 -3
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +9 -4
- package/dist/workspace/run-event.schema.json +3 -1
- package/dist/workspace/status.schema.json +3 -1
- package/dist/workspace/trust/trust-write.d.ts +109 -0
- package/dist/workspace/trust/trust-write.d.ts.map +1 -0
- package/dist/workspace/trust/trust-write.js +320 -0
- package/dist/workspace/trust-write.schema.json +265 -0
- package/dist/workspace/write-scope.d.ts +9 -0
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/workspace/write-scope.js +11 -1
- package/package.json +1 -1
- package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
- package/src/cli/commands/audit.test.ts +2 -2
- package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
- package/src/cli/commands/check-lexicon-plugin.ts +11 -8
- package/src/cli/commands/check-lexicon.test.ts +1 -1
- package/src/cli/commands/check-lexicon.ts +16 -11
- package/src/cli/commands/import.ts +40 -0
- package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
- package/src/cli/commands/init-lexicon.test.ts +8 -5
- package/src/cli/commands/onboard.test.ts +45 -0
- package/src/cli/commands/onboard.ts +8 -4
- package/src/cli/commands/update.test.ts +6 -0
- package/src/cli/commands/update.ts +4 -1
- package/src/cli/compiled-cli.e2e.test.ts +76 -11
- package/src/cli/dependencies.e2e.test.ts +101 -0
- package/src/cli/dependencies.test.ts +93 -0
- package/src/cli/dependencies.ts +122 -0
- package/src/cli/handlers/build.test.ts +69 -0
- package/src/cli/handlers/build.ts +20 -4
- package/src/cli/handlers/fan-out.test.ts +73 -0
- package/src/cli/handlers/fan-out.ts +11 -4
- package/src/cli/handlers/graph.test.ts +76 -0
- package/src/cli/handlers/graph.ts +19 -12
- package/src/cli/handlers/lint.test.ts +42 -0
- package/src/cli/handlers/lint.ts +6 -0
- package/src/cli/handlers/operator.ts +3 -3
- package/src/cli/handlers/pr-waves.test.ts +449 -0
- package/src/cli/handlers/pr.test.ts +134 -0
- package/src/cli/handlers/pr.ts +523 -31
- package/src/cli/handlers/promote.test.ts +26 -1
- package/src/cli/handlers/promote.ts +3 -2
- package/src/cli/handlers/run-components-exit.test.ts +99 -0
- package/src/cli/handlers/run-signer.test.ts +79 -0
- package/src/cli/handlers/run.ts +34 -16
- package/src/cli/main.test.ts +19 -0
- package/src/cli/main.ts +73 -12
- package/src/cli/parse-args.ts +48 -0
- package/src/cli/registry.ts +35 -4
- package/src/codegen/generate-post-synth-barrel.ts +2 -2
- package/src/codegen/lexicon-regen.ts +1 -1
- package/src/codegen/pinned-upgrade.ts +2 -2
- package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
- package/src/components/capability.ts +51 -3
- package/src/components/changed-files.test.ts +16 -1
- package/src/components/changed-files.ts +9 -1
- package/src/components/driver.test.ts +36 -1
- package/src/components/driver.ts +76 -4
- package/src/components/fan-out-record.ts +29 -1
- package/src/components/fan-out-run.test.ts +35 -0
- package/src/components/fan-out-run.ts +40 -1
- package/src/components/pr-pipeline.test.ts +100 -0
- package/src/components/pr-pipeline.ts +132 -17
- package/src/components/pr-run.test.ts +323 -18
- package/src/components/pr-run.ts +354 -75
- package/src/components/wave-plan.ts +16 -4
- package/src/discovery/fold-host-method.test.ts +87 -0
- package/src/discovery/fold-import.ts +15 -5
- package/src/discovery/fold-package-composite.test.ts +27 -0
- package/src/discovery/fold-specifiers.test.ts +8 -0
- package/src/discovery/sandbox/bundle.ts +6 -0
- package/src/discovery/specifier-resolve.ts +8 -2
- package/src/fold/fold.test.ts +72 -0
- package/src/fold/fold.ts +71 -0
- package/src/fold/subset.test.ts +31 -0
- package/src/fold/subset.ts +99 -0
- package/src/lexicon-dist.e2e.test.ts +254 -0
- package/src/lexicon-module.test.ts +47 -0
- package/src/lexicon-module.ts +49 -7
- package/src/lexicon-plugin-helpers.ts +6 -4
- package/src/lexicon.ts +2 -0
- package/src/lifecycle/affected.test.ts +7 -0
- package/src/lifecycle/affected.ts +6 -1
- package/src/lifecycle/release-ledger.ts +34 -0
- package/src/lifecycle/work-dir.test.ts +77 -0
- package/src/lifecycle/work-dir.ts +82 -0
- package/src/lint/discover.test.ts +46 -2
- package/src/lint/discover.ts +55 -49
- package/src/module-source.test.ts +85 -0
- package/src/module-source.ts +112 -0
- package/src/op/activities/factory.ts +1 -1
- package/src/op/discover-root.test.ts +31 -2
- package/src/op/discover.ts +14 -4
- package/src/op/gate.test.ts +30 -0
- package/src/op/gate.ts +3 -0
- package/src/op/local-executor.ts +31 -3
- package/src/op/op-waves-run.ts +20 -5
- package/src/op/op-waves.test.ts +49 -0
- package/src/op/operator.ts +2 -2
- package/src/op/redact.ts +93 -0
- package/src/op/step-console.ts +7 -2
- package/src/op/steward.ts +6 -4
- package/src/op/work-lease-run.test.ts +88 -2
- package/src/op/work-lease-run.ts +52 -12
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +71 -0
- package/src/pr-forge.ts +70 -6
- package/src/pr-loop-bundle.test.ts +2 -1
- package/src/pr-loop.test.ts +117 -1
- package/src/pr-loop.ts +252 -10
- package/src/pr-report-url.ts +58 -6
- package/src/terraform/__fixtures__/advise.test.ts +21 -2
- package/src/ts-loader.test.ts +89 -1
- package/src/ts-loader.ts +180 -13
- package/src/workspace/changes.test.ts +1 -1
- package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
- package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
- package/src/workspace/chant-migrations.e2e.test.ts +5 -0
- package/src/workspace/chant-migrations.ts +2 -0
- package/src/workspace/checks/boxes.ts +9 -9
- package/src/workspace/ci-green-forge.ts +6 -4
- package/src/workspace/ci-green-runs.test.ts +53 -11
- package/src/workspace/ci-green.test.ts +1 -1
- package/src/workspace/ci-green.ts +50 -22
- package/src/workspace/ci-list.schema.json +12 -6
- package/src/workspace/ci-report-url.test.ts +220 -0
- package/src/workspace/ci-report-url.ts +33 -0
- package/src/workspace/ci-run-index.schema.json +52 -0
- package/src/workspace/ci-run-index.test.ts +392 -0
- package/src/workspace/ci-run-index.ts +206 -0
- package/src/workspace/ci-run.test.ts +520 -42
- package/src/workspace/ci-run.ts +483 -51
- package/src/workspace/ci-runner-key.test.ts +204 -0
- package/src/workspace/ci-runner-key.ts +275 -0
- package/src/workspace/ci-secrets.test.ts +106 -0
- package/src/workspace/ci-secrets.ts +213 -0
- package/src/workspace/ci-serve.test.ts +282 -39
- package/src/workspace/ci-serve.ts +231 -34
- package/src/workspace/declaration.schema.json +7 -2
- package/src/workspace/declaration.test.ts +23 -0
- package/src/workspace/declaration.ts +19 -3
- package/src/workspace/export.ts +6 -3
- package/src/workspace/ls.schema.json +7 -1
- package/src/workspace/ls.ts +7 -2
- package/src/workspace/pin-integrity.test.ts +5 -0
- package/src/workspace/pin-integrity.ts +3 -2
- package/src/workspace/pr-report.schema.json +74 -7
- package/src/workspace/reason-codes.test.ts +2 -0
- package/src/workspace/reason-codes.ts +9 -4
- package/src/workspace/run-event.schema.json +3 -1
- package/src/workspace/status.schema.json +3 -1
- package/src/workspace/trust/trust-write.test.ts +179 -0
- package/src/workspace/trust/trust-write.ts +377 -0
- package/src/workspace/trust-write.schema.json +265 -0
- package/src/workspace/write-scope.test.ts +29 -1
- package/src/workspace/write-scope.ts +12 -1
package/src/cli/parse-args.ts
CHANGED
|
@@ -66,6 +66,12 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
66
66
|
"--confirm-prod",
|
|
67
67
|
"--once",
|
|
68
68
|
"--rerun-failed",
|
|
69
|
+
"--setup-key",
|
|
70
|
+
"--trust",
|
|
71
|
+
"--unsigned",
|
|
72
|
+
"--in-place",
|
|
73
|
+
"--no-push",
|
|
74
|
+
"--no-require-review",
|
|
69
75
|
"--expire",
|
|
70
76
|
"--check-live",
|
|
71
77
|
"--check-snapshot",
|
|
@@ -388,6 +394,13 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
388
394
|
const url = args[++i];
|
|
389
395
|
if (!url || url.startsWith("-")) throw new Error("--report-url needs a URL: --report-url s3://<bucket>/<prefix> or file://<directory>");
|
|
390
396
|
result.reportUrl = url;
|
|
397
|
+
} else if (arg === "--no-status") {
|
|
398
|
+
// `chant ci run|serve --no-status` (#4097): post no commit status for the job.
|
|
399
|
+
result.noStatus = true;
|
|
400
|
+
} else if (arg === "--status-url") {
|
|
401
|
+
// `chant ci run|serve --status-url <template>` (#4097): the link a job's status carries, in place of an https --report-url.
|
|
402
|
+
result.statusUrl = args[++i];
|
|
403
|
+
if (!result.statusUrl || result.statusUrl.startsWith("-")) throw new Error("--status-url needs a URL template: --status-url https://host/reports/{key}");
|
|
391
404
|
} else if (arg === "--allow-unsigned") {
|
|
392
405
|
// `chant release publish --allow-unsigned` (#4049).
|
|
393
406
|
result.allowUnsigned = true;
|
|
@@ -536,10 +549,19 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
536
549
|
const name = args[++i];
|
|
537
550
|
if (!name || name.startsWith("-")) throw new Error("--pass-env needs a variable's name, such as AWS_PROFILE: --pass-env <NAME>");
|
|
538
551
|
(result.passEnv ??= []).push(name);
|
|
552
|
+
} else if (arg === "--secrets") {
|
|
553
|
+
// `chant ci run|serve --secrets <file>` (#4096): the host's secrets file, readable only by its owner.
|
|
554
|
+
result.ciSecrets = args[++i];
|
|
555
|
+
if (!result.ciSecrets || result.ciSecrets.startsWith("-")) throw new Error("--secrets needs the host's secrets file, a dotenv or JSON file only its owner can read: --secrets <file>");
|
|
539
556
|
} else if (arg === "--repo") {
|
|
540
557
|
// `chant ci serve --repo <owner/repo>` (#4095): the repository whose pull requests and target branch it polls.
|
|
541
558
|
result.ciRepo = args[++i];
|
|
542
559
|
if (!result.ciRepo || !/^[^/\s-][^/\s]*\/[^/\s]+$/.test(result.ciRepo)) throw new Error("--repo needs the repository as owner/name: --repo <owner/repo>");
|
|
560
|
+
} else if (arg === "--run-index-limit") {
|
|
561
|
+
// `chant ci run --run-index-limit <n>` (#4098): how many runs the index at the report URL keeps.
|
|
562
|
+
const n = Number(args[++i]);
|
|
563
|
+
if (!Number.isInteger(n) || n < 1) throw new Error("--run-index-limit needs a whole number of runs, 1 or more: --run-index-limit <n>");
|
|
564
|
+
result.runIndexLimit = n;
|
|
543
565
|
} else if (arg === "--max-jobs") {
|
|
544
566
|
// `chant ci serve --max-jobs <n>` (#4095): how many jobs run at once.
|
|
545
567
|
const n = Number(args[++i]);
|
|
@@ -576,6 +598,10 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
576
598
|
// `chant workspace records review <id> --verdict agree|dissent|abstain` (#2670)
|
|
577
599
|
result.verdict = args[++i];
|
|
578
600
|
if (!result.verdict || result.verdict.startsWith("-")) throw new Error("--verdict needs agree, dissent or abstain");
|
|
601
|
+
} else if (arg === "--class") {
|
|
602
|
+
// `chant workspace trust runner add <principal> --class runner|service` (#3510).
|
|
603
|
+
result.runnerClass = args[++i];
|
|
604
|
+
if (!result.runnerClass || result.runnerClass.startsWith("-")) throw new Error("--class needs runner or service: --class <class>");
|
|
579
605
|
} else if (arg === "--by") {
|
|
580
606
|
// `chant workspace records review <id> --by <principal>` (#2670): whoever the caller says.
|
|
581
607
|
result.by = args[++i];
|
|
@@ -748,6 +774,28 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
748
774
|
}
|
|
749
775
|
} else if (arg === "--once") {
|
|
750
776
|
result.once = true;
|
|
777
|
+
} else if (arg === "--setup-key") {
|
|
778
|
+
// `chant ci serve|run --setup-key` (#4099): make the host's runner key and print its public half.
|
|
779
|
+
result.setupKey = true;
|
|
780
|
+
} else if (arg === "--trust") {
|
|
781
|
+
// `chant ci serve|run --setup-key --trust` (#4099): list the key in .chant/trust.json on a branch.
|
|
782
|
+
result.trustKey = true;
|
|
783
|
+
} else if (arg === "--in-place") {
|
|
784
|
+
// `chant ci run --in-place` (#4156): run the job in this checkout, as a generated forge job does.
|
|
785
|
+
result.ciInPlace = true;
|
|
786
|
+
} else if (arg === "--no-push") {
|
|
787
|
+
// `chant ci run --no-push` (#4156): keep the run record on the local chant/lifecycle.
|
|
788
|
+
result.noPush = true;
|
|
789
|
+
} else if (arg === "--no-require-review") {
|
|
790
|
+
// `chant ci run apply --no-require-review` (#4156): the apply without pr-apply's --require-review.
|
|
791
|
+
result.requireReview = false;
|
|
792
|
+
} else if (arg === "--unsigned") {
|
|
793
|
+
// `chant ci serve --unsigned` (#4099): serve without a runner key listed at the target's head.
|
|
794
|
+
result.unsigned = true;
|
|
795
|
+
} else if (arg === "--principal") {
|
|
796
|
+
// `chant ci serve|run --setup-key --trust --principal <name>` (#4099): the runner's principal.
|
|
797
|
+
result.ciPrincipal = args[++i];
|
|
798
|
+
if (!result.ciPrincipal || result.ciPrincipal.startsWith("-")) throw new Error("--principal needs the runner's principal: --principal <name>");
|
|
751
799
|
} else if (arg === "--work") {
|
|
752
800
|
// `chant run <op> --work <id>` (#2748)
|
|
753
801
|
const value = args[++i];
|
package/src/cli/registry.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface ParsedArgs {
|
|
|
23
23
|
/** `--version` / `-V` (#2701): print the installed chant's version. */
|
|
24
24
|
version?: boolean;
|
|
25
25
|
report?: boolean;
|
|
26
|
-
/** `chant run` — force the local in-process executor (the default). */
|
|
26
|
+
/** `chant run` — force the local in-process executor (the default). `chant ci run --local` (#4161): run the job on this machine with no push, status or report upload. */
|
|
27
27
|
local?: boolean;
|
|
28
28
|
/**
|
|
29
29
|
* `chant run <op> --on <lexicon>` (#2121) — which runtime hosts the run.
|
|
@@ -311,7 +311,8 @@ export interface ParsedArgs {
|
|
|
311
311
|
/**
|
|
312
312
|
* `chant components pr-apply --require-review` (#3183) — count an approval
|
|
313
313
|
* only when its approver has a standing approving review on the pull
|
|
314
|
-
* request. Needs `--forge`.
|
|
314
|
+
* request. Needs `--forge`. `chant ci run apply --no-require-review`
|
|
315
|
+
* (#4156) sets it false, for a pipeline generated with `requireReview: false`.
|
|
315
316
|
*/
|
|
316
317
|
requireReview?: boolean;
|
|
317
318
|
/**
|
|
@@ -462,8 +463,16 @@ export interface ParsedArgs {
|
|
|
462
463
|
commit?: string;
|
|
463
464
|
/** `chant ci run --pass-env <NAME>`, repeatable (#4094): a variable of the runner's environment the job gets, beside the few it always gets. */
|
|
464
465
|
passEnv?: string[];
|
|
466
|
+
/** `chant ci run|serve --secrets <file>` (#4096): the host's secrets file. Default the file `CHANT_CI_SECRETS` names. */
|
|
467
|
+
ciSecrets?: string;
|
|
465
468
|
/** `chant ci workflow --chant <command>` (#3573): the command the tick step runs chant with, in place of the npx pin. Also `chant ci run --chant` (#4094): what runs chant in the job. */
|
|
466
469
|
ciChant?: string;
|
|
470
|
+
/** `chant ci run|serve --no-status` (#4097): post no commit status for the job. `chant components pr-plan|pr-apply --no-status` (#4161): post neither the status nor the note. */
|
|
471
|
+
noStatus?: boolean;
|
|
472
|
+
/** `chant ci run|serve --status-url <template>` (#4097): the link a job's status carries. `{key}` is `<pr>/<commit>[/<member>]`, as `--report-url` keys it. */
|
|
473
|
+
statusUrl?: string;
|
|
474
|
+
/** `chant ci run --run-index-limit <n>` (#4098): how many runs the index at the report URL keeps. Default 200. */
|
|
475
|
+
runIndexLimit?: number;
|
|
467
476
|
/** `chant ci serve --repo <owner/repo>` (#4095): the repository it polls. */
|
|
468
477
|
ciRepo?: string;
|
|
469
478
|
/** `chant ci serve --max-jobs <n>` (#4095): how many jobs run at once. Default 1. */
|
|
@@ -490,6 +499,24 @@ export interface ParsedArgs {
|
|
|
490
499
|
* `records-write.ts`.
|
|
491
500
|
*/
|
|
492
501
|
by?: string;
|
|
502
|
+
/** `chant ci serve|run --setup-key` (#4099): make the host's runner key in the state directory and print its public half. */
|
|
503
|
+
setupKey?: boolean;
|
|
504
|
+
/** `chant ci serve|run --setup-key --trust` (#4099): list the runner key in .chant/trust.json on a branch pushed for review. */
|
|
505
|
+
trustKey?: boolean;
|
|
506
|
+
/**
|
|
507
|
+
* `chant ci serve --unsigned` (#4099): serve without signing. Since #4156
|
|
508
|
+
* that is the default without a trusted key, so it only skips the key, and
|
|
509
|
+
* is ignored when ci.green.signedSince requires signed runs.
|
|
510
|
+
*/
|
|
511
|
+
unsigned?: boolean;
|
|
512
|
+
/** `chant ci run --in-place` (#4156): run the job in this checkout at HEAD, as a generated forge job does. */
|
|
513
|
+
ciInPlace?: boolean;
|
|
514
|
+
/** `chant ci run --no-push` (#4156): write the run record on the local chant/lifecycle and push nothing. */
|
|
515
|
+
noPush?: boolean;
|
|
516
|
+
/** `chant ci serve|run --setup-key --trust --principal <name>` (#4099): the principal the runner key is listed for. */
|
|
517
|
+
ciPrincipal?: string;
|
|
518
|
+
/** `chant workspace trust runner add <principal> --class runner|service` (#3510): the runner entry's class. */
|
|
519
|
+
runnerClass?: string;
|
|
493
520
|
/**
|
|
494
521
|
* `chant workspace records review <id> --sign [<key file>]` (#2687): the key that seals the verdict, or true for git's user.signingkey.
|
|
495
522
|
* On `records new` and `records amend`, the key that seals the record's author (#2688).
|
|
@@ -739,8 +766,12 @@ export interface ResolvedCommand {
|
|
|
739
766
|
* Supports compound commands like "dev generate" where args.command="dev"
|
|
740
767
|
* and args.path="generate". Falls back to simple command matching.
|
|
741
768
|
*/
|
|
742
|
-
/**
|
|
743
|
-
|
|
769
|
+
/**
|
|
770
|
+
* Command aliases. `chant lc …` is sugar for `chant lifecycle …`, and
|
|
771
|
+
* `chant sync` for `chant update`, the command that syncs lexicon types
|
|
772
|
+
* (chant #3988).
|
|
773
|
+
*/
|
|
774
|
+
const COMMAND_ALIASES: Record<string, string> = { lc: "lifecycle", sync: "update" };
|
|
744
775
|
|
|
745
776
|
export function resolveCommand(args: ParsedArgs, registry: CommandDef[]): ResolvedCommand | null {
|
|
746
777
|
const command = COMMAND_ALIASES[args.command] ?? args.command;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { createRequire } from "module";
|
|
17
|
-
import { listRuleFiles } from "../lint/discover";
|
|
17
|
+
import { listRuleFiles, ruleModuleBase } from "../lint/discover";
|
|
18
18
|
import { isPostSynthCheck } from "../lint/post-synth";
|
|
19
19
|
|
|
20
20
|
export interface BarrelEntry {
|
|
@@ -34,7 +34,7 @@ export function scanPostSynthChecks(dir: string, importMetaUrl: string): BarrelE
|
|
|
34
34
|
const require = createRequire(importMetaUrl);
|
|
35
35
|
const entries: BarrelEntry[] = [];
|
|
36
36
|
for (const file of listRuleFiles(dir)) {
|
|
37
|
-
const base = file
|
|
37
|
+
const base = ruleModuleBase(file);
|
|
38
38
|
let mod: Record<string, unknown>;
|
|
39
39
|
try {
|
|
40
40
|
mod = require(`${dir}/${base}`) as Record<string, unknown>;
|
|
@@ -402,7 +402,7 @@ function runExampleHarness(lexiconDir: string, verbose: boolean): RegenFailure |
|
|
|
402
402
|
/**
|
|
403
403
|
* Extract the fresh surface from the generated artifacts in the lexicon directory.
|
|
404
404
|
*/
|
|
405
|
-
function extractFreshSurface(lexiconDir: string): SurfaceSnapshot | null {
|
|
405
|
+
export function extractFreshSurface(lexiconDir: string): SurfaceSnapshot | null {
|
|
406
406
|
const generatedDir = join(lexiconDir, "src", "generated");
|
|
407
407
|
if (!existsSync(generatedDir)) return null;
|
|
408
408
|
|
|
@@ -29,7 +29,7 @@ import { regenLexicon, type RegenResult } from "./lexicon-regen";
|
|
|
29
29
|
import { fetchWithRetry } from "./fetch";
|
|
30
30
|
import { isLexiconPlugin, type UpstreamPin } from "../lexicon";
|
|
31
31
|
import { importLexiconPackage } from "../lexicon-module";
|
|
32
|
-
import {
|
|
32
|
+
import { ensureTypeScriptLoaderFor } from "../ts-loader";
|
|
33
33
|
|
|
34
34
|
// ── Public types ──────────────────────────────────────────────────────
|
|
35
35
|
|
|
@@ -252,7 +252,7 @@ export async function loadUpstreamPins(lexicon: LexiconId, lexiconDir?: string):
|
|
|
252
252
|
const entry = lexiconDir ? lexiconEntry(lexiconDir) : null;
|
|
253
253
|
if (entry) {
|
|
254
254
|
try {
|
|
255
|
-
|
|
255
|
+
ensureTypeScriptLoaderFor(entry);
|
|
256
256
|
const pins = pinsOf((await import(pathToFileURL(entry).href)) as Record<string, unknown>);
|
|
257
257
|
if (pins.length > 0) return pins;
|
|
258
258
|
} catch {
|
|
@@ -26,17 +26,12 @@ describe("post-synth barrel is exported", () => {
|
|
|
26
26
|
test.each(lexiconDirs)("%s exports ./lint/post-synth", (lexicon) => {
|
|
27
27
|
const pkg = JSON.parse(readFileSync(join(lexiconsDir, lexicon, "package.json"), "utf-8"));
|
|
28
28
|
const entry = pkg.exports?.["./lint/post-synth"];
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} else {
|
|
36
|
-
expect(entry?.development).toBe("./src/lint/post-synth/index.ts");
|
|
37
|
-
expect(entry?.types).toBe("./dist/lint/post-synth/index.d.ts");
|
|
38
|
-
expect(entry?.default).toBe("./src/lint/post-synth/index.ts");
|
|
39
|
-
}
|
|
29
|
+
// A conditional export: `types` resolves to the emitted declaration,
|
|
30
|
+
// `development` keeps the repository's own resolution on source, and
|
|
31
|
+
// `default` is the compiled JS the package publishes (#3981).
|
|
32
|
+
expect(entry?.development).toBe("./src/lint/post-synth/index.ts");
|
|
33
|
+
expect(entry?.types).toBe("./dist/lint/post-synth/index.d.ts");
|
|
34
|
+
expect(entry?.default).toBe("./dist/lint/post-synth/index.js");
|
|
40
35
|
});
|
|
41
36
|
});
|
|
42
37
|
|
|
@@ -43,7 +43,9 @@ export interface DeployContext {
|
|
|
43
43
|
* (#3049), keyed by {@link CapabilityPlan.member}. Set by a gated-wave
|
|
44
44
|
* fan-out and by a pull request's apply (#3183). A capability whose `run`
|
|
45
45
|
* finds its own member here applies that plan, the one the approver
|
|
46
|
-
* approved, instead of planning again.
|
|
46
|
+
* approved, instead of planning again. A `run` that finds the target moved
|
|
47
|
+
* since that plan refuses with an error {@link isPlanMovedError} accepts
|
|
48
|
+
* (#3997), and the command that applied it exits 3, as for a moved digest.
|
|
47
49
|
*/
|
|
48
50
|
plans?: Record<string, unknown>;
|
|
49
51
|
/**
|
|
@@ -90,6 +92,27 @@ export interface CapabilityPlan {
|
|
|
90
92
|
* still counts toward the digest, with no entries.
|
|
91
93
|
*/
|
|
92
94
|
changeSet?: ChangeSetPart;
|
|
95
|
+
/**
|
|
96
|
+
* The outputs `run` will expose once it applies this plan (#3999), as far
|
|
97
|
+
* as the plan can tell. A pull request's plan reads a dependent's
|
|
98
|
+
* `stackOutput()` references into this member from here when both are in
|
|
99
|
+
* the change, instead of from the outputs standing now. Absent when the
|
|
100
|
+
* plan cannot say: every output of the step is then known only after it
|
|
101
|
+
* applies.
|
|
102
|
+
*/
|
|
103
|
+
outputs?: PlannedOutputs;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* What a plan says about the outputs its apply will leave (#3999). An
|
|
108
|
+
* output named in neither field is known only after apply, the same as one
|
|
109
|
+
* in `unknown`.
|
|
110
|
+
*/
|
|
111
|
+
export interface PlannedOutputs {
|
|
112
|
+
/** Each output whose value the plan knows, by name: what `run` will expose. */
|
|
113
|
+
known: Record<string, unknown>;
|
|
114
|
+
/** Outputs the plan names whose value is known only after apply, sorted. */
|
|
115
|
+
unknown: string[];
|
|
93
116
|
}
|
|
94
117
|
|
|
95
118
|
/**
|
|
@@ -136,8 +159,11 @@ export interface Capability<In = unknown, Out = unknown> {
|
|
|
136
159
|
/**
|
|
137
160
|
* Optional: the outputs `run` exposed when it last ran, read without
|
|
138
161
|
* changing anything (#3183). A pull request's plan reads them for each
|
|
139
|
-
* component a planned one depends on
|
|
140
|
-
*
|
|
162
|
+
* component a planned one depends on that the change does not plan
|
|
163
|
+
* (#4104), so a `stackOutput()` wiring resolves to the value standing now.
|
|
164
|
+
* A dependency the change plans is read from its plan's
|
|
165
|
+
* {@link CapabilityPlan.outputs} instead (#3999). The shape is `run`'s
|
|
166
|
+
* `outputs` field.
|
|
141
167
|
*/
|
|
142
168
|
outputs?(ctx: DeployContext, input: In): Promise<Record<string, unknown>>;
|
|
143
169
|
/**
|
|
@@ -160,6 +186,28 @@ export type CapabilityInput<C> = C extends Capability<infer In, unknown> ? In :
|
|
|
160
186
|
/** Extract a capability's `Out` type. */
|
|
161
187
|
export type CapabilityOutput<C> = C extends Capability<unknown, infer Out> ? Out : never;
|
|
162
188
|
|
|
189
|
+
/**
|
|
190
|
+
* The exit code of a command whose capability step refused a plan that moved
|
|
191
|
+
* since it was approved: 3, the code `pr-apply` gives a moved digest and a
|
|
192
|
+
* run gives a gate (#3997).
|
|
193
|
+
*/
|
|
194
|
+
export const PLAN_MOVED_EXIT_CODE = 3;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Whether `err` is a `run`'s refusal to apply a plan from
|
|
198
|
+
* `DeployContext.plans` because the target moved since it was planned
|
|
199
|
+
* (#3997). The mark is a `planMoved: true` property, not a class, so a
|
|
200
|
+
* lexicon built against an older core can set it. An `exitCode` of
|
|
201
|
+
* {@link PLAN_MOVED_EXIT_CODE} on the error is the same mark. Any other
|
|
202
|
+
* `exitCode` is ignored: a failed step exits 1, so a capability can't pick
|
|
203
|
+
* the process's exit code.
|
|
204
|
+
*/
|
|
205
|
+
export function isPlanMovedError(err: unknown): boolean {
|
|
206
|
+
if (typeof err !== "object" || err === null) return false;
|
|
207
|
+
const e = err as { planMoved?: unknown; exitCode?: unknown };
|
|
208
|
+
return e.planMoved === true || e.exitCode === PLAN_MOVED_EXIT_CODE;
|
|
209
|
+
}
|
|
210
|
+
|
|
163
211
|
/**
|
|
164
212
|
* Thrown by a stub `run`/`rollback` — the verb is specified and typed but has
|
|
165
213
|
* no cloud implementation yet. Distinguishes "not implemented" from a runtime
|
|
@@ -9,7 +9,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
11
11
|
import { afterAll, describe, expect, test } from "vitest";
|
|
12
|
-
import { changedFilesBetween } from "./changed-files";
|
|
12
|
+
import { changedFilesBetween, EMPTY_TREE_SHA, resolveMergeBase } from "./changed-files";
|
|
13
13
|
|
|
14
14
|
const repo = mkdtempSync(join(tmpdir(), "chant-changed-files-"));
|
|
15
15
|
afterAll(() => rmSync(repo, { recursive: true, force: true }));
|
|
@@ -56,3 +56,18 @@ describe("changedFilesBetween", () => {
|
|
|
56
56
|
]);
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
describe("the empty tree as the base (a root commit, #4004)", () => {
|
|
61
|
+
test("is its own merge base, and every file the head holds counts as changed", async () => {
|
|
62
|
+
expect(await resolveMergeBase(repo, EMPTY_TREE_SHA, "HEAD")).toBe(EMPTY_TREE_SHA);
|
|
63
|
+
expect(await changedFilesBetween(repo, EMPTY_TREE_SHA)).toEqual([
|
|
64
|
+
"apps/web/main.tf",
|
|
65
|
+
"infra/network/roots/vpc/main.tf",
|
|
66
|
+
"shared/modules/base/main.tf",
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("is git's own empty tree", () => {
|
|
71
|
+
expect(git("hash-object", "-t", "tree", "/dev/null")).toBe(EMPTY_TREE_SHA);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -20,13 +20,21 @@ async function git(cwd: string, args: string[]): Promise<string> {
|
|
|
20
20
|
return stdout;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Git's empty tree. A root commit has no parent, so a push of one is measured
|
|
25
|
+
* from this (#4004): every file it holds counts as changed.
|
|
26
|
+
*/
|
|
27
|
+
export const EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
28
|
+
|
|
23
29
|
/**
|
|
24
30
|
* The commit a change is measured from: the merge base of `base` and `head`.
|
|
25
31
|
* A pull request's head that branched before `base` moved is measured from
|
|
26
32
|
* where it branched, so the other changes on `base` are not counted as its
|
|
27
|
-
* own; a merge commit's first parent is its own merge base.
|
|
33
|
+
* own; a merge commit's first parent is its own merge base. The empty tree
|
|
34
|
+
* has no merge base with anything and is returned as it is.
|
|
28
35
|
*/
|
|
29
36
|
export async function resolveMergeBase(cwd: string, base: string, head = "HEAD"): Promise<string> {
|
|
37
|
+
if (base === EMPTY_TREE_SHA) return base;
|
|
30
38
|
try {
|
|
31
39
|
return (await git(cwd, ["merge-base", base, head])).trim();
|
|
32
40
|
} catch (err) {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { describe, expect, it } from "vitest";
|
|
14
|
-
import { CapabilityRegistry, type DeployContext } from "./capability";
|
|
14
|
+
import { CapabilityRegistry, isPlanMovedError, type DeployContext } from "./capability";
|
|
15
15
|
import { stubCapability } from "./verbs/stub";
|
|
16
16
|
import { describeGateMismatch, memoryGateLedgerPort } from "../op/gate";
|
|
17
17
|
import { componentPlanDigest } from "./gate-plan";
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
UnknownDependencyError,
|
|
23
23
|
accumulateComponentOutputs,
|
|
24
24
|
collectComponentOutputs,
|
|
25
|
+
failedRunExitCode,
|
|
25
26
|
resolveComponentGraph,
|
|
26
27
|
resolveWiring,
|
|
27
28
|
runComponentDeploy,
|
|
@@ -1173,3 +1174,37 @@ describe("unresolvedWiringSources (#4084)", () => {
|
|
|
1173
1174
|
).toEqual([]);
|
|
1174
1175
|
});
|
|
1175
1176
|
});
|
|
1177
|
+
|
|
1178
|
+
describe("a step that refuses a moved plan (#3997)", () => {
|
|
1179
|
+
it.each([
|
|
1180
|
+
["planMoved: true", { planMoved: true }, true],
|
|
1181
|
+
["exitCode 3", { exitCode: 3 }, true],
|
|
1182
|
+
["exitCode 7", { exitCode: 7 }, false],
|
|
1183
|
+
['exitCode "3"', { exitCode: "3" }, false],
|
|
1184
|
+
["planMoved: \"yes\"", { planMoved: "yes" }, false],
|
|
1185
|
+
["no mark", {}, false],
|
|
1186
|
+
])("isPlanMovedError reads an error with %s", (_label, mark, moved) => {
|
|
1187
|
+
expect(isPlanMovedError(Object.assign(new Error("refused"), mark))).toBe(moved);
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
it("is not fooled by a non-object", () => {
|
|
1191
|
+
expect(isPlanMovedError(null)).toBe(false);
|
|
1192
|
+
expect(isPlanMovedError("planMoved")).toBe(false);
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
it("marks the failed step's record, and failedRunExitCode answers 3 for it and 1 for any other failure", async () => {
|
|
1196
|
+
const run = async (mark: object) => {
|
|
1197
|
+
const registry = new CapabilityRegistry();
|
|
1198
|
+
registry.register(fakeCapability("apply", { run: () => { throw Object.assign(new Error("refused"), mark); } }).capability);
|
|
1199
|
+
const component: DriverComponent = { name: "c", deploy: [{ phase: "Apply", steps: [{ kind: "apply" }] }] };
|
|
1200
|
+
return runComponentDeploy(component, { env: "dev", component: "c" }, registry, {});
|
|
1201
|
+
};
|
|
1202
|
+
const moved = await run({ exitCode: 3 });
|
|
1203
|
+
expect(moved.records[0]).toMatchObject({ status: "fail", planMoved: true });
|
|
1204
|
+
expect(failedRunExitCode([moved])).toBe(3);
|
|
1205
|
+
const other = await run({ exitCode: 7 });
|
|
1206
|
+
expect(other.records[0]).not.toHaveProperty("planMoved");
|
|
1207
|
+
expect(failedRunExitCode([other])).toBe(1);
|
|
1208
|
+
expect(failedRunExitCode([other, moved])).toBe(3);
|
|
1209
|
+
});
|
|
1210
|
+
});
|
package/src/components/driver.ts
CHANGED
|
@@ -54,6 +54,7 @@ import { GateNotRecordableError } from "../lifecycle/no-record";
|
|
|
54
54
|
import { componentPlanDigest } from "./gate-plan";
|
|
55
55
|
import type { PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
56
56
|
import type { CapabilityRegistry, DeployContext } from "./capability";
|
|
57
|
+
import { isPlanMovedError, PLAN_MOVED_EXIT_CODE } from "./capability";
|
|
57
58
|
import { isPromotedArtifact } from "./auto-release";
|
|
58
59
|
import type { ReleaseIdentity } from "../telemetry-attribution";
|
|
59
60
|
import type { RunProgressEvent } from "./run-progress";
|
|
@@ -154,6 +155,8 @@ export interface DriverStepRecord {
|
|
|
154
155
|
durationMs: number;
|
|
155
156
|
output?: unknown;
|
|
156
157
|
error?: string;
|
|
158
|
+
/** Set on a failed step whose capability refused a plan that moved since it was approved (`isPlanMovedError`, #3997). */
|
|
159
|
+
planMoved?: true;
|
|
157
160
|
/** Set on a `gate` step that passed (#2119): who resolved it, when, and at what address. */
|
|
158
161
|
approval?: {
|
|
159
162
|
gate: string;
|
|
@@ -356,12 +359,51 @@ export function unresolvedWiringSources(
|
|
|
356
359
|
input: Record<string, unknown>,
|
|
357
360
|
componentOutputs: Record<string, Record<string, unknown>>,
|
|
358
361
|
): string[] {
|
|
359
|
-
|
|
362
|
+
return [...new Set(unresolvedWiringReferences(input, componentOutputs).map((r) => r.source))].sort();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** One cross-component reference a step input makes: whose output, and which. */
|
|
366
|
+
export interface WiringReference {
|
|
367
|
+
/** The component or stack whose output it reads. */
|
|
368
|
+
source: string;
|
|
369
|
+
/** `<stack>.<name>` for `stackOutput()`, `<component>.publish.<field>` for an artifact. */
|
|
370
|
+
ref: string;
|
|
371
|
+
/**
|
|
372
|
+
* Whether it reads a published artifact's identity (#3999): an
|
|
373
|
+
* `@<component>.publish.*` reference, or a `stackOutput()` whose name is
|
|
374
|
+
* under `publish`. The driver namespaces every publish-family output there
|
|
375
|
+
* (`collectComponentOutputs`), whatever capability wrote it.
|
|
376
|
+
*/
|
|
377
|
+
publish: boolean;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Every cross-component reference in `input` that has no value in
|
|
382
|
+
* `componentOutputs`, sorted by `ref` and each once (#3999). A pull
|
|
383
|
+
* request's plan names these when the output is known only after its
|
|
384
|
+
* upstream applies.
|
|
385
|
+
*/
|
|
386
|
+
export function unresolvedWiringReferences(
|
|
387
|
+
input: Record<string, unknown>,
|
|
388
|
+
componentOutputs: Record<string, Record<string, unknown>>,
|
|
389
|
+
): WiringReference[] {
|
|
390
|
+
return wiringReferences(input).filter((r) => r.value(componentOutputs) === undefined).map(({ source, ref, publish }) => ({ source, ref, publish }));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Every cross-component reference in `input`, each once, sorted by `ref`,
|
|
395
|
+
* with the value it resolves to against a given outputs map.
|
|
396
|
+
*/
|
|
397
|
+
function wiringReferences(input: Record<string, unknown>): Array<WiringReference & { value: (outputs: Record<string, Record<string, unknown>>) => unknown }> {
|
|
398
|
+
const found = new Map<string, WiringReference & { value: (outputs: Record<string, Record<string, unknown>>) => unknown }>();
|
|
360
399
|
const walk = (value: unknown): void => {
|
|
361
400
|
if (typeof value === "string") {
|
|
362
401
|
if (PRIOR_STEP_REF.test(value)) return;
|
|
363
402
|
const ref = value.match(COMPONENT_ARTIFACT_REF);
|
|
364
|
-
if (ref
|
|
403
|
+
if (ref) {
|
|
404
|
+
const [, source, field] = ref;
|
|
405
|
+
found.set(`${source}.publish.${field}`, { source, ref: `${source}.publish.${field}`, publish: true, value: (o) => resolvePath(o[source]?.publish, field) });
|
|
406
|
+
}
|
|
365
407
|
return;
|
|
366
408
|
}
|
|
367
409
|
if (Array.isArray(value)) {
|
|
@@ -371,14 +413,29 @@ export function unresolvedWiringSources(
|
|
|
371
413
|
if (value && typeof value === "object") {
|
|
372
414
|
if ("stackOutput" in value) {
|
|
373
415
|
const { stack, name } = (value as { stackOutput: { stack: string; name: string } }).stackOutput;
|
|
374
|
-
|
|
416
|
+
found.set(`${stack}.${name}`, { source: stack, ref: `${stack}.${name}`, publish: name === "publish" || name.startsWith("publish."), value: (o) => resolvePath(o[stack], name) });
|
|
375
417
|
return;
|
|
376
418
|
}
|
|
377
419
|
Object.values(value as Record<string, unknown>).forEach(walk);
|
|
378
420
|
}
|
|
379
421
|
};
|
|
380
422
|
walk(input);
|
|
381
|
-
return [...found].sort();
|
|
423
|
+
return [...found.values()].sort((a, b) => a.ref.localeCompare(b.ref));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* The cross-component references in `input` that resolve to another value
|
|
428
|
+
* against `now` than against `read`, sorted (#3999). A pull request's apply
|
|
429
|
+
* holds back a member whose plan read `read` when its upstream applied and
|
|
430
|
+
* left `now`.
|
|
431
|
+
*/
|
|
432
|
+
export function movedWiringReferences(
|
|
433
|
+
input: Record<string, unknown>,
|
|
434
|
+
read: Record<string, Record<string, unknown>>,
|
|
435
|
+
now: Record<string, Record<string, unknown>>,
|
|
436
|
+
): WiringReference[] {
|
|
437
|
+
const same = (a: unknown, b: unknown) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
438
|
+
return wiringReferences(input).filter((r) => !same(r.value(read), r.value(now))).map(({ source, ref, publish }) => ({ source, ref, publish }));
|
|
382
439
|
}
|
|
383
440
|
|
|
384
441
|
// ── Step + phase execution ───────────────────────────────────────────────
|
|
@@ -501,6 +558,7 @@ async function runCapabilityStep(
|
|
|
501
558
|
status: "fail",
|
|
502
559
|
durationMs: Date.now() - start,
|
|
503
560
|
error: errMessage(err),
|
|
561
|
+
...(isPlanMovedError(err) ? { planMoved: true as const } : {}),
|
|
504
562
|
},
|
|
505
563
|
resolvedInput,
|
|
506
564
|
};
|
|
@@ -995,6 +1053,20 @@ function findStackOutputs(
|
|
|
995
1053
|
return found;
|
|
996
1054
|
}
|
|
997
1055
|
|
|
1056
|
+
/** Whether a step in `results` refused a plan that moved since it was approved (#3997). */
|
|
1057
|
+
export function planMovedIn(results: readonly DriverComponentResult[]): boolean {
|
|
1058
|
+
return results.some((r) => r.records.some((s) => s.planMoved === true));
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* The exit code of a run that failed: {@link PLAN_MOVED_EXIT_CODE} when a
|
|
1063
|
+
* step refused a plan that moved since it was approved, 1 otherwise. Every
|
|
1064
|
+
* command that runs capability steps ends a failed run with it.
|
|
1065
|
+
*/
|
|
1066
|
+
export function failedRunExitCode(results: readonly DriverComponentResult[]): number {
|
|
1067
|
+
return planMovedIn(results) ? PLAN_MOVED_EXIT_CODE : 1;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
998
1070
|
function errMessage(err: unknown): string {
|
|
999
1071
|
return err instanceof Error ? err.message : String(err);
|
|
1000
1072
|
}
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
import { existsSync, readFileSync } from "node:fs";
|
|
39
39
|
import { readWaveRecords, writeWaveRecordFile, type WaveRecord } from "../wave-engine";
|
|
40
40
|
import type { ChangeSetDocument } from "../change-set";
|
|
41
|
-
import type { PrMember } from "../pr-loop";
|
|
41
|
+
import type { PrMember, PrWave } from "../pr-loop";
|
|
42
42
|
|
|
43
43
|
/** What a pull request's apply approved, kept so a re-run can finish it (#3464). */
|
|
44
44
|
export interface PrApplyRecord {
|
|
@@ -59,6 +59,11 @@ export interface PrApplyRecord {
|
|
|
59
59
|
* against them again, so an unchanged member plans to the same digest.
|
|
60
60
|
*/
|
|
61
61
|
planOutputs: Record<string, Record<string, unknown>>;
|
|
62
|
+
/**
|
|
63
|
+
* With `--wave-gate` (#4003): each wave reached so far and the approval it
|
|
64
|
+
* applied or waits under, so the next wave's job reports every wave.
|
|
65
|
+
*/
|
|
66
|
+
waves?: PrWave[];
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
export interface FanOutAttempt {
|
|
@@ -100,6 +105,7 @@ function readPrApplyRecord(value: unknown): PrApplyRecord | undefined {
|
|
|
100
105
|
if (isRecord(planOutputs)) {
|
|
101
106
|
for (const [name, v] of Object.entries(planOutputs)) if (isRecord(v)) outputs[name] = v;
|
|
102
107
|
}
|
|
108
|
+
const waves = Array.isArray(value.waves) ? value.waves.map(readPrWave).filter((w): w is PrWave => w !== undefined) : [];
|
|
103
109
|
return {
|
|
104
110
|
op,
|
|
105
111
|
gate,
|
|
@@ -109,6 +115,28 @@ function readPrApplyRecord(value: unknown): PrApplyRecord | undefined {
|
|
|
109
115
|
members: members as unknown as PrMember[],
|
|
110
116
|
changeSet: changeSet as unknown as ChangeSetDocument,
|
|
111
117
|
planOutputs: outputs,
|
|
118
|
+
...(waves.length > 0 ? { waves } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const WAVE_STATUSES: ReadonlySet<string> = new Set(["applied", "failed", "plan-failed", "waiting"]);
|
|
123
|
+
|
|
124
|
+
/** One `prApply.waves` entry, or `undefined` when it is malformed. */
|
|
125
|
+
function readPrWave(value: unknown): PrWave | undefined {
|
|
126
|
+
if (!isRecord(value)) return undefined;
|
|
127
|
+
const { wave, components, status, approval, gate, digest } = value;
|
|
128
|
+
if (typeof wave !== "number" || !isStringArray(components) || typeof status !== "string" || !WAVE_STATUSES.has(status)) return undefined;
|
|
129
|
+
if ((approval !== "pull-request" && approval !== "wave") || typeof gate !== "string" || typeof digest !== "string") return undefined;
|
|
130
|
+
return {
|
|
131
|
+
wave,
|
|
132
|
+
components,
|
|
133
|
+
status: status as PrWave["status"],
|
|
134
|
+
approval,
|
|
135
|
+
gate,
|
|
136
|
+
digest,
|
|
137
|
+
...(isStringArray(value.approvedBy) ? { approvedBy: value.approvedBy } : {}),
|
|
138
|
+
...(typeof value.approved === "string" ? { approved: value.approved } : {}),
|
|
139
|
+
...(isStringArray(value.moved) ? { moved: value.moved } : {}),
|
|
112
140
|
};
|
|
113
141
|
}
|
|
114
142
|
|
|
@@ -209,3 +209,38 @@ describe("one approval over the whole set", () => {
|
|
|
209
209
|
expect(ran.sort()).toEqual(["app-one", "app-three", "app-two", "cluster-b"]);
|
|
210
210
|
});
|
|
211
211
|
});
|
|
212
|
+
|
|
213
|
+
describe("holding a component back (#3999)", () => {
|
|
214
|
+
test("a held component and what is downstream of it do not run and settle waiting; independent branches run", async () => {
|
|
215
|
+
const plan = planFanOut({ components: ESTATE, changed: ["net"] });
|
|
216
|
+
const { registry, ran } = registryWith();
|
|
217
|
+
const events: Array<{ type: string; component?: string; status?: string }> = [];
|
|
218
|
+
const asked: Array<[string, string[]]> = [];
|
|
219
|
+
const result = await runFanOut(plan, ESTATE, registry, {
|
|
220
|
+
...opts(),
|
|
221
|
+
onRunEvent: (e) => events.push(e as never),
|
|
222
|
+
hold: (name, outputs) => {
|
|
223
|
+
asked.push([name, Object.keys(outputs).sort()]);
|
|
224
|
+
return name === "cluster-a" ? "its plan read net.id before that output applied as another value" : undefined;
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
expect(result.status).toBe("ok");
|
|
228
|
+
expect(ran).not.toContain("cluster-a");
|
|
229
|
+
expect(ran).not.toContain("app-one");
|
|
230
|
+
expect(ran).toContain("app-three");
|
|
231
|
+
expect(result.held).toEqual([
|
|
232
|
+
{ component: "cluster-a", reason: "its plan read net.id before that output applied as another value" },
|
|
233
|
+
{ component: "app-one", reason: "its plan read net.id before that output applied as another value", heldBy: "cluster-a" },
|
|
234
|
+
{ component: "app-two", reason: "its plan read net.id before that output applied as another value", heldBy: "cluster-a" },
|
|
235
|
+
].sort((a, b) => plan.order.indexOf(a.component) - plan.order.indexOf(b.component)));
|
|
236
|
+
// Asked once per component as its wave starts, never for one already held.
|
|
237
|
+
expect(asked.map(([name]) => name).sort()).toEqual(["app-three", "cluster-a", "cluster-b", "net"]);
|
|
238
|
+
expect(events.filter((e) => e.type === "component-settle" && e.status === "waiting").map((e) => e.component).sort()).toEqual(["app-one", "app-two", "cluster-a"]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("with no hook, or a hook that holds nothing, the result has no held list", async () => {
|
|
242
|
+
const plan = planFanOut({ components: ESTATE, changed: ["net"] });
|
|
243
|
+
expect((await runFanOut(plan, ESTATE, registryWith().registry, opts())).held).toBeUndefined();
|
|
244
|
+
expect((await runFanOut(plan, ESTATE, registryWith().registry, { ...opts(), hold: () => undefined })).held).toBeUndefined();
|
|
245
|
+
});
|
|
246
|
+
});
|