@intentius/chant-lexicon-helm 0.0.24 → 0.1.4

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.
@@ -1,36 +1,38 @@
1
1
  {
2
2
  "algorithm": "xxhash64",
3
3
  "artifacts": {
4
- "manifest.json": "2557e415419da552",
4
+ "manifest.json": "63a0898ec956a5de",
5
5
  "meta.json": "b7aab243e162dfaf",
6
6
  "types/index.d.ts": "e8011da51963f058",
7
- "rules/no-hardcoded-image.ts": "c75aa9c33016c3b9",
8
- "rules/values-no-secrets.ts": "213276bfe1fb8ff7",
7
+ "rules/values-no-helm-tpl.ts": "ed23236546936fd0",
9
8
  "rules/chart-metadata.ts": "d0cff2b78fed78d3",
10
- "rules/whm103.ts": "a777a13f2eaf1831",
9
+ "rules/values-no-secrets.ts": "213276bfe1fb8ff7",
10
+ "rules/no-hardcoded-image.ts": "c75aa9c33016c3b9",
11
+ "rules/whm301.ts": "f3ed3a269093527",
11
12
  "rules/whm104.ts": "e2d644da2a9dc605",
12
- "rules/whm201.ts": "f263fe69901552eb",
13
- "rules/whm101.ts": "533ae8f65cb2227b",
14
- "rules/whm402.ts": "f722014a723654af",
15
- "rules/whm204.ts": "7b57de71795fc102",
13
+ "rules/whm005-no-empty-wrapper.ts": "1a1baea985f0bd86",
14
+ "rules/whm103.ts": "a777a13f2eaf1831",
16
15
  "rules/whm406.ts": "c76ac5a44c3e9dcd",
17
- "rules/whm301.ts": "f3ed3a269093527",
16
+ "rules/whm405.ts": "ec82442f9120e5e0",
17
+ "rules/whm105.ts": "8977ec834713b90c",
18
+ "rules/whm403.ts": "729d10edb48b0d03",
18
19
  "rules/whm203.ts": "2bb546d268c84232",
20
+ "rules/whm407.ts": "f1fbc6942aff8e96",
21
+ "rules/whm502.ts": "2c9901ecbfaf92cb",
19
22
  "rules/whm404.ts": "8847425e930d41c1",
20
- "rules/whm102.ts": "4c0c494c253df56a",
21
- "rules/helm-helpers.ts": "2e0cc249268227ed",
23
+ "rules/whm402.ts": "f722014a723654af",
22
24
  "rules/whm302.ts": "44e58ce621ae6d1a",
23
- "rules/whm502.ts": "2c9901ecbfaf92cb",
24
- "rules/whm407.ts": "f1fbc6942aff8e96",
25
+ "rules/whm501.ts": "e9a9f2b4e034a51f",
26
+ "rules/helm-helpers.ts": "2e0cc249268227ed",
27
+ "rules/whm101.ts": "533ae8f65cb2227b",
28
+ "rules/whm102.ts": "4c0c494c253df56a",
25
29
  "rules/whm401.ts": "fe72c3c450a12f93",
26
- "rules/whm105.ts": "8977ec834713b90c",
27
- "rules/whm403.ts": "729d10edb48b0d03",
28
- "rules/whm405.ts": "ec82442f9120e5e0",
30
+ "rules/whm201.ts": "f263fe69901552eb",
29
31
  "rules/whm202.ts": "dbe1cbb3237be84f",
30
- "rules/whm501.ts": "e9a9f2b4e034a51f",
31
- "skills/chant-helm.md": "8ccf71feddc9536c",
32
- "skills/chant-helm-patterns.md": "7a2163247f44bf6d",
32
+ "rules/whm204.ts": "7b57de71795fc102",
33
+ "skills/chant-helm.md": "6476c552b9235085",
34
+ "skills/chant-helm-patterns.md": "cc9bfe5595f22710",
33
35
  "skills/chant-helm-security.md": "a7b950513dac7d37"
34
36
  },
35
- "composite": "43682c36509928ac"
37
+ "composite": "3af38f2aed6c80fd"
36
38
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helm",
3
- "version": "0.0.24",
3
+ "version": "0.1.4",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Helm",
6
6
  "intrinsics": [
@@ -0,0 +1,92 @@
1
+ /**
2
+ * WHM004: HelmTpl Expression Has No Effect in Values Constructor
3
+ *
4
+ * Detects Values constructor props that use `v.xxx` (the `values` proxy)
5
+ * or any HelmTpl-like expression. values.yaml is static YAML — it is NOT
6
+ * processed as a Go template by Helm. These expressions silently become ''.
7
+ *
8
+ * Bad: new Values({ host: v.pgHost })
9
+ * Good: new Values({ host: runtimeSlot("Cloud SQL private IP") })
10
+ */
11
+
12
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
13
+ import * as ts from "typescript";
14
+
15
+ export const valuesNoHelmTplRule: LintRule = {
16
+ id: "WHM004",
17
+ severity: "warning",
18
+ category: "correctness",
19
+ description:
20
+ "HelmTpl expression has no effect in values.yaml — use runtimeSlot() for deploy-time values",
21
+
22
+ check(context: LintContext): LintDiagnostic[] {
23
+ const { sourceFile } = context;
24
+ const diagnostics: LintDiagnostic[] = [];
25
+
26
+ function visit(node: ts.Node): void {
27
+ if (
28
+ ts.isNewExpression(node) &&
29
+ ts.isIdentifier(node.expression) &&
30
+ node.expression.text === "Values" &&
31
+ node.arguments &&
32
+ node.arguments.length > 0
33
+ ) {
34
+ const arg = node.arguments[0];
35
+ if (ts.isObjectLiteralExpression(arg)) {
36
+ checkObjectLiteral(arg, [], sourceFile, diagnostics);
37
+ }
38
+ }
39
+ ts.forEachChild(node, visit);
40
+ }
41
+
42
+ visit(sourceFile);
43
+ return diagnostics;
44
+ },
45
+ };
46
+
47
+ /**
48
+ * Get the root identifier name of a property access / call chain.
49
+ * v.foo → "v"; values.x.pipe("fn") → "values"; runtimeSlot() → "runtimeSlot"
50
+ */
51
+ function getRootIdentifier(node: ts.Node): string | null {
52
+ if (ts.isIdentifier(node)) return node.text;
53
+ if (ts.isPropertyAccessExpression(node)) return getRootIdentifier(node.expression);
54
+ if (ts.isCallExpression(node)) return getRootIdentifier(node.expression);
55
+ return null;
56
+ }
57
+
58
+ function isHelmTplExpr(node: ts.Node): boolean {
59
+ const root = getRootIdentifier(node);
60
+ return root === "v" || root === "values";
61
+ }
62
+
63
+ function checkObjectLiteral(
64
+ obj: ts.ObjectLiteralExpression,
65
+ path: string[],
66
+ sourceFile: ts.SourceFile,
67
+ diagnostics: LintDiagnostic[],
68
+ ): void {
69
+ for (const prop of obj.properties) {
70
+ if (!ts.isPropertyAssignment(prop)) continue;
71
+
72
+ const keyName = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)
73
+ ? prop.name.text
74
+ : undefined;
75
+ const propPath = keyName ? [...path, keyName] : path;
76
+
77
+ if (isHelmTplExpr(prop.initializer)) {
78
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(prop.getStart());
79
+ const pathStr = propPath.join(".");
80
+ diagnostics.push({
81
+ file: sourceFile.fileName,
82
+ line: line + 1,
83
+ column: character + 1,
84
+ ruleId: "WHM004",
85
+ severity: "warning",
86
+ message: `HelmTpl expression has no effect in values.yaml (values.yaml is not a Go template). Use runtimeSlot() for deploy-time values or a static default.${pathStr ? ` (path: ${pathStr})` : ""}`,
87
+ });
88
+ } else if (ts.isObjectLiteralExpression(prop.initializer)) {
89
+ checkObjectLiteral(prop.initializer, propPath, sourceFile, diagnostics);
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * WHM005: Chart Has Sub-chart Dependencies But Generates No Templates
3
+ *
4
+ * A chart with HelmDependency entries but no templates/*.yaml files generates
5
+ * an empty templates/ directory. Deploying it requires `helm dependency build`
6
+ * as a non-obvious prerequisite.
7
+ *
8
+ * If you only need value overrides for an upstream chart, deploy it directly:
9
+ * helm upgrade upstream-chart -f values-override.yaml
10
+ * and use ValuesOverride to generate the override file.
11
+ */
12
+
13
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
14
+ import { getChartFiles } from "./helm-helpers";
15
+
16
+ export const whm005: PostSynthCheck = {
17
+ id: "WHM005",
18
+ description:
19
+ "Chart with sub-chart dependencies but no templates should deploy upstream chart directly",
20
+
21
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
22
+ const diagnostics: PostSynthDiagnostic[] = [];
23
+
24
+ for (const [, output] of ctx.outputs) {
25
+ const files = getChartFiles(output);
26
+ const chartYaml = files["Chart.yaml"];
27
+ if (!chartYaml) continue;
28
+
29
+ // Check for non-empty dependencies block
30
+ const hasDependencies = /^dependencies:/m.test(chartYaml);
31
+ if (!hasDependencies) continue;
32
+
33
+ // Check for template files (excluding _helpers.tpl and NOTES.txt)
34
+ const hasTemplates = Object.keys(files).some((path) => {
35
+ if (!path.startsWith("templates/")) return false;
36
+ const filename = path.slice("templates/".length);
37
+ if (filename === "_helpers.tpl" || filename === "NOTES.txt") return false;
38
+ return filename.endsWith(".yaml") || filename.endsWith(".yml");
39
+ });
40
+
41
+ if (!hasTemplates) {
42
+ diagnostics.push({
43
+ checkId: "WHM005",
44
+ severity: "warning",
45
+ message:
46
+ "Chart has sub-chart dependencies but generates no templates. Deploying this chart requires 'helm dependency build' first. If you only need value overrides for an upstream chart, deploy it directly with 'helm upgrade upstream-chart -f values-override.yaml' and use ValuesOverride to generate the override file.",
47
+ lexicon: "helm",
48
+ });
49
+ }
50
+ }
51
+
52
+ return diagnostics;
53
+ },
54
+ };
@@ -215,6 +215,58 @@ ChartRef.Name // {{ .Chart.Name }}
215
215
  ChartRef.Version // {{ .Chart.Version }}
216
216
  ```
217
217
 
218
+ ## Deploying Upstream Charts with Value Overrides
219
+
220
+ When you need to deploy an upstream chart (like `gitlab/gitlab`) with custom values, avoid wrapper charts with no templates. Instead:
221
+
222
+ 1. Use `runtimeSlot()` in `new Values({...})` for deploy-time values (DB IPs, bucket names, replicas)
223
+ 2. Use `ValuesOverride` for static values shared across all environments (disabled bundled services, shared secret refs)
224
+ 3. Deploy the upstream chart directly with `-f` flags
225
+
226
+ ```typescript
227
+ import { Values, ValuesOverride, runtimeSlot } from "@intentius/chant-lexicon-helm";
228
+
229
+ // Runtime slots → values-runtime-slots.yaml (deploy-time checklist)
230
+ export const vals = new Values({
231
+ global: {
232
+ psql: { host: runtimeSlot("Cloud SQL private IP") },
233
+ redis: { host: runtimeSlot("Memorystore host") },
234
+ },
235
+ });
236
+
237
+ // Static overrides → values-base.yaml (shared across all deployments)
238
+ export const baseOverride = new ValuesOverride({
239
+ filename: "values-base",
240
+ values: {
241
+ postgresql: { install: false },
242
+ redis: { install: false },
243
+ certmanager: { install: false },
244
+ "nginx-ingress": { enabled: false },
245
+ },
246
+ });
247
+ ```
248
+
249
+ Outputs:
250
+ - `chart-dir/values.yaml` — defaults; runtime slots appear as `''`
251
+ - `chart-dir/values-base.yaml` — generated static overrides
252
+ - `chart-dir/values-runtime-slots.yaml` — deploy-time slots with descriptions as comments
253
+
254
+ Deploy:
255
+ ```bash
256
+ # chant build generates chart-dir/ including values-base.yaml
257
+ chant build
258
+
259
+ # Fill in runtime-slot values (one per environment)
260
+ # values-prod.yaml contains: global.psql.host, global.redis.host, etc.
261
+
262
+ helm upgrade --install my-release upstream/chart \
263
+ -f chart-dir/values-base.yaml \
264
+ -f values-prod.yaml \
265
+ --wait
266
+ ```
267
+
268
+ **WHM005** warns when a chart has `HelmDependency` entries but generates no templates — this is the "empty wrapper" anti-pattern that requires `helm dependency build` as a non-obvious prerequisite.
269
+
218
270
  ## Template Functions
219
271
 
220
272
  ```typescript
@@ -65,6 +65,53 @@ import { If, values } from "@intentius/chant-lexicon-helm";
65
65
  export const ingress = If(values.ingress.enabled, new Ingress({ ... }));
66
66
  ```
67
67
 
68
+ ### Runtime values and value overrides
69
+
70
+ Use `runtimeSlot()` for deploy-time values that cannot be known at build time (database IPs, bucket names, etc.):
71
+
72
+ ```typescript
73
+ import { Values, runtimeSlot } from "@intentius/chant-lexicon-helm";
74
+
75
+ export const vals = new Values({
76
+ global: {
77
+ psql: {
78
+ host: runtimeSlot("Cloud SQL private IP"), // → '' in values.yaml
79
+ },
80
+ redis: {
81
+ host: runtimeSlot("Memorystore persistent host"),
82
+ },
83
+ },
84
+ });
85
+ ```
86
+
87
+ `runtimeSlot()` generates two outputs:
88
+ - `values.yaml` — the field emits `''` (empty placeholder, safe for `helm template`)
89
+ - `values-runtime-slots.yaml` — lists only the slots with descriptions as YAML comments, for use as a deploy-time checklist
90
+
91
+ **WHM004** fires when `v.xxx` (the values proxy) is used inside `new Values({...})` — values.yaml is not a Go template, so `{{ .Values.x }}` would silently become `''`. Use `runtimeSlot()` instead.
92
+
93
+ Use `ValuesOverride` for static configuration shared across all deployments, like disabling bundled services:
94
+
95
+ ```typescript
96
+ import { ValuesOverride } from "@intentius/chant-lexicon-helm";
97
+
98
+ export const baseOverride = new ValuesOverride({
99
+ filename: "values-base", // → generates chart-dir/values-base.yaml
100
+ values: {
101
+ postgresql: { install: false },
102
+ redis: { install: false },
103
+ certmanager: { install: false },
104
+ },
105
+ });
106
+ ```
107
+
108
+ Pass the generated file at deploy time:
109
+ ```bash
110
+ helm upgrade --install my-release chart/
111
+ -f chart/values-base.yaml # generated by ValuesOverride
112
+ -f values-prod.yaml # runtime overrides (from values-runtime-slots.yaml)
113
+ ```
114
+
68
115
  ### Built-in objects
69
116
 
70
117
  ```typescript
@@ -218,28 +265,30 @@ const lifecycle = HelmCRDLifecycle({
218
265
 
219
266
  ## Lint rules
220
267
 
221
- | Rule | Description |
222
- |------|-------------|
223
- | WHM001 | Chart must have name, version, apiVersion |
224
- | WHM002 | Values should not contain bare secrets |
225
- | WHM003 | Container images should use values references |
226
- | WHM101 | Chart.yaml has valid apiVersion (v2) |
227
- | WHM102 | values.schema.json present when Values used |
228
- | WHM103 | Go template syntax valid (balanced braces) |
229
- | WHM104 | NOTES.txt exists for application charts |
230
- | WHM105 | _helpers.tpl exists |
231
- | WHM201 | Resources have standard Helm labels |
232
- | WHM301 | At least one test for application charts |
233
- | WHM302 | Resource limits set |
234
- | WHM401 | Image uses :latest tag or no tag |
235
- | WHM402 | runAsNonRoot not set |
236
- | WHM403 | readOnlyRootFilesystem not set |
237
- | WHM404 | privileged: true detected |
238
- | WHM405 | Resource spec missing cpu/memory |
239
- | WHM406 | CRD lifecycle limitation |
240
- | WHM407 | Secret with inline data |
241
- | WHM501 | Unused values keys |
242
- | WHM502 | Deprecated K8s API versions |
268
+ | Rule | Phase | Description |
269
+ |------|-------|-------------|
270
+ | WHM001 | pre-synth | Chart must have name, version, apiVersion |
271
+ | WHM002 | pre-synth | Values should not contain bare secrets |
272
+ | WHM003 | pre-synth | Container images should use values references |
273
+ | WHM004 | pre-synth | HelmTpl (`v.xxx`) has no effect in Values — use `runtimeSlot()` |
274
+ | WHM005 | post-synth | Chart with dependencies but no templates — deploy upstream chart directly |
275
+ | WHM101 | post-synth | Chart.yaml has valid apiVersion (v2) |
276
+ | WHM102 | post-synth | values.schema.json present when Values used |
277
+ | WHM103 | post-synth | Go template syntax valid (balanced braces) |
278
+ | WHM104 | post-synth | NOTES.txt exists for application charts |
279
+ | WHM105 | post-synth | _helpers.tpl exists |
280
+ | WHM201 | post-synth | Resources have standard Helm labels |
281
+ | WHM301 | post-synth | At least one test for application charts |
282
+ | WHM302 | post-synth | Resource limits set |
283
+ | WHM401 | post-synth | Image uses :latest tag or no tag |
284
+ | WHM402 | post-synth | runAsNonRoot not set |
285
+ | WHM403 | post-synth | readOnlyRootFilesystem not set |
286
+ | WHM404 | post-synth | privileged: true detected |
287
+ | WHM405 | post-synth | Resource spec missing cpu/memory |
288
+ | WHM406 | post-synth | CRD lifecycle limitation |
289
+ | WHM407 | post-synth | Secret with inline data |
290
+ | WHM501 | post-synth | Unused values keys |
291
+ | WHM502 | post-synth | Deprecated K8s API versions |
243
292
 
244
293
  ## OCI registry workflow
245
294
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-helm",
3
- "version": "0.0.24",
3
+ "version": "0.1.4",
4
4
  "description": "Helm chart lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -43,10 +43,13 @@
43
43
  "prepack": "bun run generate && bun run bundle && bun run validate"
44
44
  },
45
45
  "dependencies": {
46
- "@intentius/chant": "0.0.22",
47
- "@intentius/chant-lexicon-k8s": "0.0.22"
46
+ "@intentius/chant-lexicon-k8s": "0.1.4"
48
47
  },
49
48
  "devDependencies": {
49
+ "@intentius/chant": "0.1.4",
50
50
  "typescript": "^5.9.3"
51
+ },
52
+ "peerDependencies": {
53
+ "@intentius/chant": "^0.1.0"
51
54
  }
52
55
  }
@@ -102,6 +102,7 @@ export async function generateDocs(opts?: { verbose?: boolean }): Promise<void>
102
102
  description: "Typed constructors for parameterized Helm charts",
103
103
  distDir: join(pkgDir, "dist"),
104
104
  outDir: join(pkgDir, "docs"),
105
+ basePath: process.env.DOCS_BASE_PATH ?? "/chant/lexicons/helm/",
105
106
  overview,
106
107
  outputFormat,
107
108
  serviceFromType,
@@ -474,8 +475,8 @@ When you scaffold a new project with \`chant init --lexicon helm\`, the skill is
474
475
  ],
475
476
  };
476
477
 
477
- const result = await docsPipeline(config);
478
- writeDocsSite(result, config.outDir);
478
+ const result = docsPipeline(config);
479
+ writeDocsSite(config, result);
479
480
 
480
481
  if (opts?.verbose) {
481
482
  console.error(`Generated ${result.pages.length} documentation pages`);
package/src/index.ts CHANGED
@@ -5,7 +5,7 @@ export { helmSerializer } from "./serializer";
5
5
  export { helmPlugin } from "./plugin";
6
6
 
7
7
  // Resources
8
- export { Chart, Values, HelmTest, HelmNotes, HelmHook, HelmDependency, HelmMaintainer, HelmCRD } from "./resources";
8
+ export { Chart, Values, ValuesOverride, HelmTest, HelmNotes, HelmHook, HelmDependency, HelmMaintainer, HelmCRD } from "./resources";
9
9
 
10
10
  // Intrinsics
11
11
  export {
@@ -14,6 +14,9 @@ export {
14
14
  HELM_IF_KEY,
15
15
  HELM_RANGE_KEY,
16
16
  HELM_WITH_KEY,
17
+ RuntimeSlot,
18
+ RUNTIME_SLOT_KEY,
19
+ runtimeSlot,
17
20
  values,
18
21
  Release,
19
22
  ChartRef,
package/src/intrinsics.ts CHANGED
@@ -473,6 +473,59 @@ export function argoWave(wave: number): Record<string, string> {
473
473
  return { "argocd.argoproj.io/sync-wave": String(wave) };
474
474
  }
475
475
 
476
+ // ── RuntimeSlot ───────────────────────────────────────────
477
+
478
+ /**
479
+ * JSON marker key used by the serializer to detect runtime slot placeholders.
480
+ * Distinguished from HelmTpl: RuntimeSlot appears in values.yaml as '' AND
481
+ * is collected into values-runtime-slots.yaml with its description as a comment.
482
+ */
483
+ export const RUNTIME_SLOT_KEY = "__runtime_slot" as const;
484
+
485
+ /**
486
+ * A deploy-time value placeholder in a Values object.
487
+ *
488
+ * Marks fields that cannot be known at build time (DB IP, bucket name, etc.)
489
+ * and must be supplied via a `-f` override file when running `helm upgrade`.
490
+ *
491
+ * Two outputs are generated:
492
+ * - `values.yaml` — the field emits `''` (empty placeholder)
493
+ * - `values-runtime-slots.yaml` — lists only the RuntimeSlot fields with
494
+ * their descriptions as YAML comments, for use as a deploy-time checklist
495
+ *
496
+ * ```ts
497
+ * new Values({
498
+ * global: {
499
+ * psql: { host: runtimeSlot("Cloud SQL private IP") },
500
+ * }
501
+ * })
502
+ * ```
503
+ */
504
+ export class RuntimeSlot implements Intrinsic {
505
+ readonly [INTRINSIC_MARKER] = true as const;
506
+ readonly description: string;
507
+
508
+ constructor(description = "") {
509
+ this.description = description;
510
+ }
511
+
512
+ toJSON(): { [RUNTIME_SLOT_KEY]: string } {
513
+ return { [RUNTIME_SLOT_KEY]: this.description };
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Mark a Values field as a deploy-time runtime slot.
519
+ *
520
+ * Emits `''` in values.yaml and generates an entry in values-runtime-slots.yaml.
521
+ *
522
+ * @param description Human-readable description of what to provide.
523
+ * Emitted as a YAML comment in values-runtime-slots.yaml.
524
+ */
525
+ export function runtimeSlot(description = ""): RuntimeSlot {
526
+ return new RuntimeSlot(description);
527
+ }
528
+
476
529
  // ── Helpers ───────────────────────────────────────────────
477
530
 
478
531
  /**
@@ -18,6 +18,7 @@ import { whm406 } from "./whm406";
18
18
  import { whm407 } from "./whm407";
19
19
  import { whm501 } from "./whm501";
20
20
  import { whm502 } from "./whm502";
21
+ import { whm005 } from "./whm005-no-empty-wrapper";
21
22
 
22
23
  function makeCtx(files: Record<string, string>): PostSynthContext {
23
24
  const result: SerializerResult = { primary: files["Chart.yaml"] ?? "", files };
@@ -36,6 +37,48 @@ function makeCtx(files: Record<string, string>): PostSynthContext {
36
37
  };
37
38
  }
38
39
 
40
+ describe("WHM005: noEmptyWrapperChart", () => {
41
+ test("warns when chart has HelmDependency but no templates", () => {
42
+ const ctx = makeCtx({
43
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
44
+ "Chart.yaml.deps": "",
45
+ "templates/_helpers.tpl": "{{/* helpers */}}",
46
+ });
47
+ // Inject dependencies block
48
+ const files = {
49
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\ndependencies:\n - name: gitlab\n version: 8.7.2\n repository: https://charts.gitlab.io\n",
50
+ "templates/_helpers.tpl": "{{/* helpers */}}",
51
+ };
52
+ const ctx2 = makeCtx(files);
53
+ const diags = whm005.check(ctx2);
54
+ expect(diags).toHaveLength(1);
55
+ expect(diags[0].checkId).toBe("WHM005");
56
+ expect(diags[0].severity).toBe("warning");
57
+ });
58
+
59
+ test("passes when chart has dependencies and templates", () => {
60
+ const ctx = makeCtx({
61
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\ndependencies:\n - name: gitlab\n version: 8.7.2\n repository: https://charts.gitlab.io\n",
62
+ "templates/_helpers.tpl": "{{/* helpers */}}",
63
+ "templates/deployment.yaml": "apiVersion: apps/v1\nkind: Deployment\n",
64
+ });
65
+ expect(whm005.check(ctx)).toHaveLength(0);
66
+ });
67
+
68
+ test("passes when chart has no dependencies", () => {
69
+ const ctx = makeCtx({
70
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
71
+ "templates/deployment.yaml": "apiVersion: apps/v1\nkind: Deployment\n",
72
+ });
73
+ expect(whm005.check(ctx)).toHaveLength(0);
74
+ });
75
+
76
+ test("passes when no Chart.yaml present", () => {
77
+ const ctx = makeCtx({});
78
+ expect(whm005.check(ctx)).toHaveLength(0);
79
+ });
80
+ });
81
+
39
82
  describe("WHM101: Chart.yaml validation", () => {
40
83
  test("passes with valid Chart.yaml", () => {
41
84
  const ctx = makeCtx({
@@ -0,0 +1,54 @@
1
+ /**
2
+ * WHM005: Chart Has Sub-chart Dependencies But Generates No Templates
3
+ *
4
+ * A chart with HelmDependency entries but no templates/*.yaml files generates
5
+ * an empty templates/ directory. Deploying it requires `helm dependency build`
6
+ * as a non-obvious prerequisite.
7
+ *
8
+ * If you only need value overrides for an upstream chart, deploy it directly:
9
+ * helm upgrade upstream-chart -f values-override.yaml
10
+ * and use ValuesOverride to generate the override file.
11
+ */
12
+
13
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
14
+ import { getChartFiles } from "./helm-helpers";
15
+
16
+ export const whm005: PostSynthCheck = {
17
+ id: "WHM005",
18
+ description:
19
+ "Chart with sub-chart dependencies but no templates should deploy upstream chart directly",
20
+
21
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
22
+ const diagnostics: PostSynthDiagnostic[] = [];
23
+
24
+ for (const [, output] of ctx.outputs) {
25
+ const files = getChartFiles(output);
26
+ const chartYaml = files["Chart.yaml"];
27
+ if (!chartYaml) continue;
28
+
29
+ // Check for non-empty dependencies block
30
+ const hasDependencies = /^dependencies:/m.test(chartYaml);
31
+ if (!hasDependencies) continue;
32
+
33
+ // Check for template files (excluding _helpers.tpl and NOTES.txt)
34
+ const hasTemplates = Object.keys(files).some((path) => {
35
+ if (!path.startsWith("templates/")) return false;
36
+ const filename = path.slice("templates/".length);
37
+ if (filename === "_helpers.tpl" || filename === "NOTES.txt") return false;
38
+ return filename.endsWith(".yaml") || filename.endsWith(".yml");
39
+ });
40
+
41
+ if (!hasTemplates) {
42
+ diagnostics.push({
43
+ checkId: "WHM005",
44
+ severity: "warning",
45
+ message:
46
+ "Chart has sub-chart dependencies but generates no templates. Deploying this chart requires 'helm dependency build' first. If you only need value overrides for an upstream chart, deploy it directly with 'helm upgrade upstream-chart -f values-override.yaml' and use ValuesOverride to generate the override file.",
47
+ lexicon: "helm",
48
+ });
49
+ }
50
+ }
51
+
52
+ return diagnostics;
53
+ },
54
+ };
@@ -4,6 +4,7 @@ import type { LintContext } from "@intentius/chant/lint/rule";
4
4
  import { chartMetadataRule } from "./chart-metadata";
5
5
  import { valuesNoSecretsRule } from "./values-no-secrets";
6
6
  import { noHardcodedImageRule } from "./no-hardcoded-image";
7
+ import { valuesNoHelmTplRule } from "./values-no-helm-tpl";
7
8
 
8
9
  function makeContext(code: string): LintContext {
9
10
  const sourceFile = ts.createSourceFile("test.ts", code, ts.ScriptTarget.Latest, true);
@@ -70,6 +71,40 @@ describe("WHM002: valuesNoSecretsRule", () => {
70
71
  });
71
72
  });
72
73
 
74
+ describe("WHM004: valuesNoHelmTplRule", () => {
75
+ test("warns when Values prop uses v.xxx", () => {
76
+ const ctx = makeContext(`new Values({ host: v.myHost });`);
77
+ const diags = valuesNoHelmTplRule.check(ctx);
78
+ expect(diags).toHaveLength(1);
79
+ expect(diags[0].ruleId).toBe("WHM004");
80
+ });
81
+
82
+ test("warns on nested v.xxx", () => {
83
+ const ctx = makeContext(`new Values({ global: { hosts: { domain: v.cellDomain } } });`);
84
+ expect(valuesNoHelmTplRule.check(ctx).length).toBeGreaterThan(0);
85
+ });
86
+
87
+ test("passes when Values props are static", () => {
88
+ const ctx = makeContext(`new Values({ host: "localhost" });`);
89
+ expect(valuesNoHelmTplRule.check(ctx)).toHaveLength(0);
90
+ });
91
+
92
+ test("passes for runtimeSlot() calls", () => {
93
+ const ctx = makeContext(`new Values({ host: runtimeSlot("Cloud SQL IP") });`);
94
+ expect(valuesNoHelmTplRule.check(ctx)).toHaveLength(0);
95
+ });
96
+
97
+ test("does not fire on non-Values constructors", () => {
98
+ const ctx = makeContext(`new Deployment({ image: v.image });`);
99
+ expect(valuesNoHelmTplRule.check(ctx)).toHaveLength(0);
100
+ });
101
+
102
+ test("warns on v.xxx pipe chain", () => {
103
+ const ctx = makeContext(`new Values({ host: v.myHost.pipe("quote") });`);
104
+ expect(valuesNoHelmTplRule.check(ctx)).toHaveLength(1);
105
+ });
106
+ });
107
+
73
108
  describe("WHM003: noHardcodedImageRule", () => {
74
109
  test("warns on hardcoded image with tag", () => {
75
110
  const ctx = makeContext(`new Deployment({ spec: { containers: [{ image: "nginx:1.19" }] } });`);