@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
|
@@ -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);
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
emptyVerifier,
|
|
14
14
|
gateNamesOf,
|
|
15
15
|
planReleaseChangeSet,
|
|
16
|
+
planReleaseSet,
|
|
16
17
|
provisionalMembers,
|
|
17
18
|
RELEASE_GATE_OP,
|
|
18
19
|
} from "./release-gate";
|
|
@@ -162,6 +163,52 @@ describe("planReleaseChangeSet", () => {
|
|
|
162
163
|
});
|
|
163
164
|
});
|
|
164
165
|
|
|
166
|
+
describe("planReleaseSet: a dependent plans against its upstream's planned outputs (#4168 step 4)", () => {
|
|
167
|
+
const r1 = release(sha("7"));
|
|
168
|
+
const reg = () => {
|
|
169
|
+
const r = registry();
|
|
170
|
+
r.register({
|
|
171
|
+
kind: "tf",
|
|
172
|
+
async run() { return {}; },
|
|
173
|
+
async plan(_ctx: unknown, input: { name: string; vpc?: unknown }) {
|
|
174
|
+
return {
|
|
175
|
+
member: input.name,
|
|
176
|
+
planDigest: sha(input.vpc === "vpc-new" ? "b" : input.vpc === undefined ? "c" : "a"),
|
|
177
|
+
outputs: input.name === "net" ? { known: { id: "vpc-new" }, unknown: ["arn"] } : undefined,
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
async outputs() { return { id: "vpc-old", arn: "arn:old" }; },
|
|
181
|
+
} as never);
|
|
182
|
+
return r;
|
|
183
|
+
};
|
|
184
|
+
const net: DriverComponent = { name: "net", deploy: [{ phase: "Apply", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "tf", name: "net" }] }] };
|
|
185
|
+
const svc = (ref: string): DriverComponent => ({
|
|
186
|
+
name: "svc",
|
|
187
|
+
dependsOn: ["net"],
|
|
188
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "tf", name: "svc", vpc: { stackOutput: { stack: "net", name: ref } } }] }],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("an upstream the promote plans is read from its plan, and the read is kept", async () => {
|
|
192
|
+
const set = await planReleaseSet({ release: r1, env: "prod", components: [svc("id"), net], digests: new Map(), registry: reg(), current: new Map() });
|
|
193
|
+
expect(set.doc.members.find((m) => m.member === "svc")?.planDigest).toBe(sha("b"));
|
|
194
|
+
expect(set.reads.svc).toEqual({ net: { id: "vpc-new" } });
|
|
195
|
+
expect(set.plannedOutputs).toEqual({ net: ["id"] });
|
|
196
|
+
expect(set.unknownInputs).toBeUndefined();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("an output the upstream's plan can't say is an unknown input", async () => {
|
|
200
|
+
const set = await planReleaseSet({ release: r1, env: "prod", components: [svc("arn"), net], digests: new Map(), registry: reg(), current: new Map() });
|
|
201
|
+
expect(set.unknownInputs).toEqual({ svc: ["net.arn"] });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("an upstream outside the promote is read as it stands", async () => {
|
|
205
|
+
const set = await planReleaseSet({ release: r1, env: "prod", components: [svc("id"), net], digests: new Map(), registry: reg(), current: new Map(), only: new Set(["svc"]), seededOutputs: { net: { id: "vpc-old" } } });
|
|
206
|
+
expect(set.doc.members.map((m) => m.member)).toEqual(["svc"]);
|
|
207
|
+
expect(set.reads.svc).toEqual({ net: { id: "vpc-old" } });
|
|
208
|
+
expect(set.plannedOutputs).toBeUndefined();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
165
212
|
describe("empty change sets", () => {
|
|
166
213
|
const r1 = release(sha("7"));
|
|
167
214
|
const emptyDoc = (): ChangeSetDocument =>
|
|
@@ -39,6 +39,14 @@
|
|
|
39
39
|
* planned by that capability, as a pull request's plan is. A plan that
|
|
40
40
|
* returns no change-set part is a hole, since nothing says what it changes.
|
|
41
41
|
*
|
|
42
|
+
* A dependency the promote also deploys is read from its plan, not from the
|
|
43
|
+
* estate (#4168 step 4, the rule #3999 gave a pull request): a dependent is
|
|
44
|
+
* planned against the outputs its upstream's plan says it will expose, and a
|
|
45
|
+
* reference the plan can't say is listed in `unknownInputs`. Any other
|
|
46
|
+
* dependency is read as it stands. What each component was planned against is
|
|
47
|
+
* kept in `reads`, so the promote can hold it back when an upstream applies
|
|
48
|
+
* as another value (`./promote-rounds.ts`).
|
|
49
|
+
*
|
|
42
50
|
* Any other component is a `chant` member compared with what the
|
|
43
51
|
* environment runs now: its latest deploy record and the release that record
|
|
44
52
|
* names. The artifact digest, each template the release's target records for
|
|
@@ -52,8 +60,8 @@
|
|
|
52
60
|
* recorded since (#4035): its record names what ran before that run.
|
|
53
61
|
*/
|
|
54
62
|
|
|
55
|
-
import type { CapabilityRegistry } from "./capability";
|
|
56
|
-
import { deployContext, resolveComponentGraph, resolveStepInput, type DriverComponent, type DriverGate, type DriverPhase, type DriverStep } from "./driver";
|
|
63
|
+
import type { CapabilityRegistry, PlannedOutputs } from "./capability";
|
|
64
|
+
import { deployContext, resolveComponentGraph, resolveStepInput, unresolvedWiringReferences, type DriverComponent, type DriverGate, type DriverPhase, type DriverStep } from "./driver";
|
|
57
65
|
import { componentPlanDigest } from "./gate-plan";
|
|
58
66
|
import { gateName, RELEASE_GATE_OP } from "../op/gate-name";
|
|
59
67
|
import { capabilitySteps, planContext, readComponentOutputs } from "./wave-plan";
|
|
@@ -123,6 +131,23 @@ export interface ReleaseChangeSetInput {
|
|
|
123
131
|
seededOutputs?: Record<string, Record<string, unknown>>;
|
|
124
132
|
/** The chant version to record in the document. */
|
|
125
133
|
chant?: string;
|
|
134
|
+
/**
|
|
135
|
+
* Plan only these components (a later round or wave, ./promote-rounds.ts).
|
|
136
|
+
* The others are not planned; a dependency among them is read from
|
|
137
|
+
* `seededOutputs`, which then holds what the run left. Default: every one.
|
|
138
|
+
*/
|
|
139
|
+
only?: ReadonlySet<string>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** A release's change set for an environment, and what each component was planned against. */
|
|
143
|
+
export interface ReleasePlan {
|
|
144
|
+
doc: ChangeSetDocument;
|
|
145
|
+
/** Per planned component, the outputs of its dependencies as its plan read them. */
|
|
146
|
+
reads: Record<string, Record<string, Record<string, unknown>>>;
|
|
147
|
+
/** Per dependency read from its plan, the names of the outputs the plan knows, sorted. Absent when none. */
|
|
148
|
+
plannedOutputs?: Record<string, string[]>;
|
|
149
|
+
/** Per component, the references to a promoted upstream that no value stands for until it applies, sorted. Absent when none. */
|
|
150
|
+
unknownInputs?: Record<string, string[]>;
|
|
126
151
|
}
|
|
127
152
|
|
|
128
153
|
const byString = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
|
|
@@ -240,65 +265,105 @@ export function chantComponentPart(input: {
|
|
|
240
265
|
* that fails to plan: it becomes a failed member.
|
|
241
266
|
*/
|
|
242
267
|
export async function planReleaseChangeSet(input: ReleaseChangeSetInput): Promise<ChangeSetDocument> {
|
|
268
|
+
return (await planReleaseSet(input)).doc;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* {@link planReleaseChangeSet}, with what each component read. Components are
|
|
273
|
+
* planned a wave at a time, so a dependency the promote deploys has planned
|
|
274
|
+
* before its dependents read its outputs.
|
|
275
|
+
*/
|
|
276
|
+
export async function planReleaseSet(input: ReleaseChangeSetInput): Promise<ReleasePlan> {
|
|
243
277
|
const { release, env, registry } = input;
|
|
244
278
|
const components = [...input.components];
|
|
245
279
|
const byName = new Map(components.map((c) => [c.name, c]));
|
|
246
280
|
const outputs: Record<string, Record<string, unknown>> = { ...(input.seededOutputs ?? {}) };
|
|
247
281
|
const read = new Set(Object.keys(outputs));
|
|
248
282
|
const parts: ChangeSetPart[] = [];
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
283
|
+
const plannedBy = new Map<string, PlannedOutputs | undefined>();
|
|
284
|
+
const planned: Record<string, string[]> = {};
|
|
285
|
+
const unknownInputs: Record<string, string[]> = {};
|
|
286
|
+
const reads: ReleasePlan["reads"] = {};
|
|
287
|
+
const inPlan = (name: string): boolean => input.only === undefined || input.only.has(name);
|
|
288
|
+
const isPlannable = (c: DriverComponent): boolean =>
|
|
289
|
+
capabilitySteps(c.deploy).some((s) => registry.has(s.kind) && registry.resolve(s.kind).plan !== undefined);
|
|
290
|
+
const { waves } = resolveComponentGraph(components.map((c) => ({ ...c, dependsOn: (c.dependsOn ?? []).filter((d) => byName.has(d)) })));
|
|
291
|
+
for (const wave of waves) {
|
|
292
|
+
const planning = wave.filter(inPlan);
|
|
293
|
+
for (const name of planning) {
|
|
294
|
+
const component = byName.get(name)!;
|
|
295
|
+
const digest = input.digests.get(name) ?? "";
|
|
296
|
+
const plannable = capabilitySteps(component.deploy).filter((s) => registry.has(s.kind) && registry.resolve(s.kind).plan !== undefined);
|
|
297
|
+
if (plannable.length === 0) {
|
|
298
|
+
parts.push(chantComponentPart({ component, release, env, digest, current: input.current.get(name), unsettled: input.unsettled?.get(name) }));
|
|
299
|
+
continue;
|
|
265
300
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
301
|
+
const ctx = deployContext({ env }, name);
|
|
302
|
+
try {
|
|
303
|
+
for (const dep of component.dependsOn ?? []) {
|
|
304
|
+
if (read.has(dep) || !byName.has(dep)) continue;
|
|
305
|
+
read.add(dep);
|
|
306
|
+
// A dependency this promote plans exposes what its plan says (#3999).
|
|
307
|
+
if (plannedBy.has(dep) && isPlannable(byName.get(dep)!)) {
|
|
308
|
+
const known = plannedBy.get(dep)?.known ?? {};
|
|
309
|
+
outputs[dep] = { ...known };
|
|
310
|
+
planned[dep] = Object.keys(known).sort();
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const found = await readComponentOutputs(byName.get(dep)!, registry, deployContext({ env }, dep), outputs);
|
|
314
|
+
if (found) outputs[dep] = found;
|
|
315
|
+
}
|
|
316
|
+
reads[name] = Object.fromEntries((component.dependsOn ?? []).filter((d) => d in outputs).map((d) => [d, structuredClone(outputs[d])]));
|
|
317
|
+
const unknown = new Set<string>();
|
|
318
|
+
let exposes: PlannedOutputs | undefined;
|
|
319
|
+
for (const step of plannable) {
|
|
320
|
+
const { kind, ...rest } = step;
|
|
321
|
+
for (const r of unresolvedWiringReferences(rest, outputs)) if (plannedBy.has(r.source) && !r.publish) unknown.add(r.ref);
|
|
322
|
+
const result = await registry.resolve(kind).plan!(planContext(ctx, rest, outputs), resolveStepInput(rest, {}, outputs) as never);
|
|
323
|
+
if (result.outputs) exposes = { known: { ...(exposes?.known ?? {}), ...result.outputs.known }, unknown: [...new Set([...(exposes?.unknown ?? []), ...result.outputs.unknown])].sort() };
|
|
324
|
+
parts.push(
|
|
325
|
+
result.changeSet ?? {
|
|
326
|
+
member: {
|
|
327
|
+
member: result.member,
|
|
328
|
+
lexicon: "chant",
|
|
329
|
+
planner: "chant",
|
|
330
|
+
status: "planned",
|
|
331
|
+
planDigest: result.planDigest,
|
|
332
|
+
holes: [{ address: result.member, reason: `its ${kind} plan describes no changes, so what it changes is unknown` }],
|
|
333
|
+
},
|
|
334
|
+
entries: [],
|
|
278
335
|
},
|
|
279
|
-
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
plannedBy.set(name, exposes);
|
|
339
|
+
if (unknown.size > 0) unknownInputs[name] = [...unknown].sort();
|
|
340
|
+
} catch (err) {
|
|
341
|
+
plannedBy.set(name, undefined);
|
|
342
|
+
parts.push({
|
|
343
|
+
member: {
|
|
344
|
+
member: name,
|
|
345
|
+
lexicon: "chant",
|
|
346
|
+
planner: "chant",
|
|
347
|
+
status: "failed",
|
|
348
|
+
error: err instanceof Error ? err.message : String(err),
|
|
349
|
+
planDigest: null,
|
|
350
|
+
holes: [],
|
|
280
351
|
},
|
|
281
|
-
|
|
352
|
+
entries: [],
|
|
353
|
+
});
|
|
282
354
|
}
|
|
283
|
-
} catch (err) {
|
|
284
|
-
parts.push({
|
|
285
|
-
member: {
|
|
286
|
-
member: name,
|
|
287
|
-
lexicon: "chant",
|
|
288
|
-
planner: "chant",
|
|
289
|
-
status: "failed",
|
|
290
|
-
error: err instanceof Error ? err.message : String(err),
|
|
291
|
-
planDigest: null,
|
|
292
|
-
holes: [],
|
|
293
|
-
},
|
|
294
|
-
entries: [],
|
|
295
|
-
});
|
|
296
355
|
}
|
|
297
356
|
}
|
|
298
|
-
|
|
357
|
+
const doc = composeChangeSet(parts, {
|
|
299
358
|
...(input.chant !== undefined ? { chant: input.chant } : {}),
|
|
300
359
|
planned: { releaseDigest: release.releaseDigest, environment: env },
|
|
301
360
|
});
|
|
361
|
+
return {
|
|
362
|
+
doc,
|
|
363
|
+
reads,
|
|
364
|
+
...(Object.keys(planned).length > 0 ? { plannedOutputs: planned } : {}),
|
|
365
|
+
...(Object.keys(unknownInputs).length > 0 ? { unknownInputs } : {}),
|
|
366
|
+
};
|
|
302
367
|
}
|
|
303
368
|
|
|
304
369
|
/** The members a release gate refuses: provisional ones, which no approval of the document covers (`change-set.mdx`). */
|
|
@@ -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/) });
|