@intentius/chant-lexicon-terraform 0.102.0 → 0.104.0
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/__fixtures__/terragrunt/affected-cases.d.ts +23 -0
- package/dist/__fixtures__/terragrunt/affected-cases.d.ts.map +1 -0
- package/dist/__fixtures__/terragrunt/record.d.ts +17 -0
- package/dist/__fixtures__/terragrunt/record.d.ts.map +1 -0
- package/dist/change-set.d.ts +67 -0
- package/dist/change-set.d.ts.map +1 -0
- package/dist/changed-roots.d.ts +30 -0
- package/dist/changed-roots.d.ts.map +1 -0
- package/dist/commands.d.ts +26 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/components/capability-plugin.d.ts +54 -0
- package/dist/components/capability-plugin.d.ts.map +1 -0
- package/dist/composites/terraform-pin-rollout-op.d.ts +44 -0
- package/dist/composites/terraform-pin-rollout-op.d.ts.map +1 -0
- package/dist/hcl/descend.d.ts +1 -1
- package/dist/hcl/descend.d.ts.map +1 -1
- package/dist/hcl/edges.d.ts +2 -2
- package/dist/hcl/edges.d.ts.map +1 -1
- package/dist/hcl/parse.d.ts +29 -1
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/roots.d.ts +1 -1
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/hcl/suppressions.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +13 -4
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/module-source.d.ts +27 -1
- package/dist/lint/post-synth/module-source.d.ts.map +1 -1
- package/dist/lint/post-synth/terragrunt.d.ts +37 -0
- package/dist/lint/post-synth/terragrunt.d.ts.map +1 -0
- package/dist/lint/post-synth/tf002.d.ts +3 -1
- package/dist/lint/post-synth/tf002.d.ts.map +1 -1
- package/dist/lint/post-synth/tf038.d.ts +22 -0
- package/dist/lint/post-synth/tf038.d.ts.map +1 -0
- package/dist/lint/post-synth/tf039.d.ts +23 -0
- package/dist/lint/post-synth/tf039.d.ts.map +1 -0
- package/dist/lint/post-synth/tf040.d.ts +30 -0
- package/dist/lint/post-synth/tf040.d.ts.map +1 -0
- package/dist/lint/post-synth/tf041.d.ts +20 -0
- package/dist/lint/post-synth/tf041.d.ts.map +1 -0
- package/dist/lint/post-synth/tf042.d.ts +18 -0
- package/dist/lint/post-synth/tf042.d.ts.map +1 -0
- package/dist/lint/post-synth/tf043.d.ts +17 -0
- package/dist/lint/post-synth/tf043.d.ts.map +1 -0
- package/dist/lint/post-synth/tf044.d.ts +17 -0
- package/dist/lint/post-synth/tf044.d.ts.map +1 -0
- package/dist/lint/post-synth/tf045.d.ts +22 -0
- package/dist/lint/post-synth/tf045.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +8 -0
- package/dist/okf/rules/TF038.md +11 -0
- package/dist/okf/rules/TF039.md +11 -0
- package/dist/okf/rules/TF040.md +11 -0
- package/dist/okf/rules/TF041.md +11 -0
- package/dist/okf/rules/TF042.md +11 -0
- package/dist/okf/rules/TF043.md +11 -0
- package/dist/okf/rules/TF044.md +11 -0
- package/dist/okf/rules/TF045.md +11 -0
- package/dist/op/activities/index.d.ts +4 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/pin-rollout.d.ts +46 -0
- package/dist/op/activities/pin-rollout.d.ts.map +1 -0
- package/dist/op/activities/terraform.d.ts +24 -50
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +48 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/op/builders.d.ts +9 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/pin/edit-ts.d.ts +21 -0
- package/dist/pin/edit-ts.d.ts.map +1 -0
- package/dist/pin/edit.d.ts +52 -0
- package/dist/pin/edit.d.ts.map +1 -0
- package/dist/pin/forge.d.ts +39 -0
- package/dist/pin/forge.d.ts.map +1 -0
- package/dist/pin/index.d.ts +22 -0
- package/dist/pin/index.d.ts.map +1 -0
- package/dist/pin/rollout.d.ts +116 -0
- package/dist/pin/rollout.d.ts.map +1 -0
- package/dist/pin/scan.d.ts +54 -0
- package/dist/pin/scan.d.ts.map +1 -0
- package/dist/pin/source.d.ts +93 -0
- package/dist/pin/source.d.ts.map +1 -0
- package/dist/pin/waves.d.ts +66 -0
- package/dist/pin/waves.d.ts.map +1 -0
- package/dist/plan-digest.d.ts +59 -0
- package/dist/plan-digest.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/module-source.ts +56 -1
- package/dist/rules/terragrunt.ts +76 -0
- package/dist/rules/tf002.ts +1 -1
- package/dist/rules/tf038.ts +71 -0
- package/dist/rules/tf039.ts +64 -0
- package/dist/rules/tf040.ts +76 -0
- package/dist/rules/tf041.ts +56 -0
- package/dist/rules/tf042.ts +42 -0
- package/dist/rules/tf043.ts +49 -0
- package/dist/rules/tf044.ts +51 -0
- package/dist/rules/tf045.ts +47 -0
- package/dist/terragrunt/affected.d.ts +119 -0
- package/dist/terragrunt/affected.d.ts.map +1 -0
- package/dist/terragrunt/index.d.ts +11 -0
- package/dist/terragrunt/index.d.ts.map +1 -0
- package/dist/terragrunt/mocks.d.ts +147 -0
- package/dist/terragrunt/mocks.d.ts.map +1 -0
- package/dist/terragrunt/run.d.ts +130 -0
- package/dist/terragrunt/run.d.ts.map +1 -0
- package/dist/terragrunt/units.d.ts +103 -0
- package/dist/terragrunt/units.d.ts.map +1 -0
- package/dist/terragrunt/wave.d.ts +89 -0
- package/dist/terragrunt/wave.d.ts.map +1 -0
- package/package.json +12 -2
- package/src/__fixtures__/change-set/choudoufu-set-plan.change-set.golden.json +95 -0
- package/src/__fixtures__/change-set/choudoufu-set-plan.json +76 -0
- package/src/__fixtures__/change-set/choudoufu.part.golden.json +173 -0
- package/src/__fixtures__/change-set/choudoufu.plan.json +610 -0
- package/src/__fixtures__/change-set/terraform.import-forget.plan.json +81 -0
- package/src/__fixtures__/change-set/terraform.part.golden.json +173 -0
- package/src/__fixtures__/change-set/terraform.plan.json +589 -0
- package/src/__fixtures__/change-set/tofu.part.golden.json +173 -0
- package/src/__fixtures__/change-set/tofu.plan.json +588 -0
- package/src/__fixtures__/change-set/v1.tf.txt +21 -0
- package/src/__fixtures__/change-set/v2-module-app.tf.txt +4 -0
- package/src/__fixtures__/change-set/v2.tf.txt +21 -0
- package/src/__fixtures__/pin/choudoufu-live-waves-n5.json +83 -0
- package/src/__fixtures__/pin/largeset-oci-n5/apply-order.txt +5 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/main.tf +33 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/main.tf +45 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/main.tf +47 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/main.tf +31 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/main.tf +27 -0
- package/src/__fixtures__/pin/largeset-oci-n5/fixture.json +77 -0
- package/src/__fixtures__/plan-summary/largeset-n5-bump.json +2071 -0
- package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.json +95 -0
- package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.md +22 -0
- package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.txt +12 -0
- package/src/__fixtures__/terragrunt/affected/catalog/units/web/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/gitignore +3 -0
- package/src/__fixtures__/terragrunt/affected/live/dev/app/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/live/dev/vpc/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/live/prod/app/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/live/stg/terragrunt.stack.hcl +5 -0
- package/src/__fixtures__/terragrunt/affected/modules/app/main.tf +6 -0
- package/src/__fixtures__/terragrunt/affected/modules/app/policy.json +1 -0
- package/src/__fixtures__/terragrunt/affected/modules/net/main.tf +2 -0
- package/src/__fixtures__/terragrunt/affected/modules/thing/main.tf +3 -0
- package/src/__fixtures__/terragrunt/affected/root.hcl +5 -0
- package/src/__fixtures__/terragrunt/affected-cases.ts +126 -0
- package/src/__fixtures__/terragrunt/five-units/catalog/units/thing/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/five-units/live/dev/app/terragrunt.hcl +13 -0
- package/src/__fixtures__/terragrunt/five-units/live/dev/db/terragrunt.hcl +8 -0
- package/src/__fixtures__/terragrunt/five-units/live/dev/vpc/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/five-units/live/prod/app/terragrunt.hcl +8 -0
- package/src/__fixtures__/terragrunt/five-units/live/prod/vpc/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/five-units/modules/thing/main.tf +7 -0
- package/src/__fixtures__/terragrunt/five-units/root.hcl +5 -0
- package/src/__fixtures__/terragrunt/record.ts +56 -0
- package/src/__fixtures__/terragrunt/recorded/affected-find.json +50 -0
- package/src/__fixtures__/terragrunt/recorded/failed-plan-report.json +36 -0
- package/src/__fixtures__/terragrunt/recorded/find.json +67 -0
- package/src/__fixtures__/terragrunt/recorded/version.txt +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/apply-report.json +22 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/dev/vpc/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/prod/vpc/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/plan-report.json +22 -0
- package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/dev/db/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/prod/app/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-2/plan-report.json +22 -0
- package/src/change-set-combined-run.test.ts +156 -0
- package/src/change-set.test.ts +151 -0
- package/src/change-set.ts +273 -0
- package/src/changed-roots.test.ts +58 -0
- package/src/changed-roots.ts +102 -0
- package/src/commands.ts +112 -0
- package/src/components/capability-plugin.test.ts +91 -0
- package/src/components/capability-plugin.ts +131 -0
- package/src/components/pr-loop.tofu.test.ts +244 -0
- package/src/composites/terraform-pin-rollout-op.test.ts +69 -0
- package/src/composites/terraform-pin-rollout-op.ts +60 -0
- package/src/hcl/descend.ts +1 -1
- package/src/hcl/edges.ts +2 -2
- package/src/hcl/parse.test.ts +25 -0
- package/src/hcl/parse.ts +91 -6
- package/src/hcl/roots.ts +1 -1
- package/src/hcl/suppressions.ts +8 -2
- package/src/index.ts +18 -0
- package/src/lint/audit-catalog.ts +82 -0
- package/src/lint/audit.test.ts +3 -1
- package/src/lint/post-synth/fixtures/TF038/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF039/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF041/negative/terragrunt.hcl +12 -0
- package/src/lint/post-synth/fixtures/TF041/positive/terragrunt.hcl +7 -0
- package/src/lint/post-synth/fixtures/TF041/positive-apply/terragrunt.hcl +8 -0
- package/src/lint/post-synth/fixtures/TF042/negative/terragrunt.hcl +13 -0
- package/src/lint/post-synth/fixtures/TF042/positive/terragrunt.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF043/negative/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/negative-other/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/positive-git/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/positive-oci/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/positive-tfr/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF044/negative/terragrunt.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF044/positive/terragrunt.hcl +7 -0
- package/src/lint/post-synth/fixtures/TF044/positive-generate/terragrunt.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF045/negative/root.hcl +1 -0
- package/src/lint/post-synth/fixtures/TF045/negative/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF045/positive/root.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF045/positive/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF045/unit-only/terragrunt.hcl +3 -0
- package/src/lint/post-synth/index.ts +16 -0
- package/src/lint/post-synth/module-source.test.ts +42 -1
- package/src/lint/post-synth/module-source.ts +56 -1
- package/src/lint/post-synth/terragrunt.ts +76 -0
- package/src/lint/post-synth/tf002.ts +1 -1
- package/src/lint/post-synth/tf038.test.ts +51 -0
- package/src/lint/post-synth/tf038.ts +71 -0
- package/src/lint/post-synth/tf039.test.ts +40 -0
- package/src/lint/post-synth/tf039.ts +64 -0
- package/src/lint/post-synth/tf040.test.ts +55 -0
- package/src/lint/post-synth/tf040.ts +76 -0
- package/src/lint/post-synth/tf041.test.ts +50 -0
- package/src/lint/post-synth/tf041.ts +56 -0
- package/src/lint/post-synth/tf042.test.ts +19 -0
- package/src/lint/post-synth/tf042.ts +42 -0
- package/src/lint/post-synth/tf043.test.ts +65 -0
- package/src/lint/post-synth/tf043.ts +49 -0
- package/src/lint/post-synth/tf044.test.ts +27 -0
- package/src/lint/post-synth/tf044.ts +51 -0
- package/src/lint/post-synth/tf045.test.ts +30 -0
- package/src/lint/post-synth/tf045.ts +47 -0
- package/src/op/activities/index.ts +4 -0
- package/src/op/activities/pin-rollout.ts +77 -0
- package/src/op/activities/terraform.test.ts +13 -0
- package/src/op/activities/terraform.ts +46 -86
- package/src/op/activity-contracts.test.ts +4 -1
- package/src/op/activity-contracts.ts +42 -0
- package/src/op/builders.ts +13 -0
- package/src/pin/bundle.test.ts +53 -0
- package/src/pin/edit-ts.test.ts +39 -0
- package/src/pin/edit-ts.ts +77 -0
- package/src/pin/edit.test.ts +199 -0
- package/src/pin/edit.ts +157 -0
- package/src/pin/forge.ts +85 -0
- package/src/pin/index.ts +22 -0
- package/src/pin/rollout.test.ts +232 -0
- package/src/pin/rollout.ts +451 -0
- package/src/pin/scan.ts +309 -0
- package/src/pin/source.ts +206 -0
- package/src/pin/waves.test.ts +101 -0
- package/src/pin/waves.ts +125 -0
- package/src/plan-digest.ts +99 -0
- package/src/plan-summary.test.ts +154 -0
- package/src/plugin.ts +25 -2
- package/src/terragrunt/affected.acceptance.test.ts +83 -0
- package/src/terragrunt/affected.test.ts +177 -0
- package/src/terragrunt/affected.ts +348 -0
- package/src/terragrunt/index.ts +11 -0
- package/src/terragrunt/mocks.acceptance.test.ts +105 -0
- package/src/terragrunt/mocks.test.ts +309 -0
- package/src/terragrunt/mocks.ts +324 -0
- package/src/terragrunt/run.ts +369 -0
- package/src/terragrunt/terragrunt.acceptance.test.ts +122 -0
- package/src/terragrunt/terragrunt.test.ts +332 -0
- package/src/terragrunt/units.ts +235 -0
- package/src/terragrunt/wave.ts +181 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pin edit for a root chant generates (#3189): move the pin in the
|
|
3
|
+
* module declaration of its TypeScript source, not in the `.tf` a build
|
|
4
|
+
* writes from it.
|
|
5
|
+
*
|
|
6
|
+
* A module declaration in TypeScript is an object literal with the same
|
|
7
|
+
* arguments the HCL block has: a `source` string, and a `version` string
|
|
8
|
+
* beside a registry source. Any object literal whose `source` is a plain
|
|
9
|
+
* string naming the module counts, whatever function or constructor receives
|
|
10
|
+
* it. The TypeScript compiler parses the file, so a `source:` inside a
|
|
11
|
+
* comment, a template with substitutions, or another string is never a
|
|
12
|
+
* match, and the edit replaces the string literal's content only. Every other
|
|
13
|
+
* byte of the file stays as it was.
|
|
14
|
+
*
|
|
15
|
+
* It needs the TypeScript compiler, so `./index.ts` (the bundled subpath,
|
|
16
|
+
* #3421) does not import it. The rollout takes it as `editTs`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import * as ts from "typescript";
|
|
20
|
+
import type { PinCallResult, PinEditResult, PinRequest } from "./edit";
|
|
21
|
+
import { movePin } from "./source";
|
|
22
|
+
|
|
23
|
+
function propertyName(name: ts.PropertyName): string | undefined {
|
|
24
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name)) return name.text;
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stringInitializer(obj: ts.ObjectLiteralExpression, key: string): ts.StringLiteral | ts.NoSubstitutionTemplateLiteral | "expression" | undefined {
|
|
29
|
+
for (const p of obj.properties) {
|
|
30
|
+
if (!ts.isPropertyAssignment(p) || propertyName(p.name) !== key) continue;
|
|
31
|
+
const init = p.initializer;
|
|
32
|
+
return ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init) ? init : "expression";
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Write a value as a literal with the same quote the old one used. */
|
|
38
|
+
function quote(value: string, old: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral, file: ts.SourceFile): string {
|
|
39
|
+
const q = old.getText(file)[0]!;
|
|
40
|
+
const escaped = value.replace(/\\/g, "\\\\").replaceAll(q, `\\${q}`);
|
|
41
|
+
return `${q}${q === "`" ? escaped.replace(/\$\{/g, "\\${") : escaped}${q}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Move `request.module`'s pin in one TypeScript file. */
|
|
45
|
+
export function editPinsInTs(text: string, file: string, request: PinRequest): PinEditResult {
|
|
46
|
+
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
47
|
+
const calls: PinCallResult[] = [];
|
|
48
|
+
const splices: Array<{ start: number; end: number; text: string }> = [];
|
|
49
|
+
|
|
50
|
+
const visit = (node: ts.Node): void => {
|
|
51
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
52
|
+
const source = stringInitializer(node, "source");
|
|
53
|
+
const version = stringInitializer(node, "version");
|
|
54
|
+
if (source && source !== "expression") {
|
|
55
|
+
const call = `line ${sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1}`;
|
|
56
|
+
const move = movePin(source.text, version && version !== "expression" ? version.text : undefined, request);
|
|
57
|
+
if (move) {
|
|
58
|
+
if (version === "expression") {
|
|
59
|
+
calls.push({ outcome: "refused", reason: "version is an expression, not a string literal", file, call });
|
|
60
|
+
} else if (move.outcome === "moved") {
|
|
61
|
+
if (move.source !== source.text) splices.push({ start: source.getStart(sf), end: source.getEnd(), text: quote(move.source, source, sf) });
|
|
62
|
+
if (version && move.version !== undefined && move.version !== version.text) splices.push({ start: version.getStart(sf), end: version.getEnd(), text: quote(move.version, version, sf) });
|
|
63
|
+
calls.push({ ...move, file, call });
|
|
64
|
+
} else {
|
|
65
|
+
calls.push({ ...move, file, call });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
ts.forEachChild(node, visit);
|
|
71
|
+
};
|
|
72
|
+
visit(sf);
|
|
73
|
+
|
|
74
|
+
let content = text;
|
|
75
|
+
for (const s of splices.sort((a, b) => b.start - a.start)) content = content.slice(0, s.start) + s.text + content.slice(s.end);
|
|
76
|
+
return { content, calls };
|
|
77
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { loadHcl2json } from "@intentius/chant/terraform/hcl2json";
|
|
3
|
+
import { editPins, type PinRequest } from "./edit";
|
|
4
|
+
import { readModulePin } from "./source";
|
|
5
|
+
|
|
6
|
+
const parser = await loadHcl2json();
|
|
7
|
+
|
|
8
|
+
const OCI = "oci://registry.example.com/modules/vpc";
|
|
9
|
+
const REGISTRY = "app.terraform.io/acme/vpc/aws";
|
|
10
|
+
const GIT = "git::https://github.com/acme/modules.git//vpc";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One row: a file, the request, and the whole file the edit must produce. The
|
|
14
|
+
* expected text is the entire file, so every row also proves the bytes around
|
|
15
|
+
* the pin are untouched: comments, heredocs, other modules, spacing.
|
|
16
|
+
*/
|
|
17
|
+
interface Row {
|
|
18
|
+
name: string;
|
|
19
|
+
file: string;
|
|
20
|
+
request: PinRequest;
|
|
21
|
+
before: string;
|
|
22
|
+
after: string;
|
|
23
|
+
outcomes: string[];
|
|
24
|
+
reason?: RegExp;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ROWS: Row[] = [
|
|
28
|
+
{
|
|
29
|
+
name: "an oci tag",
|
|
30
|
+
file: "main.tf",
|
|
31
|
+
request: { module: OCI, from: "1.3.0", to: "1.4.0" },
|
|
32
|
+
before: `module "vpc" {\n source = "${OCI}?tag=1.3.0"\n cidr = "10.0.0.0/16"\n}\n`,
|
|
33
|
+
after: `module "vpc" {\n source = "${OCI}?tag=1.4.0"\n cidr = "10.0.0.0/16"\n}\n`,
|
|
34
|
+
outcomes: ["moved"],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "an oci digest",
|
|
38
|
+
file: "main.tf",
|
|
39
|
+
request: { module: OCI, from: `sha256:${"a".repeat(64)}`, to: `sha256:${"b".repeat(64)}` },
|
|
40
|
+
before: `module "vpc" {\n source = "${OCI}?digest=sha256:${"a".repeat(64)}"\n}\n`,
|
|
41
|
+
after: `module "vpc" {\n source = "${OCI}?digest=sha256:${"b".repeat(64)}"\n}\n`,
|
|
42
|
+
outcomes: ["moved"],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "an oci tag moved to a digest changes the argument",
|
|
46
|
+
file: "main.tf",
|
|
47
|
+
request: { module: OCI, from: "1.3.0", to: `sha256:${"c".repeat(64)}` },
|
|
48
|
+
before: `module "vpc" { source = "${OCI}?tag=1.3.0" }\n`,
|
|
49
|
+
after: `module "vpc" { source = "${OCI}?digest=sha256:${"c".repeat(64)}" }\n`,
|
|
50
|
+
outcomes: ["moved"],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "a git ref, with comments and other modules left byte for byte",
|
|
54
|
+
file: "main.tf",
|
|
55
|
+
request: { module: GIT, from: "v1.3.0", to: "v1.4.0" },
|
|
56
|
+
before: [
|
|
57
|
+
`# source = "${GIT}?ref=v1.3.0" in a comment is not a call`,
|
|
58
|
+
`module "vpc" {`,
|
|
59
|
+
` source = "${GIT}?ref=v1.3.0" # pinned`,
|
|
60
|
+
` /* version = "1.3.0" */`,
|
|
61
|
+
` tags = { note = "source = \\"${GIT}?ref=v1.3.0\\"" }`,
|
|
62
|
+
`}`,
|
|
63
|
+
``,
|
|
64
|
+
`module "other" {`,
|
|
65
|
+
` source = "${GIT.replace("vpc", "dns")}?ref=v1.3.0"`,
|
|
66
|
+
`}`,
|
|
67
|
+
``,
|
|
68
|
+
].join("\n"),
|
|
69
|
+
after: [
|
|
70
|
+
`# source = "${GIT}?ref=v1.3.0" in a comment is not a call`,
|
|
71
|
+
`module "vpc" {`,
|
|
72
|
+
` source = "${GIT}?ref=v1.4.0" # pinned`,
|
|
73
|
+
` /* version = "1.3.0" */`,
|
|
74
|
+
` tags = { note = "source = \\"${GIT}?ref=v1.3.0\\"" }`,
|
|
75
|
+
`}`,
|
|
76
|
+
``,
|
|
77
|
+
`module "other" {`,
|
|
78
|
+
` source = "${GIT.replace("vpc", "dns")}?ref=v1.3.0"`,
|
|
79
|
+
`}`,
|
|
80
|
+
``,
|
|
81
|
+
].join("\n"),
|
|
82
|
+
outcomes: ["moved"],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "an exact registry version, and a second call of the same module",
|
|
86
|
+
file: "main.tf",
|
|
87
|
+
request: { module: REGISTRY, from: "1.3.0", to: "1.4.0" },
|
|
88
|
+
before: `module "a" {\n source = "${REGISTRY}"\n version = "1.3.0"\n}\n\nmodule "b" {\n source = "${REGISTRY}"\n version = "= 1.3.0"\n description = <<EOT\nversion = "1.3.0"\nEOT\n}\n`,
|
|
89
|
+
after: `module "a" {\n source = "${REGISTRY}"\n version = "1.4.0"\n}\n\nmodule "b" {\n source = "${REGISTRY}"\n version = "= 1.4.0"\n description = <<EOT\nversion = "1.3.0"\nEOT\n}\n`,
|
|
90
|
+
outcomes: ["moved", "moved"],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "a terragrunt source with ?ref=",
|
|
94
|
+
file: "live/prod/vpc/terragrunt.hcl",
|
|
95
|
+
request: { module: GIT, from: "v1.3.0", to: "v1.4.0" },
|
|
96
|
+
before: `include "root" {\n path = find_in_parent_folders()\n}\n\nterraform {\n source = "${GIT}?ref=v1.3.0"\n}\n\ninputs = {\n name = "vpc"\n}\n`,
|
|
97
|
+
after: `include "root" {\n path = find_in_parent_folders()\n}\n\nterraform {\n source = "${GIT}?ref=v1.4.0"\n}\n\ninputs = {\n name = "vpc"\n}\n`,
|
|
98
|
+
outcomes: ["moved"],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "a terragrunt tfr source with ?version=",
|
|
102
|
+
file: "terragrunt.hcl",
|
|
103
|
+
request: { module: "tfr:///terraform-aws-modules/vpc/aws", from: "5.1.0", to: "5.2.0" },
|
|
104
|
+
before: `terraform {\n source = "tfr:///terraform-aws-modules/vpc/aws?version=5.1.0"\n}\n`,
|
|
105
|
+
after: `terraform {\n source = "tfr:///terraform-aws-modules/vpc/aws?version=5.2.0"\n}\n`,
|
|
106
|
+
outcomes: ["moved"],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "a terragrunt oci source",
|
|
110
|
+
file: "terragrunt.hcl",
|
|
111
|
+
request: { module: OCI, from: "1.3.0", to: "1.4.0" },
|
|
112
|
+
before: `terraform { source = "${OCI}?tag=1.3.0" }\n`,
|
|
113
|
+
after: `terraform { source = "${OCI}?tag=1.4.0" }\n`,
|
|
114
|
+
outcomes: ["moved"],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "a floating constraint is refused with its reason",
|
|
118
|
+
file: "main.tf",
|
|
119
|
+
request: { module: REGISTRY, from: "1.3.0", to: "1.4.0" },
|
|
120
|
+
before: `module "a" {\n source = "${REGISTRY}"\n version = "~> 1.3"\n}\n`,
|
|
121
|
+
after: `module "a" {\n source = "${REGISTRY}"\n version = "~> 1.3"\n}\n`,
|
|
122
|
+
outcomes: ["refused"],
|
|
123
|
+
reason: /version "~> 1.3" is a constraint, not a pin/,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "a comma-separated range is refused",
|
|
127
|
+
file: "main.tf",
|
|
128
|
+
request: { module: REGISTRY, from: "1.3.0", to: "1.4.0" },
|
|
129
|
+
before: `module "a" {\n source = "${REGISTRY}"\n version = ">= 1.3.0, < 2.0.0"\n}\n`,
|
|
130
|
+
after: `module "a" {\n source = "${REGISTRY}"\n version = ">= 1.3.0, < 2.0.0"\n}\n`,
|
|
131
|
+
outcomes: ["refused"],
|
|
132
|
+
reason: /is a constraint, not a pin/,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "an unpinned oci source is refused",
|
|
136
|
+
file: "main.tf",
|
|
137
|
+
request: { module: OCI, from: "1.3.0", to: "1.4.0" },
|
|
138
|
+
before: `module "vpc" {\n source = "${OCI}"\n}\n`,
|
|
139
|
+
after: `module "vpc" {\n source = "${OCI}"\n}\n`,
|
|
140
|
+
outcomes: ["refused"],
|
|
141
|
+
reason: /names no tag or digest/,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "a terragrunt source built from an expression is refused",
|
|
145
|
+
file: "terragrunt.hcl",
|
|
146
|
+
request: { module: GIT, from: "v1.3.0", to: "v1.4.0" },
|
|
147
|
+
before: `locals {\n ref = "v1.3.0"\n}\nterraform {\n source = "${GIT}?ref=\${local.ref}"\n}\n`,
|
|
148
|
+
after: `locals {\n ref = "v1.3.0"\n}\nterraform {\n source = "${GIT}?ref=\${local.ref}"\n}\n`,
|
|
149
|
+
outcomes: ["refused"],
|
|
150
|
+
reason: /is an expression/,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "a call at the new version already is reported and left",
|
|
154
|
+
file: "main.tf",
|
|
155
|
+
request: { module: OCI, from: "1.3.0", to: "1.4.0" },
|
|
156
|
+
before: `module "vpc" {\n source = "${OCI}?tag=1.4.0"\n}\n`,
|
|
157
|
+
after: `module "vpc" {\n source = "${OCI}?tag=1.4.0"\n}\n`,
|
|
158
|
+
outcomes: ["already"],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "a call pinned at a third version is reported and left",
|
|
162
|
+
file: "main.tf",
|
|
163
|
+
request: { module: OCI, from: "1.3.0", to: "1.4.0" },
|
|
164
|
+
before: `module "vpc" {\n source = "${OCI}?tag=1.2.0"\n}\n`,
|
|
165
|
+
after: `module "vpc" {\n source = "${OCI}?tag=1.2.0"\n}\n`,
|
|
166
|
+
outcomes: ["elsewhere"],
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
describe("editPins (#3189)", () => {
|
|
171
|
+
test.each(ROWS)("$name", async (row) => {
|
|
172
|
+
const result = await editPins(row.before, row.file, row.request, parser);
|
|
173
|
+
expect(result.content).toBe(row.after);
|
|
174
|
+
expect(result.calls.map((c) => c.outcome)).toEqual(row.outcomes);
|
|
175
|
+
if (row.reason) {
|
|
176
|
+
const refused = result.calls.find((c) => c.outcome === "refused");
|
|
177
|
+
expect(refused && "reason" in refused ? refused.reason : "").toMatch(row.reason);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("readModulePin", () => {
|
|
183
|
+
test.each([
|
|
184
|
+
[`${OCI}?tag=1.3.0`, undefined, { module: OCI, pin: "1.3.0", param: "tag" }],
|
|
185
|
+
[`oci://localhost:4890/largeset/shared?tag=1.0.0`, undefined, { module: "oci://localhost:4890/largeset/shared", pin: "1.0.0", param: "tag" }],
|
|
186
|
+
[`${GIT}?depth=1&ref=v2`, undefined, { module: `${GIT}?depth=1`, pin: "v2", param: "ref" }],
|
|
187
|
+
[REGISTRY, "1.3.0", { module: REGISTRY, pin: "1.3.0", at: "version" }],
|
|
188
|
+
[REGISTRY, "= v1.3.0", { module: REGISTRY, pin: "v1.3.0", at: "version" }],
|
|
189
|
+
[REGISTRY, "~> 1.3", { module: REGISTRY, pin: null }],
|
|
190
|
+
[REGISTRY, ">= 1.3", { module: REGISTRY, pin: null }],
|
|
191
|
+
[REGISTRY, undefined, { module: REGISTRY, pin: null }],
|
|
192
|
+
[REGISTRY, "${var.vpc_version}", { module: REGISTRY, pin: null, unpinned: expect.stringContaining("is an expression") }],
|
|
193
|
+
[GIT, undefined, { module: GIT, pin: null }],
|
|
194
|
+
["./modules/vpc", undefined, { module: "./modules/vpc", pin: null }],
|
|
195
|
+
[`${OCI}?tag=1&digest=sha256:${"a".repeat(64)}`, undefined, { module: OCI, pin: null }],
|
|
196
|
+
] as const)("%s %s", (source, version, want) => {
|
|
197
|
+
expect(readModulePin(source, version)).toMatchObject(want);
|
|
198
|
+
});
|
|
199
|
+
});
|
package/src/pin/edit.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pin edit for hand-written HCL (#3189): move one module's pin from an old
|
|
3
|
+
* version to a new one in a `.tf` file or a `terragrunt.hcl`, and leave every
|
|
4
|
+
* other byte of the file as it was.
|
|
5
|
+
*
|
|
6
|
+
* Three steps, each one a check on the last:
|
|
7
|
+
*
|
|
8
|
+
* 1. The HCL reader (`@cdktn/hcl2json`, the parser the rest of this lexicon
|
|
9
|
+
* reads `.tf` with) parses the file. Its tree says which calls exist and
|
|
10
|
+
* what each one's `source` and `version` are: `module "<name>"` blocks in a
|
|
11
|
+
* `.tf`, and the `terraform` block's `source` in a `terragrunt.hcl`.
|
|
12
|
+
* 2. `./source.ts` decides, per call, whether it names the module and how its
|
|
13
|
+
* pin moves. `./scan.ts` finds the byte offsets of the literal to rewrite,
|
|
14
|
+
* and its unescaped content has to equal the value the reader returned, or
|
|
15
|
+
* the call is refused rather than guessed at.
|
|
16
|
+
* 3. The edited text is parsed again. Its tree has to equal the first tree
|
|
17
|
+
* with only the moved values changed. Anything else throws, and nothing is
|
|
18
|
+
* written.
|
|
19
|
+
*
|
|
20
|
+
* The parser is passed in rather than imported. It is a 1.8 MB wasm package
|
|
21
|
+
* that core loads lazily (`loadHcl2json`), and a caller that bundles this
|
|
22
|
+
* module (terragucci's `tf-rollout` stage, #3421) decides how it is loaded.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { Hcl2Json } from "@intentius/chant/terraform/hcl2json";
|
|
26
|
+
import { movePin, type PinMove } from "./source";
|
|
27
|
+
import { escapeHcl, scanBlocks, unescapeHcl, type ScannedAttribute, type ScannedBlock } from "./scan";
|
|
28
|
+
|
|
29
|
+
/** What to move: a module, named by its source without the pin, from one pin to another. */
|
|
30
|
+
export interface PinRequest {
|
|
31
|
+
module: string;
|
|
32
|
+
from: string;
|
|
33
|
+
to: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One module call the edit looked at, and what happened to it. */
|
|
37
|
+
export type PinCallResult = PinMove & {
|
|
38
|
+
/** The file, as the caller named it. */
|
|
39
|
+
file: string;
|
|
40
|
+
/** `module.<name>` in a `.tf`, `terraform` in a `terragrunt.hcl`, or `line <n>` in TypeScript source. */
|
|
41
|
+
call: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** The edited text and every call that names the module. */
|
|
45
|
+
export interface PinEditResult {
|
|
46
|
+
content: string;
|
|
47
|
+
calls: PinCallResult[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Whether a file is read as a Terragrunt configuration rather than a `.tf` file. */
|
|
51
|
+
export function isTerragruntFile(file: string): boolean {
|
|
52
|
+
return file === "terragrunt.hcl" || file.endsWith("/terragrunt.hcl");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type Tree = Record<string, unknown>;
|
|
56
|
+
|
|
57
|
+
interface Site {
|
|
58
|
+
call: string;
|
|
59
|
+
block: ScannedBlock;
|
|
60
|
+
/** The reader's body for this call, which the verification re-reads. */
|
|
61
|
+
body: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function attr(block: ScannedBlock, name: string): ScannedAttribute | undefined {
|
|
65
|
+
return block.attributes.find((a) => a.name === name);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Pair each call in the reader's tree with the block the scanner found for it. */
|
|
69
|
+
function sites(tree: Tree, blocks: ScannedBlock[], terragrunt: boolean): Site[] {
|
|
70
|
+
const out: Site[] = [];
|
|
71
|
+
if (terragrunt) {
|
|
72
|
+
const bodies = (tree.terraform as Record<string, unknown>[] | undefined) ?? [];
|
|
73
|
+
const scanned = blocks.filter((b) => b.type === "terraform" && b.labels.length === 0);
|
|
74
|
+
bodies.forEach((body, k) => {
|
|
75
|
+
if (scanned[k]) out.push({ call: "terraform", block: scanned[k]!, body });
|
|
76
|
+
});
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
const modules = (tree.module as Record<string, Record<string, unknown>[]> | undefined) ?? {};
|
|
80
|
+
for (const [name, bodies] of Object.entries(modules)) {
|
|
81
|
+
const scanned = blocks.filter((b) => b.type === "module" && b.labels.length === 1 && b.labels[0] === name);
|
|
82
|
+
bodies.forEach((body, k) => {
|
|
83
|
+
if (scanned[k]) out.push({ call: `module.${name}`, block: scanned[k]!, body });
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return out.sort((a, b) => a.block.start - b.block.start);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Pin edits are applied back to front, so earlier offsets stay valid. */
|
|
90
|
+
interface Splice {
|
|
91
|
+
start: number;
|
|
92
|
+
end: number;
|
|
93
|
+
text: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Move `request.module`'s pin in one file. Calls that name another module are
|
|
98
|
+
* left alone and not reported. A call that names the module and cannot move
|
|
99
|
+
* is reported with its reason and left as it was.
|
|
100
|
+
*/
|
|
101
|
+
export async function editPins(text: string, file: string, request: PinRequest, parser: Hcl2Json): Promise<PinEditResult> {
|
|
102
|
+
const terragrunt = isTerragruntFile(file);
|
|
103
|
+
const tree = (await parser.parse(file, text)) as Tree;
|
|
104
|
+
const blocks = scanBlocks(text, file);
|
|
105
|
+
const calls: PinCallResult[] = [];
|
|
106
|
+
const splices: Splice[] = [];
|
|
107
|
+
const expected = structuredClone(tree);
|
|
108
|
+
const expectedSites = sites(expected, scanBlocks(text, file), terragrunt);
|
|
109
|
+
|
|
110
|
+
sites(tree, blocks, terragrunt).forEach((site, index) => {
|
|
111
|
+
const source = site.body.source;
|
|
112
|
+
const version = site.body.version;
|
|
113
|
+
if (typeof source !== "string") return;
|
|
114
|
+
const move = movePin(source, typeof version === "string" ? version : undefined, request);
|
|
115
|
+
if (!move) return;
|
|
116
|
+
const at = { file, call: site.call };
|
|
117
|
+
if (move.outcome !== "moved") {
|
|
118
|
+
calls.push({ ...move, ...at });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const edits: Array<[name: "source" | "version", value: string]> = [];
|
|
122
|
+
if (move.source !== source) edits.push(["source", move.source]);
|
|
123
|
+
if (move.version !== undefined && move.version !== version) edits.push(["version", move.version]);
|
|
124
|
+
for (const [name] of edits) {
|
|
125
|
+
const scanned = attr(site.block, name);
|
|
126
|
+
const was = site.body[name] as string;
|
|
127
|
+
if (!scanned?.literal || unescapeHcl(scanned.literal.raw) !== was) {
|
|
128
|
+
calls.push({ outcome: "refused", reason: `${name} is not a plain string literal, so there is no one place to write the pin`, ...at });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const [name, value] of edits) {
|
|
133
|
+
const literal = attr(site.block, name)!.literal!;
|
|
134
|
+
splices.push({ start: literal.start, end: literal.end, text: rewriteLiteral(literal.raw, site.body[name] as string, value) });
|
|
135
|
+
expectedSites[index]!.body[name] = value;
|
|
136
|
+
}
|
|
137
|
+
calls.push({ ...move, ...at });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
let content = text;
|
|
141
|
+
for (const s of splices.sort((a, b) => b.start - a.start)) content = content.slice(0, s.start) + s.text + content.slice(s.end);
|
|
142
|
+
if (splices.length > 0) {
|
|
143
|
+
const reread = (await parser.parse(file, content)) as Tree;
|
|
144
|
+
if (JSON.stringify(reread) !== JSON.stringify(expected)) {
|
|
145
|
+
throw new Error(`${file}: the pin edit would change more than the pin, so nothing was written`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { content, calls };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The new literal content. When the old literal needed no escapes, the old
|
|
153
|
+
* value's text is replaced in place, so any spacing (`= 1.3.0`) survives.
|
|
154
|
+
*/
|
|
155
|
+
function rewriteLiteral(raw: string, was: string, value: string): string {
|
|
156
|
+
return raw === was ? value : escapeHcl(value);
|
|
157
|
+
}
|
package/src/pin/forge.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a pin-bump rollout asks the forge (#3189): find a wave's pull request
|
|
3
|
+
* by its branch, open one, and read the checks on the commit a merge made.
|
|
4
|
+
*
|
|
5
|
+
* The default answers through `gh`, the way core's `proposeWorkspaceUpgrade`
|
|
6
|
+
* (#2550) opens its pull request: `gh pr list --head`, `gh pr create`, and
|
|
7
|
+
* `gh api` for the commit's check runs and statuses. A test, or a caller with
|
|
8
|
+
* its own forge client, passes a {@link PinForge} instead.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { CommandRunner } from "@intentius/chant/op/activities/propose-upgrade";
|
|
12
|
+
|
|
13
|
+
/** A wave's pull request, as the forge reports it. */
|
|
14
|
+
export interface PinPullRequest {
|
|
15
|
+
url: string;
|
|
16
|
+
state: "open" | "merged" | "closed";
|
|
17
|
+
body: string;
|
|
18
|
+
/** The commit the merge made on the base branch, once merged. */
|
|
19
|
+
mergeCommit?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One check or status on a commit. */
|
|
23
|
+
export interface PinCommitCheck {
|
|
24
|
+
name: string;
|
|
25
|
+
state: "success" | "pending" | "failure";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PinForge {
|
|
29
|
+
/** The newest pull request whose head is `branch`, in any state, or null. */
|
|
30
|
+
findPullRequest(branch: string): Promise<PinPullRequest | null>;
|
|
31
|
+
/** Open a pull request and return its URL. */
|
|
32
|
+
createPullRequest(input: { base: string; head: string; title: string; body: string }): Promise<string>;
|
|
33
|
+
/** The checks and statuses on a commit. */
|
|
34
|
+
commitChecks(sha: string): Promise<PinCommitCheck[]>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const CHECK_STATE: Record<string, PinCommitCheck["state"]> = {
|
|
38
|
+
success: "success",
|
|
39
|
+
neutral: "success",
|
|
40
|
+
skipped: "success",
|
|
41
|
+
failure: "failure",
|
|
42
|
+
cancelled: "failure",
|
|
43
|
+
timed_out: "failure",
|
|
44
|
+
action_required: "failure",
|
|
45
|
+
startup_failure: "failure",
|
|
46
|
+
stale: "failure",
|
|
47
|
+
error: "failure",
|
|
48
|
+
pending: "pending",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** A {@link PinForge} over `gh`, run in `repo`. */
|
|
52
|
+
export function ghPinForge(run: CommandRunner, repo: string): PinForge {
|
|
53
|
+
return {
|
|
54
|
+
async findPullRequest(branch) {
|
|
55
|
+
const out = await run("gh", ["pr", "list", "--head", branch, "--state", "all", "--limit", "1", "--json", "url,state,body,mergeCommit"], repo);
|
|
56
|
+
const list = JSON.parse(out || "[]") as Array<{ url: string; state: string; body?: string; mergeCommit?: { oid?: string } | null }>;
|
|
57
|
+
const pr = list[0];
|
|
58
|
+
if (!pr) return null;
|
|
59
|
+
const state = pr.state.toLowerCase() === "merged" ? "merged" : pr.state.toLowerCase() === "open" ? "open" : "closed";
|
|
60
|
+
return { url: pr.url, state, body: pr.body ?? "", ...(pr.mergeCommit?.oid ? { mergeCommit: pr.mergeCommit.oid } : {}) };
|
|
61
|
+
},
|
|
62
|
+
async createPullRequest({ base, head, title, body }) {
|
|
63
|
+
const out = await run("gh", ["pr", "create", "--base", base, "--head", head, "--title", title, "--body", body], repo);
|
|
64
|
+
return out.trim().split("\n").pop() ?? "";
|
|
65
|
+
},
|
|
66
|
+
async commitChecks(sha) {
|
|
67
|
+
const checks: PinCommitCheck[] = [];
|
|
68
|
+
const runs = await run("gh", ["api", "--paginate", `repos/{owner}/{repo}/commits/${sha}/check-runs`, "--jq", ".check_runs[] | [.name, .status, (.conclusion // \"\")] | @tsv"], repo);
|
|
69
|
+
for (const line of runs.split("\n").filter(Boolean)) {
|
|
70
|
+
const [name, status, conclusion] = line.split("\t");
|
|
71
|
+
checks.push({ name: name!, state: status === "completed" ? (CHECK_STATE[conclusion ?? ""] ?? "failure") : "pending" });
|
|
72
|
+
}
|
|
73
|
+
// Statuses come newest first; the first one per context is its state.
|
|
74
|
+
const statuses = await run("gh", ["api", "--paginate", `repos/{owner}/{repo}/commits/${sha}/statuses`, "--jq", ".[] | [.context, .state] | @tsv"], repo);
|
|
75
|
+
const seen = new Set<string>();
|
|
76
|
+
for (const line of statuses.split("\n").filter(Boolean)) {
|
|
77
|
+
const [name, state] = line.split("\t");
|
|
78
|
+
if (seen.has(name!)) continue;
|
|
79
|
+
seen.add(name!);
|
|
80
|
+
checks.push({ name: name!, state: CHECK_STATE[state ?? ""] ?? "pending" });
|
|
81
|
+
}
|
|
82
|
+
return checks;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
package/src/pin/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pin-bump rollouts (#3189): the pin edit, the wave plan, and the run that
|
|
3
|
+
* opens one pull request per wave.
|
|
4
|
+
*
|
|
5
|
+
* This subpath, `@intentius/chant-lexicon-terraform/pin`, bundles without the
|
|
6
|
+
* TypeScript toolchain, zod, or chant's fold, lint and codegen modules, so
|
|
7
|
+
* terragucci's `tf-rollout` stage can run it from one bundled file (#3421).
|
|
8
|
+
* `./bundle.test.ts` holds it to that. The HCL parser is passed in, and the
|
|
9
|
+
* TypeScript editor for generated roots lives in `./edit-ts.ts`, which this
|
|
10
|
+
* index does not import.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { readModulePin, movePin, moduleOf, exactVersion, isPinValue, checkPinRequest } from "./source";
|
|
14
|
+
export type { ModuleCallPin, PinMove, PinParam } from "./source";
|
|
15
|
+
export { editPins, isTerragruntFile } from "./edit";
|
|
16
|
+
export type { PinRequest, PinCallResult, PinEditResult } from "./edit";
|
|
17
|
+
export { planPinWaves, wavesFromChoudoufu, restrictWaves, terragruntDependencies, PinWavePlanError } from "./waves";
|
|
18
|
+
export type { PinRoot, PinWave } from "./waves";
|
|
19
|
+
export { ghPinForge } from "./forge";
|
|
20
|
+
export type { PinForge, PinPullRequest, PinCommitCheck } from "./forge";
|
|
21
|
+
export { runPinRollout, renderPinRollout, pinWaveBranch } from "./rollout";
|
|
22
|
+
export type { PinRolloutOptions, PinRolloutResult, PinRolloutMode, PinRootState, PinWaveState, PinWaveStatus, TsPinEditor } from "./rollout";
|