@intentius/chant 0.124.32 → 0.124.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ci-jobs.d.ts +4 -0
- package/dist/ci-jobs.d.ts.map +1 -1
- package/dist/ci-jobs.js +9 -2
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +18 -3
- package/dist/cli/handlers/lifecycle-health.d.ts +16 -0
- package/dist/cli/handlers/lifecycle-health.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle-health.js +150 -0
- package/dist/cli/handlers/lifecycle.d.ts +26 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.js +1 -1
- package/dist/cli/handlers/pr.d.ts +2 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +124 -2
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +277 -74
- package/dist/cli/handlers/release.d.ts +16 -1
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +83 -11
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +99 -22
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +30 -1
- package/dist/cli/registry.d.ts +21 -5
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/deploy-inputs.d.ts +91 -0
- package/dist/components/deploy-inputs.d.ts.map +1 -0
- package/dist/components/deploy-inputs.js +184 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +9 -0
- package/dist/components/gate-plan.d.ts +8 -0
- package/dist/components/gate-plan.d.ts.map +1 -1
- package/dist/components/gate-plan.js +6 -0
- package/dist/components/pr-pipeline.d.ts +33 -10
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +42 -11
- package/dist/components/pr-run.d.ts +1 -1
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/promote-rounds.d.ts +102 -0
- package/dist/components/promote-rounds.d.ts.map +1 -0
- package/dist/components/promote-rounds.js +174 -0
- package/dist/components/release-compile.d.ts +37 -2
- package/dist/components/release-compile.d.ts.map +1 -1
- package/dist/components/release-compile.js +64 -8
- package/dist/components/release-gate.d.ts +30 -0
- package/dist/components/release-gate.d.ts.map +1 -1
- package/dist/components/release-gate.js +92 -42
- package/dist/components/release-promote.d.ts +17 -3
- package/dist/components/release-promote.d.ts.map +1 -1
- package/dist/components/release-promote.js +79 -18
- package/dist/components/verbs/bom-writer.d.ts +1 -1
- package/dist/components/verbs/bom-writer.d.ts.map +1 -1
- package/dist/components/verbs/lockfile-sbom-generator.d.ts +20 -0
- package/dist/components/verbs/lockfile-sbom-generator.d.ts.map +1 -1
- package/dist/components/verbs/lockfile-sbom-generator.js +41 -4
- package/dist/components/wave-plan.d.ts +10 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +32 -4
- package/dist/graph-declared.d.ts +4 -0
- package/dist/graph-declared.d.ts.map +1 -1
- package/dist/graph-declared.js +11 -1
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +32 -7
- package/dist/health.d.ts +110 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +105 -0
- package/dist/health.schema.json +191 -0
- package/dist/lexicon.d.ts +27 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/merge-record.d.ts +104 -0
- package/dist/lifecycle/merge-record.d.ts.map +1 -0
- package/dist/lifecycle/merge-record.js +102 -0
- package/dist/lifecycle/publish-attachments.d.ts +8 -5
- package/dist/lifecycle/publish-attachments.d.ts.map +1 -1
- package/dist/lifecycle/publish-attachments.js +27 -13
- package/dist/lifecycle/publish-npm.d.ts +3 -2
- package/dist/lifecycle/publish-npm.d.ts.map +1 -1
- package/dist/lifecycle/publish-npm.js +4 -3
- package/dist/lifecycle/release-at.d.ts +40 -0
- package/dist/lifecycle/release-at.d.ts.map +1 -0
- package/dist/lifecycle/release-at.js +83 -0
- package/dist/lifecycle/release-cut.d.ts.map +1 -1
- package/dist/lifecycle/release-cut.js +23 -12
- package/dist/lifecycle/release-publish.d.ts +28 -12
- package/dist/lifecycle/release-publish.d.ts.map +1 -1
- package/dist/lifecycle/release-publish.js +29 -17
- package/dist/lifecycle/release-store.d.ts +25 -2
- package/dist/lifecycle/release-store.d.ts.map +1 -1
- package/dist/lifecycle/release-store.js +40 -13
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +1 -0
- package/dist/lint/rules/op/index.d.ts +2 -1
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.js +4 -2
- package/dist/lint/rules/op/ops018-shell-image-digest.d.ts +10 -0
- package/dist/lint/rules/op/ops018-shell-image-digest.d.ts.map +1 -0
- package/dist/lint/rules/op/ops018-shell-image-digest.js +38 -0
- package/dist/op/activities/activity-contracts.d.ts +5 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +3 -0
- package/dist/op/activities/shell.d.ts +38 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +89 -3
- package/dist/op/builders.d.ts +7 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +7 -0
- package/dist/op/runtime.d.ts +2 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/pr-loop.d.ts +14 -3
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +15 -5
- package/dist/pr-report-url.d.ts +2 -2
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/runner-start.d.ts +179 -0
- package/dist/runner-start.d.ts.map +1 -0
- package/dist/runner-start.js +277 -0
- package/dist/wave-runtime.mjs +236 -229
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.js +13 -2
- package/dist/workspace/check.schema.json +88 -2
- package/dist/workspace/checks/ci-workflow.d.ts +17 -0
- package/dist/workspace/checks/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/checks/ci-workflow.js +123 -0
- package/dist/workspace/checks/security.d.ts +50 -0
- package/dist/workspace/checks/security.d.ts.map +1 -0
- package/dist/workspace/checks/security.js +305 -0
- package/dist/workspace/checks.d.ts +2 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +8 -0
- package/dist/workspace/ci-builds.d.ts +163 -0
- package/dist/workspace/ci-builds.d.ts.map +1 -0
- package/dist/workspace/ci-builds.js +315 -0
- package/dist/workspace/ci-green-cli.d.ts +2 -1
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.js +25 -5
- package/dist/workspace/ci-green-forge.d.ts +12 -0
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green-forge.js +12 -0
- package/dist/workspace/ci-green-workflow.d.ts +55 -1
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +128 -1
- package/dist/workspace/ci-green.d.ts +89 -4
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +310 -23
- package/dist/workspace/ci-init.d.ts +100 -0
- package/dist/workspace/ci-init.d.ts.map +1 -0
- package/dist/workspace/ci-init.js +434 -0
- package/dist/workspace/ci-last-green.schema.json +49 -0
- package/dist/workspace/ci-list.schema.json +35 -1
- package/dist/workspace/ci-run-index.d.ts +1 -1
- package/dist/workspace/ci-run-index.d.ts.map +1 -1
- package/dist/workspace/ci-run-index.schema.json +1 -1
- package/dist/workspace/ci-run.d.ts +12 -6
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +72 -16
- package/dist/workspace/ci-runner-start.d.ts +77 -0
- package/dist/workspace/ci-runner-start.d.ts.map +1 -0
- package/dist/workspace/ci-runner-start.js +185 -0
- package/dist/workspace/ci-serve.d.ts +76 -7
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +153 -19
- package/dist/workspace/ci-workflow.d.ts +91 -0
- package/dist/workspace/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/ci-workflow.js +33 -0
- package/dist/workspace/declaration.d.ts +35 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +12 -2
- package/dist/workspace/declaration.schema.json +51 -0
- package/dist/workspace/init.js +2 -2
- package/dist/workspace/ls.d.ts +2 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +1 -1
- package/dist/workspace/ls.schema.json +2 -1
- package/dist/workspace/member-pipeline.d.ts +1 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +2 -0
- package/dist/workspace/merge-record.schema.json +49 -0
- package/dist/workspace/pr-report.schema.json +3 -3
- package/dist/workspace/reason-codes.d.ts +26 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +28 -0
- package/dist/workspace/release-candidates.d.ts +129 -0
- package/dist/workspace/release-candidates.d.ts.map +1 -0
- package/dist/workspace/release-candidates.js +233 -0
- package/dist/workspace/release-candidates.schema.json +353 -0
- package/dist/workspace/release-workflow.d.ts +31 -0
- package/dist/workspace/release-workflow.d.ts.map +1 -0
- package/dist/workspace/release-workflow.js +150 -0
- package/dist/workspace/security.d.ts +40 -0
- package/dist/workspace/security.d.ts.map +1 -0
- package/dist/workspace/security.js +99 -0
- package/package.json +8 -1
- package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +1 -1
- package/src/ci-jobs.ts +11 -2
- package/src/cli/handlers/build.test.ts +36 -0
- package/src/cli/handlers/build.ts +18 -3
- package/src/cli/handlers/lifecycle-health.test.ts +123 -0
- package/src/cli/handlers/lifecycle-health.ts +157 -0
- package/src/cli/handlers/lifecycle.ts +1 -1
- package/src/cli/handlers/pr-deploy-inputs.test.ts +209 -0
- package/src/cli/handlers/pr-record.test.ts +201 -0
- package/src/cli/handlers/pr.test.ts +22 -0
- package/src/cli/handlers/pr.ts +127 -4
- package/src/cli/handlers/promote.test.ts +17 -0
- package/src/cli/handlers/promote.ts +296 -84
- package/src/cli/handlers/release-cut-at.e2e.test.ts +115 -0
- package/src/cli/handlers/release-publish-targets.e2e.test.ts +2 -2
- package/src/cli/handlers/release-verify.e2e.test.ts +12 -10
- package/src/cli/handlers/release.e2e.test.ts +19 -28
- package/src/cli/handlers/release.ts +81 -11
- package/src/cli/main.ts +99 -22
- package/src/cli/parse-args.ts +22 -1
- package/src/cli/registry.ts +21 -5
- package/src/components/deploy-inputs.test.ts +108 -0
- package/src/components/deploy-inputs.ts +225 -0
- package/src/components/driver.ts +11 -0
- package/src/components/gate-plan.ts +9 -0
- package/src/components/pr-pipeline.ts +56 -15
- package/src/components/pr-run.ts +1 -1
- package/src/components/promote-rounds.test.ts +220 -0
- package/src/components/promote-rounds.ts +232 -0
- package/src/components/release-compile.test.ts +34 -0
- package/src/components/release-compile.ts +89 -8
- package/src/components/release-gate.test.ts +47 -0
- package/src/components/release-gate.ts +111 -46
- package/src/components/release-promote.test.ts +55 -2
- package/src/components/release-promote.ts +93 -23
- package/src/components/verbs/bom-writer.ts +1 -1
- package/src/components/verbs/lockfile-sbom-generator.test.ts +36 -0
- package/src/components/verbs/lockfile-sbom-generator.ts +43 -5
- package/src/components/wave-plan.ts +32 -4
- package/src/graph-declared-exports.test.ts +40 -0
- package/src/graph-declared.ts +9 -1
- package/src/graph-detail.test.ts +57 -0
- package/src/graph-detail.ts +33 -7
- package/src/health.schema.json +191 -0
- package/src/health.test.ts +62 -0
- package/src/health.ts +178 -0
- package/src/lexicon.ts +41 -1
- package/src/lifecycle/merge-record.test.ts +111 -0
- package/src/lifecycle/merge-record.ts +154 -0
- package/src/lifecycle/publish-attachments.ts +25 -13
- package/src/lifecycle/publish-npm.ts +4 -3
- package/src/lifecycle/publish-targets.test.ts +34 -4
- package/src/lifecycle/release-at.test.ts +48 -0
- package/src/lifecycle/release-at.ts +100 -0
- package/src/lifecycle/release-cut.test.ts +41 -0
- package/src/lifecycle/release-cut.ts +22 -11
- package/src/lifecycle/release-publish.test.ts +43 -13
- package/src/lifecycle/release-publish.ts +38 -18
- package/src/lifecycle/release-store.ts +57 -13
- package/src/lifecycle/run-ledger.test.ts +13 -0
- package/src/lifecycle/run-ledger.ts +1 -0
- package/src/lint/rules/op/index.ts +4 -2
- package/src/lint/rules/op/ops018-shell-image-digest.test.ts +60 -0
- package/src/lint/rules/op/ops018-shell-image-digest.ts +48 -0
- package/src/op/activities/activity-contracts.ts +3 -0
- package/src/op/activities/shell-container.e2e.test.ts +67 -0
- package/src/op/activities/shell.test.ts +44 -1
- package/src/op/activities/shell.ts +110 -3
- package/src/op/builders.ts +7 -0
- package/src/op/runtime.ts +2 -0
- package/src/op/step-console.test.ts +4 -2
- package/src/pr-loop.test.ts +29 -0
- package/src/pr-loop.ts +26 -7
- package/src/pr-report-url.ts +2 -2
- package/src/runner-start-bundle.test.ts +29 -0
- package/src/runner-start.test.ts +179 -0
- package/src/runner-start.ts +329 -0
- package/src/workspace/__fixtures__/contract-repo.ts +11 -2
- package/src/workspace/check-contract.test.ts +4 -2
- package/src/workspace/check.schema.json +88 -2
- package/src/workspace/checks/ci-workflow.test.ts +150 -0
- package/src/workspace/checks/ci-workflow.ts +129 -0
- package/src/workspace/checks/security.test.ts +138 -0
- package/src/workspace/checks/security.ts +305 -0
- package/src/workspace/checks.ts +8 -0
- package/src/workspace/ci-builds.e2e.test.ts +208 -0
- package/src/workspace/ci-builds.test.ts +174 -0
- package/src/workspace/ci-builds.ts +385 -0
- package/src/workspace/ci-green-builds.test.ts +180 -0
- package/src/workspace/ci-green-cli.test.ts +54 -0
- package/src/workspace/ci-green-cli.ts +26 -5
- package/src/workspace/ci-green-forge.ts +24 -0
- package/src/workspace/ci-green-workflow.test.ts +84 -1
- package/src/workspace/ci-green-workflow.ts +139 -2
- package/src/workspace/ci-green.test.ts +5 -2
- package/src/workspace/ci-green.ts +359 -27
- package/src/workspace/ci-init.test.ts +159 -0
- package/src/workspace/ci-init.ts +445 -0
- package/src/workspace/ci-last-green.schema.json +49 -0
- package/src/workspace/ci-list.schema.json +35 -1
- package/src/workspace/ci-report-url.test.ts +6 -1
- package/src/workspace/ci-run-index.schema.json +1 -1
- package/src/workspace/ci-run-index.ts +1 -1
- package/src/workspace/ci-run.test.ts +24 -7
- package/src/workspace/ci-run.ts +78 -16
- package/src/workspace/ci-runner-start.test.ts +151 -0
- package/src/workspace/ci-runner-start.ts +193 -0
- package/src/workspace/ci-serve-builds.test.ts +86 -0
- package/src/workspace/ci-serve.test.ts +59 -0
- package/src/workspace/ci-serve.ts +177 -22
- package/src/workspace/ci-workflow.ts +71 -0
- package/src/workspace/declaration.schema.json +51 -0
- package/src/workspace/declaration.test.ts +17 -0
- package/src/workspace/declaration.ts +53 -3
- package/src/workspace/init.ts +2 -2
- package/src/workspace/ls.schema.json +2 -1
- package/src/workspace/ls.ts +3 -2
- package/src/workspace/member-pipeline.ts +2 -1
- package/src/workspace/merge-record.schema.json +49 -0
- package/src/workspace/pr-report.schema.json +3 -3
- package/src/workspace/reason-codes.test.ts +4 -0
- package/src/workspace/reason-codes.ts +28 -0
- package/src/workspace/release-candidates.schema.json +353 -0
- package/src/workspace/release-candidates.test.ts +106 -0
- package/src/workspace/release-candidates.ts +315 -0
- package/src/workspace/release-workflow.ts +167 -0
- package/src/workspace/security.test.ts +70 -0
- package/src/workspace/security.ts +118 -0
package/src/cli/registry.ts
CHANGED
|
@@ -311,8 +311,10 @@ 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`. `chant ci run apply --
|
|
315
|
-
*
|
|
314
|
+
* request. Needs `--forge`. `chant ci run apply --require-review`
|
|
315
|
+
* passes it on, and `chant build --pr-loop --require-review` generates a
|
|
316
|
+
* pipeline that does (#4168); `--no-require-review` (#4156) sets it false,
|
|
317
|
+
* which is the default.
|
|
316
318
|
*/
|
|
317
319
|
requireReview?: boolean;
|
|
318
320
|
/**
|
|
@@ -323,12 +325,20 @@ export interface ParsedArgs {
|
|
|
323
325
|
reportUrl?: string;
|
|
324
326
|
/**
|
|
325
327
|
* `chant build --components --generate <lexicon> --pr-loop` (#3183) —
|
|
326
|
-
* generate the pull-request pipeline: plan on the pull request,
|
|
327
|
-
* merge
|
|
328
|
+
* generate the pull-request pipeline: plan on the pull request, and on
|
|
329
|
+
* merge record the plan (#4168), or apply it with `--apply-on-merge`.
|
|
328
330
|
*/
|
|
329
331
|
prLoop?: boolean;
|
|
332
|
+
/**
|
|
333
|
+
* `chant build --pr-loop --apply-on-merge` and `chant ci serve
|
|
334
|
+
* --apply-on-merge` (#4168): on merge apply the approved plan, as before
|
|
335
|
+
* #4168, in place of recording it. Opt-in: a merge deploys nothing without it.
|
|
336
|
+
*/
|
|
337
|
+
applyOnMerge?: boolean;
|
|
330
338
|
/** `chant build --pr-loop --pr-build` (#4088) — build and lint before pr-plan and pr-apply. */
|
|
331
339
|
prBuild?: boolean;
|
|
340
|
+
/** `chant ci serve --build-command <line>` (#4169) — the line that builds a commit a `ci/build/<sha>` tag asks for, in a worktree of it. */
|
|
341
|
+
ciBuildCommand?: string;
|
|
332
342
|
/** `chant build --pr-loop --pr-build-command <line>` (#4088) — the build line to run in place of `chant build --env <env>`. */
|
|
333
343
|
prBuildCommand?: string;
|
|
334
344
|
/**
|
|
@@ -457,8 +467,10 @@ export interface ParsedArgs {
|
|
|
457
467
|
label?: string;
|
|
458
468
|
/** `chant workspace wip --branch <branch>` (#3172): list one branch's snapshots. */
|
|
459
469
|
branch?: string;
|
|
460
|
-
/** `chant ci workflow --workflow <name>`, repeatable (#3573): a workflow whose completion triggers a tick, beside those the scan finds. */
|
|
470
|
+
/** `chant ci workflow --workflow <name>`, repeatable (#3573): a workflow whose completion triggers a tick, beside those the scan finds. Also `chant ci init --workflow <name>` (#4168 step 6), the prescribed workflow to set up. */
|
|
461
471
|
ciWorkflows?: string[];
|
|
472
|
+
/** `chant ci init --security <group>`, repeatable (#4168 step 6): a security group to turn on. */
|
|
473
|
+
securityGroups?: string[];
|
|
462
474
|
/** `chant ci run plan|apply --commit <sha>` (#4094): the commit the job runs at. Default `HEAD`. */
|
|
463
475
|
commit?: string;
|
|
464
476
|
/** `chant ci run --pass-env <NAME>`, repeatable (#4094): a variable of the runner's environment the job gets, beside the few it always gets. */
|
|
@@ -511,6 +523,10 @@ export interface ParsedArgs {
|
|
|
511
523
|
unsigned?: boolean;
|
|
512
524
|
/** `chant ci run --in-place` (#4156): run the job in this checkout at HEAD, as a generated forge job does. */
|
|
513
525
|
ciInPlace?: boolean;
|
|
526
|
+
/** `chant ci run --require-runner-start` (#4147): refuse the job unless a runner-start statement names it. */
|
|
527
|
+
requireRunnerStart?: boolean;
|
|
528
|
+
/** `chant ci runner-start sign --job <json|@file>` (#4147): the CI job a runner was started for. */
|
|
529
|
+
ciJob?: string;
|
|
514
530
|
/** `chant ci run --no-push` (#4156): write the run record on the local chant/lifecycle and push nothing. */
|
|
515
531
|
noPush?: boolean;
|
|
516
532
|
/** `chant ci serve|run --setup-key --trust --principal <name>` (#4099): the principal the runner key is listed for. */
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
6
|
+
import { componentDeployInputs, directoryDigest, directoryFiles, stepInputRefs } from "./deploy-inputs";
|
|
7
|
+
import { componentPlanDigest } from "./gate-plan";
|
|
8
|
+
import type { DriverComponent, DriverPhase } from "./driver";
|
|
9
|
+
|
|
10
|
+
let root: string;
|
|
11
|
+
|
|
12
|
+
const write = (path: string, content: string): void => {
|
|
13
|
+
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
14
|
+
writeFileSync(join(root, path), content);
|
|
15
|
+
};
|
|
16
|
+
const deploy = (...steps: Array<Record<string, unknown>>): DriverPhase[] => [{ phase: "Deploy", steps: steps as never }];
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
root = mkdtempSync(join(tmpdir(), "deploy-inputs-"));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
rmSync(root, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("stepInputRefs", () => {
|
|
27
|
+
test("a kind's own fields and the template fields, literal paths only", () => {
|
|
28
|
+
const refs = stepInputRefs("Deploy", { kind: "helm-upgrade", release: "web", chart: "chart", values: ["a.yaml", "@Cfg.values"] } as never);
|
|
29
|
+
expect(refs.map((r) => [r.field, r.path])).toEqual([["values", "a.yaml"], ["chart", "chart"]]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("an archive path or a wired value is a build output and is not named", () => {
|
|
33
|
+
expect(stepInputRefs("Deploy", { kind: "cfn-deploy", template: "archive:stack.json" } as never)).toEqual([]);
|
|
34
|
+
expect(stepInputRefs("Deploy", { kind: "s3-sync", from: "@Build.dir", to: "s3://b" } as never)).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("a Docker build's Dockerfile is named under its context", () => {
|
|
38
|
+
const refs = stepInputRefs("Build", { kind: "docker-build", context: "app", dockerfile: "Dockerfile.prod" } as never);
|
|
39
|
+
expect(refs.map((r) => r.path)).toEqual(["app", "app/Dockerfile.prod"]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("componentDeployInputs", () => {
|
|
44
|
+
test("a file by its bytes, a directory by its files; a missing path is left out", () => {
|
|
45
|
+
write("values.yaml", "a: 1\n");
|
|
46
|
+
write("chart/Chart.yaml", "name: c\n");
|
|
47
|
+
const read = componentDeployInputs(deploy({ kind: "helm-upgrade", chart: "chart", values: ["values.yaml", "gone.yaml"] }), root);
|
|
48
|
+
expect(read.inputs.map((i) => i.path)).toEqual(["chart", "values.yaml"]);
|
|
49
|
+
expect(read.inputs.find((i) => i.path === "values.yaml")?.digest).toMatch(/^sha256:[0-9a-f]{64}$/);
|
|
50
|
+
expect(read.unbound).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a directory's digest moves with a file's bytes, a new file and a renamed one", () => {
|
|
54
|
+
write("site/index.html", "<h1>a</h1>");
|
|
55
|
+
const before = directoryDigest(root, "site");
|
|
56
|
+
write("site/index.html", "<h1>b</h1>");
|
|
57
|
+
const edited = directoryDigest(root, "site");
|
|
58
|
+
expect(edited).not.toBe(before);
|
|
59
|
+
write("site/about.html", "x");
|
|
60
|
+
expect(directoryDigest(root, "site")).not.toBe(edited);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("the same files under another parent hash the same: the digest reads paths from the directory", () => {
|
|
64
|
+
write("a/chart/Chart.yaml", "name: c\n");
|
|
65
|
+
write("b/chart/Chart.yaml", "name: c\n");
|
|
66
|
+
expect(directoryDigest(root, "a/chart")).toBe(directoryDigest(root, "b/chart"));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("in a git repository, a directory leaves out what git ignores, and a directory git ignores is a build output", () => {
|
|
70
|
+
execFileSync("git", ["init", "-q"], { cwd: root });
|
|
71
|
+
write(".gitignore", "node_modules/\ndist/\n");
|
|
72
|
+
write("app/server.js", "1");
|
|
73
|
+
write("app/node_modules/dep/index.js", "1");
|
|
74
|
+
write("dist/index.html", "1");
|
|
75
|
+
expect(directoryFiles(root, "app")).toEqual(["app/server.js"]);
|
|
76
|
+
const read = componentDeployInputs(deploy({ kind: "docker-build", context: "app" }, { kind: "s3-sync", from: "dist", to: "s3://b" }), root);
|
|
77
|
+
expect(read.inputs.map((i) => i.path)).toEqual(["app"]);
|
|
78
|
+
expect(read.unbound).toEqual([{ phase: "Deploy", kind: "s3-sync", reason: expect.stringMatching(/"dist" is a directory git ignores, so a build output/) }]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("a file git ignores is still hashed when a step names it, as a template chant build writes", () => {
|
|
82
|
+
execFileSync("git", ["init", "-q"], { cwd: root });
|
|
83
|
+
write(".gitignore", "dist/\n");
|
|
84
|
+
write("dist/stack.json", "{}");
|
|
85
|
+
expect(componentDeployInputs(deploy({ kind: "cfn-deploy", template: "dist/stack.json" }), root).inputs.map((i) => i.path)).toEqual(["dist/stack.json"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("a shell step is unbound; `include` limits the steps read", () => {
|
|
89
|
+
write("chart/Chart.yaml", "x");
|
|
90
|
+
const phases = deploy({ kind: "shell", cmd: "./go.sh", reason: "t" }, { kind: "helm-upgrade", chart: "chart" });
|
|
91
|
+
expect(componentDeployInputs(phases, root).unbound.map((u) => u.kind)).toEqual(["shell"]);
|
|
92
|
+
expect(componentDeployInputs(phases, root, (s) => s.kind !== "helm-upgrade").inputs).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("componentPlanDigest with inputs", () => {
|
|
97
|
+
const component: DriverComponent = { name: "web", dependsOn: [], deploy: deploy({ kind: "helm-upgrade", chart: "chart" }) };
|
|
98
|
+
|
|
99
|
+
test("no inputs keeps the digest a composition had before #4155", () => {
|
|
100
|
+
expect(componentPlanDigest({ environment: "prod", component, inputs: [] })).toBe(componentPlanDigest({ environment: "prod", component }));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("an input's digest moves the plan digest", () => {
|
|
104
|
+
const a = componentPlanDigest({ environment: "prod", component, inputs: [{ path: "chart", digest: "sha256:aa" }] });
|
|
105
|
+
const b = componentPlanDigest({ environment: "prod", component, inputs: [{ path: "chart", digest: "sha256:bb" }] });
|
|
106
|
+
expect(a).not.toBe(b);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local files a component's steps deploy from, and their content digests
|
|
3
|
+
* (#4155).
|
|
4
|
+
*
|
|
5
|
+
* A component gate approves `componentPlanDigest` (./gate-plan.ts). A step
|
|
6
|
+
* whose capability has a `plan` is bound by what it plans. Every other step
|
|
7
|
+
* was bound only by its input as written, so a Helm chart, a kustomize
|
|
8
|
+
* directory or a static site's tree could change after review without moving
|
|
9
|
+
* the digest. This module names those files and hashes them, so the digest
|
|
10
|
+
* moves when they do.
|
|
11
|
+
*
|
|
12
|
+
* Which fields name a path is data, as the template fields are in
|
|
13
|
+
* ./release-compile.ts and the unit fields in ./deploy-units.ts: a kind's
|
|
14
|
+
* fields in {@link DEPLOY_INPUT_FIELDS}, plus `template`, `manifest` and
|
|
15
|
+
* `values` on any step ({@link TEMPLATE_FIELDS}). Only a literal path counts:
|
|
16
|
+
* a value wired from another step (`@Build.uri`) or an `archive:` path is a
|
|
17
|
+
* build output, and binding it would make an approval unusable by a build
|
|
18
|
+
* that is not reproducible. The build steps' own sources are bound instead.
|
|
19
|
+
*
|
|
20
|
+
* - A file is hashed by its bytes.
|
|
21
|
+
* - A directory is hashed over the files under it that git does not ignore
|
|
22
|
+
* (`git ls-files --cached --others --exclude-standard`), each by its path
|
|
23
|
+
* and bytes, so `node_modules` and a build's `dist` inside a Docker context
|
|
24
|
+
* are left out. Outside a git repository every file is hashed except under
|
|
25
|
+
* `.git` and `node_modules`.
|
|
26
|
+
* - A directory git ignores as a whole is a build output. It is not hashed;
|
|
27
|
+
* the step is listed as unbound, with that reason.
|
|
28
|
+
* - A path that does not exist is left out. One that appears later moves the
|
|
29
|
+
* digest.
|
|
30
|
+
*
|
|
31
|
+
* A step whose kind runs content chant can't name, such as `shell`, is listed
|
|
32
|
+
* as unbound ({@link UNBOUND_KINDS}), so the plan note can show a reviewer
|
|
33
|
+
* what the approval does not cover.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { spawnSync } from "node:child_process";
|
|
37
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
38
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
39
|
+
import { sha256Hex } from "../content-digest";
|
|
40
|
+
import { computePlanDigest } from "../lifecycle/plan-digest";
|
|
41
|
+
import type { DriverGate, DriverPhase, DriverStep } from "./driver";
|
|
42
|
+
|
|
43
|
+
/** The fields that name every step's deployed file, whatever its kind (as ./release-compile.ts reads them). */
|
|
44
|
+
export const TEMPLATE_FIELDS: readonly string[] = ["template", "manifest", "values"];
|
|
45
|
+
|
|
46
|
+
/** Per kind, the fields beyond {@link TEMPLATE_FIELDS} that name a local file or directory the step deploys or builds from. */
|
|
47
|
+
export const DEPLOY_INPUT_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
48
|
+
"helm-upgrade": ["chart"],
|
|
49
|
+
"kustomize-apply": ["dir"],
|
|
50
|
+
"fly-release": ["plan"],
|
|
51
|
+
"fly-rollback": ["plan"],
|
|
52
|
+
"s3-sync": ["from"],
|
|
53
|
+
"r2-sync": ["from"],
|
|
54
|
+
"copy-to-host": ["from"],
|
|
55
|
+
"wrangler-deploy": ["config"],
|
|
56
|
+
"wrangler-versions-promote": ["config"],
|
|
57
|
+
"docker-build": ["context"],
|
|
58
|
+
"zip-package": ["source"],
|
|
59
|
+
"jvm-build": ["path"],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Kinds that deploy content chant can't name from their input: a command, an agent's work. */
|
|
63
|
+
export const UNBOUND_KINDS: readonly string[] = ["shell", "run-agent"];
|
|
64
|
+
|
|
65
|
+
/** One path a step names. */
|
|
66
|
+
export interface DeployInputRef {
|
|
67
|
+
phase: string;
|
|
68
|
+
kind: string;
|
|
69
|
+
field: string;
|
|
70
|
+
/** Relative to the member's directory, as written. */
|
|
71
|
+
path: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A named path and the digest of what is there. */
|
|
75
|
+
export interface DeployInputDigest {
|
|
76
|
+
path: string;
|
|
77
|
+
/** `sha256:<hex>` of a file's bytes, or a plan digest over a directory's files. */
|
|
78
|
+
digest: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A step the digest does not cover, and why. */
|
|
82
|
+
export interface UnboundStep {
|
|
83
|
+
phase: string;
|
|
84
|
+
kind: string;
|
|
85
|
+
reason: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** What a component's steps deploy from: the digests, and the steps they can't cover. */
|
|
89
|
+
export interface ComponentDeployInputs {
|
|
90
|
+
inputs: DeployInputDigest[];
|
|
91
|
+
unbound: UnboundStep[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isGate(step: DriverStep | DriverGate | DriverPhase): step is DriverGate {
|
|
95
|
+
return (step as { kind?: unknown }).kind === "gate";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isPhase(step: DriverStep | DriverGate | DriverPhase): step is DriverPhase {
|
|
99
|
+
return typeof (step as { phase?: unknown }).phase === "string" && Array.isArray((step as DriverPhase).steps);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isLiteralPath = (v: unknown): v is string =>
|
|
103
|
+
typeof v === "string" && v !== "" && !v.startsWith("@") && !v.startsWith("$") && !v.startsWith("archive:");
|
|
104
|
+
|
|
105
|
+
/** Every capability step of a composition with its phase, nested phases included, gates left out. */
|
|
106
|
+
export function stepsWithPhase(phases: readonly DriverPhase[]): Array<{ phase: string; step: DriverStep }> {
|
|
107
|
+
const out: Array<{ phase: string; step: DriverStep }> = [];
|
|
108
|
+
const walk = (phase: DriverPhase): void => {
|
|
109
|
+
for (const step of phase.steps) {
|
|
110
|
+
if (isPhase(step)) walk(step);
|
|
111
|
+
else if (!isGate(step)) out.push({ phase: phase.phase, step });
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
phases.forEach(walk);
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The literal paths one step names, in field order. A Docker build's Dockerfile is named under its context. */
|
|
119
|
+
export function stepInputRefs(phase: string, step: DriverStep): DeployInputRef[] {
|
|
120
|
+
const out: DeployInputRef[] = [];
|
|
121
|
+
for (const field of [...TEMPLATE_FIELDS, ...(DEPLOY_INPUT_FIELDS[step.kind] ?? [])]) {
|
|
122
|
+
const raw = step[field];
|
|
123
|
+
for (const value of Array.isArray(raw) ? raw : [raw]) {
|
|
124
|
+
if (isLiteralPath(value)) out.push({ phase, kind: step.kind, field, path: value });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (step.kind === "docker-build" && isLiteralPath(step.context) && isLiteralPath(step.dockerfile)) {
|
|
128
|
+
out.push({ phase, kind: step.kind, field: "dockerfile", path: join(step.context, step.dockerfile) });
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function git(root: string, args: string[]): { ok: boolean; out: string } {
|
|
134
|
+
const r = spawnSync("git", ["-C", root, ...args], { encoding: "utf-8", maxBuffer: 256 * 1024 * 1024 });
|
|
135
|
+
return { ok: r.status === 0, out: r.stdout ?? "" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The files under `dir` (relative to `root`) a directory digest covers, relative to `root`, sorted. */
|
|
139
|
+
export function directoryFiles(root: string, dir: string): string[] {
|
|
140
|
+
const listed = git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", dir]);
|
|
141
|
+
if (listed.ok) {
|
|
142
|
+
return [...new Set(listed.out.split("\0").filter((p) => p !== ""))]
|
|
143
|
+
.filter((p) => {
|
|
144
|
+
const full = resolve(root, p);
|
|
145
|
+
return existsSync(full) && statSync(full).isFile();
|
|
146
|
+
})
|
|
147
|
+
.sort();
|
|
148
|
+
}
|
|
149
|
+
return walkFiles(root, dir);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Every file under `dir`, relative to `root`, sorted, leaving out `.git` and `node_modules`. What a cut records of a directory git ignores. */
|
|
153
|
+
export function walkFiles(root: string, dir: string): string[] {
|
|
154
|
+
const out: string[] = [];
|
|
155
|
+
const walk = (abs: string): void => {
|
|
156
|
+
for (const entry of readdirSync(abs, { withFileTypes: true })) {
|
|
157
|
+
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
158
|
+
const full = join(abs, entry.name);
|
|
159
|
+
if (entry.isDirectory()) walk(full);
|
|
160
|
+
else if (entry.isFile()) out.push(relative(root, full).split(sep).join("/"));
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
walk(resolve(root, dir));
|
|
164
|
+
return out.sort();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Whether git ignores `path` (relative to `root`). False outside a git repository. */
|
|
168
|
+
export function ignoredByGit(root: string, path: string): boolean {
|
|
169
|
+
return git(root, ["check-ignore", "-q", "--", path]).ok;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** `sha256:<hex>` of a file's bytes. */
|
|
173
|
+
export function fileDigest(file: string): string {
|
|
174
|
+
return `sha256:${sha256Hex(readFileSync(file))}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The digest of the files under `dir`: a plan digest over each file's path (from `dir`) and bytes. */
|
|
178
|
+
export function directoryDigest(root: string, dir: string): string {
|
|
179
|
+
const base = resolve(root, dir);
|
|
180
|
+
const files = directoryFiles(root, dir).map((p) => ({
|
|
181
|
+
path: relative(base, resolve(root, p)).split(sep).join("/"),
|
|
182
|
+
digest: fileDigest(resolve(root, p)),
|
|
183
|
+
}));
|
|
184
|
+
return computePlanDigest("directory", files);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* What a component's steps deploy from, read under `root` (the member's
|
|
189
|
+
* directory). `include` picks the steps to read, all of them by default; the
|
|
190
|
+
* wave planner passes the steps without a `plan`, which their plan does not
|
|
191
|
+
* already cover.
|
|
192
|
+
*/
|
|
193
|
+
export function componentDeployInputs(
|
|
194
|
+
phases: readonly DriverPhase[],
|
|
195
|
+
root: string,
|
|
196
|
+
include: (step: DriverStep) => boolean = () => true,
|
|
197
|
+
): ComponentDeployInputs {
|
|
198
|
+
const inputs = new Map<string, string>();
|
|
199
|
+
const unbound: UnboundStep[] = [];
|
|
200
|
+
for (const { phase, step } of stepsWithPhase(phases)) {
|
|
201
|
+
if (!include(step)) continue;
|
|
202
|
+
if (UNBOUND_KINDS.includes(step.kind)) {
|
|
203
|
+
unbound.push({ phase, kind: step.kind, reason: `a ${step.kind} step runs content chant can't name from its input` });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
for (const ref of stepInputRefs(phase, step)) {
|
|
207
|
+
if (inputs.has(ref.path)) continue;
|
|
208
|
+
const full = resolve(root, ref.path);
|
|
209
|
+
if (!existsSync(full)) continue;
|
|
210
|
+
if (statSync(full).isDirectory()) {
|
|
211
|
+
if (ignoredByGit(root, ref.path)) {
|
|
212
|
+
unbound.push({ phase, kind: step.kind, reason: `its ${ref.field} "${ref.path}" is a directory git ignores, so a build output; bind its sources through the step that builds it` });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
inputs.set(ref.path, directoryDigest(root, ref.path));
|
|
216
|
+
} else {
|
|
217
|
+
inputs.set(ref.path, fileDigest(full));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
inputs: [...inputs].map(([path, digest]) => ({ path, digest })).sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
|
|
223
|
+
unbound,
|
|
224
|
+
};
|
|
225
|
+
}
|
package/src/components/driver.ts
CHANGED
|
@@ -52,6 +52,7 @@ import { evaluateGate, gitGateLedgerPort, type GateDigestMismatch, type GateLedg
|
|
|
52
52
|
import { gateName } from "../op/gate-name";
|
|
53
53
|
import { GateNotRecordableError } from "../lifecycle/no-record";
|
|
54
54
|
import { componentPlanDigest } from "./gate-plan";
|
|
55
|
+
import { componentDeployInputs } from "./deploy-inputs";
|
|
55
56
|
import type { PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
56
57
|
import type { CapabilityRegistry, DeployContext } from "./capability";
|
|
57
58
|
import { isPlanMovedError, PLAN_MOVED_EXIT_CODE } from "./capability";
|
|
@@ -853,6 +854,13 @@ async function rollbackExecuted(
|
|
|
853
854
|
|
|
854
855
|
// ── Per-component deploy ─────────────────────────────────────────────────
|
|
855
856
|
|
|
857
|
+
/** Whether a component's composition has a gate step anywhere, nested phases and `rollback` included. */
|
|
858
|
+
function hasGateStep(component: DriverComponent): boolean {
|
|
859
|
+
const walk = (steps: DriverPhase["steps"]): boolean =>
|
|
860
|
+
steps.some((step) => isGateStep(step) || (Array.isArray((step as DriverPhase).steps) && walk((step as DriverPhase).steps)));
|
|
861
|
+
return [...component.deploy, ...(component.rollback ?? [])].some((p) => walk(p.steps));
|
|
862
|
+
}
|
|
863
|
+
|
|
856
864
|
/**
|
|
857
865
|
* Run one component's `deploy` composition. On terminal failure: unwind every
|
|
858
866
|
* executed step in reverse via its capability's `rollback` (saga
|
|
@@ -900,6 +908,9 @@ export async function runComponentDeploy(
|
|
|
900
908
|
component,
|
|
901
909
|
...(ctx.vars ? { vars: ctx.vars } : {}),
|
|
902
910
|
...(release !== undefined ? { release } : {}),
|
|
911
|
+
// #4155: the files the steps deploy from, read where the steps read
|
|
912
|
+
// them. Only a gate reads the digest, so a run with none skips the hashing.
|
|
913
|
+
...(hasGateStep(component) ? { inputs: componentDeployInputs(component.deploy, process.cwd()).inputs } : {}),
|
|
903
914
|
}),
|
|
904
915
|
};
|
|
905
916
|
const records: DriverStepRecord[] = [];
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
* its publish output against a recorded digest that is not in the
|
|
18
18
|
* composition, so it passes that digest here.
|
|
19
19
|
* - `vars`, the environment config the caller resolved ahead of the run.
|
|
20
|
+
* - The content digests of the local files the steps deploy from (#4155,
|
|
21
|
+
* ./deploy-inputs.ts): a chart, a manifest, a site's tree, a build step's
|
|
22
|
+
* sources. A file changed after review moves the digest. The key is left
|
|
23
|
+
* out when no step names an existing file, so a composition that deploys
|
|
24
|
+
* none keeps the digest it had before #4155.
|
|
20
25
|
*
|
|
21
26
|
* ## What it leaves out
|
|
22
27
|
*
|
|
@@ -28,6 +33,7 @@
|
|
|
28
33
|
*/
|
|
29
34
|
|
|
30
35
|
import { computePlanDigest } from "../lifecycle/plan-digest";
|
|
36
|
+
import type { DeployInputDigest } from "./deploy-inputs";
|
|
31
37
|
import type { DriverComponent } from "./driver";
|
|
32
38
|
|
|
33
39
|
/** What a component gate's plan digest is taken over. */
|
|
@@ -37,6 +43,8 @@ export interface ComponentGatePlan {
|
|
|
37
43
|
vars?: Record<string, unknown>;
|
|
38
44
|
/** The recorded digest a promote or rollback deploys, when the caller has one. */
|
|
39
45
|
release?: string;
|
|
46
|
+
/** What the steps deploy from (`componentDeployInputs`), when the caller read it. */
|
|
47
|
+
inputs?: readonly DeployInputDigest[];
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
/** The plan digest a component gate records and approves. */
|
|
@@ -49,5 +57,6 @@ export function componentPlanDigest(plan: ComponentGatePlan): string {
|
|
|
49
57
|
rollback: component.rollback ?? null,
|
|
50
58
|
vars: plan.vars ?? null,
|
|
51
59
|
release: plan.release ?? null,
|
|
60
|
+
...(plan.inputs && plan.inputs.length > 0 ? { inputs: plan.inputs } : {}),
|
|
52
61
|
});
|
|
53
62
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The jobs of a generated pull-request pipeline (#3183): a plan job on every
|
|
3
|
-
* pull request and
|
|
3
|
+
* pull request and, on every push to the target branch, a record job, or an
|
|
4
|
+
* apply job when the pipeline opts into apply on merge (#4168).
|
|
4
5
|
*
|
|
5
6
|
* Each forge's generator (the github, gitlab and forgejo lexicons) adds its
|
|
6
7
|
* own triggers, permissions and artifact handling; the commands and the
|
|
@@ -9,11 +10,19 @@
|
|
|
9
10
|
* - The plan job runs `chant components pr-plan` against the pull request's
|
|
10
11
|
* target commit. It posts the note and the `chant/plan` status and writes
|
|
11
12
|
* nothing to `chant/lifecycle`, since it runs the pull request's code.
|
|
12
|
-
* - The
|
|
13
|
+
* - The record job (#4168 step 1, the default) runs `chant components
|
|
14
|
+
* pr-record` against the commit the push replaced. It plans the same
|
|
15
|
+
* members on the merged commit, stores the change set and writes the merge
|
|
16
|
+
* record, `_merges/<commit>.json` on `chant/lifecycle`. It applies nothing:
|
|
17
|
+
* a release that carries the commit deploys it. It needs write access to
|
|
18
|
+
* `chant/lifecycle`. One record runs at a time per environment.
|
|
19
|
+
* - With `applyOnMerge` the apply job takes the record job's place. It runs
|
|
20
|
+
* `chant components pr-apply` against the commit the
|
|
13
21
|
* push replaced, finds the pull request that merged the pushed commit
|
|
14
22
|
* through the forge, and applies only when an approval stands for the
|
|
15
|
-
* digest it plans. With `requireReview` (
|
|
16
|
-
*
|
|
23
|
+
* digest it plans. With `requireReview` (off by default since #4168, on
|
|
24
|
+
* under the `review-rules` security group) the approver must also have
|
|
25
|
+
* approved the pull request on the forge. It needs write access
|
|
17
26
|
* to `chant/lifecycle`, to record the pending fact when the gate stops it.
|
|
18
27
|
* One apply runs at a time per environment.
|
|
19
28
|
*
|
|
@@ -35,6 +44,7 @@
|
|
|
35
44
|
* it too. With `reportUrl` both pass `--report-url`, which copies the report,
|
|
36
45
|
* the change set and the events where a reader outside CI can fetch them;
|
|
37
46
|
* the job's own environment brings the credentials an `s3://` URL needs.
|
|
47
|
+
* The record job passes the same flags as the apply.
|
|
38
48
|
*
|
|
39
49
|
* With `build` (#4088) both jobs run the project's build and `chant lint`
|
|
40
50
|
* before their command, and fail on an error. A `cfn-deploy` member's plan
|
|
@@ -123,8 +133,16 @@ export interface PrLoopPipelineOptions {
|
|
|
123
133
|
* GitHub and Forgejo; on GitLab, the project's default branch.
|
|
124
134
|
*/
|
|
125
135
|
branch?: string;
|
|
126
|
-
/** Count an approval only from someone who approved the pull request on the forge. Default
|
|
136
|
+
/** Count an approval only from someone who approved the pull request on the forge. Default false (#4168): review rules are opt-in. */
|
|
127
137
|
requireReview?: boolean;
|
|
138
|
+
/** The job on merge (apply, or record since #4168) has `chant ci run` refuse to start without a runner-start statement for it (`--require-runner-start`, #4147), as under the `isolated-runners` security group (#4168). Default false. */
|
|
139
|
+
requireRunnerStart?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Apply the approved plan on merge (#4168), as every pull-request pipeline
|
|
142
|
+
* did before #4168: the apply job in place of the record job. Default
|
|
143
|
+
* false, so a merge deploys nothing. `waveGate` needs it.
|
|
144
|
+
*/
|
|
145
|
+
applyOnMerge?: boolean;
|
|
128
146
|
/**
|
|
129
147
|
* Stream each job's run events (#4007): `--events-json` on both commands,
|
|
130
148
|
* and `CHANT_RUN_EVENTS_DIR` set to {@link PR_LOOP_EVENTS_DIR}. Default false.
|
|
@@ -166,8 +184,8 @@ export interface PrLoopPipelineOptions {
|
|
|
166
184
|
|
|
167
185
|
/** One job's command and the forge values it reads, as environment variables for its step. */
|
|
168
186
|
export interface PrLoopJob {
|
|
169
|
-
/** `plan` or `
|
|
170
|
-
jobName: "plan" | "apply";
|
|
187
|
+
/** `plan`, `apply` or `record` (#4168). */
|
|
188
|
+
jobName: "plan" | "apply" | "record";
|
|
171
189
|
/** The command line. Reads `$BASE_SHA` and, for the plan, `$PR_NUMBER`. */
|
|
172
190
|
command: string;
|
|
173
191
|
/**
|
|
@@ -200,9 +218,10 @@ export function prLoopBuildLines(env: string, options: PrLoopPipelineOptions = {
|
|
|
200
218
|
}
|
|
201
219
|
|
|
202
220
|
/**
|
|
203
|
-
* The
|
|
204
|
-
*
|
|
205
|
-
*
|
|
221
|
+
* The jobs for `forge`: plan, apply and record, in that order. A generator
|
|
222
|
+
* writes the plan and, on merge, {@link prLoopMergeJob}'s choice of the
|
|
223
|
+
* other two. `member` is the workspace member the pipeline is generated for;
|
|
224
|
+
* the commands then run in its directory, so `--output` is relative to it.
|
|
206
225
|
*/
|
|
207
226
|
export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelineOptions = {}, member?: string): PrLoopJob[] {
|
|
208
227
|
const gate = options.gate ?? PR_APPLY_GATE;
|
|
@@ -220,13 +239,14 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
|
|
|
220
239
|
...(options.events ? { CHANT_RUN_EVENTS_DIR: PR_LOOP_EVENTS_DIR } : {}),
|
|
221
240
|
};
|
|
222
241
|
// #4156: the flags `chant ci run` rebuilds `command` from. It finds the member from the directory it runs in.
|
|
223
|
-
const ciRun = (job: "
|
|
242
|
+
const ciRun = (job: PrLoopJob["jobName"]): string[] => [
|
|
224
243
|
"chant", "ci", "run", job, "--in-place", ...(job === "plan" ? ["--no-push"] : []),
|
|
225
244
|
"--base", '"$BASE_SHA"', ...(job === "plan" ? ["--pr", '"$PR_NUMBER"'] : []),
|
|
226
245
|
"--env", env, "--gate", gate, "--forge", forge,
|
|
227
|
-
...(options.waveGate ? ["--wave-gate", options.waveGate] : []),
|
|
228
|
-
...(job
|
|
229
|
-
...(job === "apply" && options.requireReview ===
|
|
246
|
+
...(job !== "record" && options.waveGate ? ["--wave-gate", options.waveGate] : []),
|
|
247
|
+
...(job !== "plan" && options.branch ? ["--branch", JSON.stringify(options.branch)] : []),
|
|
248
|
+
...(job === "apply" && options.requireReview === true ? ["--require-review"] : []),
|
|
249
|
+
...(job !== "plan" && options.requireRunnerStart === true ? ["--require-runner-start"] : []),
|
|
230
250
|
...(options.events ? ["--events-json"] : []),
|
|
231
251
|
...(options.reportUrl ? ["--report-url", JSON.stringify(options.reportUrl)] : []),
|
|
232
252
|
];
|
|
@@ -248,7 +268,7 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
|
|
|
248
268
|
jobName: "apply",
|
|
249
269
|
command: [
|
|
250
270
|
"chant", "components", "pr-apply", "--base", '"$BASE_SHA"', ...common,
|
|
251
|
-
...(options.requireReview ===
|
|
271
|
+
...(options.requireReview === true ? ["--require-review"] : []),
|
|
252
272
|
...(options.waveGate ? ["--wave-gate", options.waveGate] : []),
|
|
253
273
|
"--resume", PR_APPLY_RECORD,
|
|
254
274
|
].join(" "),
|
|
@@ -257,9 +277,22 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
|
|
|
257
277
|
...build,
|
|
258
278
|
run: { command: ciRun("apply").join(" "), env: runEnv(applyEnv) },
|
|
259
279
|
},
|
|
280
|
+
{
|
|
281
|
+
jobName: "record",
|
|
282
|
+
command: ["chant", "components", "pr-record", "--base", '"$BASE_SHA"', ...common].join(" "),
|
|
283
|
+
env: applyEnv,
|
|
284
|
+
setup: [github ? githubBaseFallback(options.branch) : gitlabBaseFallback(options.branch)],
|
|
285
|
+
...build,
|
|
286
|
+
run: { command: ciRun("record").join(" "), env: runEnv(applyEnv) },
|
|
287
|
+
},
|
|
260
288
|
];
|
|
261
289
|
}
|
|
262
290
|
|
|
291
|
+
/** The job a push to the target branch runs (#4168): `record`, or `apply` when the pipeline opts into apply on merge. */
|
|
292
|
+
export function prLoopMergeJob(jobs: readonly PrLoopJob[], options: PrLoopPipelineOptions = {}): PrLoopJob {
|
|
293
|
+
return jobs.find((j) => j.jobName === (options.applyOnMerge ? "apply" : "record"))!;
|
|
294
|
+
}
|
|
295
|
+
|
|
263
296
|
/**
|
|
264
297
|
* The pull-request options with `--wave-gate` beside `--pr-loop` (#4003):
|
|
265
298
|
* the apply gets a gate per wave. A canary or a fixed base is refused, since
|
|
@@ -268,6 +301,9 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
|
|
|
268
301
|
*/
|
|
269
302
|
export function prLoopWithWaves(prLoop: PrLoopPipelineOptions, gatedWaves?: GatedWavePipelineOptions): PrLoopPipelineOptions {
|
|
270
303
|
if (!gatedWaves) return prLoop;
|
|
304
|
+
if (!prLoop.applyOnMerge) {
|
|
305
|
+
throw new Error("--wave-gate beside --pr-loop gates the apply on merge, so it needs --apply-on-merge (#4168); without it a merge records the plan and a release deploys it");
|
|
306
|
+
}
|
|
271
307
|
if (gatedWaves.canary?.length) throw new Error("a pull-request pipeline's waves are the change's derivation, so it takes no --canary; drop --canary");
|
|
272
308
|
if (gatedWaves.base !== undefined) throw new Error("a pull-request pipeline measures each push from the commit before it, so it takes no --base; drop --base");
|
|
273
309
|
return { ...prLoop, waveGate: gatedWaves.gate };
|
|
@@ -310,6 +346,11 @@ export function prApplyGroup(env: string, member?: string): string {
|
|
|
310
346
|
return member ? `chant-apply.${member}.${env}` : `chant-apply-${env}`;
|
|
311
347
|
}
|
|
312
348
|
|
|
349
|
+
/** The group that keeps one record job at a time per environment and member (#4168), named as {@link prApplyGroup} names the apply's. */
|
|
350
|
+
export function prRecordGroup(env: string, member?: string): string {
|
|
351
|
+
return member ? `chant-record.${member}.${env}` : `chant-record-${env}`;
|
|
352
|
+
}
|
|
353
|
+
|
|
313
354
|
/**
|
|
314
355
|
* The cache key the apply job keeps its attempt record under (#3543), with
|
|
315
356
|
* `commit` the forge's expression for the pushed commit. A push of another
|
package/src/components/pr-run.ts
CHANGED
|
@@ -737,7 +737,7 @@ export function waveNotCovered(approved: ChangeSetDocument, fresh: ChangeSetDocu
|
|
|
737
737
|
}
|
|
738
738
|
|
|
739
739
|
export interface PrReportInput {
|
|
740
|
-
stage: "plan" | "apply";
|
|
740
|
+
stage: "plan" | "apply" | "record";
|
|
741
741
|
pr: number | null;
|
|
742
742
|
env: string;
|
|
743
743
|
/** The workspace member whose pipeline runs the stage (#3465). */
|