From f9026916686c8a3ac9ff373fb73525f70f4105b0 Mon Sep 17 00:00:00 2001 From: Alexander <60811310+NoobCoder1209@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:30:06 +0300 Subject: [PATCH 1/2] docs: README.md generated by helm-docs + CI freshness gate - README.md.gotmpl: full README structure (what it shows, skills, quick start incl. OCI install, releasing, ExternalSecrets path, verification table). helm-docs renders the values table inline. - README.md: regenerated. Includes CI badges for the lint and smoke workflows. - lint.yml: add a "helm-docs is up to date" step that runs helm-docs and fails the build if README.md drifts from values.yaml. Catches stale docs on PRs. --- .github/workflows/lint.yml | 19 ++++ README.md | 191 ++++++++++++++++++++++++++++++++++++- README.md.gotmpl | 94 ++++++++++++++++++ 3 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 README.md.gotmpl diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 531c835..8226a48 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -77,3 +77,22 @@ jobs: helm template release . -f "$f" | kube-linter lint - echo "::endgroup::" done + + - name: Install helm-docs + run: | + set -euo pipefail + HELM_DOCS_VERSION=v1.14.2 + curl -sSLo helm-docs.tar.gz \ + "https://github.com/norwoodj/helm-docs/releases/download/${HELM_DOCS_VERSION}/helm-docs_${HELM_DOCS_VERSION#v}_Linux_x86_64.tar.gz" + tar -xzf helm-docs.tar.gz helm-docs + sudo mv helm-docs /usr/local/bin/ + helm-docs --version + + - name: helm-docs is up to date + run: | + set -euo pipefail + helm-docs + if ! git diff --exit-code README.md; then + echo "::error::README.md is out of date. Run 'helm-docs' locally and commit the result." + exit 1 + fi diff --git a/README.md b/README.md index 2ca70b4..d44dda1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,190 @@ -# helm-chart-template +# helm-chart-template — Production-shaped Helm chart starter -Production-shaped Helm chart starter for a stateless HTTP service — probes, HPA, PDB, NetworkPolicy, ExternalSecrets, schema validation, kind smoke test. +[![lint](https://github.com/NoobCoder1209/helm-chart-template/actions/workflows/lint.yml/badge.svg)](https://github.com/NoobCoder1209/helm-chart-template/actions/workflows/lint.yml) +[![smoke](https://github.com/NoobCoder1209/helm-chart-template/actions/workflows/smoke.yml/badge.svg)](https://github.com/NoobCoder1209/helm-chart-template/actions/workflows/smoke.yml) -> **Coming soon.** This repo is being built. See [`PLAN.md`](./PLAN.md) for the full execution plan. +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.2.3](https://img.shields.io/badge/AppVersion-0.2.3-informational?style=flat-square) + +Production-shaped Helm chart starter for a generic stateless HTTP service + +The chart deploys a stateless HTTP service (defaults to [`hashicorp/http-echo`](https://hub.docker.com/r/hashicorp/http-echo)) with the manifests you'd actually want in production — non-root + read-only-root-fs `securityContext`, pinned image tag, startup/liveness/readiness probes, HPA, PDB, NetworkPolicy, optional Ingress, optional ExternalSecret, default soft pod-anti-affinity, and a JSON schema that fails fast on bad values. + +## What it shows + +- A complete, production-shaped Helm chart structured the way Bitnami / Grafana / ingress-nginx structure theirs. +- `values.schema.json` (draft-07) catching `--set replicaCount=foo`, enum violations, typos at the root, and PDB `maxUnavailable`/`minAvailable` mutual-exclusion at install time. +- An External Secrets Operator path (`external-secrets.io/v1`) gated behind `externalSecrets.enabled`, mutually exclusive with the plain `Secret`. +- A NetworkPolicy default-deny posture with `ingressFrom`/`extraIngress` extension hooks. +- CI that runs `helm lint`, renders + `kubeconform -strict`, runs `kube-linter`, and stands up a kind 1.28 cluster to `helm install`/`helm test` against every matrix entry on every PR. +- An OCI release workflow that publishes the chart to GHCR on `vX.Y.Z` tags. + +## Skills demonstrated + +Helm · Kubernetes · GitOps · Production manifests · GitHub Actions · Chart Testing · kubeconform · kube-linter · External Secrets Operator · OCI registries · JSON Schema (draft-07) + +## Quick start + +```sh +# Local install from this directory: +helm install demo . --namespace demo --create-namespace +helm test demo --namespace demo + +# Or pull from GHCR (requires a published release): +helm install demo oci://ghcr.io/noobcoder1209/charts/http-echo \ + --version 0.1.0 \ + --namespace demo --create-namespace +``` + +Try the schema: + +```sh +helm template demo . --set replicaCount=foo +# Error: at '/replicaCount': got string, want integer +``` + +## Releasing + +```sh +# Bump Chart.yaml `version:`, commit, then: +git tag v0.1.0 +git push origin v0.1.0 +# release.yml verifies the tag matches Chart.yaml and pushes the chart +# to oci://ghcr.io//charts on GHCR. +``` + +## ExternalSecrets + +Flip `externalSecrets.enabled=true` and provide a `secretStoreRef`: + +```sh +helm install demo . \ + --set externalSecrets.enabled=true \ + --set externalSecrets.secretStoreRef.name=my-store \ + --set 'externalSecrets.remoteRefs[0].secretKey=DEMO_VALUE' \ + --set 'externalSecrets.remoteRefs[0].remoteRef.key=prod/http-echo' \ + --set 'externalSecrets.remoteRefs[0].remoteRef.property=demo_value' +``` + +The plain `Secret` template stops rendering automatically — the chart enforces "one source of secret data, never both". + +## Verification + +| Gate | What it catches | +|---|---| +| `helm lint .` | Chart structure, schema parse errors | +| `helm template . \| kubeconform -strict -kubernetes-version 1.28.0` | Invalid k8s manifests | +| `helm template . \| kube-linter lint -` | PDB without `unhealthyPodEvictionPolicy`, missing anti-affinity, unset resource requirements, latest-tag, default SA, etc. | +| `kind` + `helm install --wait` + `helm test` | Real apply, real probes, real connectivity | +| `values.schema.json` | Type errors, enum violations, mutex violations on `--set` | + +CI runs the first four on every PR (`lint.yml` and `smoke.yml`). + +## Requirements + +Kubernetes: `>=1.28.0-0` + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | Pod affinity. When empty, the chart synthesizes a soft pod-anti-affinity rule (preferred, weight 100) that spreads replicas across nodes by hostname, using the chart's selectorLabels — so it stays correct under `nameOverride`. Set to `{ }` (the default) to use the synthesized rule, or provide a full affinity spec to override entirely. | +| args | list | `["-text=hello from helm-chart-template","-listen=:5678"]` | Args passed to the http-echo binary. Default echoes a string on /. | +| autoscaling.behavior | object | `{}` | Optional `behavior` block (HPA v2). Empty by default. | +| autoscaling.enabled | bool | `false` | Enable HorizontalPodAutoscaler. When true, replicaCount is ignored. | +| autoscaling.maxReplicas | int | `10` | | +| autoscaling.minReplicas | int | `2` | | +| autoscaling.targetCPUUtilizationPercentage | int | `75` | Target average CPU utilization percentage. Set to null to disable. | +| autoscaling.targetMemoryUtilizationPercentage | int | `80` | Target average memory utilization percentage. Set to null to disable. | +| configMap.data | object | `{}` | Key/value map of environment variables. | +| configMap.enabled | bool | `false` | | +| env | list | `[]` | Extra environment variables on the container (beyond envFrom). | +| externalSecrets.creationPolicy | string | `"Owner"` | Owner | Orphan | Merge | None. | +| externalSecrets.dataFrom | list | `[]` | Bulk references (e.g. `extract:` or `find:`). | +| externalSecrets.enabled | bool | `false` | Render an ExternalSecret instead of a plain Secret. Requires the External Secrets Operator (https://external-secrets.io) and a configured SecretStore. | +| externalSecrets.refreshInterval | string | `"1h"` | Refresh interval for the ExternalSecret. | +| externalSecrets.remoteRefs | list | `[]` | Per-key references. | +| externalSecrets.secretStoreRef.kind | string | `"SecretStore"` | SecretStore (namespaced) or ClusterSecretStore. | +| externalSecrets.secretStoreRef.name | string | `""` | Required when externalSecrets.enabled=true. | +| extraVolumeMounts | list | `[]` | Extra volume mounts (in addition to /tmp). | +| extraVolumes | list | `[]` | Extra volumes (in addition to the emptyDir for /tmp). | +| fullnameOverride | string | `""` | Override the full resource name (release-name + chart name) entirely. | +| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy. | +| image.repository | string | `"hashicorp/http-echo"` | Container image repository. | +| image.tag | string | `"0.2.3"` | Image tag. Pinned to a specific version; do not use "latest" in production. | +| imagePullSecrets | list | `[]` | Image pull secrets, e.g. `[{name: my-registry-secret}]`. | +| ingress.annotations | object | `{}` | Annotations on the Ingress. | +| ingress.className | string | `"nginx"` | IngressClass name. Leave empty to omit `ingressClassName`. | +| ingress.enabled | bool | `false` | Enable Ingress object. | +| ingress.hosts | list | `[{"host":"chart-example.local","paths":[{"path":"/","pathType":"Prefix"}]}]` | Hosts and paths. | +| ingress.tls | list | `[]` | TLS config, e.g. `[{secretName: chart-example-tls, hosts: [chart-example.local]}]`. | +| nameOverride | string | `""` | Override the chart name in resource names. | +| networkPolicy.egressTo | list | `[]` | Extra `egress.to` peers appended to the kube-dns rule. Same caveat as `ingressFrom` — for distinct ports use `extraEgress`. | +| networkPolicy.enabled | bool | `false` | Enable a default-deny NetworkPolicy that allows same-namespace ingress and kube-dns egress. | +| networkPolicy.extraEgress | list | `[]` | Additional, fully-formed egress rules. | +| networkPolicy.extraIngress | list | `[]` | Additional, fully-formed ingress rules (each with its own `from` and `ports`). | +| networkPolicy.ingressFrom | list | `[]` | Extra `ingress.from` peers appended to the same rule that targets the service port. Use for additional same-port sources (e.g. specific namespaceSelectors). Use `extraIngress` for rules that target other ports. | +| nodeSelector | object | `{}` | | +| podAnnotations | object | `{}` | Extra annotations on every Pod. | +| podDisruptionBudget.enabled | bool | `true` | Enable a PodDisruptionBudget. Skipped when replicaCount=1. | +| podDisruptionBudget.maxUnavailable | int | `1` | Maximum unavailable pods during voluntary disruption. | +| podDisruptionBudget.minAvailable | string | `nil` | Minimum available pods. Mutually exclusive with maxUnavailable. | +| podDisruptionBudget.unhealthyPodEvictionPolicy | string | `"AlwaysAllow"` | Eviction policy for unhealthy pods. AlwaysAllow lets unhealthy pods be evicted regardless of the budget; IfHealthyBudget blocks until the budget is met. AlwaysAllow is the safe default for stateless services. | +| podLabels | object | `{}` | Extra labels on every Pod. | +| podSecurityContext.fsGroup | int | `65532` | | +| podSecurityContext.runAsGroup | int | `65532` | | +| podSecurityContext.runAsNonRoot | bool | `true` | | +| podSecurityContext.runAsUser | int | `65532` | | +| podSecurityContext.seccompProfile.type | string | `"RuntimeDefault"` | | +| priorityClassName | string | `""` | Pod priorityClassName. Empty string omits the field. | +| probes.liveness.enabled | bool | `true` | | +| probes.liveness.failureThreshold | int | `5` | | +| probes.liveness.httpGet.path | string | `"/"` | | +| probes.liveness.httpGet.port | string | `"http"` | | +| probes.liveness.periodSeconds | int | `10` | | +| probes.liveness.timeoutSeconds | int | `1` | | +| probes.readiness.enabled | bool | `true` | | +| probes.readiness.failureThreshold | int | `3` | | +| probes.readiness.httpGet.path | string | `"/"` | | +| probes.readiness.httpGet.port | string | `"http"` | | +| probes.readiness.periodSeconds | int | `10` | | +| probes.readiness.timeoutSeconds | int | `1` | | +| probes.startup.enabled | bool | `true` | | +| probes.startup.failureThreshold | int | `30` | | +| probes.startup.httpGet.path | string | `"/"` | | +| probes.startup.httpGet.port | string | `"http"` | | +| probes.startup.periodSeconds | int | `10` | | +| probes.startup.timeoutSeconds | int | `1` | | +| replicaCount | int | `2` | Number of replicas. Ignored when autoscaling.enabled=true. | +| resources.limits.cpu | string | `"200m"` | | +| resources.limits.memory | string | `"128Mi"` | | +| resources.requests.cpu | string | `"50m"` | | +| resources.requests.memory | string | `"64Mi"` | | +| revisionHistoryLimit | int | `5` | | +| secret.data | object | `{}` | Pre-base64-encoded values. | +| secret.enabled | bool | `false` | | +| secret.stringData | object | `{}` | Plaintext values (Helm encodes them). | +| securityContext.allowPrivilegeEscalation | bool | `false` | | +| securityContext.capabilities.drop[0] | string | `"ALL"` | | +| securityContext.readOnlyRootFilesystem | bool | `true` | | +| securityContext.runAsNonRoot | bool | `true` | | +| securityContext.runAsUser | int | `65532` | | +| service.annotations | object | `{}` | Annotations on the Service. | +| service.port | int | `80` | Service port (cluster-side). | +| service.targetPort | int | `5678` | Container port the Service forwards to. http-echo listens on 5678. | +| service.type | string | `"ClusterIP"` | Service type. | +| serviceAccount.annotations | object | `{}` | Annotations on the ServiceAccount (e.g. for IRSA / Workload Identity). | +| serviceAccount.automount | bool | `true` | Whether the ServiceAccount token is automounted. | +| serviceAccount.create | bool | `true` | Whether to create a dedicated ServiceAccount. | +| serviceAccount.name | string | `""` | Override the ServiceAccount name. When empty, uses the chart fullname. | +| terminationGracePeriodSeconds | int | `30` | | +| tolerations | list | `[]` | | +| topologySpreadConstraints | list | `[]` | | + +## License + +MIT — see [`LICENSE`](./LICENSE). + +--- + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/README.md.gotmpl b/README.md.gotmpl new file mode 100644 index 0000000..ee37f15 --- /dev/null +++ b/README.md.gotmpl @@ -0,0 +1,94 @@ +# helm-chart-template — Production-shaped Helm chart starter + +[![lint](https://github.com/NoobCoder1209/helm-chart-template/actions/workflows/lint.yml/badge.svg)](https://github.com/NoobCoder1209/helm-chart-template/actions/workflows/lint.yml) +[![smoke](https://github.com/NoobCoder1209/helm-chart-template/actions/workflows/smoke.yml/badge.svg)](https://github.com/NoobCoder1209/helm-chart-template/actions/workflows/smoke.yml) + +{{ template "chart.deprecationWarning" . }} + +{{ template "chart.versionBadge" . }}{{ template "chart.typeBadge" . }}{{ template "chart.appVersionBadge" . }} + +{{ template "chart.description" . }} + +The chart deploys a stateless HTTP service (defaults to [`hashicorp/http-echo`](https://hub.docker.com/r/hashicorp/http-echo)) with the manifests you'd actually want in production — non-root + read-only-root-fs `securityContext`, pinned image tag, startup/liveness/readiness probes, HPA, PDB, NetworkPolicy, optional Ingress, optional ExternalSecret, default soft pod-anti-affinity, and a JSON schema that fails fast on bad values. + +## What it shows + +- A complete, production-shaped Helm chart structured the way Bitnami / Grafana / ingress-nginx structure theirs. +- `values.schema.json` (draft-07) catching `--set replicaCount=foo`, enum violations, typos at the root, and PDB `maxUnavailable`/`minAvailable` mutual-exclusion at install time. +- An External Secrets Operator path (`external-secrets.io/v1`) gated behind `externalSecrets.enabled`, mutually exclusive with the plain `Secret`. +- A NetworkPolicy default-deny posture with `ingressFrom`/`extraIngress` extension hooks. +- CI that runs `helm lint`, renders + `kubeconform -strict`, runs `kube-linter`, and stands up a kind 1.28 cluster to `helm install`/`helm test` against every matrix entry on every PR. +- An OCI release workflow that publishes the chart to GHCR on `vX.Y.Z` tags. + +## Skills demonstrated + +Helm · Kubernetes · GitOps · Production manifests · GitHub Actions · Chart Testing · kubeconform · kube-linter · External Secrets Operator · OCI registries · JSON Schema (draft-07) + +## Quick start + +```sh +# Local install from this directory: +helm install demo . --namespace demo --create-namespace +helm test demo --namespace demo + +# Or pull from GHCR (requires a published release): +helm install demo oci://ghcr.io/noobcoder1209/charts/http-echo \ + --version {{ .Version }} \ + --namespace demo --create-namespace +``` + +Try the schema: + +```sh +helm template demo . --set replicaCount=foo +# Error: at '/replicaCount': got string, want integer +``` + +## Releasing + +```sh +# Bump Chart.yaml `version:`, commit, then: +git tag v0.1.0 +git push origin v0.1.0 +# release.yml verifies the tag matches Chart.yaml and pushes the chart +# to oci://ghcr.io//charts on GHCR. +``` + +## ExternalSecrets + +Flip `externalSecrets.enabled=true` and provide a `secretStoreRef`: + +```sh +helm install demo . \ + --set externalSecrets.enabled=true \ + --set externalSecrets.secretStoreRef.name=my-store \ + --set 'externalSecrets.remoteRefs[0].secretKey=DEMO_VALUE' \ + --set 'externalSecrets.remoteRefs[0].remoteRef.key=prod/http-echo' \ + --set 'externalSecrets.remoteRefs[0].remoteRef.property=demo_value' +``` + +The plain `Secret` template stops rendering automatically — the chart enforces "one source of secret data, never both". + +## Verification + +| Gate | What it catches | +|---|---| +| `helm lint .` | Chart structure, schema parse errors | +| `helm template . \| kubeconform -strict -kubernetes-version 1.28.0` | Invalid k8s manifests | +| `helm template . \| kube-linter lint -` | PDB without `unhealthyPodEvictionPolicy`, missing anti-affinity, unset resource requirements, latest-tag, default SA, etc. | +| `kind` + `helm install --wait` + `helm test` | Real apply, real probes, real connectivity | +| `values.schema.json` | Type errors, enum violations, mutex violations on `--set` | + +CI runs the first four on every PR (`lint.yml` and `smoke.yml`). + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +## License + +MIT — see [`LICENSE`](./LICENSE). + +--- + +{{ template "helm-docs.versionFooter" . }} From cb2e7de3f55720e490042dfea0222411031dfdfd Mon Sep 17 00:00:00 2001 From: Alexander <60811310+NoobCoder1209@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:35:36 +0300 Subject: [PATCH 2/2] docs: address phase/4-readme review nits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop {{ chart.description }} (was duplicating the H1 line and the hand-written prose right after). - Releasing snippet uses {{ .Version }} so `git tag` lines track Chart.yaml on bumps instead of staying at v0.1.0 forever. - Verification table: rewrite as a bullet list. Markdown table cells inside a fenced row require escaping `|` as `\|`, but inside an inline code span GitHub renders `\|` literally — so the rendered table showed `\|` instead of `|`. The bullet form is cleaner and copy-paste-safe. helm-docs run is idempotent after these changes. --- README.md | 16 ++++++---------- README.md.gotmpl | 20 ++++++++------------ 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index d44dda1..409bafc 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ ![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.2.3](https://img.shields.io/badge/AppVersion-0.2.3-informational?style=flat-square) -Production-shaped Helm chart starter for a generic stateless HTTP service - The chart deploys a stateless HTTP service (defaults to [`hashicorp/http-echo`](https://hub.docker.com/r/hashicorp/http-echo)) with the manifests you'd actually want in production — non-root + read-only-root-fs `securityContext`, pinned image tag, startup/liveness/readiness probes, HPA, PDB, NetworkPolicy, optional Ingress, optional ExternalSecret, default soft pod-anti-affinity, and a JSON schema that fails fast on bad values. ## What it shows @@ -69,15 +67,13 @@ The plain `Secret` template stops rendering automatically — the chart enforces ## Verification -| Gate | What it catches | -|---|---| -| `helm lint .` | Chart structure, schema parse errors | -| `helm template . \| kubeconform -strict -kubernetes-version 1.28.0` | Invalid k8s manifests | -| `helm template . \| kube-linter lint -` | PDB without `unhealthyPodEvictionPolicy`, missing anti-affinity, unset resource requirements, latest-tag, default SA, etc. | -| `kind` + `helm install --wait` + `helm test` | Real apply, real probes, real connectivity | -| `values.schema.json` | Type errors, enum violations, mutex violations on `--set` | +The chart is gated by five layers, four of which run in CI on every PR (`lint.yml` and `smoke.yml`): -CI runs the first four on every PR (`lint.yml` and `smoke.yml`). +- **`helm lint .`** — chart structure, schema parse errors. +- **`helm template . | kubeconform -strict -kubernetes-version 1.28.0`** — invalid Kubernetes manifests. +- **`helm template . | kube-linter lint -`** — PDB without `unhealthyPodEvictionPolicy`, missing anti-affinity, unset resource requirements, latest-tag, default ServiceAccount, etc. +- **`kind` + `helm install --wait` + `helm test`** — real apply, real probes, real connectivity. +- **`values.schema.json`** — type errors, enum violations, mutex violations on `--set`. ## Requirements diff --git a/README.md.gotmpl b/README.md.gotmpl index ee37f15..f046e92 100644 --- a/README.md.gotmpl +++ b/README.md.gotmpl @@ -7,8 +7,6 @@ {{ template "chart.versionBadge" . }}{{ template "chart.typeBadge" . }}{{ template "chart.appVersionBadge" . }} -{{ template "chart.description" . }} - The chart deploys a stateless HTTP service (defaults to [`hashicorp/http-echo`](https://hub.docker.com/r/hashicorp/http-echo)) with the manifests you'd actually want in production — non-root + read-only-root-fs `securityContext`, pinned image tag, startup/liveness/readiness probes, HPA, PDB, NetworkPolicy, optional Ingress, optional ExternalSecret, default soft pod-anti-affinity, and a JSON schema that fails fast on bad values. ## What it shows @@ -48,8 +46,8 @@ helm template demo . --set replicaCount=foo ```sh # Bump Chart.yaml `version:`, commit, then: -git tag v0.1.0 -git push origin v0.1.0 +git tag v{{ .Version }} +git push origin v{{ .Version }} # release.yml verifies the tag matches Chart.yaml and pushes the chart # to oci://ghcr.io//charts on GHCR. ``` @@ -71,15 +69,13 @@ The plain `Secret` template stops rendering automatically — the chart enforces ## Verification -| Gate | What it catches | -|---|---| -| `helm lint .` | Chart structure, schema parse errors | -| `helm template . \| kubeconform -strict -kubernetes-version 1.28.0` | Invalid k8s manifests | -| `helm template . \| kube-linter lint -` | PDB without `unhealthyPodEvictionPolicy`, missing anti-affinity, unset resource requirements, latest-tag, default SA, etc. | -| `kind` + `helm install --wait` + `helm test` | Real apply, real probes, real connectivity | -| `values.schema.json` | Type errors, enum violations, mutex violations on `--set` | +The chart is gated by five layers, four of which run in CI on every PR (`lint.yml` and `smoke.yml`): -CI runs the first four on every PR (`lint.yml` and `smoke.yml`). +- **`helm lint .`** — chart structure, schema parse errors. +- **`helm template . | kubeconform -strict -kubernetes-version 1.28.0`** — invalid Kubernetes manifests. +- **`helm template . | kube-linter lint -`** — PDB without `unhealthyPodEvictionPolicy`, missing anti-affinity, unset resource requirements, latest-tag, default ServiceAccount, etc. +- **`kind` + `helm install --wait` + `helm test`** — real apply, real probes, real connectivity. +- **`values.schema.json`** — type errors, enum violations, mutex violations on `--set`. {{ template "chart.requirementsSection" . }}