@intentius/chant 0.124.32 → 0.124.33
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 +16 -1
- 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 +81 -21
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +23 -1
- package/dist/cli/registry.d.ts +18 -4
- 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/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-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-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 +235 -228
- package/dist/workspace/check.schema.json +70 -2
- package/dist/workspace/checks/security.d.ts +35 -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 +5 -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-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/declaration.d.ts +17 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +3 -0
- package/dist/workspace/declaration.schema.json +23 -0
- 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 +17 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +18 -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/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 +16 -1
- 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 +81 -21
- package/src/cli/parse-args.ts +17 -1
- package/src/cli/registry.ts +18 -4
- 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/release-compile.test.ts +34 -0
- package/src/components/release-compile.ts +89 -8
- 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-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/check-contract.test.ts +3 -2
- package/src/workspace/check.schema.json +70 -2
- package/src/workspace/checks/security.test.ts +138 -0
- package/src/workspace/checks/security.ts +305 -0
- package/src/workspace/checks.ts +5 -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-last-green.schema.json +49 -0
- package/src/workspace/ci-list.schema.json +35 -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/declaration.schema.json +23 -0
- package/src/workspace/declaration.test.ts +17 -0
- package/src/workspace/declaration.ts +22 -1
- 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 +2 -0
- package/src/workspace/reason-codes.ts +18 -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/security.test.ts +70 -0
- package/src/workspace/security.ts +118 -0
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). */
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
buildReleaseArtifacts,
|
|
14
14
|
buildStepsOf,
|
|
15
15
|
compileEnvironment,
|
|
16
|
+
recordedInputRefs,
|
|
16
17
|
ReleaseCompileError,
|
|
17
18
|
templateRefs,
|
|
18
19
|
withOnlyArchiveSteps,
|
|
@@ -49,6 +50,18 @@ describe("templateRefs", () => {
|
|
|
49
50
|
});
|
|
50
51
|
});
|
|
51
52
|
|
|
53
|
+
describe("recordedInputRefs (#4155)", () => {
|
|
54
|
+
test("adds each kind's own fields to the template fields: a chart; a missing one need not exist", () => {
|
|
55
|
+
expect(recordedInputRefs(api).map((r) => [r.kind, r.field, r.path, r.required])).toEqual([
|
|
56
|
+
["cfn-deploy", "template", "dist/api.template.json", true],
|
|
57
|
+
["cfn-deploy", "template", "dist/edge.template.json", true],
|
|
58
|
+
["helm-upgrade", "values", "values/base.yaml", true],
|
|
59
|
+
["helm-upgrade", "chart", "charts/api", false],
|
|
60
|
+
["kubectl-apply", "manifest", "k8s/api.yaml", true],
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
52
65
|
describe("withOnlyArchiveSteps", () => {
|
|
53
66
|
test("keeps the archive steps alone, drops gates and every other step, and the dependencies", () => {
|
|
54
67
|
expect(withOnlyArchiveSteps(api)).toEqual({
|
|
@@ -100,6 +113,27 @@ describe("compileEnvironment", () => {
|
|
|
100
113
|
});
|
|
101
114
|
});
|
|
102
115
|
|
|
116
|
+
test("records a chart directory file by file and names it in trees; a file that is not UTF-8 as base64 (#4155)", async () => {
|
|
117
|
+
await withTestDir(async (dir) => {
|
|
118
|
+
writeFileSync(
|
|
119
|
+
join(dir, "web.component.ts"),
|
|
120
|
+
`export const web = { name: "web", dependsOn: [], deploy: [{ phase: "Deploy", steps: [
|
|
121
|
+
{ kind: "helm-upgrade", release: "web", chart: "chart" },
|
|
122
|
+
{ kind: "helm-upgrade", release: "remote", chart: "bitnami/nginx", repo: "https://charts.example" },
|
|
123
|
+
] }] };`,
|
|
124
|
+
);
|
|
125
|
+
mkdirSync(join(dir, "chart", "charts"), { recursive: true });
|
|
126
|
+
writeFileSync(join(dir, "chart", "Chart.yaml"), "name: web\n");
|
|
127
|
+
writeFileSync(join(dir, "chart", "charts", "dep.tgz"), Buffer.from([0x1f, 0x8b, 0xff, 0x00]));
|
|
128
|
+
const target = await compileEnvironment({ projectDir: dir }, "prod");
|
|
129
|
+
expect(target.trees).toEqual([{ component: "web", path: "chart" }]);
|
|
130
|
+
expect(target.templates).toEqual([
|
|
131
|
+
{ component: "web", path: "chart/Chart.yaml", content: "name: web\n" },
|
|
132
|
+
{ component: "web", path: "chart/charts/dep.tgz", content: Buffer.from([0x1f, 0x8b, 0xff, 0x00]).toString("base64"), encoding: "base64" },
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
103
137
|
test("refuses when a template a deploy names does not exist after the build", async () => {
|
|
104
138
|
await withTestDir(async (dir) => {
|
|
105
139
|
project(dir);
|
|
@@ -15,7 +15,14 @@
|
|
|
15
15
|
* ./promote.ts: a literal path in a deploy step's `template`, `manifest` or
|
|
16
16
|
* `values` field ({@link TEMPLATE_FIELDS}), read after the environment's build
|
|
17
17
|
* command has run. A path with the `archive:` prefix names the file after it.
|
|
18
|
-
*
|
|
18
|
+
*
|
|
19
|
+
* Since #4155 a cut also records the other local paths a deploy step names
|
|
20
|
+
* ({@link recordedInputRefs}: a Helm chart, a kustomize directory, a sync's
|
|
21
|
+
* source tree, the fields `./deploy-inputs.ts` lists). A directory is
|
|
22
|
+
* recorded file by file and named in the target's `trees`; a file that is not
|
|
23
|
+
* UTF-8 is recorded as base64. Such a path that does not exist after the
|
|
24
|
+
* build, as a chart named from a repository, is not recorded, and the
|
|
25
|
+
* promote leaves it as written.
|
|
19
26
|
*
|
|
20
27
|
* A project with no components (#4046) is compiled the way `chant build --env
|
|
21
28
|
* <env> -o <output>` builds it ({@link compileBuildOutput}): every file that
|
|
@@ -25,6 +32,7 @@
|
|
|
25
32
|
*/
|
|
26
33
|
|
|
27
34
|
import { spawn, spawnSync } from "node:child_process";
|
|
35
|
+
import { isUtf8 } from "node:buffer";
|
|
28
36
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
29
37
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
30
38
|
import { loadChantConfig, type ChantConfig } from "../config";
|
|
@@ -40,7 +48,8 @@ import { memoryGateLedgerPort } from "../op/gate";
|
|
|
40
48
|
import type { ReleaseArtifact, ReleaseParam } from "../lifecycle/release-store";
|
|
41
49
|
|
|
42
50
|
/** The deploy step fields whose literal file paths are a component's templates. */
|
|
43
|
-
export
|
|
51
|
+
export { TEMPLATE_FIELDS } from "./deploy-inputs";
|
|
52
|
+
import { DEPLOY_INPUT_FIELDS, directoryFiles, ignoredByGit, TEMPLATE_FIELDS, walkFiles } from "./deploy-inputs";
|
|
44
53
|
|
|
45
54
|
/** The prefix a step may write before an archive-relative template path. */
|
|
46
55
|
const ARCHIVE_PREFIX = "archive:";
|
|
@@ -61,7 +70,9 @@ export interface TemplateRef {
|
|
|
61
70
|
export interface CompiledTemplate {
|
|
62
71
|
component: string | null;
|
|
63
72
|
path: string;
|
|
73
|
+
/** The text, or the base64 of the bytes under `encoding: "base64"`. */
|
|
64
74
|
content: string;
|
|
75
|
+
encoding?: "base64";
|
|
65
76
|
}
|
|
66
77
|
|
|
67
78
|
/** What one environment compiles to. */
|
|
@@ -84,6 +95,8 @@ export interface CompiledTarget {
|
|
|
84
95
|
/** For each component, the file that declares it, relative to the member's directory. */
|
|
85
96
|
componentFiles: Record<string, string>;
|
|
86
97
|
templates: CompiledTemplate[];
|
|
98
|
+
/** The directories recorded file by file in `templates` (#4155). Absent from a compiler written before #4155. */
|
|
99
|
+
trees?: Array<{ component: string | null; path: string }>;
|
|
87
100
|
}
|
|
88
101
|
|
|
89
102
|
/** Compiles a member for one environment, and builds its artifacts. The cut talks to a compiler only, so a test can hand it fixtures. */
|
|
@@ -134,6 +147,40 @@ export function templateRefs(component: DriverComponent): TemplateRef[] {
|
|
|
134
147
|
return out;
|
|
135
148
|
}
|
|
136
149
|
|
|
150
|
+
/** A path a deploy step names that a cut records (#4155): a {@link TemplateRef}, and whether its field is a template field, which must exist. */
|
|
151
|
+
export interface RecordedInputRef extends TemplateRef {
|
|
152
|
+
/** True for `template`, `manifest` and `values`, whose path must exist after the build. */
|
|
153
|
+
required: boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Every literal path a component's deploy names that a cut records: {@link templateRefs}, plus each kind's own fields in `DEPLOY_INPUT_FIELDS`. */
|
|
157
|
+
export function recordedInputRefs(component: DriverComponent): RecordedInputRef[] {
|
|
158
|
+
const out: RecordedInputRef[] = [];
|
|
159
|
+
for (const { phase, step } of stepsOf(component.deploy)) {
|
|
160
|
+
if (BUILD_STEP_KINDS.includes(step.kind) || PUBLISH_STEP_KINDS.includes(step.kind)) continue;
|
|
161
|
+
for (const field of recordedFields(step.kind)) {
|
|
162
|
+
const raw = step[field];
|
|
163
|
+
for (const value of Array.isArray(raw) ? raw : [raw]) {
|
|
164
|
+
if (!isLiteralPath(value)) continue;
|
|
165
|
+
const path = value.startsWith(ARCHIVE_PREFIX) ? value.slice(ARCHIVE_PREFIX.length) : value;
|
|
166
|
+
out.push({ component: component.name, phase, kind: step.kind, field, value, path, required: TEMPLATE_FIELDS.includes(field) });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The fields of a step of `kind` whose paths a cut records and a promote points at the recorded copy. */
|
|
174
|
+
export function recordedFields(kind: string): string[] {
|
|
175
|
+
return [...TEMPLATE_FIELDS, ...(DEPLOY_INPUT_FIELDS[kind] ?? [])];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A file's bytes as a template: its text when it is UTF-8, else base64. */
|
|
179
|
+
export function templateOf(component: string | null, path: string, file: string): CompiledTemplate {
|
|
180
|
+
const bytes = readFileSync(file);
|
|
181
|
+
return isUtf8(bytes) ? { component, path, content: bytes.toString("utf-8") } : { component, path, content: bytes.toString("base64"), encoding: "base64" };
|
|
182
|
+
}
|
|
183
|
+
|
|
137
184
|
/** A component's build-time steps ({@link BUILD_STEP_KINDS}), each as declared, in composition order. */
|
|
138
185
|
export function buildStepsOf(component: DriverComponent): DriverStep[] {
|
|
139
186
|
return stepsOf(component.deploy).map((s) => s.step).filter((s) => BUILD_STEP_KINDS.includes(s.kind));
|
|
@@ -191,8 +238,19 @@ export interface CompileInputs {
|
|
|
191
238
|
output?: string;
|
|
192
239
|
/** For a project with no components: build only this lexicon, as `chant build --lexicon` does. */
|
|
193
240
|
lexicon?: string;
|
|
241
|
+
/**
|
|
242
|
+
* The release's label (#4168, decision 5), passed to every compile and
|
|
243
|
+
* artifact build as `CHANT_RELEASE_LABEL`, so a version reaches the build
|
|
244
|
+
* without a version-bump commit. A build parameter reads it by declaring
|
|
245
|
+
* `env: "CHANT_RELEASE_LABEL"`, and a build step's own tool (cargo, npm)
|
|
246
|
+
* reads the variable. Unset for a cut with no label.
|
|
247
|
+
*/
|
|
248
|
+
releaseLabel?: string;
|
|
194
249
|
}
|
|
195
250
|
|
|
251
|
+
/** The variable a cut's label reaches the build through (#4168). */
|
|
252
|
+
export const RELEASE_LABEL_ENV = "CHANT_RELEASE_LABEL";
|
|
253
|
+
|
|
196
254
|
/**
|
|
197
255
|
* Where a project with no components writes its build output when the cut
|
|
198
256
|
* names no `-o`. YAML, since the k8s and helm serializers write YAML whatever
|
|
@@ -258,21 +316,33 @@ export async function compileEnvironment(inputs: CompileInputs, env: string): Pr
|
|
|
258
316
|
if (declared === 0) return compileBuildOutput(inputs, env, config);
|
|
259
317
|
const rel = (p: string): string => (isAbsolute(p) ? relative(inputs.projectDir, p) : p);
|
|
260
318
|
const templates: CompiledTemplate[] = [];
|
|
319
|
+
const trees: Array<{ component: string; path: string }> = [];
|
|
261
320
|
const seen = new Set<string>();
|
|
262
321
|
for (const { component } of components) {
|
|
263
|
-
for (const ref of
|
|
322
|
+
for (const ref of recordedInputRefs(component)) {
|
|
264
323
|
const key = `${ref.component}\u0000${ref.path}`;
|
|
265
324
|
if (seen.has(key)) continue;
|
|
266
325
|
seen.add(key);
|
|
267
326
|
const file = resolve(inputs.projectDir, ref.path);
|
|
268
327
|
if (!existsSync(file)) {
|
|
328
|
+
if (!ref.required) continue;
|
|
269
329
|
throw new ReleaseCompileError(
|
|
270
330
|
`component "${ref.component}": its ${ref.kind} step in phase "${ref.phase}" reads ${ref.field} "${ref.value}", ` +
|
|
271
331
|
`and no such file exists after the build for "${env}"`,
|
|
272
332
|
);
|
|
273
333
|
}
|
|
274
|
-
if (
|
|
275
|
-
|
|
334
|
+
if (statSync(file).isFile()) {
|
|
335
|
+
templates.push(templateOf(ref.component, ref.path, file));
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
// #4155: a directory, file by file. One git ignores is the build's output, recorded whole.
|
|
339
|
+
const files = ignoredByGit(inputs.projectDir, ref.path) ? walkFiles(inputs.projectDir, ref.path) : directoryFiles(inputs.projectDir, ref.path);
|
|
340
|
+
const dir = ref.path.replace(/\/+$/, "");
|
|
341
|
+
for (const f of files) {
|
|
342
|
+
const under = relative(resolve(inputs.projectDir, dir), resolve(inputs.projectDir, f)).split("\\").join("/");
|
|
343
|
+
templates.push(templateOf(ref.component, `${dir}/${under}`, resolve(inputs.projectDir, f)));
|
|
344
|
+
}
|
|
345
|
+
trees.push({ component: ref.component, path: dir });
|
|
276
346
|
}
|
|
277
347
|
}
|
|
278
348
|
return {
|
|
@@ -284,6 +354,7 @@ export async function compileEnvironment(inputs: CompileInputs, env: string): Pr
|
|
|
284
354
|
buildInputs: Object.fromEntries(components.map(({ component }) => [component.name, buildInputPaths(component)])),
|
|
285
355
|
componentFiles: Object.fromEntries(components.map(({ component, file }) => [component.name, rel(file)])),
|
|
286
356
|
templates,
|
|
357
|
+
trees,
|
|
287
358
|
};
|
|
288
359
|
}
|
|
289
360
|
|
|
@@ -408,10 +479,20 @@ export function ownChantCommand(): string[] {
|
|
|
408
479
|
return [process.execPath, ...process.execArgv, main];
|
|
409
480
|
}
|
|
410
481
|
|
|
411
|
-
function runChild(
|
|
482
|
+
function runChild(
|
|
483
|
+
command: readonly string[],
|
|
484
|
+
args: readonly string[],
|
|
485
|
+
cwd: string,
|
|
486
|
+
env: string,
|
|
487
|
+
releaseLabel?: string,
|
|
488
|
+
): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
412
489
|
return new Promise((resolvePromise, reject) => {
|
|
413
490
|
const [bin, ...lead] = command;
|
|
414
|
-
const
|
|
491
|
+
const childEnv: NodeJS.ProcessEnv = { ...process.env, CHANT_ENV: env };
|
|
492
|
+
// A label never leaks in from the caller's shell: only the cut sets it.
|
|
493
|
+
delete childEnv[RELEASE_LABEL_ENV];
|
|
494
|
+
if (releaseLabel !== undefined) childEnv[RELEASE_LABEL_ENV] = releaseLabel;
|
|
495
|
+
const child = spawn(bin, [...lead, ...args], { cwd, stdio: ["ignore", "pipe", "pipe"], env: childEnv });
|
|
415
496
|
const out: Buffer[] = [];
|
|
416
497
|
const err: Buffer[] = [];
|
|
417
498
|
child.stdout.on("data", (c: Buffer) => out.push(c));
|
|
@@ -440,7 +521,7 @@ export function spawnTargetCompiler(inputs: CompileInputs, command: readonly str
|
|
|
440
521
|
return args;
|
|
441
522
|
};
|
|
442
523
|
const call = async (env: string, extra: string[]): Promise<unknown> => {
|
|
443
|
-
const run = await runChild(command, ["release", "compile", "--env", env, ...common(), ...extra, "--json"], inputs.projectDir, env);
|
|
524
|
+
const run = await runChild(command, ["release", "compile", "--env", env, ...common(), ...extra, "--json"], inputs.projectDir, env, inputs.releaseLabel);
|
|
444
525
|
if (run.code !== 0) throw new ReleaseCompileError(`compiling "${env}" failed (exit ${run.code})`);
|
|
445
526
|
try {
|
|
446
527
|
return JSON.parse(run.stdout);
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
import { describe, test, expect } from "vitest";
|
|
7
7
|
import { contentDigest } from "../content-digest";
|
|
8
|
-
import { makeRelease } from "../lifecycle/release-store";
|
|
8
|
+
import { makeRelease, templateBytesDigest } from "../lifecycle/release-store";
|
|
9
9
|
import type { DriverComponent } from "./driver";
|
|
10
10
|
import { isImplicitRelease, pinReleaseTemplates, releaseArtifactMismatches, removePinnedTemplates, targetFiles, targetFor, writePinnedTemplates, RELEASE_TEMPLATES_DIR } from "./release-promote";
|
|
11
11
|
import type { ReleaseRecord } from "../lifecycle/release-ledger";
|
|
12
12
|
import type { BuildArchiveManifest } from "./verbs/build-archive";
|
|
13
13
|
import { withTestDir } from "@intentius/chant-test-utils";
|
|
14
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
|
|
17
17
|
const prodTemplate = `{"env":"prod"}\n`;
|
|
@@ -63,6 +63,59 @@ describe("pinReleaseTemplates", () => {
|
|
|
63
63
|
expect("errors" in absent && absent.errors[0]).toMatch(/are not on the chant\/lifecycle branch/);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
test("a target cut since #4155 points a chart at its recorded tree, which a reader of the release can trust", async () => {
|
|
67
|
+
const chartFile = "name: web\n";
|
|
68
|
+
const tgz = Buffer.from([0x1f, 0x8b, 0xff]).toString("base64");
|
|
69
|
+
const tgzDigest = templateBytesDigest(tgz, "base64");
|
|
70
|
+
const cut = makeRelease({
|
|
71
|
+
member: null,
|
|
72
|
+
commit: "c".repeat(40),
|
|
73
|
+
artifactDigests: [],
|
|
74
|
+
targets: [{
|
|
75
|
+
env: "prod",
|
|
76
|
+
params: [],
|
|
77
|
+
templates: [
|
|
78
|
+
{ component: "web", path: "chart/Chart.yaml", digest: contentDigest(chartFile) },
|
|
79
|
+
{ component: "web", path: "chart/charts/dep.tgz", digest: tgzDigest, encoding: "base64" },
|
|
80
|
+
],
|
|
81
|
+
trees: [{ component: "web", path: "chart" }],
|
|
82
|
+
}],
|
|
83
|
+
});
|
|
84
|
+
const web: DriverComponent = {
|
|
85
|
+
name: "web",
|
|
86
|
+
deploy: [{ phase: "Deploy", steps: [
|
|
87
|
+
{ kind: "helm-upgrade", release: "web", chart: "chart" },
|
|
88
|
+
{ kind: "helm-upgrade", release: "remote", chart: "bitnami/nginx" },
|
|
89
|
+
] }],
|
|
90
|
+
};
|
|
91
|
+
const bytes: Record<string, string> = { [contentDigest(chartFile)]: chartFile, [tgzDigest]: tgz };
|
|
92
|
+
const pinned = await pinReleaseTemplates([web], cut, cut.targets[0]!, async (d, encoding) => (encoding === "base64" && d !== tgzDigest ? null : bytes[d] ?? null));
|
|
93
|
+
if ("errors" in pinned) throw new Error(pinned.errors.join("\n"));
|
|
94
|
+
const step = pinned.components[0]!.deploy[0]!.steps[0] as unknown as { chart: string };
|
|
95
|
+
expect(step.chart).toMatch(/^\.chant\/release-templates\/[^/]+\/chart$/);
|
|
96
|
+
expect(pinned.files.map((f) => f.path.slice(step.chart.length + 1)).sort()).toEqual(["Chart.yaml", "charts/dep.tgz"]);
|
|
97
|
+
expect(pinned.files.find((f) => f.path.endsWith("dep.tgz"))).toMatchObject({ encoding: "base64", content: tgz });
|
|
98
|
+
// A chart named from a repository is not a local path the release records, and is left as written.
|
|
99
|
+
expect(pinned.components[0]!.deploy[0]!.steps[1]).toEqual(web.deploy[0]!.steps[1]);
|
|
100
|
+
|
|
101
|
+
await withTestDir(async (dir) => {
|
|
102
|
+
writePinnedTemplates(dir, pinned.files);
|
|
103
|
+
expect(readFileSync(join(dir, step.chart, "charts", "dep.tgz"))).toEqual(Buffer.from([0x1f, 0x8b, 0xff]));
|
|
104
|
+
removePinnedTemplates(dir, pinned.files);
|
|
105
|
+
expect(existsSync(join(dir, RELEASE_TEMPLATES_DIR))).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const edited = await pinReleaseTemplates([web], cut, cut.targets[0]!, async (d) => (d === contentDigest(chartFile) ? "name: edited\n" : bytes[d] ?? null));
|
|
109
|
+
expect("errors" in edited && edited.errors.join("\n")).toMatch(/template "chart\/Chart.yaml" hashes to .* refusing to deploy it/);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("a target cut before #4155 (no trees) is pinned as then: the chart is read from the checkout", async () => {
|
|
113
|
+
const web: DriverComponent = { name: "api", deploy: [{ phase: "Deploy", steps: [{ kind: "helm-upgrade", release: "web", chart: "chart" }, ...api.deploy[1]!.steps] }] };
|
|
114
|
+
const pinned = await pinReleaseTemplates([web], release, prod, store(prodTemplate));
|
|
115
|
+
if ("errors" in pinned) throw new Error(pinned.errors.join("\n"));
|
|
116
|
+
expect(pinned.components[0]!.deploy[0]!.steps[0]).toEqual({ kind: "helm-upgrade", release: "web", chart: "chart" });
|
|
117
|
+
});
|
|
118
|
+
|
|
66
119
|
test("targetFor names the environments a release was compiled for", () => {
|
|
67
120
|
expect(targetFor(release, "prod")).toEqual({ target: prod });
|
|
68
121
|
expect(targetFor(release, "qa")).toEqual({ error: expect.stringMatching(/has no target for "qa"; it was compiled for prod, staging/) });
|