CI that keeps up

How do I lint Helm charts and check rendered templates in CI?

Helm chart checks in CI meet four requirements. They use one Helm version everywhere, run helm lint on every chart, and render every chart with each CI values file through helm template. They also build chart dependencies before they render. Teams meet these with Helm commands or chart-testing in a CI job. Dagger meets the same four requirements in one place. Its official Helm module finds every chart and runs the same checks on a laptop and in Dagger Cloud Checks.

What good Helm chart checks need

Helm chart checks in CI have four requirements. Each one comes from the docs of Helm and its tools, and this guide judges each approach by them:

  1. Use one Helm version everywhere. Helm 4 came out in November 2025, and Helm 3 gets security fixes until February 2027. A laptop and a CI runner can have different major versions. The azure/setup-helm action for GitHub Actions installs the latest Helm unless you give it a version. Then different Helm releases check the same chart on a laptop and in CI.
  2. Lint every chart. helm lint runs “a series of tests to verify that the chart is well-formed”. It emits [ERROR] messages for problems that “will cause the chart to fail installation”.
  3. Render every chart with each CI values file. Helm’s chart-testing tool checks a chart once for each *-values.yaml file in its ci directory. helm template renders the templates locally. It does no “server-side testing of chart validity (e.g. whether an API is supported)”, so it needs no cluster.
  4. Build chart dependencies before you render. A chart that lists dependencies in Chart.yaml needs them in its charts/ directory. helm dependency build does this, and it builds “out the charts/ directory from the Chart.lock file”.

Meet the requirements with Helm commands

Teams meet the four requirements with the Helm CLI. The examples in this guide use one chart at charts/api, with two CI values files:

charts/api/
  Chart.yaml
  values.yaml
  templates/
    deployment.yaml
  ci/
    prod-values.yaml
    minimal-values.yaml

Use one Helm version everywhere. Check the version on each machine:

helm version --short
v3.17.2+gcc0bbbd

This guide’s laptop had Helm 3.17.2. In CI, give azure/setup-helm the same version in its version input.

Lint every chart. Run helm lint on each chart:

helm lint charts/api
==> Linting charts/api
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, 0 chart(s) failed

Render every chart with each CI values file. helm lint alone does not prove that each values file renders. In this guide’s project, ci/prod-values.yaml set image: null, and a template reads .Values.image.repository. helm lint charts/api --values charts/api/ci/prod-values.yaml printed 0 chart(s) failed, with Helm 3.17.2 and with Helm 4.0.1. helm template with the same file failed:

helm template charts/api --values charts/api/ci/prod-values.yaml
Error: template: api/templates/deployment.yaml:17:28: executing "api/templates/deployment.yaml" at <.Values.image.repository>: nil pointer evaluating interface {}.repository

Use --debug flag to render out invalid YAML

Run helm template once for each values file. chart-testing’s ct lint runs helm lint with each ci/*-values.yaml file. It also checks that the chart version went up, and lints Chart.yaml and the values files (ct lint).

Build chart dependencies before you render. Commit Chart.lock. Then fill charts/ from it before the other commands:

helm dependency build charts/api --skip-refresh
Saving 1 charts
Downloading redis from repo oci://registry-1.docker.io/bitnamicharts
Pulled: registry-1.docker.io/bitnamicharts/redis:25.5.3
Digest: sha256:4987cbf877710e3c083cfbb907a1a4ac4ce389803dbadb7b1f8022d8b3fcb85e
Deleting outdated charts

The --skip-refresh flag tells Helm to “not refresh the local repository cache”. This dependency comes from an OCI registry, so Helm needs no repository index.

In CI, a GitHub Actions job installs Helm with azure/setup-helm and runs the same commands. Helm also has a chart-testing-action that installs ct.

Where the hand-built setup strains

Each requirement becomes a step that someone keeps up to date:

  • Use one Helm version everywhere. The version is in each CI workflow, and each laptop runs the Helm that its owner installed.
  • Lint every chart. A job that lists chart paths misses a new chart until someone adds it. ct finds charts, but by default it checks only the charts that changed.
  • Render every chart with each CI values file. helm template needs a loop over each chart and each values file in a script. helm lint alone passed the broken values file in this guide’s project.
  • Build chart dependencies before you render. Each job must run helm dependency build first, with access to each chart repository.

Meet the requirements with the Dagger Helm module

Dagger is a CI orchestration engine that runs the same pipelines on a laptop and in CI. A Dagger module is a package of functions that do work on the project, such as a lint or a test run. The official Helm module runs helm lint and helm template on every chart. You write no Dagger code.

Complete the Quickstart first. It creates dagger.toml, the workspace configuration. Then install the Helm module from the project root:

dagger module install dagger.io/kubernetes/helm
Installed module "helm" in /work/dagger.toml

The command adds the module to dagger.toml and writes dagger.lock. The lock file pins the image and Git references that the workspace resolved, so every machine loads the same module revision.

A check is a function that passes or fails. dagger check runs every check in the workspace in parallel and exits non-zero if one fails. List the Helm module’s checks:

dagger check -l
MODULE  CHECK            DESCRIPTION
helm    assert-template  Assert every discovered values file renders with helm template --dry-run=client.
helm    lint             Lint every Helm chart visible from the client's working directory.

Use one Helm version everywhere, with Dagger

The Helm module installs Helm at the version in its version setting (Helm module reference). Pin the Helm release that your team uses:

dagger module settings helm version 4.0.1

The setting goes into dagger.toml, so local runs and CI use the same Helm release:

[modules.helm]
source = "dagger.io/kubernetes/helm"

[modules.helm.settings]
version = "4.0.1"

Every machine that runs dagger check gets Helm 4.0.1 in the check’s container, whatever Helm the machine has installed.

Lint every chart with Dagger

The lint check runs helm lint once on each chart with its default values. Then it runs it once more for each CI values file. The module finds every Chart.yaml at or below the directory where dagger runs. It uses the directory that holds that file as the chart root. Thus templates, subcharts, CRDs, and files that templates read through .Files are all available to Helm.

The module checks a new chart without a change to any configuration. To see the charts it found, call its charts function:

dagger api call helm charts path
charts/api

A subchart that the repository keeps under a chart’s charts/ directory has its own Chart.yaml. The module lists it and checks it as a separate chart too. Run both checks:

dagger check
== CHECKS ==  ✔ 2 passed
✔ dag://?check=helm/assert-template 0.5s OK
✔ dag://?check=helm/lint 0.5s OK

To run one check, give its address: dagger check dag://helm/lint. In this example, Chart.yaml has no version field, and lint fails with Helm’s [ERROR] messages:

✘ dag://?check=helm/lint 0.1s ERROR
  ✘ withExec helm lint . --values ci/prod-values.yaml 0.0s ERROR
  ┃ Error: 1 chart(s) linted, 1 chart(s) failed
  ┃ ==> Linting .
  ┃ [ERROR] Chart.yaml: version is required
  ┃ [INFO] Chart.yaml: icon is recommended
  ┃ [WARNING] Chart.yaml: version '' is not a valid SemVerV2
  ┃ [ERROR] templates/: validation: chart.metadata.version is required
  ┃ [ERROR] : unable to load chart
  ┃         validation: chart.metadata.version is required
  ! exit code: 1

== RUN LOCALLY ==
  dagger check "dag://?check=helm/lint"

Render every chart with each CI values file, with Dagger

The assert-template check renders each chart with each values file through helm template --dry-run=client. It fails when a template does not render. It does not render the chart with only its default values, because some charts render only with one of their explicit values files. In this example, ci/prod-values.yaml sets image: null, and a template reads .Values.image.repository:

✘ dag://?check=helm/assert-template 0.6s ERROR
  ✘ withExec helm template . --values ci/prod-values.yaml --dry-run=client 0.4s ERROR
  ┃ Error: api/templates/deployment.yaml:17:28
  ┃   executing "api/templates/deployment.yaml" at <.Values.image.repository>:
  ┃     nil pointer evaluating interface {}.repository
  ┃ 
  ┃ Use --debug flag to render out invalid YAML
  ! exit code: 1

== RUN LOCALLY ==
  dagger check "dag://?check=helm/assert-template"

The values files come from the valuesGlob setting. Its default, ci/*-values.yaml, is the chart-testing convention. If your charts follow it, add a values file such as charts/api/ci/staging-values.yaml to check one more configuration. If your values files are in a different location, change the glob. The glob is relative to each chart root:

dagger module settings helm valuesGlob "values/*.yaml"
[modules.helm]
source = "dagger.io/kubernetes/helm"

[modules.helm.settings]
version = "4.0.1"
valuesGlob = "values/*.yaml"

The module runs each pair of chart and values file in its own container. Dagger caches the result of each pair separately (module source).

Build chart dependencies before you render, with Dagger

The module runs Helm on the chart directory as it is in the repository, and does not run helm dependency build (module source). When Chart.yaml lists a dependency that the chart’s charts/ directory does not hold, assert-template fails:

== CHECKS ==  ✘ 1 failed  ✔ 1 passed
✘ dag://?check=helm/assert-template 0.1s ERROR
  ✘ withExec helm template . --values ci/prod-values.yaml --dry-run=client 0.0s ERROR
  ┃ Error: an error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: found in Chart.yaml, but missing in charts/ directory: redis
  ! exit code: 1
✔ dag://?check=helm/lint 0.1s OK

== RUN LOCALLY ==
dagger check "dag://?check=helm/assert-template"

Run helm dependency build as in the hand-built setup, and commit the archives that it writes to charts/. In this guide’s project, both checks passed after the commit of charts/api/charts/redis-25.5.3.tgz. The checks then need no access to the chart repository.

Run the same checks locally and with Cloud Checks

Run dagger check on your machine before you push. Commit dagger.toml and dagger.lock. To run the same checks on every push and pull request, use Cloud Checks. Cloud Checks run your checks on Cloud Engines, Dagger’s managed engines, with no CI workflow file. The project must be a GitHub repository, and its Git origin remote must point to that repository. Turn them on from the project root:

dagger cloud checks on

In an interactive terminal, the command offers to create a Dagger Cloud account and to connect the GitHub organization. Without a terminal, it prints the commands to run first:

Error: Cloud checks need a Dagger Cloud account and organization.
Complete the prerequisite, then retry:

dagger cloud signup
dagger cloud checks on

Cloud Checks run the same lint and assert-template checks as dagger check on your machine, with the Helm version from dagger.toml. To see the runs, use dagger workspace activity.

Bottom line: Pin one Helm version, run helm lint on every chart, render each CI values file with helm template, and build dependencies first. By hand, that is azure/setup-helm with a version, a loop over charts and values files, and helm dependency build. Dagger’s Helm module finds every chart, pins Helm in dagger.toml, and runs both checks on a laptop and with Cloud Checks.