@intentius/chant-lexicon-helm 0.0.16

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.
Files changed (110) hide show
  1. package/README.md +22 -0
  2. package/dist/integrity.json +36 -0
  3. package/dist/manifest.json +37 -0
  4. package/dist/meta.json +208 -0
  5. package/dist/rules/chart-metadata.ts +64 -0
  6. package/dist/rules/helm-helpers.ts +64 -0
  7. package/dist/rules/no-hardcoded-image.ts +62 -0
  8. package/dist/rules/values-no-secrets.ts +82 -0
  9. package/dist/rules/whm101.ts +46 -0
  10. package/dist/rules/whm102.ts +33 -0
  11. package/dist/rules/whm103.ts +59 -0
  12. package/dist/rules/whm104.ts +35 -0
  13. package/dist/rules/whm105.ts +30 -0
  14. package/dist/rules/whm201.ts +36 -0
  15. package/dist/rules/whm202.ts +50 -0
  16. package/dist/rules/whm203.ts +39 -0
  17. package/dist/rules/whm204.ts +60 -0
  18. package/dist/rules/whm301.ts +41 -0
  19. package/dist/rules/whm302.ts +40 -0
  20. package/dist/rules/whm401.ts +57 -0
  21. package/dist/rules/whm402.ts +45 -0
  22. package/dist/rules/whm403.ts +45 -0
  23. package/dist/rules/whm404.ts +36 -0
  24. package/dist/rules/whm405.ts +53 -0
  25. package/dist/rules/whm406.ts +34 -0
  26. package/dist/rules/whm407.ts +83 -0
  27. package/dist/rules/whm501.ts +103 -0
  28. package/dist/rules/whm502.ts +94 -0
  29. package/dist/skills/chant-helm-chart-patterns.md +229 -0
  30. package/dist/skills/chant-helm-chart-security-patterns.md +192 -0
  31. package/dist/skills/chant-helm-create-chart.md +211 -0
  32. package/dist/types/index.d.ts +132 -0
  33. package/package.json +34 -0
  34. package/src/codegen/docs-cli.ts +4 -0
  35. package/src/codegen/docs.ts +483 -0
  36. package/src/codegen/generate-cli.ts +28 -0
  37. package/src/codegen/generate.ts +249 -0
  38. package/src/codegen/naming.ts +38 -0
  39. package/src/codegen/package.ts +64 -0
  40. package/src/composites/composites.test.ts +1050 -0
  41. package/src/composites/helm-batch-job.ts +209 -0
  42. package/src/composites/helm-crd-lifecycle.ts +184 -0
  43. package/src/composites/helm-cron-job.ts +177 -0
  44. package/src/composites/helm-daemon-set.ts +169 -0
  45. package/src/composites/helm-external-secret.ts +93 -0
  46. package/src/composites/helm-library.ts +51 -0
  47. package/src/composites/helm-microservice.ts +331 -0
  48. package/src/composites/helm-monitored-service.ts +252 -0
  49. package/src/composites/helm-namespace-env.ts +154 -0
  50. package/src/composites/helm-secure-ingress.ts +114 -0
  51. package/src/composites/helm-stateful-service.ts +213 -0
  52. package/src/composites/helm-web-app.ts +264 -0
  53. package/src/composites/helm-worker.ts +207 -0
  54. package/src/composites/index.ts +38 -0
  55. package/src/coverage.test.ts +21 -0
  56. package/src/coverage.ts +50 -0
  57. package/src/generated/index.d.ts +132 -0
  58. package/src/generated/index.ts +13 -0
  59. package/src/generated/lexicon-helm.json +208 -0
  60. package/src/helpers.test.ts +51 -0
  61. package/src/helpers.ts +100 -0
  62. package/src/import/generator.ts +285 -0
  63. package/src/import/import.test.ts +224 -0
  64. package/src/import/parser.ts +160 -0
  65. package/src/import/template-stripper.ts +123 -0
  66. package/src/index.ts +108 -0
  67. package/src/intrinsics.test.ts +380 -0
  68. package/src/intrinsics.ts +484 -0
  69. package/src/lint/post-synth/helm-helpers.ts +64 -0
  70. package/src/lint/post-synth/post-synth.test.ts +504 -0
  71. package/src/lint/post-synth/whm101.ts +46 -0
  72. package/src/lint/post-synth/whm102.ts +33 -0
  73. package/src/lint/post-synth/whm103.ts +59 -0
  74. package/src/lint/post-synth/whm104.ts +35 -0
  75. package/src/lint/post-synth/whm105.ts +30 -0
  76. package/src/lint/post-synth/whm201.ts +36 -0
  77. package/src/lint/post-synth/whm202.ts +50 -0
  78. package/src/lint/post-synth/whm203.ts +39 -0
  79. package/src/lint/post-synth/whm204.ts +60 -0
  80. package/src/lint/post-synth/whm301.ts +41 -0
  81. package/src/lint/post-synth/whm302.ts +40 -0
  82. package/src/lint/post-synth/whm401.ts +57 -0
  83. package/src/lint/post-synth/whm402.ts +45 -0
  84. package/src/lint/post-synth/whm403.ts +45 -0
  85. package/src/lint/post-synth/whm404.ts +36 -0
  86. package/src/lint/post-synth/whm405.ts +53 -0
  87. package/src/lint/post-synth/whm406.ts +34 -0
  88. package/src/lint/post-synth/whm407.ts +83 -0
  89. package/src/lint/post-synth/whm501.ts +103 -0
  90. package/src/lint/post-synth/whm502.ts +94 -0
  91. package/src/lint/rules/chart-metadata.ts +64 -0
  92. package/src/lint/rules/lint-rules.test.ts +97 -0
  93. package/src/lint/rules/no-hardcoded-image.ts +62 -0
  94. package/src/lint/rules/values-no-secrets.ts +82 -0
  95. package/src/lsp/completions.test.ts +72 -0
  96. package/src/lsp/completions.ts +20 -0
  97. package/src/lsp/hover.test.ts +46 -0
  98. package/src/lsp/hover.ts +46 -0
  99. package/src/package-cli.ts +28 -0
  100. package/src/plugin.test.ts +71 -0
  101. package/src/plugin.ts +206 -0
  102. package/src/resources.ts +77 -0
  103. package/src/serializer.test.ts +930 -0
  104. package/src/serializer.ts +835 -0
  105. package/src/skills/chart-patterns.md +229 -0
  106. package/src/skills/chart-security-patterns.md +192 -0
  107. package/src/skills/create-chart.md +211 -0
  108. package/src/validate-cli.ts +21 -0
  109. package/src/validate.test.ts +37 -0
  110. package/src/validate.ts +36 -0
@@ -0,0 +1,504 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import type { PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
3
+ import type { SerializerResult } from "@intentius/chant/serializer";
4
+ import { whm101 } from "./whm101";
5
+ import { whm102 } from "./whm102";
6
+ import { whm103 } from "./whm103";
7
+ import { whm104 } from "./whm104";
8
+ import { whm105 } from "./whm105";
9
+ import { whm201 } from "./whm201";
10
+ import { whm301 } from "./whm301";
11
+ import { whm302 } from "./whm302";
12
+ import { whm401 } from "./whm401";
13
+ import { whm402 } from "./whm402";
14
+ import { whm403 } from "./whm403";
15
+ import { whm404 } from "./whm404";
16
+ import { whm405 } from "./whm405";
17
+ import { whm406 } from "./whm406";
18
+ import { whm407 } from "./whm407";
19
+ import { whm501 } from "./whm501";
20
+ import { whm502 } from "./whm502";
21
+
22
+ function makeCtx(files: Record<string, string>): PostSynthContext {
23
+ const result: SerializerResult = { primary: files["Chart.yaml"] ?? "", files };
24
+ const outputs = new Map<string, string | SerializerResult>();
25
+ outputs.set("helm", result);
26
+ return {
27
+ outputs,
28
+ entities: new Map(),
29
+ buildResult: {
30
+ outputs,
31
+ entities: new Map(),
32
+ warnings: [],
33
+ errors: [],
34
+ sourceFileCount: 1,
35
+ },
36
+ };
37
+ }
38
+
39
+ describe("WHM101: Chart.yaml validation", () => {
40
+ test("passes with valid Chart.yaml", () => {
41
+ const ctx = makeCtx({
42
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
43
+ });
44
+ expect(whm101.check(ctx)).toHaveLength(0);
45
+ });
46
+
47
+ test("fails when Chart.yaml is missing", () => {
48
+ const ctx = makeCtx({});
49
+ const diags = whm101.check(ctx);
50
+ expect(diags.length).toBeGreaterThan(0);
51
+ expect(diags[0].message).toContain("missing");
52
+ });
53
+
54
+ test("fails when apiVersion is not v2", () => {
55
+ const ctx = makeCtx({
56
+ "Chart.yaml": "apiVersion: v1\nname: test\nversion: 0.1.0\n",
57
+ });
58
+ const diags = whm101.check(ctx);
59
+ expect(diags.some((d) => d.message.includes("v2"))).toBe(true);
60
+ });
61
+
62
+ test("fails when name is missing", () => {
63
+ const ctx = makeCtx({
64
+ "Chart.yaml": "apiVersion: v2\nversion: 0.1.0\n",
65
+ });
66
+ const diags = whm101.check(ctx);
67
+ expect(diags.some((d) => d.message.includes("name"))).toBe(true);
68
+ });
69
+ });
70
+
71
+ describe("WHM102: values.schema.json", () => {
72
+ test("passes when schema exists", () => {
73
+ const ctx = makeCtx({
74
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
75
+ "values.yaml": "replicaCount: 1\n",
76
+ "values.schema.json": "{}",
77
+ });
78
+ expect(whm102.check(ctx)).toHaveLength(0);
79
+ });
80
+
81
+ test("warns when values are non-empty but schema is missing", () => {
82
+ const ctx = makeCtx({
83
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
84
+ "values.yaml": "replicaCount: 1\n",
85
+ });
86
+ const diags = whm102.check(ctx);
87
+ expect(diags).toHaveLength(1);
88
+ expect(diags[0].checkId).toBe("WHM102");
89
+ });
90
+
91
+ test("passes when values.yaml is empty", () => {
92
+ const ctx = makeCtx({
93
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
94
+ "values.yaml": "{}",
95
+ });
96
+ expect(whm102.check(ctx)).toHaveLength(0);
97
+ });
98
+ });
99
+
100
+ describe("WHM103: template syntax", () => {
101
+ test("passes with balanced braces", () => {
102
+ const ctx = makeCtx({
103
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
104
+ "templates/deploy.yaml": "name: {{ .Values.name }}\n",
105
+ });
106
+ expect(whm103.check(ctx)).toHaveLength(0);
107
+ });
108
+
109
+ test("fails with unbalanced braces", () => {
110
+ const ctx = makeCtx({
111
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
112
+ "templates/deploy.yaml": "name: {{ .Values.name }\n",
113
+ });
114
+ const diags = whm103.check(ctx);
115
+ expect(diags).toHaveLength(1);
116
+ expect(diags[0].message).toContain("Unbalanced");
117
+ });
118
+ });
119
+
120
+ describe("WHM104: NOTES.txt", () => {
121
+ test("info when NOTES.txt is missing for application chart", () => {
122
+ const ctx = makeCtx({
123
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\ntype: application\n",
124
+ });
125
+ const diags = whm104.check(ctx);
126
+ expect(diags).toHaveLength(1);
127
+ expect(diags[0].severity).toBe("info");
128
+ });
129
+
130
+ test("passes for library charts without NOTES.txt", () => {
131
+ const ctx = makeCtx({
132
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\ntype: library\n",
133
+ });
134
+ expect(whm104.check(ctx)).toHaveLength(0);
135
+ });
136
+
137
+ test("passes when NOTES.txt exists", () => {
138
+ const ctx = makeCtx({
139
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\ntype: application\n",
140
+ "templates/NOTES.txt": "Hello!",
141
+ });
142
+ expect(whm104.check(ctx)).toHaveLength(0);
143
+ });
144
+ });
145
+
146
+ describe("WHM105: _helpers.tpl", () => {
147
+ test("warns when _helpers.tpl is missing", () => {
148
+ const ctx = makeCtx({
149
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
150
+ });
151
+ const diags = whm105.check(ctx);
152
+ expect(diags).toHaveLength(1);
153
+ });
154
+
155
+ test("passes when _helpers.tpl exists", () => {
156
+ const ctx = makeCtx({
157
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
158
+ "templates/_helpers.tpl": "{{/* helpers */}}",
159
+ });
160
+ expect(whm105.check(ctx)).toHaveLength(0);
161
+ });
162
+ });
163
+
164
+ describe("WHM201: standard labels", () => {
165
+ test("info when template lacks labels", () => {
166
+ const ctx = makeCtx({
167
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
168
+ "templates/deploy.yaml": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: test\n",
169
+ });
170
+ const diags = whm201.check(ctx);
171
+ expect(diags).toHaveLength(1);
172
+ expect(diags[0].checkId).toBe("WHM201");
173
+ });
174
+
175
+ test("passes when template includes labels helper", () => {
176
+ const ctx = makeCtx({
177
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
178
+ "templates/deploy.yaml": 'apiVersion: apps/v1\nkind: Deployment\nmetadata:\n labels: {{ include "test.labels" . }}\n',
179
+ });
180
+ expect(whm201.check(ctx)).toHaveLength(0);
181
+ });
182
+ });
183
+
184
+ describe("WHM301: Helm tests", () => {
185
+ test("info when no tests defined", () => {
186
+ const ctx = makeCtx({
187
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\ntype: application\n",
188
+ "templates/deploy.yaml": "kind: Deployment\n",
189
+ });
190
+ const diags = whm301.check(ctx);
191
+ expect(diags).toHaveLength(1);
192
+ expect(diags[0].checkId).toBe("WHM301");
193
+ });
194
+
195
+ test("passes when test exists", () => {
196
+ const ctx = makeCtx({
197
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\ntype: application\n",
198
+ "templates/tests/test-connection.yaml": "helm.sh/hook: test\n",
199
+ });
200
+ expect(whm301.check(ctx)).toHaveLength(0);
201
+ });
202
+ });
203
+
204
+ describe("WHM302: resource limits", () => {
205
+ test("info when containers lack resources", () => {
206
+ const ctx = makeCtx({
207
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
208
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n",
209
+ });
210
+ const diags = whm302.check(ctx);
211
+ expect(diags).toHaveLength(1);
212
+ expect(diags[0].checkId).toBe("WHM302");
213
+ });
214
+
215
+ test("passes when resources are set", () => {
216
+ const ctx = makeCtx({
217
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
218
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n resources:\n limits:\n cpu: 100m\n",
219
+ });
220
+ expect(whm302.check(ctx)).toHaveLength(0);
221
+ });
222
+
223
+ test("passes when resources use values reference", () => {
224
+ const ctx = makeCtx({
225
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
226
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n resources: {{ toYaml .Values.resources }}\n",
227
+ });
228
+ expect(whm302.check(ctx)).toHaveLength(0);
229
+ });
230
+ });
231
+
232
+ describe("WHM401: image tag", () => {
233
+ test("warns on :latest tag", () => {
234
+ const ctx = makeCtx({
235
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
236
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n image: nginx:latest\n",
237
+ });
238
+ const diags = whm401.check(ctx);
239
+ expect(diags).toHaveLength(1);
240
+ expect(diags[0].checkId).toBe("WHM401");
241
+ expect(diags[0].severity).toBe("warning");
242
+ });
243
+
244
+ test("warns on untagged image", () => {
245
+ const ctx = makeCtx({
246
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
247
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n image: nginx\n",
248
+ });
249
+ const diags = whm401.check(ctx);
250
+ expect(diags).toHaveLength(1);
251
+ });
252
+
253
+ test("passes with pinned tag", () => {
254
+ const ctx = makeCtx({
255
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
256
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n image: nginx:1.25.0\n",
257
+ });
258
+ expect(whm401.check(ctx)).toHaveLength(0);
259
+ });
260
+
261
+ test("passes with .Values reference", () => {
262
+ const ctx = makeCtx({
263
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
264
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n image: {{ .Values.image.repository }}:{{ .Values.image.tag }}\n",
265
+ });
266
+ expect(whm401.check(ctx)).toHaveLength(0);
267
+ });
268
+ });
269
+
270
+ describe("WHM402: runAsNonRoot", () => {
271
+ test("warns when containers lack runAsNonRoot", () => {
272
+ const ctx = makeCtx({
273
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
274
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n",
275
+ });
276
+ const diags = whm402.check(ctx);
277
+ expect(diags).toHaveLength(1);
278
+ expect(diags[0].checkId).toBe("WHM402");
279
+ expect(diags[0].severity).toBe("warning");
280
+ });
281
+
282
+ test("passes with runAsNonRoot: true", () => {
283
+ const ctx = makeCtx({
284
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
285
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n securityContext:\n runAsNonRoot: true\n",
286
+ });
287
+ expect(whm402.check(ctx)).toHaveLength(0);
288
+ });
289
+
290
+ test("passes with .Values.securityContext ref", () => {
291
+ const ctx = makeCtx({
292
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
293
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n securityContext: {{ toYaml .Values.securityContext }}\n",
294
+ });
295
+ expect(whm402.check(ctx)).toHaveLength(0);
296
+ });
297
+ });
298
+
299
+ describe("WHM403: readOnlyRootFilesystem", () => {
300
+ test("info when containers lack readOnlyRootFilesystem", () => {
301
+ const ctx = makeCtx({
302
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
303
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n",
304
+ });
305
+ const diags = whm403.check(ctx);
306
+ expect(diags).toHaveLength(1);
307
+ expect(diags[0].severity).toBe("info");
308
+ });
309
+
310
+ test("passes with readOnlyRootFilesystem: true", () => {
311
+ const ctx = makeCtx({
312
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
313
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n securityContext:\n readOnlyRootFilesystem: true\n",
314
+ });
315
+ expect(whm403.check(ctx)).toHaveLength(0);
316
+ });
317
+ });
318
+
319
+ describe("WHM404: privileged mode", () => {
320
+ test("error on privileged: true", () => {
321
+ const ctx = makeCtx({
322
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
323
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n securityContext:\n privileged: true\n",
324
+ });
325
+ const diags = whm404.check(ctx);
326
+ expect(diags).toHaveLength(1);
327
+ expect(diags[0].severity).toBe("error");
328
+ });
329
+
330
+ test("passes without privileged mode", () => {
331
+ const ctx = makeCtx({
332
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
333
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n",
334
+ });
335
+ expect(whm404.check(ctx)).toHaveLength(0);
336
+ });
337
+ });
338
+
339
+ describe("WHM405: resource spec detail", () => {
340
+ test("warns when resources lack cpu", () => {
341
+ const ctx = makeCtx({
342
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
343
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n resources:\n limits:\n memory: 256Mi\n",
344
+ });
345
+ const diags = whm405.check(ctx);
346
+ expect(diags).toHaveLength(1);
347
+ expect(diags[0].checkId).toBe("WHM405");
348
+ });
349
+
350
+ test("passes with both cpu and memory", () => {
351
+ const ctx = makeCtx({
352
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
353
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n resources:\n limits:\n cpu: 100m\n memory: 256Mi\n",
354
+ });
355
+ expect(whm405.check(ctx)).toHaveLength(0);
356
+ });
357
+
358
+ test("passes when resources use .Values", () => {
359
+ const ctx = makeCtx({
360
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
361
+ "templates/deploy.yaml": "kind: Deployment\nspec:\n containers:\n - name: app\n resources: {{ toYaml .Values.resources }}\n",
362
+ });
363
+ expect(whm405.check(ctx)).toHaveLength(0);
364
+ });
365
+ });
366
+
367
+ describe("WHM406: CRD lifecycle", () => {
368
+ test("info when crds/ directory exists", () => {
369
+ const ctx = makeCtx({
370
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
371
+ "crds/my-crd.yaml": "apiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\n",
372
+ });
373
+ const diags = whm406.check(ctx);
374
+ expect(diags).toHaveLength(1);
375
+ expect(diags[0].checkId).toBe("WHM406");
376
+ expect(diags[0].severity).toBe("info");
377
+ });
378
+
379
+ test("passes without crds/ directory", () => {
380
+ const ctx = makeCtx({
381
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
382
+ "templates/deploy.yaml": "kind: Deployment\n",
383
+ });
384
+ expect(whm406.check(ctx)).toHaveLength(0);
385
+ });
386
+ });
387
+
388
+ describe("WHM407: inline secrets", () => {
389
+ test("warns on Secret with inline data", () => {
390
+ const ctx = makeCtx({
391
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
392
+ "templates/secret.yaml": "apiVersion: v1\nkind: Secret\nmetadata:\n name: my-secret\ndata:\n password: c2VjcmV0\n",
393
+ });
394
+ const diags = whm407.check(ctx);
395
+ expect(diags).toHaveLength(1);
396
+ expect(diags[0].checkId).toBe("WHM407");
397
+ expect(diags[0].severity).toBe("warning");
398
+ });
399
+
400
+ test("passes when ExternalSecret is used in chart", () => {
401
+ const ctx = makeCtx({
402
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
403
+ "templates/secret.yaml": "apiVersion: v1\nkind: Secret\nmetadata:\n name: my-secret\ndata:\n password: c2VjcmV0\n",
404
+ "templates/external-secret.yaml": "apiVersion: external-secrets.io/v1beta1\nkind: ExternalSecret\n",
405
+ });
406
+ expect(whm407.check(ctx)).toHaveLength(0);
407
+ });
408
+
409
+ test("passes when data uses template values", () => {
410
+ const ctx = makeCtx({
411
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
412
+ "templates/secret.yaml": "apiVersion: v1\nkind: Secret\nmetadata:\n name: my-secret\ndata:\n password: {{ .Values.secret.password }}\n",
413
+ });
414
+ expect(whm407.check(ctx)).toHaveLength(0);
415
+ });
416
+ });
417
+
418
+ describe("WHM501: unused values", () => {
419
+ test("info on values key not referenced in templates", () => {
420
+ const ctx = makeCtx({
421
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
422
+ "values.yaml": "replicaCount: 1\nunusedKey: hello\n",
423
+ "templates/deploy.yaml": "replicas: {{ .Values.replicaCount }}\n",
424
+ });
425
+ const diags = whm501.check(ctx);
426
+ expect(diags.some((d) => d.message.includes("unusedKey"))).toBe(true);
427
+ });
428
+
429
+ test("passes when all values are referenced", () => {
430
+ const ctx = makeCtx({
431
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
432
+ "values.yaml": "replicaCount: 1\n",
433
+ "templates/deploy.yaml": "replicas: {{ .Values.replicaCount }}\n",
434
+ });
435
+ expect(whm501.check(ctx)).toHaveLength(0);
436
+ });
437
+
438
+ test("excludes nameOverride and fullnameOverride", () => {
439
+ const ctx = makeCtx({
440
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
441
+ "values.yaml": "nameOverride: \"\"\nfullnameOverride: \"\"\n",
442
+ "templates/deploy.yaml": "kind: Deployment\n",
443
+ });
444
+ expect(whm501.check(ctx)).toHaveLength(0);
445
+ });
446
+
447
+ test("parent key is not unused when child is referenced", () => {
448
+ const ctx = makeCtx({
449
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
450
+ "values.yaml": "image:\n repository: nginx\n tag: latest\n",
451
+ "templates/deploy.yaml": "image: {{ .Values.image.repository }}:{{ .Values.image.tag }}\n",
452
+ });
453
+ expect(whm501.check(ctx)).toHaveLength(0);
454
+ });
455
+
456
+ test("passes with empty values", () => {
457
+ const ctx = makeCtx({
458
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
459
+ "values.yaml": "{}\n",
460
+ });
461
+ expect(whm501.check(ctx)).toHaveLength(0);
462
+ });
463
+ });
464
+
465
+ describe("WHM502: deprecated API versions", () => {
466
+ test("warns on extensions/v1beta1 Ingress", () => {
467
+ const ctx = makeCtx({
468
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
469
+ "templates/ingress.yaml": "apiVersion: extensions/v1beta1\nkind: Ingress\n",
470
+ });
471
+ const diags = whm502.check(ctx);
472
+ expect(diags).toHaveLength(1);
473
+ expect(diags[0].checkId).toBe("WHM502");
474
+ expect(diags[0].severity).toBe("warning");
475
+ expect(diags[0].message).toContain("networking.k8s.io/v1");
476
+ });
477
+
478
+ test("warns on apps/v1beta2", () => {
479
+ const ctx = makeCtx({
480
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
481
+ "templates/deploy.yaml": "apiVersion: apps/v1beta2\nkind: Deployment\n",
482
+ });
483
+ const diags = whm502.check(ctx);
484
+ expect(diags).toHaveLength(1);
485
+ expect(diags[0].message).toContain("apps/v1");
486
+ });
487
+
488
+ test("passes with current API versions", () => {
489
+ const ctx = makeCtx({
490
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
491
+ "templates/deploy.yaml": "apiVersion: apps/v1\nkind: Deployment\n",
492
+ "templates/ingress.yaml": "apiVersion: networking.k8s.io/v1\nkind: Ingress\n",
493
+ });
494
+ expect(whm502.check(ctx)).toHaveLength(0);
495
+ });
496
+
497
+ test("skips template expression apiVersions", () => {
498
+ const ctx = makeCtx({
499
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
500
+ "templates/deploy.yaml": "apiVersion: {{ .Capabilities.APIVersions }}\nkind: Deployment\n",
501
+ });
502
+ expect(whm502.check(ctx)).toHaveLength(0);
503
+ });
504
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * WHM101: Chart.yaml has required fields and valid apiVersion (v2).
3
+ */
4
+
5
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
6
+ import { getChartFiles, parseChartYaml } from "./helm-helpers";
7
+
8
+ export const whm101: PostSynthCheck = {
9
+ id: "WHM101",
10
+ description: "Chart.yaml must have required fields (apiVersion v2, name, version)",
11
+
12
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
13
+ const diagnostics: PostSynthDiagnostic[] = [];
14
+
15
+ for (const [, output] of ctx.outputs) {
16
+ const files = getChartFiles(output);
17
+ const chartYaml = files["Chart.yaml"];
18
+ if (!chartYaml) {
19
+ diagnostics.push({
20
+ checkId: "WHM101",
21
+ severity: "error",
22
+ message: "Chart.yaml is missing from serializer output",
23
+ lexicon: "helm",
24
+ });
25
+ continue;
26
+ }
27
+
28
+ const parsed = parseChartYaml(chartYaml);
29
+
30
+ if (!parsed.apiVersion) {
31
+ diagnostics.push({ checkId: "WHM101", severity: "error", message: "Chart.yaml missing apiVersion", lexicon: "helm" });
32
+ } else if (parsed.apiVersion !== "v2") {
33
+ diagnostics.push({ checkId: "WHM101", severity: "error", message: `Chart.yaml apiVersion should be "v2", got "${parsed.apiVersion}"`, lexicon: "helm" });
34
+ }
35
+
36
+ if (!parsed.name) {
37
+ diagnostics.push({ checkId: "WHM101", severity: "error", message: "Chart.yaml missing name", lexicon: "helm" });
38
+ }
39
+ if (!parsed.version) {
40
+ diagnostics.push({ checkId: "WHM101", severity: "error", message: "Chart.yaml missing version", lexicon: "helm" });
41
+ }
42
+ }
43
+
44
+ return diagnostics;
45
+ },
46
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * WHM102: values.schema.json present when Values type is used.
3
+ */
4
+
5
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
6
+ import { getChartFiles } from "./helm-helpers";
7
+
8
+ export const whm102: PostSynthCheck = {
9
+ id: "WHM102",
10
+ description: "values.schema.json should be present when Values are non-empty",
11
+
12
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
13
+ const diagnostics: PostSynthDiagnostic[] = [];
14
+
15
+ for (const [, output] of ctx.outputs) {
16
+ const files = getChartFiles(output);
17
+ const valuesYaml = files["values.yaml"];
18
+ const valuesSchema = files["values.schema.json"];
19
+
20
+ // If values.yaml has content (not just "{}"), schema should exist
21
+ if (valuesYaml && valuesYaml.trim() !== "{}" && !valuesSchema) {
22
+ diagnostics.push({
23
+ checkId: "WHM102",
24
+ severity: "warning",
25
+ message: "values.schema.json is missing — consider adding typed Values to generate a schema",
26
+ lexicon: "helm",
27
+ });
28
+ }
29
+ }
30
+
31
+ return diagnostics;
32
+ },
33
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * WHM103: Go template syntax valid (balanced braces, valid function names).
3
+ */
4
+
5
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
6
+ import { getChartFiles, hasBalancedBraces } from "./helm-helpers";
7
+
8
+ const VALID_FUNCTIONS = new Set([
9
+ // Built-in Go template functions
10
+ "if", "else", "end", "range", "with", "define", "template", "block",
11
+ // Helm/Sprig functions
12
+ "include", "required", "default", "toYaml", "toJson", "fromYaml", "fromJson",
13
+ "quote", "squote", "upper", "lower", "title", "trim", "trimSuffix", "trimPrefix",
14
+ "printf", "print", "println", "tpl", "lookup",
15
+ "nindent", "indent", "replace", "contains", "hasPrefix", "hasSuffix",
16
+ "repeat", "substr", "trunc", "abbrev", "randAlphaNum", "randAlpha",
17
+ "b64enc", "b64dec", "sha256sum", "now", "date",
18
+ "list", "dict", "get", "set", "unset", "hasKey", "pluck", "keys", "values",
19
+ "append", "prepend", "first", "last", "uniq", "without", "has",
20
+ "empty", "coalesce", "ternary", "cat", "join", "split", "splitList",
21
+ "toStrings", "toString", "int", "int64", "float64", "atoi",
22
+ "add", "sub", "mul", "div", "mod", "max", "min", "ceil", "floor", "round",
23
+ "and", "or", "not", "eq", "ne", "lt", "le", "gt", "ge",
24
+ "typeOf", "kindOf", "typeIs", "kindIs", "deepEqual",
25
+ "semver", "semverCompare",
26
+ "regexMatch", "regexFind", "regexReplaceAll",
27
+ "sha1sum", "derivePassword", "genPrivateKey", "buildCustomCert", "genCA", "genSignedCert",
28
+ "env", "expandenv",
29
+ "fail",
30
+ ]);
31
+
32
+ export const whm103: PostSynthCheck = {
33
+ id: "WHM103",
34
+ description: "Go template syntax must be valid (balanced braces)",
35
+
36
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
37
+ const diagnostics: PostSynthDiagnostic[] = [];
38
+
39
+ for (const [, output] of ctx.outputs) {
40
+ const files = getChartFiles(output);
41
+
42
+ for (const [filename, content] of Object.entries(files)) {
43
+ if (!filename.startsWith("templates/")) continue;
44
+
45
+ if (!hasBalancedBraces(content)) {
46
+ diagnostics.push({
47
+ checkId: "WHM103",
48
+ severity: "error",
49
+ message: `Unbalanced template braces in ${filename}`,
50
+ entity: filename,
51
+ lexicon: "helm",
52
+ });
53
+ }
54
+ }
55
+ }
56
+
57
+ return diagnostics;
58
+ },
59
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * WHM104: NOTES.txt exists for application charts.
3
+ */
4
+
5
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
6
+ import { getChartFiles, parseChartYaml } from "./helm-helpers";
7
+
8
+ export const whm104: PostSynthCheck = {
9
+ id: "WHM104",
10
+ description: "NOTES.txt should exist for application charts",
11
+
12
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
13
+ const diagnostics: PostSynthDiagnostic[] = [];
14
+
15
+ for (const [, output] of ctx.outputs) {
16
+ const files = getChartFiles(output);
17
+ const chartYaml = files["Chart.yaml"];
18
+ if (!chartYaml) continue;
19
+
20
+ const parsed = parseChartYaml(chartYaml);
21
+ if (parsed.type === "library") continue; // Libraries don't need NOTES.txt
22
+
23
+ if (!files["templates/NOTES.txt"]) {
24
+ diagnostics.push({
25
+ checkId: "WHM104",
26
+ severity: "info",
27
+ message: "NOTES.txt is missing — consider adding installation notes for users",
28
+ lexicon: "helm",
29
+ });
30
+ }
31
+ }
32
+
33
+ return diagnostics;
34
+ },
35
+ };