3
New Chart
Justin Klein edited this page 2026-10-12 15:23:46 +00:00

Creating a new chart

Copy template-chart/, replace every xxx, adapt what the template cannot know, then wire up CI. The order matters: the wiring steps are the ones that fail silently if forgotten.

1. Copy the scaffold and replace xxx

cp -r template-chart <chart-name>

xxx appears in five places, and missing one of them leaves a chart that renders but does not do what it says:

  • template file names — templates/xxx.deployment.yaml → templates/<app>.deployment.yaml
  • Chart.yaml — name, description, icon: (mandatory), version, appVersion
  • values.yaml — the top-level keys
  • the templates — every .Values.xxx.* reference
  • values.schema.json — both the values paths and the xxx. helper prefixes

2. Adapt what the template cannot know

The schema and the null-safe fallback helpers come with the template, but their content is chart-specific and wrong until adapted. Walk the six points of §12 Values that must not lie:

  1. Does anything outside the Ingress read ingress.domain? Then that read needs its own required().
  2. Probes, security contexts and resources go through the null-safe helper — never behind an if.
  3. Liveness must not hang on an external dependency where a dependency-free path exists; readiness must hang on it, and a separate path is not proof that it does.
  4. The fallbacks reproduce this chart's defaults, deviations included, not the repo standard.
  5. Probe defaults carry the probe's context — port names, Host headers, exec commands.
  6. A value that reads as the negation of the variable it feeds must actually be negated, and Go's not cannot do it.

Validate the schema against the chart's real bundle values, not only against its defaults. Defaults are valid by construction, so they prove nothing.

templates/NOTES.txt also comes as a worked example and is placeholder text until replaced. Its leading {{/* … */}} block explains the bar and is meant to be deleted. What it must print: the rendered access URL, which fields the 1Password item must carry, the options actually active in this release, and the chart's known traps — all conditional on the values really set.

3. Wire up CI — four steps, each silent when skipped

  1. .github/workflows/publish-<chart-name>.yml — copy an existing one (e.g. publish-outline.yml). It triggers on pushes to main touching <chart-name>/** and calls publish-helm-chart.yml, which lives in workflow-util/pipelines and is bound to a tag. Copy, do not write from memory: the uses: path, the tag, registry-owner: ${{ github.repository_owner }} and secrets: inherit all have to be right, and a hull missing the owner input fails only when the publish actually runs. Without the file at all the chart is never published, and nothing complains. See CI/CD.
  2. .github/workflows/change-app-version.yml — add the chart to the chart-name choice list.
  3. ci/<chart-name>/ — install-test fixtures: test-values.yaml (required), plus fixtures.yaml, fixtures.env.yaml and settings.env where the chart needs them. Alternatively an entry with justification in ci/excluded-charts.txt. The install-test fails if both are missing. See CI/CD. If the chart offers Gateway mode (§4), either the fixture installs in that mode or the chart gets a render-only overlay at ci/httproute/<chart-name>.yaml — without one of the two the httproute-match job is green over nothing for it, which that job fails on.
  4. renovate.json5 — a customManagers block for the chart's main image. Without it the app version is never updated. Check the image's tag list first: a flavour, date or prerelease suffix needs a versioningTemplate with a comment saying why, and floating tags like latest/stable belong outside the pattern. See Renovate.

4. Only if the chart needs a dependency

Read §14 Chart dependencies first — it is a bar, not a formality. Nothing has to be added to the pipelines (ci/helm-dependencies.sh already runs everywhere and keys off the dependencies: block), but the subchart's updates then depend entirely on Renovate's built-in helmv3 manager plus a chart-version bump. Write that down in a comment at the pinned version.

5. Validate before opening the PR

helm lint --strict <chart-name>
helm template <chart-name> --set ingress.domain=example.com --set ingress.clusterIssuer=letsencrypt

Required values vary per chart, so the second command usually needs more --set flags. Then verify at runtime in a throwaway k3d cluster — see Local development.

6. Document it

A new chart needs a wiki page in the same round of work: copy the structure of outline, add a row to the chart index and an entry to _Sidebar.md. A chart that exists without a page is one nobody else can install without reading its templates.

Checklist

  • every xxx replaced, file names included
  • icon: set in Chart.yaml
  • values.schema.json adapted, additionalProperties: false on every chart-owned level
  • null-safe fallbacks reproduce this chart's own defaults
  • probes: liveness, readiness, startup, with calibrated budgets
  • hardening defaults, every deviation commented in values.yaml
  • replicas per workload and the global scaleDown
  • checksum/config on every pod template consuming a ConfigMap
  • additive env block per container, rendered through tpl
  • NOTES.txt rewritten, no real domain
  • publish workflow added
  • change-app-version.yml choice list extended
  • CI fixtures or a justified exclusion
  • if the chart offers Gateway mode: its fixture installs in that mode, or ci/httproute/<chart>.yaml exists
  • renovate.json5 block added
  • helm lint --strict clean, render verified, k3d install verified
  • wiki page, chart index row, sidebar entry