@intentius/chant 0.58.0 → 0.59.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/audit/core.d.ts +17 -1
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +15 -4
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/carve-bridge.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +13 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts +0 -16
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +22 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +4 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +19 -4
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts +8 -2
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts +1 -7
- package/dist/components/verbs/run-agent.d.ts.map +1 -1
- package/dist/detectLexicon.d.ts +13 -0
- package/dist/detectLexicon.d.ts.map +1 -1
- package/dist/lexicon.d.ts +170 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +9 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +2 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/reconcile.d.ts +62 -2
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/builders.d.ts +2 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/change-signal.d.ts +91 -0
- package/dist/op/change-signal.d.ts.map +1 -0
- package/dist/op/composites/apply-op.d.ts +7 -2
- package/dist/op/composites/apply-op.d.ts.map +1 -1
- package/dist/op/composites/reconcile-op.d.ts.map +1 -1
- package/dist/op/gate-name.d.ts +40 -0
- package/dist/op/gate-name.d.ts.map +1 -0
- package/dist/op/gate-summary.d.ts +43 -0
- package/dist/op/gate-summary.d.ts.map +1 -0
- package/dist/op/index.d.ts +6 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +2 -1
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/operator.d.ts +63 -0
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/types.d.ts +18 -3
- package/dist/op/types.d.ts.map +1 -1
- package/dist/terraform/bridge.d.ts +26 -7
- package/dist/terraform/bridge.d.ts.map +1 -1
- package/dist/terraform/carve-provider.d.ts +28 -2
- package/dist/terraform/carve-provider.d.ts.map +1 -1
- package/dist/terraform/data-source-shape.d.ts +77 -0
- package/dist/terraform/data-source-shape.d.ts.map +1 -0
- package/dist/terraform/graph.d.ts.map +1 -1
- package/dist/terraform/providers/kubernetes.d.ts +5 -0
- package/dist/terraform/providers/kubernetes.d.ts.map +1 -1
- package/dist/terraform/tier-map.d.ts +16 -4
- package/dist/terraform/tier-map.d.ts.map +1 -1
- package/dist/terraform/types.d.ts +8 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audit/core.ts +25 -3
- package/src/audit/discover.ts +122 -14
- package/src/cli/commands/audit.test.ts +5 -3
- package/src/cli/commands/audit.ts +5 -1
- package/src/cli/commands/build.test.ts +39 -1
- package/src/cli/commands/build.ts +14 -0
- package/src/cli/commands/carve-bridge.test.ts +237 -14
- package/src/cli/commands/carve-bridge.ts +21 -17
- package/src/cli/commands/carve-emit-k8s.test.ts +17 -10
- package/src/cli/commands/lint.test.ts +146 -1
- package/src/cli/commands/lint.ts +82 -7
- package/src/cli/handlers/operator.ts +81 -0
- package/src/cli/handlers/run.test.ts +132 -3
- package/src/cli/handlers/run.ts +87 -3
- package/src/cli/main.test.ts +9 -11
- package/src/cli/main.ts +5 -27
- package/src/cli/plugins.test.ts +68 -2
- package/src/cli/plugins.ts +39 -0
- package/src/cli/registry.ts +14 -0
- package/src/components/README.md +2 -2
- package/src/components/__fixtures__/neo4j-fanout.json +1 -1
- package/src/components/cli-support.test.ts +18 -7
- package/src/components/cli-support.ts +8 -3
- package/src/components/component-schema.test.ts +18 -2
- package/src/components/component.schema.json +17 -4
- package/src/components/component.test.ts +2 -2
- package/src/components/component.ts +25 -5
- package/src/components/config-defaults.test.ts +2 -2
- package/src/components/driver.test.ts +20 -1
- package/src/components/driver.ts +12 -5
- package/src/components/pilots/neo4j-fanout.pilot.ts +3 -3
- package/src/components/verbs/run-agent.test.ts +19 -0
- package/src/components/verbs/run-agent.ts +1 -7
- package/src/detectLexicon.ts +18 -1
- package/src/discovery/fold-import.test.ts +1 -1
- package/src/fold/foldable-helpers.ts +1 -1
- package/src/graph-ops.test.ts +1 -1
- package/src/lexicon.ts +177 -3
- package/src/lifecycle/gate-ledger.ts +12 -1
- package/src/lint/pipeline-change-gate.test.ts +2 -2
- package/src/lint/rules/comp/comp.test.ts +26 -0
- package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +4 -2
- package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +1 -1
- package/src/op/activities/index.ts +2 -2
- package/src/op/activities/reconcile.test.ts +82 -2
- package/src/op/activities/reconcile.ts +169 -2
- package/src/op/builders.ts +3 -3
- package/src/op/change-signal.test.ts +117 -0
- package/src/op/change-signal.ts +169 -0
- package/src/op/composites/apply-op.ts +14 -4
- package/src/op/composites/composites.test.ts +17 -4
- package/src/op/composites/reconcile-op.ts +7 -4
- package/src/op/effect-step.test.ts +3 -3
- package/src/op/gate-name.test.ts +65 -0
- package/src/op/gate-name.ts +60 -0
- package/src/op/gate-summary.ts +84 -0
- package/src/op/index.ts +9 -2
- package/src/op/local-executor.test.ts +16 -1
- package/src/op/local-executor.ts +4 -3
- package/src/op/op-ir.test.ts +12 -1
- package/src/op/op-ir.ts +5 -3
- package/src/op/op-verb-class.test.ts +2 -2
- package/src/op/op.test.ts +2 -2
- package/src/op/operator.test.ts +368 -0
- package/src/op/operator.ts +141 -5
- package/src/op/runtimes/local.test.ts +1 -1
- package/src/op/types.ts +23 -3
- package/src/terraform/aws-resources.test.ts +13 -4
- package/src/terraform/bridge.test.ts +22 -9
- package/src/terraform/bridge.ts +89 -28
- package/src/terraform/carve-provider.ts +38 -2
- package/src/terraform/data-source-shape.ts +95 -0
- package/src/terraform/graph.ts +35 -7
- package/src/terraform/providers/kubernetes.ts +48 -2
- package/src/terraform/tier-map.ts +21 -6
- package/src/terraform/types.ts +8 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a carved Terraform type is read back as a `data` source (#2034).
|
|
3
|
+
*
|
|
4
|
+
* `carve bridge` replaces a carved resource with a data source the survivors
|
|
5
|
+
* read instead. Until now a carve provider said only which attribute carries
|
|
6
|
+
* the physical name (`identityAttrs`), and the bridge assumed the data source
|
|
7
|
+
* was the same type as the resource, with that one attribute in its body:
|
|
8
|
+
*
|
|
9
|
+
* data "aws_s3_bucket" "assets" { bucket = "myapp-assets-prod" }
|
|
10
|
+
*
|
|
11
|
+
* That assumption holds for AWS and google, and breaks for Kubernetes twice
|
|
12
|
+
* over. `kubernetes_manifest` names itself by `manifest.metadata.name`, a path
|
|
13
|
+
* into a nested value that a flat `attr = value` body cannot express; and the
|
|
14
|
+
* kubernetes provider ships no `kubernetes_manifest` data source at all
|
|
15
|
+
* (verified against hashicorp/kubernetes v3.2.1: 27 data sources, none a
|
|
16
|
+
* manifest). Its generic read is `data "kubernetes_resource"`, with a
|
|
17
|
+
* different type, different arguments, a nested `metadata` block, and its own
|
|
18
|
+
* attribute path for survivors to read through.
|
|
19
|
+
*
|
|
20
|
+
* So a provider contributes a *shape*: the data-source type, where each
|
|
21
|
+
* argument comes from in the carved body, and how a survivor's attribute path
|
|
22
|
+
* translates. An identity attribute is one instance of that shape
|
|
23
|
+
* ({@link identityAttrShape}), so nothing that bridges today changes.
|
|
24
|
+
*/
|
|
25
|
+
/** One argument of the data-source body, and where its value comes from. */
|
|
26
|
+
export interface ShapeField {
|
|
27
|
+
/** Argument name in the emitted data source, e.g. `api_version`. */
|
|
28
|
+
name: string;
|
|
29
|
+
/**
|
|
30
|
+
* Dotted path into the carved resource's HCL body the literal is read from,
|
|
31
|
+
* e.g. `manifest.apiVersion`. Nested blocks are walked by segment.
|
|
32
|
+
*/
|
|
33
|
+
from: string;
|
|
34
|
+
/**
|
|
35
|
+
* A shape that cannot resolve a required field renders the TODO body rather
|
|
36
|
+
* than a data source missing an argument Terraform needs.
|
|
37
|
+
*/
|
|
38
|
+
required?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** A nested block in the data-source body, e.g. `metadata { name = "x" }`. */
|
|
41
|
+
export interface ShapeBlock {
|
|
42
|
+
name: string;
|
|
43
|
+
fields: readonly ShapeField[];
|
|
44
|
+
}
|
|
45
|
+
export interface DataSourceShape {
|
|
46
|
+
/** The `data` type standing in for the carved resource, e.g. `kubernetes_resource`. */
|
|
47
|
+
type: string;
|
|
48
|
+
/** Flat `name = value` arguments of the body. */
|
|
49
|
+
args?: readonly ShapeField[];
|
|
50
|
+
/** Nested `name { ... }` blocks of the body. */
|
|
51
|
+
blocks?: readonly ShapeBlock[];
|
|
52
|
+
/**
|
|
53
|
+
* The carved resource's top-level attribute → the data source's, for the
|
|
54
|
+
* survivor rewrite. `kubernetes_manifest` exposes both `manifest` (what the
|
|
55
|
+
* config declared) and `object` (the API server's read-back), and
|
|
56
|
+
* `kubernetes_resource` exposes only `object`, so both map to `object`. An
|
|
57
|
+
* attribute with no entry passes through unchanged, which is what every
|
|
58
|
+
* same-type identity-attribute shape wants.
|
|
59
|
+
*/
|
|
60
|
+
readAttrs?: Readonly<Record<string, string>>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The shape a plain identity attribute implies: the same type as the resource,
|
|
64
|
+
* one required argument, no path translation. Returns undefined for a dotted
|
|
65
|
+
* attribute, which is a path into nested values and needs a declared shape.
|
|
66
|
+
*/
|
|
67
|
+
export declare function identityAttrShape(tfType: string, attr: string): DataSourceShape | undefined;
|
|
68
|
+
/** Every source path the shape reads, for the graph to resolve out of the carved block. */
|
|
69
|
+
export declare function shapeSourcePaths(shape: DataSourceShape): string[];
|
|
70
|
+
/**
|
|
71
|
+
* Translate a survivor's attribute path onto the data source. `path` is the
|
|
72
|
+
* text after the address, leading dot included (`.manifest.data.log_level`) or
|
|
73
|
+
* empty. Only the first segment is mapped: everything under it is the object's
|
|
74
|
+
* own structure, which the data source reproduces.
|
|
75
|
+
*/
|
|
76
|
+
export declare function rewriteReadPath(shape: DataSourceShape | undefined, path: string): string;
|
|
77
|
+
//# sourceMappingURL=data-source-shape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-source-shape.d.ts","sourceRoot":"","sources":["../../src/terraform/data-source-shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,4EAA4E;AAC5E,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,UAAU,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,IAAI,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAC7B,gDAAgD;IAChD,MAAM,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAC/B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC9C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAG3F;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,EAAE,CAIjE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,eAAe,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMxF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/terraform/graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/terraform/graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,EAAU,MAAM,SAAS,CAAC;AAErE;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;AAKpE,UAAU,MAAM;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AA4BD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAe/D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,EAAE,CAe/D;AAsFD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,cAAc,GAAG,OAAO,CAqFhF;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAEtE;AAED,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvE"}
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
* can only name the family, and `adopt` reads the body to learn the rest —
|
|
19
19
|
* which is also why one rule here covers every CRD.
|
|
20
20
|
*
|
|
21
|
+
* It has no `kubernetes_manifest` data source either, so it does not read back
|
|
22
|
+
* as its own type. `MANIFEST_READ_BACK` below is the shape `carve bridge` uses
|
|
23
|
+
* instead: `data "kubernetes_resource"`, built from the manifest's own
|
|
24
|
+
* apiVersion/kind/metadata (#2034).
|
|
25
|
+
*
|
|
21
26
|
* It has no live adoption path. `carve emit --env` filters a live export by a
|
|
22
27
|
* native type, and there is no type to filter by until the body is read; state
|
|
23
28
|
* is where the body is. `liveSelectorType` therefore returns undefined and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kubernetes.d.ts","sourceRoot":"","sources":["../../../src/terraform/providers/kubernetes.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"kubernetes.d.ts","sourceRoot":"","sources":["../../../src/terraform/providers/kubernetes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAiB,aAAa,EAA2B,MAAM,mBAAmB,CAAC;AA+N/F,eAAO,MAAM,uBAAuB,EAAE,aAcrC,CAAC"}
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
* provider registered after module load has to be visible here.
|
|
18
18
|
*/
|
|
19
19
|
import { type TierInfo } from "./carve-provider.js";
|
|
20
|
+
import type { DataSourceShape } from "./data-source-shape.js";
|
|
20
21
|
export { carveEmitTypes, type TierInfo } from "./carve-provider.js";
|
|
22
|
+
export type { DataSourceShape } from "./data-source-shape.js";
|
|
21
23
|
/** TF resource type → native tier, over every registered provider. Absent = unsupported (score 0). */
|
|
22
24
|
export declare function tierMap(): Readonly<Record<string, TierInfo>>;
|
|
23
25
|
export declare function resolveTier(tfType: string): TierInfo | null;
|
|
@@ -42,10 +44,20 @@ export declare function foldParentOf(tfType: string): string | undefined;
|
|
|
42
44
|
*/
|
|
43
45
|
export declare function canCarveEmit(tfType: string): boolean;
|
|
44
46
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* How `chant carve bridge` reads this type back as a `data` source: the
|
|
48
|
+
* data-source type, where each argument comes from in the carved body, and how
|
|
49
|
+
* a survivor's attribute path translates (#2034). Undefined when the type
|
|
50
|
+
* cannot be read back at all.
|
|
51
|
+
*/
|
|
52
|
+
export declare function dataSourceShapeOf(tfType: string): DataSourceShape | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Can `chant carve bridge` render a Terraform `data` source for this type?
|
|
55
|
+
* True whenever its provider contributes a shape — declared, or implied by a
|
|
56
|
+
* plain identity attribute. False for a dotted identity attribute with no
|
|
57
|
+
* declared shape: that is a path into nested values, and a flat `attr = value`
|
|
58
|
+
* body cannot express it (`manifest.metadata.name = "x"` is not valid HCL). A
|
|
59
|
+
* type with no identity attribute at all still bridges; the bridge writes a
|
|
60
|
+
* TODO comment for the body.
|
|
49
61
|
*/
|
|
50
62
|
export declare function canBridge(tfType: string): boolean;
|
|
51
63
|
//# sourceMappingURL=tier-map.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tier-map.d.ts","sourceRoot":"","sources":["../../src/terraform/tier-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"tier-map.d.ts","sourceRoot":"","sources":["../../src/terraform/tier-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAML,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjE,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,sGAAsG;AACtG,wBAAgB,OAAO,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAE5D;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAE3D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEjE;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE/D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAE7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEjD"}
|
|
@@ -33,6 +33,14 @@ export interface TfNode {
|
|
|
33
33
|
* emit. Undefined when the identity attribute is absent or interpolated.
|
|
34
34
|
*/
|
|
35
35
|
identity?: string;
|
|
36
|
+
/**
|
|
37
|
+
* The literals the type's data-source shape reads out of the block (#2034),
|
|
38
|
+
* keyed by the shape field's source path (`manifest.metadata.name`). This is
|
|
39
|
+
* what `carve bridge` renders the `data` body from — a superset of
|
|
40
|
+
* `identity`, since a shape can need more than one value (`api_version`,
|
|
41
|
+
* `kind` and `metadata.name` for a `kubernetes_manifest`).
|
|
42
|
+
*/
|
|
43
|
+
dataSourceValues?: Record<string, string>;
|
|
36
44
|
}
|
|
37
45
|
/**
|
|
38
46
|
* A directed dependency edge: `from` references `to`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/terraform/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,+EAA+E;AAC/E,MAAM,WAAW,MAAM;IACrB,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC5B,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/terraform/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,+EAA+E;AAC/E,MAAM,WAAW,MAAM;IACrB,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC5B,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;;;;;OAKG;IACH,GAAG,EAAE,MAAM,EAAE,CAAC;IACd;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;IACjD,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IACnC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB"}
|
package/package.json
CHANGED
package/src/audit/core.ts
CHANGED
|
@@ -25,7 +25,7 @@ import type { Severity } from "../lint/rule";
|
|
|
25
25
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "../lint/post-synth";
|
|
26
26
|
import { applyInlineSuppressions, type SuppressionMetaFinding } from "../lint/suppressions";
|
|
27
27
|
import type { SerializerResult } from "../serializer";
|
|
28
|
-
import type { LexiconPlugin } from "../lexicon";
|
|
28
|
+
import type { AuditEntitiesInput, LexiconPlugin } from "../lexicon";
|
|
29
29
|
import type { Declarable } from "../declarable";
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -61,6 +61,16 @@ export interface AuditInput {
|
|
|
61
61
|
* — checks that read `output.files` (helm, docker) see the whole bundle.
|
|
62
62
|
*/
|
|
63
63
|
files?: Record<string, string>;
|
|
64
|
+
/**
|
|
65
|
+
* Absolute directory this input's files were read from, set by discovery on
|
|
66
|
+
* a local walk for the inputs whose `path` names a directory (#2217). It
|
|
67
|
+
* reaches a lexicon's `auditEntities` as `AuditEntitiesInput.dir`, which is
|
|
68
|
+
* what lets terraform follow a root module's local `module` calls. Undefined
|
|
69
|
+
* for a remote tree fetch and for a single-file input.
|
|
70
|
+
*/
|
|
71
|
+
dir?: string;
|
|
72
|
+
/** Absolute path of the audited root, when discovery walked one (#2217). The boundary `dir` may read within. */
|
|
73
|
+
baseDir?: string;
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
/** A finding produced by a post-synth check against an audited file. */
|
|
@@ -99,8 +109,16 @@ export type ChecksProvider = (lexicon: AuditLexicon) => Promise<PostSynthCheck[]
|
|
|
99
109
|
* `ctx.entities` holds during a build, so entity-reading checks run unchanged.
|
|
100
110
|
* May return a `Promise` for a lexicon whose parser is inherently async (e.g.
|
|
101
111
|
* terraform's HCL parser); `auditLexicon`'s `entitiesFor` awaits it.
|
|
112
|
+
*
|
|
113
|
+
* The second argument carries the input's discovery facts (its path, and the
|
|
114
|
+
* directory it was read from on a local walk), so a lexicon whose unit of
|
|
115
|
+
* parsing is a directory can name its scope after that directory and read the
|
|
116
|
+
* directories it references (#2217).
|
|
102
117
|
*/
|
|
103
|
-
export type EntitiesParser = (
|
|
118
|
+
export type EntitiesParser = (
|
|
119
|
+
content: string,
|
|
120
|
+
input?: AuditEntitiesInput,
|
|
121
|
+
) => Map<string, Declarable> | Promise<Map<string, Declarable>>;
|
|
104
122
|
|
|
105
123
|
/**
|
|
106
124
|
* Resolve the entities parser for a lexicon (its plugin's `auditEntities`,
|
|
@@ -322,7 +340,11 @@ async function auditLexicon(
|
|
|
322
340
|
const entitiesFor = async (file: AuditInput): Promise<Map<string, Declarable>> => {
|
|
323
341
|
if (!parseEntities) return new Map();
|
|
324
342
|
try {
|
|
325
|
-
return await parseEntities(file.content
|
|
343
|
+
return await parseEntities(file.content, {
|
|
344
|
+
path: file.path,
|
|
345
|
+
...(file.dir !== undefined ? { dir: file.dir } : {}),
|
|
346
|
+
...(file.baseDir !== undefined ? { baseDir: file.baseDir } : {}),
|
|
347
|
+
});
|
|
326
348
|
} catch {
|
|
327
349
|
// The audit contract is "runs against any repo" — unparseable content
|
|
328
350
|
// contributes no entities, never a crash.
|
package/src/audit/discover.ts
CHANGED
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
* read `output.files`, so the whole chart is one AuditInput.
|
|
24
24
|
* - Terraform is also a directory BUNDLE, keyed by that directory's own
|
|
25
25
|
* `.tf` filenames, not recursively: a nested directory with its own
|
|
26
|
-
* `.tf` files (e.g. `modules/foo`) is
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* `.tf` files (e.g. `modules/foo`) is its own AuditInput, UNLESS another
|
|
27
|
+
* directory's `.tf` calls it as a local module, in which case it is a
|
|
28
|
+
* child module of that root and is parsed as part of it (#2217). `.tf`
|
|
29
|
+
* isn't YAML/JSON either, so `detectTemplate` can't see it.
|
|
29
30
|
* - k8s `detectTemplate` matches any `apiVersion`+`kind`, including GCP Config
|
|
30
31
|
* Connector (`cnrm.cloud.google.com`) resources and fountain
|
|
31
32
|
* (`fountain.dev/v1`) manifests, so gcp and fountain must be tried first.
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
*/
|
|
35
36
|
|
|
36
37
|
import { readdirSync, readFileSync, statSync } from "fs";
|
|
37
|
-
import { basename, join, relative } from "path";
|
|
38
|
+
import { basename, join, relative, resolve } from "path";
|
|
38
39
|
import { parseYAML } from "../yaml";
|
|
39
40
|
import type { LexiconPlugin } from "../lexicon";
|
|
40
41
|
import type { AuditInput, AuditLexicon } from "./core";
|
|
@@ -391,17 +392,103 @@ function classifyHelm(files: RepoFile[], plugin: DetectPlugin | undefined): { in
|
|
|
391
392
|
return { inputs, prefixes };
|
|
392
393
|
}
|
|
393
394
|
|
|
395
|
+
/**
|
|
396
|
+
* A `source` attribute naming a local path, the only module source Terraform
|
|
397
|
+
* reads from the repository itself. A registry or git source ("hashicorp/aws",
|
|
398
|
+
* "git::https://...") never starts with `./` or `../`, and `source` inside
|
|
399
|
+
* `required_providers` is a registry address, so the prefix is what tells the
|
|
400
|
+
* two apart without an HCL parser here. `classifyTerraform` uses this to tell
|
|
401
|
+
* a root module from a directory that is somebody's child module; the lexicon
|
|
402
|
+
* plugin parses the calls properly once it has the root.
|
|
403
|
+
*/
|
|
404
|
+
const LOCAL_MODULE_SOURCE = /(?:^|[\s{,])source\s*=\s*"(\.{1,2}\/[^"]*)"/g;
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Resolve `rel` against the repo-relative directory `dir`. Returns undefined
|
|
408
|
+
* when the path climbs above the audited root, which is a directory this walk
|
|
409
|
+
* never read and so can never be one of its inputs.
|
|
410
|
+
*/
|
|
411
|
+
function resolveRepoDir(dir: string, rel: string): string | undefined {
|
|
412
|
+
const parts = dir === "" ? [] : dir.split("/");
|
|
413
|
+
for (const segment of rel.split("/")) {
|
|
414
|
+
if (segment === "" || segment === ".") continue;
|
|
415
|
+
if (segment === "..") {
|
|
416
|
+
if (parts.length === 0) return undefined;
|
|
417
|
+
parts.pop();
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
parts.push(segment);
|
|
421
|
+
}
|
|
422
|
+
return parts.join("/");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The directories that are somebody's local child module: a `.tf` file in
|
|
427
|
+
* another directory of this walk calls them with a relative `source`.
|
|
428
|
+
*
|
|
429
|
+
* They are dropped as audit inputs of their own, because the root that calls
|
|
430
|
+
* them parses them as child scopes (the terraform plugin's `auditEntities`
|
|
431
|
+
* descends from `AuditInput.dir`, #2217). Auditing them twice would report
|
|
432
|
+
* every finding in them twice, once per scope, and auditing them as roots is
|
|
433
|
+
* what made `chant audit` disagree with `chant build` about which rules apply
|
|
434
|
+
* to a child module at all.
|
|
435
|
+
*
|
|
436
|
+
* A directory a cycle reaches but no root does is kept: `a` calling `b`
|
|
437
|
+
* calling `a` is a configuration Terraform refuses, and dropping both would
|
|
438
|
+
* leave the files audited by nothing.
|
|
439
|
+
*/
|
|
440
|
+
function calledAsLocalModule(byDir: Map<string, Record<string, string>>): Set<string> {
|
|
441
|
+
const callees = new Map<string, Set<string>>();
|
|
442
|
+
const called = new Set<string>();
|
|
443
|
+
for (const [dir, bundle] of byDir) {
|
|
444
|
+
const targets = new Set<string>();
|
|
445
|
+
for (const source of Object.values(bundle)) {
|
|
446
|
+
for (const match of source.matchAll(LOCAL_MODULE_SOURCE)) {
|
|
447
|
+
const target = resolveRepoDir(dir, match[1]);
|
|
448
|
+
if (target === undefined || target === dir || !byDir.has(target)) continue;
|
|
449
|
+
targets.add(target);
|
|
450
|
+
called.add(target);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
callees.set(dir, targets);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const queue = [...byDir.keys()].filter((dir) => !called.has(dir));
|
|
457
|
+
const reached = new Set<string>(queue);
|
|
458
|
+
while (queue.length > 0) {
|
|
459
|
+
for (const target of callees.get(queue.shift()!) ?? []) {
|
|
460
|
+
if (reached.has(target)) continue;
|
|
461
|
+
reached.add(target);
|
|
462
|
+
queue.push(target);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return new Set([...called].filter((dir) => reached.has(dir)));
|
|
466
|
+
}
|
|
467
|
+
|
|
394
468
|
/**
|
|
395
469
|
* Collect Terraform root modules as directory bundles from an in-memory file
|
|
396
470
|
* set, modeled on `classifyHelm`. Unlike a Helm chart (its whole subtree is
|
|
397
471
|
* one bundle), a Terraform root module references only its own sibling
|
|
398
472
|
* files, so grouping is by each `.tf` file's immediate directory, not
|
|
399
|
-
* recursive
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
473
|
+
* recursive.
|
|
474
|
+
*
|
|
475
|
+
* A directory another one calls as a local module is not a root module and
|
|
476
|
+
* gets no input of its own (#2217): the root that calls it parses it as a
|
|
477
|
+
* child scope, which is what Terraform does and what `chant build` already
|
|
478
|
+
* did. That descent reads the child's directory from disk, so it only happens
|
|
479
|
+
* when `baseDir` says a local walk found these files; an in-memory file set
|
|
480
|
+
* with no directory behind it keeps every `.tf` directory as its own input,
|
|
481
|
+
* as before.
|
|
482
|
+
*
|
|
483
|
+
* Returns inputs plus the exact `.tf` paths claimed, so the loose-file pass in
|
|
484
|
+
* `classifyFiles` can skip them — a child module's files are claimed too, by
|
|
485
|
+
* the root that will parse them.
|
|
403
486
|
*/
|
|
404
|
-
function classifyTerraform(
|
|
487
|
+
function classifyTerraform(
|
|
488
|
+
files: RepoFile[],
|
|
489
|
+
plugin: DetectPlugin | undefined,
|
|
490
|
+
baseDir?: string,
|
|
491
|
+
): { inputs: AuditInput[]; claimed: Set<string> } {
|
|
405
492
|
const inputs: AuditInput[] = [];
|
|
406
493
|
const claimed = new Set<string>();
|
|
407
494
|
if (!plugin) return { inputs, claimed };
|
|
@@ -416,20 +503,40 @@ function classifyTerraform(files: RepoFile[], plugin: DetectPlugin | undefined):
|
|
|
416
503
|
byDir.set(dir, bundle);
|
|
417
504
|
claimed.add(f.path);
|
|
418
505
|
}
|
|
506
|
+
const childModules = baseDir === undefined ? new Set<string>() : calledAsLocalModule(byDir);
|
|
419
507
|
for (const [dir, bundle] of byDir) {
|
|
508
|
+
if (childModules.has(dir)) continue;
|
|
420
509
|
// `content` is the whole root module, every `.tf` in filename order with a
|
|
421
510
|
// `# file:` line comment (legal HCL) marking each boundary, so a lexicon's
|
|
422
511
|
// `auditEntities(content)` sees the module Terraform itself would load,
|
|
423
|
-
// not one arbitrary file of it. `files` keeps the per-file split
|
|
512
|
+
// not one arbitrary file of it. `files` keeps the per-file split, and
|
|
513
|
+
// `dir` is where the local modules it calls are read from.
|
|
424
514
|
const content = Object.keys(bundle)
|
|
425
515
|
.sort()
|
|
426
516
|
.map((name) => `# file: ${name}\n${bundle[name]}`)
|
|
427
517
|
.join("\n");
|
|
428
|
-
inputs.push({
|
|
518
|
+
inputs.push({
|
|
519
|
+
path: dir === "" ? "." : dir,
|
|
520
|
+
content,
|
|
521
|
+
lexicon: "terraform",
|
|
522
|
+
files: bundle,
|
|
523
|
+
...(baseDir === undefined ? {} : { dir: resolve(baseDir, dir), baseDir }),
|
|
524
|
+
});
|
|
429
525
|
}
|
|
430
526
|
return { inputs, claimed };
|
|
431
527
|
}
|
|
432
528
|
|
|
529
|
+
/** Where a set of classified files came from, when they came from a local walk (#2217). */
|
|
530
|
+
export interface ClassifyOptions {
|
|
531
|
+
/**
|
|
532
|
+
* Absolute path of the directory that was walked. Travels onto the inputs
|
|
533
|
+
* whose `path` names a directory, so a lexicon's `auditEntities` can read
|
|
534
|
+
* the files that directory references (terraform's local module calls).
|
|
535
|
+
* Omit for an in-memory or fetched file set: there is no directory to read.
|
|
536
|
+
*/
|
|
537
|
+
baseDir?: string;
|
|
538
|
+
}
|
|
539
|
+
|
|
433
540
|
/**
|
|
434
541
|
* Classify an in-memory set of repo files into per-lexicon audit inputs. Pure
|
|
435
542
|
* (no fs, no network) so the local walk and the remote tree-fetch share exactly
|
|
@@ -439,15 +546,16 @@ function classifyTerraform(files: RepoFile[], plugin: DetectPlugin | undefined):
|
|
|
439
546
|
* (delegating to each plugin's `detectTemplate`). Lexicons whose plugin isn't
|
|
440
547
|
* provided are skipped.
|
|
441
548
|
*/
|
|
442
|
-
export function classifyFiles(files: RepoFile[], plugins: DetectPlugin[]): AuditInput[] {
|
|
549
|
+
export function classifyFiles(files: RepoFile[], plugins: DetectPlugin[], opts: ClassifyOptions = {}): AuditInput[] {
|
|
443
550
|
const byName = new Map(plugins.map((p) => [p.name, p]));
|
|
551
|
+
const baseDir = opts.baseDir === undefined ? undefined : resolve(opts.baseDir);
|
|
444
552
|
|
|
445
553
|
// Helm claims whole chart directories first; chart-internal files are excluded
|
|
446
554
|
// from the loose-file pass so templates aren't double-audited.
|
|
447
555
|
const helm = classifyHelm(files, byName.get("helm"));
|
|
448
556
|
const underChart = (p: string): boolean => helm.prefixes.some((pre) => (pre === "" ? true : p.startsWith(pre)));
|
|
449
557
|
|
|
450
|
-
const terraform = classifyTerraform(files, byName.get("terraform"));
|
|
558
|
+
const terraform = classifyTerraform(files, byName.get("terraform"), baseDir);
|
|
451
559
|
|
|
452
560
|
const inputs: AuditInput[] = [...helm.inputs, ...terraform.inputs];
|
|
453
561
|
for (const { path, content } of files) {
|
|
@@ -483,7 +591,7 @@ export function classifyFiles(files: RepoFile[], plugins: DetectPlugin[]): Audit
|
|
|
483
591
|
* lexicon whose plugin isn't provided, so a caller can scope discovery.
|
|
484
592
|
*/
|
|
485
593
|
export function discoverByDetection(root: string, plugins: DetectPlugin[]): AuditInput[] {
|
|
486
|
-
return classifyFiles(collectCandidates(root), plugins);
|
|
594
|
+
return classifyFiles(collectCandidates(root), plugins, { baseDir: root });
|
|
487
595
|
}
|
|
488
596
|
|
|
489
597
|
/** The walk half of `discoverByDetection`: candidate files read into memory, paths relative to the root. */
|
|
@@ -470,6 +470,8 @@ describe("auditCommand", () => {
|
|
|
470
470
|
expect(valid).toBe(true);
|
|
471
471
|
});
|
|
472
472
|
|
|
473
|
+
// The scope is the discovered root module's own directory (`infra`), which
|
|
474
|
+
// is what the terraform plugin names an audited scope after since #2217.
|
|
473
475
|
describe("TF001's missing-resource shape renders in all three reporters (chant #2113)", () => {
|
|
474
476
|
const dir = join(tmpdir(), `chant-audit-tf-missing-${process.pid}`);
|
|
475
477
|
|
|
@@ -484,7 +486,7 @@ describe("auditCommand", () => {
|
|
|
484
486
|
test("stylish names the missing kind and scope instead of falling back to entity", async () => {
|
|
485
487
|
setup();
|
|
486
488
|
const result = await auditCommand({ path: dir, format: "stylish" });
|
|
487
|
-
expect(result.output).toContain("(missing backend in
|
|
489
|
+
expect(result.output).toContain("(missing backend in infra)");
|
|
488
490
|
teardown();
|
|
489
491
|
});
|
|
490
492
|
|
|
@@ -493,7 +495,7 @@ describe("auditCommand", () => {
|
|
|
493
495
|
const result = await auditCommand({ path: dir, format: "json" });
|
|
494
496
|
const doc = JSON.parse(result.output) as { findings: Array<{ checkId: string; missing?: { kind: string; scope: string } }> };
|
|
495
497
|
const tf001 = doc.findings.find((f) => f.checkId === "TF001");
|
|
496
|
-
expect(tf001?.missing).toEqual({ kind: "backend", scope: "
|
|
498
|
+
expect(tf001?.missing).toEqual({ kind: "backend", scope: "infra" });
|
|
497
499
|
teardown();
|
|
498
500
|
});
|
|
499
501
|
|
|
@@ -504,7 +506,7 @@ describe("auditCommand", () => {
|
|
|
504
506
|
runs: Array<{ results: Array<{ ruleId: string; properties?: { missing?: { kind: string; scope: string } } }> }>;
|
|
505
507
|
};
|
|
506
508
|
const tf001 = doc.runs[0].results.find((r) => r.ruleId === "TF001");
|
|
507
|
-
expect(tf001?.properties?.missing).toEqual({ kind: "backend", scope: "
|
|
509
|
+
expect(tf001?.properties?.missing).toEqual({ kind: "backend", scope: "infra" });
|
|
508
510
|
|
|
509
511
|
const ajv = new Ajv({ strict: false, allErrors: true });
|
|
510
512
|
const validate = ajv.compile(loadSarifSchema() as object);
|
|
@@ -408,7 +408,11 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
408
408
|
// shape alone can't disambiguate them.
|
|
409
409
|
candidates = collectCandidates(options.path);
|
|
410
410
|
}
|
|
411
|
-
|
|
411
|
+
// A local target hands its directory to the classifier, so an input whose
|
|
412
|
+
// path names a directory carries the directory itself (#2217): terraform's
|
|
413
|
+
// parse-to-graph follows a root module's local `module` calls from there. A
|
|
414
|
+
// URL target has no local tree, and never fetches `.tf` in the first place.
|
|
415
|
+
const inputs = classifyFiles(candidates, plugins, isUrl ? {} : { baseDir: options.path });
|
|
412
416
|
const unclaimed = unclaimedFiles(candidates, inputs, plugins);
|
|
413
417
|
const scanned = inputs.map((i) => i.path);
|
|
414
418
|
|
|
@@ -1445,7 +1445,7 @@ export const pruneStaging = {
|
|
|
1445
1445
|
name: "prune-staging",
|
|
1446
1446
|
overview: "test",
|
|
1447
1447
|
phases: [
|
|
1448
|
-
{ name: "Approve", steps: [{ kind: "gate",
|
|
1448
|
+
{ name: "Approve", steps: [{ kind: "gate", gate: "approve-x" }] },
|
|
1449
1449
|
{ name: "Apply", steps: [
|
|
1450
1450
|
{ kind: "activity", fn: "nativeApply", args: { target: "kubectl", env: "staging", output: "dist", deleteMode: "gated" } },
|
|
1451
1451
|
] },
|
|
@@ -1502,6 +1502,44 @@ export const converge = {
|
|
|
1502
1502
|
),
|
|
1503
1503
|
).toBe(true);
|
|
1504
1504
|
});
|
|
1505
|
+
|
|
1506
|
+
// #2202: a gate step spelling its name `signalName` still builds, and the
|
|
1507
|
+
// build says so once, naming the Ops involved rather than each gate step.
|
|
1508
|
+
test("warns once, naming the Ops, when a gate step still uses the deprecated `signalName` key", async () => {
|
|
1509
|
+
const opFile = (name: string): string => `
|
|
1510
|
+
export const ${name.replace(/-/g, "")} = {
|
|
1511
|
+
[Symbol.for("chant.declarable")]: true,
|
|
1512
|
+
entityType: "Chant::Op",
|
|
1513
|
+
lexicon: "chant",
|
|
1514
|
+
kind: "resource",
|
|
1515
|
+
props: {
|
|
1516
|
+
name: "${name}",
|
|
1517
|
+
overview: "test",
|
|
1518
|
+
phases: [{ name: "Approve", steps: [{ kind: "gate", signalName: "approve-x" }] }],
|
|
1519
|
+
},
|
|
1520
|
+
};
|
|
1521
|
+
`;
|
|
1522
|
+
await writeFile(join(testDir, "old-a.op.ts"), opFile("old-a"));
|
|
1523
|
+
await writeFile(join(testDir, "old-b.op.ts"), opFile("old-b"));
|
|
1524
|
+
await writeFile(
|
|
1525
|
+
join(testDir, "new-c.op.ts"),
|
|
1526
|
+
opFile("new-c").replace("signalName", "gate"),
|
|
1527
|
+
);
|
|
1528
|
+
|
|
1529
|
+
const result = await buildCommand({
|
|
1530
|
+
path: testDir,
|
|
1531
|
+
format: "json",
|
|
1532
|
+
serializers: [mockSerializer],
|
|
1533
|
+
plugins: [],
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
const deprecation = result.warnings.filter((w) => w.includes("signalName"));
|
|
1537
|
+
expect(deprecation).toHaveLength(1);
|
|
1538
|
+
expect(deprecation[0]).toContain("old-a");
|
|
1539
|
+
expect(deprecation[0]).toContain("old-b");
|
|
1540
|
+
expect(deprecation[0]).not.toContain("new-c");
|
|
1541
|
+
expect(deprecation[0]).toContain("0.60.0");
|
|
1542
|
+
});
|
|
1505
1543
|
});
|
|
1506
1544
|
|
|
1507
1545
|
// ── #284 bug 1: -o extension drives format when --format is absent ────────
|
|
@@ -18,6 +18,7 @@ import { runPostSynthChecks, type PostSynthDiagnostic } from "../../lint/post-sy
|
|
|
18
18
|
import { loadActivityContracts } from "../../op/activity-contract-registry";
|
|
19
19
|
import { isOpEntity } from "../../op/resource";
|
|
20
20
|
import { serializeOpIR } from "../../op/op-ir";
|
|
21
|
+
import { opUsesDeprecatedGateKey, DEPRECATED_GATE_KEY_WARNING } from "../../op/gate-name";
|
|
21
22
|
import type { OpConfig } from "../../op/types";
|
|
22
23
|
import { coreReceiptChecks } from "../../lint/receipt-checks";
|
|
23
24
|
import { coreOutputChecks } from "../../lint/output-checks";
|
|
@@ -639,6 +640,19 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
639
640
|
// not a resource manifest, so an Ops-only project legitimately produces no
|
|
640
641
|
// serializer output and must not be reported as a broken import.
|
|
641
642
|
const declaredOps = [...result.entities.values()].filter(isOpEntity);
|
|
643
|
+
|
|
644
|
+
// The gate step's name key was renamed from `signalName` to `gate` in #2202.
|
|
645
|
+
// The old key is still read; a build that finds an Op still using it says so
|
|
646
|
+
// once, naming every Op involved, rather than once per gate step.
|
|
647
|
+
const opsOnOldGateKey = declaredOps
|
|
648
|
+
.map((entity) => (entity as unknown as { props?: OpConfig }).props)
|
|
649
|
+
.filter((config): config is OpConfig => Boolean(config?.name) && opUsesDeprecatedGateKey(config as OpConfig))
|
|
650
|
+
.map((config) => config.name);
|
|
651
|
+
if (opsOnOldGateKey.length > 0) {
|
|
652
|
+
warnings.push(
|
|
653
|
+
formatWarning({ message: `${DEPRECATED_GATE_KEY_WARNING} Ops: ${opsOnOldGateKey.join(", ")}.` }),
|
|
654
|
+
);
|
|
655
|
+
}
|
|
642
656
|
if (
|
|
643
657
|
result.sourceFileCount > 0 &&
|
|
644
658
|
result.outputs.size === 0 &&
|