@intentius/chant 0.124.29 → 0.124.31
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/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +4 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +27 -5
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +64 -0
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +99 -0
- package/dist/cli/registry.d.ts +35 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/pr-pipeline.d.ts +24 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +16 -0
- package/dist/components/pr-run.d.ts +20 -2
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +26 -13
- package/dist/components/wave-plan.d.ts +11 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +26 -0
- package/dist/lifecycle/release-ledger.d.ts +8 -3
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +2 -0
- package/dist/lifecycle/run-ledger.d.ts +1 -1
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +9 -0
- package/dist/op/activities/activity-contracts.d.ts +1 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +1 -0
- package/dist/op/activities/shell.d.ts +6 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +1 -1
- package/dist/op/local-executor.d.ts +35 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +31 -6
- 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/runtime.d.ts +27 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtime.js +20 -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/pr-forge.d.ts +40 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +56 -6
- package/dist/pr-loop.d.ts +10 -0
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +7 -0
- package/dist/pr-report-url.d.ts +8 -0
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/pr-report-url.js +47 -1
- package/dist/run-event-stream.d.ts +2 -2
- package/dist/run-event-stream.d.ts.map +1 -1
- package/dist/run-event-stream.js +2 -2
- package/dist/run-event.d.ts +15 -0
- package/dist/run-event.d.ts.map +1 -1
- package/dist/run-event.js +2 -0
- package/dist/wave-runtime.mjs +171 -171
- package/dist/workspace/ci-green-forge.d.ts +2 -0
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green.d.ts +2 -0
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +4 -2
- package/dist/workspace/ci-run-index.d.ts +96 -0
- package/dist/workspace/ci-run-index.d.ts.map +1 -0
- package/dist/workspace/ci-run-index.js +162 -0
- package/dist/workspace/ci-run-index.schema.json +50 -0
- package/dist/workspace/ci-run.d.ts +299 -0
- package/dist/workspace/ci-run.d.ts.map +1 -0
- package/dist/workspace/ci-run.js +691 -0
- package/dist/workspace/ci-runner-key.d.ts +120 -0
- package/dist/workspace/ci-runner-key.d.ts.map +1 -0
- package/dist/workspace/ci-runner-key.js +217 -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 +227 -0
- package/dist/workspace/ci-serve.d.ts.map +1 -0
- package/dist/workspace/ci-serve.js +468 -0
- package/dist/workspace/pr-report.schema.json +5 -0
- package/dist/workspace/reason-codes.d.ts +6 -2
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +7 -2
- package/dist/workspace/run-event.schema.json +14 -3
- package/dist/workspace/status-stewards.d.ts +2 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status-stewards.js +1 -0
- package/dist/workspace/status.schema.json +12 -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/package.json +1 -1
- package/src/cli/handlers/build.test.ts +15 -0
- package/src/cli/handlers/build.ts +4 -1
- package/src/cli/handlers/fan-out.test.ts +82 -0
- package/src/cli/handlers/fan-out.ts +34 -5
- package/src/cli/handlers/pr.ts +5 -0
- package/src/cli/main.test.ts +1 -0
- package/src/cli/main.ts +64 -0
- package/src/cli/parse-args.ts +71 -0
- package/src/cli/registry.ts +35 -1
- package/src/components/pr-pipeline.ts +32 -0
- package/src/components/pr-run.test.ts +127 -0
- package/src/components/pr-run.ts +32 -12
- package/src/components/wave-plan.ts +30 -0
- package/src/lifecycle/release-ledger.ts +9 -3
- package/src/lifecycle/run-ledger.ts +7 -1
- package/src/op/activities/activity-contracts.ts +1 -0
- package/src/op/activities/shell.ts +7 -1
- package/src/op/local-executor.ts +58 -7
- package/src/op/redact.ts +93 -0
- package/src/op/runtime.ts +42 -0
- package/src/op/step-console.ts +7 -2
- package/src/pr-forge.test.ts +54 -0
- package/src/pr-forge.ts +92 -6
- package/src/pr-loop.test.ts +13 -1
- package/src/pr-loop.ts +16 -0
- package/src/pr-report-url.ts +53 -1
- package/src/run-event-stream.ts +4 -3
- package/src/run-event.ts +17 -0
- package/src/workspace/ci-green-forge.ts +2 -0
- package/src/workspace/ci-green.ts +6 -2
- package/src/workspace/ci-run-index.schema.json +50 -0
- package/src/workspace/ci-run-index.test.ts +353 -0
- package/src/workspace/ci-run-index.ts +192 -0
- package/src/workspace/ci-run.test.ts +716 -0
- package/src/workspace/ci-run.ts +833 -0
- package/src/workspace/ci-runner-key.test.ts +166 -0
- package/src/workspace/ci-runner-key.ts +250 -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 +367 -0
- package/src/workspace/ci-serve.ts +574 -0
- package/src/workspace/pr-report.schema.json +5 -0
- package/src/workspace/reason-codes.test.ts +2 -0
- package/src/workspace/reason-codes.ts +7 -2
- package/src/workspace/run-event.schema.json +14 -3
- package/src/workspace/status-stewards.ts +3 -0
- package/src/workspace/status.schema.json +12 -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/cli/parse-args.ts
CHANGED
|
@@ -42,6 +42,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
42
42
|
"--composites",
|
|
43
43
|
"--stacks",
|
|
44
44
|
"--components",
|
|
45
|
+
"--pr-build",
|
|
45
46
|
"--up",
|
|
46
47
|
"--down",
|
|
47
48
|
"--include-dependents",
|
|
@@ -64,6 +65,10 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
64
65
|
"--yes",
|
|
65
66
|
"--confirm-prod",
|
|
66
67
|
"--once",
|
|
68
|
+
"--rerun-failed",
|
|
69
|
+
"--setup-key",
|
|
70
|
+
"--trust",
|
|
71
|
+
"--unsigned",
|
|
67
72
|
"--expire",
|
|
68
73
|
"--check-live",
|
|
69
74
|
"--check-snapshot",
|
|
@@ -386,6 +391,13 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
386
391
|
const url = args[++i];
|
|
387
392
|
if (!url || url.startsWith("-")) throw new Error("--report-url needs a URL: --report-url s3://<bucket>/<prefix> or file://<directory>");
|
|
388
393
|
result.reportUrl = url;
|
|
394
|
+
} else if (arg === "--no-status") {
|
|
395
|
+
// `chant ci run|serve --no-status` (#4097): post no commit status for the job.
|
|
396
|
+
result.noStatus = true;
|
|
397
|
+
} else if (arg === "--status-url") {
|
|
398
|
+
// `chant ci run|serve --status-url <template>` (#4097): the link a job's status carries, in place of an https --report-url.
|
|
399
|
+
result.statusUrl = args[++i];
|
|
400
|
+
if (!result.statusUrl || result.statusUrl.startsWith("-")) throw new Error("--status-url needs a URL template: --status-url https://host/reports/{key}");
|
|
389
401
|
} else if (arg === "--allow-unsigned") {
|
|
390
402
|
// `chant release publish --allow-unsigned` (#4049).
|
|
391
403
|
result.allowUnsigned = true;
|
|
@@ -394,6 +406,13 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
394
406
|
result.requireProvenance = true;
|
|
395
407
|
} else if (arg === "--pr-loop") {
|
|
396
408
|
result.prLoop = true;
|
|
409
|
+
} else if (arg === "--pr-build") {
|
|
410
|
+
// `chant build --pr-loop --pr-build` (#4088), and `chant ci run --pr-build` (#4094).
|
|
411
|
+
result.prBuild = true;
|
|
412
|
+
} else if (arg === "--pr-build-command") {
|
|
413
|
+
const line = args[++i];
|
|
414
|
+
if (line === undefined || line === "") throw new Error("--pr-build-command needs a shell line: --pr-build-command '<line>'");
|
|
415
|
+
result.prBuildCommand = line;
|
|
397
416
|
} else if (arg === "--before-script") {
|
|
398
417
|
const line = args[++i];
|
|
399
418
|
if (line === undefined || line === "") throw new Error("--before-script needs a shell line: --before-script '<line>'");
|
|
@@ -518,6 +537,41 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
518
537
|
// `chant ci workflow --chant <command>` (#3573): what the tick step runs chant with.
|
|
519
538
|
result.ciChant = args[++i];
|
|
520
539
|
if (!result.ciChant || result.ciChant.startsWith("-")) throw new Error("--chant needs the command that runs chant, such as \"npx tsx packages/core/src/cli/main.ts\": --chant <command>");
|
|
540
|
+
} else if (arg === "--commit") {
|
|
541
|
+
// `chant ci run plan|apply --commit <sha>` (#4094): the commit the job runs at.
|
|
542
|
+
result.commit = args[++i];
|
|
543
|
+
if (!result.commit || result.commit.startsWith("-")) throw new Error("--commit needs the commit the job runs at: --commit <sha>");
|
|
544
|
+
} else if (arg === "--pass-env") {
|
|
545
|
+
// `chant ci run --pass-env <NAME>`, repeatable (#4094): a variable of the runner's environment the job gets.
|
|
546
|
+
const name = args[++i];
|
|
547
|
+
if (!name || name.startsWith("-")) throw new Error("--pass-env needs a variable's name, such as AWS_PROFILE: --pass-env <NAME>");
|
|
548
|
+
(result.passEnv ??= []).push(name);
|
|
549
|
+
} else if (arg === "--secrets") {
|
|
550
|
+
// `chant ci run|serve --secrets <file>` (#4096): the host's secrets file, readable only by its owner.
|
|
551
|
+
result.ciSecrets = args[++i];
|
|
552
|
+
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>");
|
|
553
|
+
} else if (arg === "--repo") {
|
|
554
|
+
// `chant ci serve --repo <owner/repo>` (#4095): the repository whose pull requests and target branch it polls.
|
|
555
|
+
result.ciRepo = args[++i];
|
|
556
|
+
if (!result.ciRepo || !/^[^/\s-][^/\s]*\/[^/\s]+$/.test(result.ciRepo)) throw new Error("--repo needs the repository as owner/name: --repo <owner/repo>");
|
|
557
|
+
} else if (arg === "--run-index-limit") {
|
|
558
|
+
// `chant ci run --run-index-limit <n>` (#4098): how many runs the index at the report URL keeps.
|
|
559
|
+
const n = Number(args[++i]);
|
|
560
|
+
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>");
|
|
561
|
+
result.runIndexLimit = n;
|
|
562
|
+
} else if (arg === "--max-jobs") {
|
|
563
|
+
// `chant ci serve --max-jobs <n>` (#4095): how many jobs run at once.
|
|
564
|
+
const n = Number(args[++i]);
|
|
565
|
+
if (!Number.isInteger(n) || n < 1) throw new Error("--max-jobs needs a whole number of jobs, 1 or more: --max-jobs <n>");
|
|
566
|
+
result.maxJobs = n;
|
|
567
|
+
} else if (arg === "--author") {
|
|
568
|
+
// `chant ci serve --author <login>`, repeatable (#4095): plan only pull requests these people opened.
|
|
569
|
+
const login = args[++i];
|
|
570
|
+
if (!login || login.startsWith("-")) throw new Error("--author needs a forge login: --author <login>");
|
|
571
|
+
(result.ciAuthors ??= []).push(login);
|
|
572
|
+
} else if (arg === "--rerun-failed") {
|
|
573
|
+
// `chant ci serve --rerun-failed` (#4095): run again the failed apply at the target branch's head, and failed plans of open heads.
|
|
574
|
+
result.rerunFailed = true;
|
|
521
575
|
} else if (arg === "--install") {
|
|
522
576
|
// `chant ci workflow --install <command>` (#3573): a step before the tick that installs what --chant runs.
|
|
523
577
|
result.ciInstall = args[++i];
|
|
@@ -541,6 +595,10 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
541
595
|
// `chant workspace records review <id> --verdict agree|dissent|abstain` (#2670)
|
|
542
596
|
result.verdict = args[++i];
|
|
543
597
|
if (!result.verdict || result.verdict.startsWith("-")) throw new Error("--verdict needs agree, dissent or abstain");
|
|
598
|
+
} else if (arg === "--class") {
|
|
599
|
+
// `chant workspace trust runner add <principal> --class runner|service` (#3510).
|
|
600
|
+
result.runnerClass = args[++i];
|
|
601
|
+
if (!result.runnerClass || result.runnerClass.startsWith("-")) throw new Error("--class needs runner or service: --class <class>");
|
|
544
602
|
} else if (arg === "--by") {
|
|
545
603
|
// `chant workspace records review <id> --by <principal>` (#2670): whoever the caller says.
|
|
546
604
|
result.by = args[++i];
|
|
@@ -713,6 +771,19 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
713
771
|
}
|
|
714
772
|
} else if (arg === "--once") {
|
|
715
773
|
result.once = true;
|
|
774
|
+
} else if (arg === "--setup-key") {
|
|
775
|
+
// `chant ci serve|run --setup-key` (#4099): make the host's runner key and print its public half.
|
|
776
|
+
result.setupKey = true;
|
|
777
|
+
} else if (arg === "--trust") {
|
|
778
|
+
// `chant ci serve|run --setup-key --trust` (#4099): list the key in .chant/trust.json on a branch.
|
|
779
|
+
result.trustKey = true;
|
|
780
|
+
} else if (arg === "--unsigned") {
|
|
781
|
+
// `chant ci serve --unsigned` (#4099): serve without a runner key listed at the target's head.
|
|
782
|
+
result.unsigned = true;
|
|
783
|
+
} else if (arg === "--principal") {
|
|
784
|
+
// `chant ci serve|run --setup-key --trust --principal <name>` (#4099): the runner's principal.
|
|
785
|
+
result.ciPrincipal = args[++i];
|
|
786
|
+
if (!result.ciPrincipal || result.ciPrincipal.startsWith("-")) throw new Error("--principal needs the runner's principal: --principal <name>");
|
|
716
787
|
} else if (arg === "--work") {
|
|
717
788
|
// `chant run <op> --work <id>` (#2748)
|
|
718
789
|
const value = args[++i];
|
package/src/cli/registry.ts
CHANGED
|
@@ -326,6 +326,10 @@ export interface ParsedArgs {
|
|
|
326
326
|
* merge bound to the approved digest.
|
|
327
327
|
*/
|
|
328
328
|
prLoop?: boolean;
|
|
329
|
+
/** `chant build --pr-loop --pr-build` (#4088) — build and lint before pr-plan and pr-apply. */
|
|
330
|
+
prBuild?: boolean;
|
|
331
|
+
/** `chant build --pr-loop --pr-build-command <line>` (#4088) — the build line to run in place of `chant build --env <env>`. */
|
|
332
|
+
prBuildCommand?: string;
|
|
329
333
|
/**
|
|
330
334
|
* `chant components fan-out --resume <file>` (#2420) — the attempt record
|
|
331
335
|
* this fan-out reads before it starts and writes when it finishes. Repeating
|
|
@@ -454,8 +458,28 @@ export interface ParsedArgs {
|
|
|
454
458
|
branch?: string;
|
|
455
459
|
/** `chant ci workflow --workflow <name>`, repeatable (#3573): a workflow whose completion triggers a tick, beside those the scan finds. */
|
|
456
460
|
ciWorkflows?: string[];
|
|
457
|
-
/** `chant ci
|
|
461
|
+
/** `chant ci run plan|apply --commit <sha>` (#4094): the commit the job runs at. Default `HEAD`. */
|
|
462
|
+
commit?: string;
|
|
463
|
+
/** `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
|
+
passEnv?: string[];
|
|
465
|
+
/** `chant ci run|serve --secrets <file>` (#4096): the host's secrets file. Default the file `CHANT_CI_SECRETS` names. */
|
|
466
|
+
ciSecrets?: string;
|
|
467
|
+
/** `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. */
|
|
458
468
|
ciChant?: string;
|
|
469
|
+
/** `chant ci run|serve --no-status` (#4097): post no commit status for the job. */
|
|
470
|
+
noStatus?: boolean;
|
|
471
|
+
/** `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. */
|
|
472
|
+
statusUrl?: string;
|
|
473
|
+
/** `chant ci run --run-index-limit <n>` (#4098): how many runs the index at the report URL keeps. Default 200. */
|
|
474
|
+
runIndexLimit?: number;
|
|
475
|
+
/** `chant ci serve --repo <owner/repo>` (#4095): the repository it polls. */
|
|
476
|
+
ciRepo?: string;
|
|
477
|
+
/** `chant ci serve --max-jobs <n>` (#4095): how many jobs run at once. Default 1. */
|
|
478
|
+
maxJobs?: number;
|
|
479
|
+
/** `chant ci serve --author <login>`, repeatable (#4095): plan only pull requests these logins opened. */
|
|
480
|
+
ciAuthors?: string[];
|
|
481
|
+
/** `chant ci serve --rerun-failed` (#4095): run again the failed apply at the target's head and failed plans of open heads. */
|
|
482
|
+
rerunFailed?: boolean;
|
|
459
483
|
/** `chant ci workflow --install <command>` (#3573): a step before the tick that installs what `--chant` runs. */
|
|
460
484
|
ciInstall?: string;
|
|
461
485
|
/** `chant ci workflow --token-secret <NAME>` (#3573): the repository secret the tick's tag push authenticates with. */
|
|
@@ -474,6 +498,16 @@ export interface ParsedArgs {
|
|
|
474
498
|
* `records-write.ts`.
|
|
475
499
|
*/
|
|
476
500
|
by?: string;
|
|
501
|
+
/** `chant ci serve|run --setup-key` (#4099): make the host's runner key in the state directory and print its public half. */
|
|
502
|
+
setupKey?: boolean;
|
|
503
|
+
/** `chant ci serve|run --setup-key --trust` (#4099): list the runner key in .chant/trust.json on a branch pushed for review. */
|
|
504
|
+
trustKey?: boolean;
|
|
505
|
+
/** `chant ci serve --unsigned` (#4099): start without a runner key listed in .chant/trust.json at the target's head. */
|
|
506
|
+
unsigned?: boolean;
|
|
507
|
+
/** `chant ci serve|run --setup-key --trust --principal <name>` (#4099): the principal the runner key is listed for. */
|
|
508
|
+
ciPrincipal?: string;
|
|
509
|
+
/** `chant workspace trust runner add <principal> --class runner|service` (#3510): the runner entry's class. */
|
|
510
|
+
runnerClass?: string;
|
|
477
511
|
/**
|
|
478
512
|
* `chant workspace records review <id> --sign [<key file>]` (#2687): the key that seals the verdict, or true for git's user.signingkey.
|
|
479
513
|
* On `records new` and `records amend`, the key that seals the record's author (#2688).
|
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
* the change set and the events where a reader outside CI can fetch them;
|
|
37
37
|
* the job's own environment brings the credentials an `s3://` URL needs.
|
|
38
38
|
*
|
|
39
|
+
* With `build` (#4088) both jobs run the project's build and `chant lint`
|
|
40
|
+
* before their command, and fail on an error. A `cfn-deploy` member's plan
|
|
41
|
+
* and apply read the built template, so a project with one needs it. The
|
|
42
|
+
* build is `chant build --env <env>`, or `buildCommand` when the templates
|
|
43
|
+
* come from several builds (`npm run build`).
|
|
44
|
+
*
|
|
39
45
|
* In a workspace member (#3465) both commands run in the member's directory
|
|
40
46
|
* and pass `--member <name>`, so the member's gate (`pr-<number>-<member>`),
|
|
41
47
|
* note and statuses stay apart from every other member's on the same pull
|
|
@@ -105,6 +111,20 @@ export interface PrLoopPipelineOptions {
|
|
|
105
111
|
* such as `$CHANT_REPORT_URL`.
|
|
106
112
|
*/
|
|
107
113
|
reportUrl?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Build and lint before `pr-plan` and before `pr-apply` (#4088): the build
|
|
116
|
+
* line, then `chant lint`, each failing the job on an error. A `cfn-deploy`
|
|
117
|
+
* member reads the built template, so a project with one sets this. The
|
|
118
|
+
* build line is `chant build --env <env>` unless `buildCommand` names
|
|
119
|
+
* another. Default false.
|
|
120
|
+
*/
|
|
121
|
+
build?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* The shell line that builds the project in place of `chant build --env
|
|
124
|
+
* <env>`, for a project whose templates come from several builds, such as
|
|
125
|
+
* `npm run build`. Setting it turns `build` on.
|
|
126
|
+
*/
|
|
127
|
+
buildCommand?: string;
|
|
108
128
|
/**
|
|
109
129
|
* Which GitHub-shaped forge the workflow is for. Set by the forgejo
|
|
110
130
|
* generator, which reuses the github one; a caller never needs to.
|
|
@@ -126,6 +146,14 @@ export interface PrLoopJob {
|
|
|
126
146
|
env: Record<string, string>;
|
|
127
147
|
/** Shell lines to run before the command, to settle values the forge may leave unusable. */
|
|
128
148
|
setup?: string[];
|
|
149
|
+
/** With `build` (#4088): the build line and the lint line, run in this order before the command. */
|
|
150
|
+
build?: string[];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The lines `build` (#4088) runs before a job's command: the build, then the lint. Empty without the option. */
|
|
154
|
+
export function prLoopBuildLines(env: string, options: PrLoopPipelineOptions = {}): string[] {
|
|
155
|
+
if (!options.build && !options.buildCommand) return [];
|
|
156
|
+
return [options.buildCommand ?? `chant build --env ${env}`, "chant lint"];
|
|
129
157
|
}
|
|
130
158
|
|
|
131
159
|
/**
|
|
@@ -142,6 +170,8 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
|
|
|
142
170
|
...(options.reportUrl ? ["--report-url", JSON.stringify(options.reportUrl)] : []),
|
|
143
171
|
];
|
|
144
172
|
const github = forge !== "gitlab";
|
|
173
|
+
const buildLines = prLoopBuildLines(env, options);
|
|
174
|
+
const build = buildLines.length > 0 ? { build: buildLines } : {};
|
|
145
175
|
const jobEnv: Record<string, string> = {
|
|
146
176
|
...(github ? { GITHUB_TOKEN: GITHUB_JOB_TOKEN } : {}),
|
|
147
177
|
...(options.events ? { CHANT_RUN_EVENTS_DIR: PR_LOOP_EVENTS_DIR } : {}),
|
|
@@ -153,6 +183,7 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
|
|
|
153
183
|
env: github
|
|
154
184
|
? { BASE_SHA: "${{ github.event.pull_request.base.sha }}", PR_NUMBER: "${{ github.event.pull_request.number }}", ...jobEnv }
|
|
155
185
|
: { BASE_SHA: "$CI_MERGE_REQUEST_DIFF_BASE_SHA", PR_NUMBER: "$CI_MERGE_REQUEST_IID", ...jobEnv },
|
|
186
|
+
...build,
|
|
156
187
|
},
|
|
157
188
|
{
|
|
158
189
|
jobName: "apply",
|
|
@@ -163,6 +194,7 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
|
|
|
163
194
|
].join(" "),
|
|
164
195
|
env: github ? { BASE_SHA: "${{ github.event.before }}", ...jobEnv } : { BASE_SHA: "$CI_COMMIT_BEFORE_SHA", ...jobEnv },
|
|
165
196
|
...(github ? {} : { setup: [gitlabBaseFallback(options.branch)] }),
|
|
197
|
+
...build,
|
|
166
198
|
},
|
|
167
199
|
];
|
|
168
200
|
}
|
|
@@ -60,6 +60,8 @@ function world() {
|
|
|
60
60
|
planned: [] as Array<{ root: string; vars?: Record<string, unknown> }>,
|
|
61
61
|
outputsRead: [] as string[],
|
|
62
62
|
applied: [] as string[],
|
|
63
|
+
/** The vars each root applied with, after wiring resolved. */
|
|
64
|
+
ranWith: {} as Record<string, Record<string, unknown> | undefined>,
|
|
63
65
|
fromPlan: {} as Record<string, boolean>,
|
|
64
66
|
failing: new Set<string>(),
|
|
65
67
|
planFailing: new Set<string>(),
|
|
@@ -100,6 +102,7 @@ function world() {
|
|
|
100
102
|
},
|
|
101
103
|
async run(ctx: DeployContext, input: RootInput) {
|
|
102
104
|
w.fromPlan[input.root] = ctx.plans?.[input.root] !== undefined;
|
|
105
|
+
w.ranWith[input.root] = input.vars;
|
|
103
106
|
if (w.failing.has(input.root)) throw new Error(`${input.root} failed`);
|
|
104
107
|
w.applied.push(input.root);
|
|
105
108
|
if (input.root === "a") w.state.a = { id: `a-${w.versions.a ?? "v1"}` };
|
|
@@ -166,6 +169,130 @@ describe("planPrSet", () => {
|
|
|
166
169
|
});
|
|
167
170
|
});
|
|
168
171
|
|
|
172
|
+
describe("a dependency outside the selection (#4104)", () => {
|
|
173
|
+
/** Two members: `up`, and `down` reading its `id`. The pull request changes `down` alone. */
|
|
174
|
+
const PAIR: DriverComponent[] = [root("up"), root("down", ["up"], { upstream: { stackOutput: { stack: "up", name: "id" } } })];
|
|
175
|
+
|
|
176
|
+
function pairWorld() {
|
|
177
|
+
const w = world();
|
|
178
|
+
w.state = { up: { id: "up-live", arn: "arn:up" } };
|
|
179
|
+
w.versions.down = "v2";
|
|
180
|
+
return w;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
test("the dependent's plan resolves the upstream's outputs from its deployed outputs, and the report lists them", async () => {
|
|
184
|
+
const w = pairWorld();
|
|
185
|
+
const plan = planFanOut({ components: PAIR, changed: ["down"] });
|
|
186
|
+
expect(plan.order).toEqual(["down"]);
|
|
187
|
+
expect(plan.seeds).toEqual(["up"]);
|
|
188
|
+
|
|
189
|
+
const set = await planPrSet({ components: PAIR, plan, registry: w.registry, env: "prod" });
|
|
190
|
+
expect(w.outputsRead).toEqual(["up"]);
|
|
191
|
+
expect(w.planned).toEqual([{ root: "down", vars: { upstream: "up-live" } }]);
|
|
192
|
+
expect(set.outputs).toEqual({ up: { id: "up-live", arn: "arn:up" } });
|
|
193
|
+
expect(set.deployedOutputs).toEqual({ up: ["arn", "id"] });
|
|
194
|
+
|
|
195
|
+
const report = prReport({
|
|
196
|
+
stage: "plan",
|
|
197
|
+
pr: 12,
|
|
198
|
+
env: "prod",
|
|
199
|
+
base: "1".repeat(40),
|
|
200
|
+
head: "2".repeat(40),
|
|
201
|
+
op: "pr-12",
|
|
202
|
+
gate: "pr-apply",
|
|
203
|
+
plan,
|
|
204
|
+
signal: { changed: ["down"], indeterminate: [], unclaimed: [] },
|
|
205
|
+
set,
|
|
206
|
+
approval: { status: "pending" },
|
|
207
|
+
status: "planned",
|
|
208
|
+
});
|
|
209
|
+
expect(report.deployedOutputs).toEqual({ up: ["arn", "id"] });
|
|
210
|
+
expect(report.selection).toMatchObject({ changed: ["down"], dependents: [] });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("the apply's plan reads the same outputs, gets the approved digest, and applies against them", async () => {
|
|
214
|
+
const w = pairWorld();
|
|
215
|
+
const plan = planFanOut({ components: PAIR, changed: ["down"] });
|
|
216
|
+
const approved = await planPrSet({ components: PAIR, plan, registry: w.registry, env: "prod" });
|
|
217
|
+
|
|
218
|
+
// pr-apply plans the members again before anything applies.
|
|
219
|
+
w.outputsRead.length = 0;
|
|
220
|
+
const set = await planPrSet({ components: PAIR, plan, registry: w.registry, env: "prod" });
|
|
221
|
+
expect(w.outputsRead).toEqual(["up"]);
|
|
222
|
+
expect(set.doc.digest).toBe(approved.doc.digest);
|
|
223
|
+
const check = await decidePrGate(memoryGateLedgerPort({ resolutions: [approval(approved.doc.digest)] }), {
|
|
224
|
+
op: "pr-12",
|
|
225
|
+
gate: "pr-apply",
|
|
226
|
+
digest: set.doc.digest,
|
|
227
|
+
now: NOW,
|
|
228
|
+
});
|
|
229
|
+
expect(check.satisfied).toBe(true);
|
|
230
|
+
|
|
231
|
+
const { members, status } = await applyPrSet({ components: PAIR, plan, registry: w.registry, env: "prod", set });
|
|
232
|
+
expect(status).toBe("applied");
|
|
233
|
+
expect(w.applied).toEqual(["down"]);
|
|
234
|
+
expect(w.ranWith.down).toEqual({ upstream: "up-live" });
|
|
235
|
+
expect(members.map((m) => [m.member, m.status, m.inputsMoved])).toEqual([["down", "applied", undefined]]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("an upstream whose deployed output moved after approval moves the apply's digest", async () => {
|
|
239
|
+
const w = pairWorld();
|
|
240
|
+
const plan = planFanOut({ components: PAIR, changed: ["down"] });
|
|
241
|
+
const approved = await planPrSet({ components: PAIR, plan, registry: w.registry, env: "prod" });
|
|
242
|
+
w.state.up = { id: "up-moved", arn: "arn:up" };
|
|
243
|
+
const set = await planPrSet({ components: PAIR, plan, registry: w.registry, env: "prod" });
|
|
244
|
+
expect(set.doc.digest).not.toBe(approved.doc.digest);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("--seed-outputs wins: a seeded upstream is not read and not listed", async () => {
|
|
248
|
+
const w = pairWorld();
|
|
249
|
+
const plan = planFanOut({ components: PAIR, changed: ["down"] });
|
|
250
|
+
const set = await planPrSet({ components: PAIR, plan, registry: w.registry, env: "prod", seededOutputs: { up: { id: "up-seeded" } } });
|
|
251
|
+
expect(w.outputsRead).toEqual([]);
|
|
252
|
+
expect(w.planned).toEqual([{ root: "down", vars: { upstream: "up-seeded" } }]);
|
|
253
|
+
expect(set.deployedOutputs).toBeUndefined();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("an upstream whose steps cannot say leaves the reference unresolved", async () => {
|
|
257
|
+
const w = pairWorld();
|
|
258
|
+
const silent: DriverComponent[] = [
|
|
259
|
+
{ name: "up", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "no-outputs" }] }] },
|
|
260
|
+
PAIR[1]!,
|
|
261
|
+
];
|
|
262
|
+
w.registry.register({ kind: "no-outputs", async run() { return {}; } } as never);
|
|
263
|
+
const plan = planFanOut({ components: silent, changed: ["down"] });
|
|
264
|
+
const set = await planPrSet({ components: silent, plan, registry: w.registry, env: "prod" });
|
|
265
|
+
expect(w.outputsRead).toEqual([]);
|
|
266
|
+
expect(set.outputs).toEqual({});
|
|
267
|
+
expect(set.deployedOutputs).toBeUndefined();
|
|
268
|
+
expect(w.planned).toHaveLength(1);
|
|
269
|
+
expect(w.planned[0]!.vars?.upstream).not.toBe("up-live");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("the selection in place of the whole graph is refused, naming the dependency", async () => {
|
|
273
|
+
const w = pairWorld();
|
|
274
|
+
const plan = planFanOut({ components: PAIR, changed: ["down"] });
|
|
275
|
+
await expect(planPrSet({ components: [PAIR[1]!], plan, registry: w.registry, env: "prod" })).rejects.toThrow(/up, a dependency of a planned member/);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("a resume plans against the record's outputs and reads no dependency they cover", async () => {
|
|
279
|
+
const w = world();
|
|
280
|
+
w.versions.a = "v2";
|
|
281
|
+
const plan = planFanOut({ components: ESTATE, changed: ["a"] });
|
|
282
|
+
const { set, first, record } = await firstAttempt(w, plan, ["app"]);
|
|
283
|
+
expect(first.run.completed).toEqual(["a"]);
|
|
284
|
+
expect(set.deployedOutputs).toEqual({ a: ["id"], net: ["cidr"] });
|
|
285
|
+
|
|
286
|
+
w.outputsRead.length = 0;
|
|
287
|
+
w.state.net = { cidr: "10.9.0.0/16" };
|
|
288
|
+
const resumed = await resumePrSet({ components: ESTATE, plan, registry: w.registry, env: "prod", record, completed: ["a"] });
|
|
289
|
+
if (!resumed.ok) throw new Error(resumed.reasons.join("; "));
|
|
290
|
+
expect(w.outputsRead).toEqual([]);
|
|
291
|
+
expect(resumed.set.doc.digest).toBe(set.doc.digest);
|
|
292
|
+
expect(resumed.set.outputs).toBe(record.planOutputs);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
169
296
|
describe("the gate", () => {
|
|
170
297
|
test("pending with no approval, approved for the digest, changed for another one", async () => {
|
|
171
298
|
const w = world();
|
package/src/components/pr-run.ts
CHANGED
|
@@ -19,6 +19,16 @@
|
|
|
19
19
|
* its dependents in one rollout is what gated waves are for (#3049): each
|
|
20
20
|
* wave is planned after the one before it applied.
|
|
21
21
|
*
|
|
22
|
+
* A dependency the change did not select is read the same way (#4104), so
|
|
23
|
+
* a change to one cluster plans it against the network it sits on. That is
|
|
24
|
+
* why `components` is every component in the project and not the selection.
|
|
25
|
+
* `--seed-outputs` wins over a read, and a dependency whose steps cannot say
|
|
26
|
+
* leaves its dependents' references unresolved. The apply stage's plan reads
|
|
27
|
+
* the same dependencies, so on the same estate it gets the digest the plan
|
|
28
|
+
* stage showed, and it applies against the outputs its plans read
|
|
29
|
+
* (`set.outputs`). A resume plans against the outputs the approved plans
|
|
30
|
+
* read, from the record (`planOutputs`), and reads nothing those cover.
|
|
31
|
+
*
|
|
22
32
|
* ## Finishing an apply that failed partway
|
|
23
33
|
*
|
|
24
34
|
* {@link resumePrSet} (#3464) is fan-out's resume (#2417, #3049) for a pull
|
|
@@ -53,10 +63,10 @@ import type { PrApplyRecord } from "./fan-out-record";
|
|
|
53
63
|
import type { CapabilityRegistry } from "./capability";
|
|
54
64
|
import type { RunProgressEvent } from "./run-progress";
|
|
55
65
|
import type { RunEventSink } from "../run-event-stream";
|
|
56
|
-
import { planWaveComponent,
|
|
66
|
+
import { planWaveComponent, readDeployedOutputs } from "./wave-plan";
|
|
57
67
|
|
|
58
68
|
export interface PrPlanSetOptions {
|
|
59
|
-
/** Every component in the project. */
|
|
69
|
+
/** Every component in the project, not the selection: a planned member's dependency outside it is read too (#4104). */
|
|
60
70
|
components: DriverComponent[];
|
|
61
71
|
/** The fan-out the change derives (`planFanOut`). Its `order` is what is planned. */
|
|
62
72
|
plan: FanOutPlan;
|
|
@@ -85,6 +95,12 @@ export interface PrPlanSet {
|
|
|
85
95
|
plans: Record<string, Record<string, unknown>>;
|
|
86
96
|
/** The outputs the plans read: the seeds, and each dependency's outputs as they stood. */
|
|
87
97
|
outputs: Record<string, Record<string, unknown>>;
|
|
98
|
+
/**
|
|
99
|
+
* Per dependency read from the deployed estate (#4104), the names of the
|
|
100
|
+
* outputs the read gave, sorted. The seeds are not here. Absent when
|
|
101
|
+
* nothing was read. The report's `deployedOutputs`.
|
|
102
|
+
*/
|
|
103
|
+
deployedOutputs?: Record<string, string[]>;
|
|
88
104
|
/** Whether any component failed to plan. */
|
|
89
105
|
failed: boolean;
|
|
90
106
|
}
|
|
@@ -101,12 +117,15 @@ function failedParts(component: DriverComponent, error: string): ChangeSetPart[]
|
|
|
101
117
|
/**
|
|
102
118
|
* Plan every component in `plan.order`, a wave at a time, each against the
|
|
103
119
|
* outputs its dependencies have now. Never throws for a component that fails
|
|
104
|
-
* to plan: it becomes a failed member, and `failed` is set.
|
|
120
|
+
* to plan: it becomes a failed member, and `failed` is set. Throws when a
|
|
121
|
+
* planned member's dependency is not among `components`, which means the
|
|
122
|
+
* caller passed the selection where the whole graph belongs.
|
|
105
123
|
*/
|
|
106
124
|
export async function planPrSet(options: PrPlanSetOptions): Promise<PrPlanSet> {
|
|
107
125
|
const byName = new Map(options.components.map((c) => [c.name, c]));
|
|
108
126
|
const outputs: Record<string, Record<string, unknown>> = { ...(options.seededOutputs ?? {}) };
|
|
109
127
|
const read = new Set(Object.keys(outputs));
|
|
128
|
+
const deployed: Record<string, string[]> = {};
|
|
110
129
|
const parts: ChangeSetPart[] = [];
|
|
111
130
|
const componentOf = new Map<string, string>();
|
|
112
131
|
const plans: Record<string, Record<string, unknown>> = {};
|
|
@@ -117,15 +136,14 @@ export async function planPrSet(options: PrPlanSetOptions): Promise<PrPlanSet> {
|
|
|
117
136
|
for (const wave of options.plan.waves) {
|
|
118
137
|
// The dependencies' outputs first, each read once. A dependency that
|
|
119
138
|
// cannot say leaves its references unresolved, which its dependent's
|
|
120
|
-
// plan reports if it needs them.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (found) outputs[dep] = found;
|
|
127
|
-
}
|
|
139
|
+
// plan reports if it needs them. A dependency outside the selection is
|
|
140
|
+
// read the same way (#4104).
|
|
141
|
+
const deps = [...new Set(wave.flatMap((name) => byName.get(name)?.dependsOn ?? []))].filter((dep) => !read.has(dep));
|
|
142
|
+
for (const dep of deps) {
|
|
143
|
+
read.add(dep);
|
|
144
|
+
if (!byName.has(dep)) throw new Error(`planPrSet: ${dep}, a dependency of a planned member, is not among the components; pass every component in the project`);
|
|
128
145
|
}
|
|
146
|
+
Object.assign(deployed, await readDeployedOutputs(deps, byName, options.registry, (dep) => deployContext(options, dep), outputs));
|
|
129
147
|
const planned = await Promise.all(
|
|
130
148
|
wave.map(async (name) => {
|
|
131
149
|
const at = { component: name, phase: PR_PLAN_PHASE, step: "plan" };
|
|
@@ -170,7 +188,8 @@ export async function planPrSet(options: PrPlanSetOptions): Promise<PrPlanSet> {
|
|
|
170
188
|
counts: memberCounts(doc, m.member),
|
|
171
189
|
...(m.error ? { error: m.error } : {}),
|
|
172
190
|
}));
|
|
173
|
-
|
|
191
|
+
const deployedOutputs = Object.fromEntries(Object.entries(deployed).sort(([a], [b]) => a.localeCompare(b)));
|
|
192
|
+
return { doc, members, plans, outputs, failed, ...(Object.keys(deployedOutputs).length > 0 ? { deployedOutputs } : {}) };
|
|
174
193
|
}
|
|
175
194
|
|
|
176
195
|
/**
|
|
@@ -488,6 +507,7 @@ export function prReport(input: PrReportInput): PrReport {
|
|
|
488
507
|
...(input.refusal ? { refusal: input.refusal } : {}),
|
|
489
508
|
...(input.message ? { message: input.message } : {}),
|
|
490
509
|
...(input.resumed ? { resumed: { applied: [...input.resumed].sort() } } : {}),
|
|
510
|
+
...(input.set.deployedOutputs ? { deployedOutputs: input.set.deployedOutputs } : {}),
|
|
491
511
|
approval: input.approval,
|
|
492
512
|
selection: {
|
|
493
513
|
changed: input.plan.order.filter((c) => changed.has(c)),
|
|
@@ -135,3 +135,33 @@ export async function readComponentOutputs(
|
|
|
135
135
|
}
|
|
136
136
|
return found;
|
|
137
137
|
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Read the deployed outputs of each of `names` that `outputs` holds nothing
|
|
141
|
+
* for yet (#4104), through {@link readComponentOutputs}, and add them to
|
|
142
|
+
* `outputs`. A name that is not among `components`, or whose steps cannot
|
|
143
|
+
* say, is left out, so its dependents' references stay unresolved. Returns,
|
|
144
|
+
* per component read, the names of the outputs the read gave, sorted.
|
|
145
|
+
*
|
|
146
|
+
* `components` must be the whole graph, not the selection: the dependency a
|
|
147
|
+
* selected member reads is often one the change did not select.
|
|
148
|
+
*/
|
|
149
|
+
export async function readDeployedOutputs(
|
|
150
|
+
names: readonly string[],
|
|
151
|
+
components: ReadonlyMap<string, DriverComponent>,
|
|
152
|
+
registry: CapabilityRegistry,
|
|
153
|
+
ctxOf: (component: string) => DeployContext,
|
|
154
|
+
outputs: Record<string, Record<string, unknown>>,
|
|
155
|
+
): Promise<Record<string, string[]>> {
|
|
156
|
+
const read: Record<string, string[]> = {};
|
|
157
|
+
for (const name of names) {
|
|
158
|
+
if (name in outputs || name in read) continue;
|
|
159
|
+
const component = components.get(name);
|
|
160
|
+
if (!component) continue;
|
|
161
|
+
const found = await readComponentOutputs(component, registry, ctxOf(name), outputs);
|
|
162
|
+
if (!found) continue;
|
|
163
|
+
outputs[name] = found;
|
|
164
|
+
read[name] = Object.keys(found).sort();
|
|
165
|
+
}
|
|
166
|
+
return read;
|
|
167
|
+
}
|
|
@@ -47,16 +47,22 @@ export interface RunOrigin {
|
|
|
47
47
|
* Which id space minted `runId`: `github` (a GitHub Actions run id — also
|
|
48
48
|
* the spelling Forgejo Actions uses, since it implements the same env
|
|
49
49
|
* contract; `url` disambiguates the actual host), `gitlab` (a CI pipeline
|
|
50
|
-
* id), `op` (an orchestrator/Op run id),
|
|
51
|
-
* that ran the deploy, resolvable nowhere)
|
|
50
|
+
* id), `op` (an orchestrator/Op run id), `local` (minted on the machine
|
|
51
|
+
* that ran the deploy, resolvable nowhere), or `chant` (a job the chant
|
|
52
|
+
* runner ran, `chant ci run`, #4094, named by its Op run id). `chant` was
|
|
53
|
+
* added within contract 1 by {@link RUN_ORIGIN_CHANT_FLOOR}, so a reader
|
|
54
|
+
* that switches on `forge` keeps a case for a value it does not know.
|
|
52
55
|
*/
|
|
53
|
-
forge: "github" | "gitlab" | "op" | "local";
|
|
56
|
+
forge: "github" | "gitlab" | "op" | "local" | "chant";
|
|
54
57
|
/** Repo/project slug on the forge (`GITHUB_REPOSITORY` / `CI_PROJECT_PATH`), when the environment named one. */
|
|
55
58
|
repo?: string;
|
|
56
59
|
/** Resolved link to the run, when the environment provided enough to build one. */
|
|
57
60
|
url?: string;
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
/** The first chant that writes `runOrigin.forge: "chant"`, for a job `chant ci run` ran (#4094). */
|
|
64
|
+
export const RUN_ORIGIN_CHANT_FLOOR = "0.124.30";
|
|
65
|
+
|
|
60
66
|
/**
|
|
61
67
|
* The gate resolution that admitted a deploy (#4020): the gate's address in the
|
|
62
68
|
* gate ledger (`_gates/<op>.jsonl`, `op` and `gate`) and when the resolution
|
|
@@ -38,7 +38,7 @@ import { randomUUID } from "node:crypto";
|
|
|
38
38
|
import { sortedJsonReplacer } from "../utils";
|
|
39
39
|
import type { OpConfig } from "../op/types";
|
|
40
40
|
import type { StepRecord } from "../op/local-executor";
|
|
41
|
-
import type
|
|
41
|
+
import { opRunFailureOf, type OpRunRecord, type OpRunRecordInput, type OpRunPhaseRecord, type OpRunStepRecord } from "../op/runtime";
|
|
42
42
|
import { readBlobFromPath, readPathSha, readBlobBySha, writeBlobToPath, RefCASConflictError } from "./git";
|
|
43
43
|
|
|
44
44
|
/** Prefix folding the per-op run ledgers into one flat name under `<env>/`. See the module doc. */
|
|
@@ -215,6 +215,12 @@ export async function readRunLedger(
|
|
|
215
215
|
malformed++;
|
|
216
216
|
continue;
|
|
217
217
|
}
|
|
218
|
+
// A `failure` (#4094) that is not one is dropped and the line kept, as a gate line's wrong-typed `gitSha` is.
|
|
219
|
+
if ("failure" in parsed) {
|
|
220
|
+
const failure = opRunFailureOf(parsed.failure);
|
|
221
|
+
if (failure) parsed.failure = failure;
|
|
222
|
+
else delete parsed.failure;
|
|
223
|
+
}
|
|
218
224
|
records.push(parsed as OpRunRecord);
|
|
219
225
|
} catch {
|
|
220
226
|
malformed++;
|
|
@@ -54,6 +54,7 @@ export const shellCmdContract = activityContract(
|
|
|
54
54
|
cmd: z.string(),
|
|
55
55
|
env: z.record(z.string(), z.string()).optional(),
|
|
56
56
|
cwd: z.string().optional(),
|
|
57
|
+
inheritEnv: z.boolean().optional(),
|
|
57
58
|
okExit: z.array(z.number()).optional(),
|
|
58
59
|
gatedExit: z.number().optional(),
|
|
59
60
|
gate: z.strictObject({ op: z.string(), gate: z.string() }).optional(),
|
|
@@ -13,6 +13,12 @@ export interface ShellCmdArgs {
|
|
|
13
13
|
env?: Record<string, string>;
|
|
14
14
|
/** Working directory. Default: process.cwd(). */
|
|
15
15
|
cwd?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the command sees this process's environment. Default true. With
|
|
18
|
+
* false it sees `env` alone, plus the in-flight record's variable, so a job
|
|
19
|
+
* the chant runner runs (`chant ci run`, #4094) gets a clean environment.
|
|
20
|
+
*/
|
|
21
|
+
inheritEnv?: boolean;
|
|
16
22
|
/**
|
|
17
23
|
* Exit codes that count as success. Default `[0]`.
|
|
18
24
|
*
|
|
@@ -129,7 +135,7 @@ export async function shellCmd(args: ShellCmdArgs, signal?: AbortSignal): Promis
|
|
|
129
135
|
cwd: args.cwd,
|
|
130
136
|
// In a run that keeps an in-flight record, CHANT_RUN_ACTIVITY names the
|
|
131
137
|
// file the command appends activity lines to (../run-live.ts).
|
|
132
|
-
env: { ...process.env, ...liveRunEnv(), ...args.env },
|
|
138
|
+
env: { ...(args.inheritEnv === false ? {} : process.env), ...liveRunEnv(), ...args.env },
|
|
133
139
|
maxBuffer: MAX_STDOUT_BYTES,
|
|
134
140
|
signal,
|
|
135
141
|
});
|