2
Local Development
Justin Klein edited this page 2026-09-18 11:19:36 +00:00

Local development

Three levels of validation, cheapest first. The pipeline runs all three; doing them locally is how you avoid finding out in a PR.

1. Render and lint

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

Required values vary per chart, so the render usually needs more --set flags than these two — minecraft-router needs mappings, samba needs shares and a storage class, freesailarr needs media claims. The fastest way to find out is to run it: required() fails with the values path in the message.

A shortcut that keeps you honest: render with the chart's own CI values, which are a realistic input rather than the defaults.

helm template <chart> -f ci/<chart>/test-values.yaml

If the chart declares a dependency, every helm command exits 1 until charts/ is filled:

ci/helm-dependencies.sh <chart>

2. Run the repo's own checks

ci/run-quantity-render-test.sh <chart>   # no argument = every chart
ci/run-trivy-config.sh <chart>
ci/run-install-test.sh <chart>           # needs a cluster, see below
ci/run-install-test.sh --list-excluded   # the excluded charts and their reasons

The quantity render test is worth running on any chart you touched the resources helper in — it is the one check that covers every chart on every PR, so a regression there is found by someone else's PR otherwise.

3. Runtime verification in k3d

Rendering proves the manifests are well-formed. It does not prove the workload starts, the probes pass, or the hardening is survivable — and in this repo those are the parts that break.

k3d cluster create <name>
ci/run-install-test.sh <chart>
k3d cluster delete <name>

The script does the rest: it applies the stub OnePasswordItem CRD and the chart's fixtures, installs with ci/<chart>/test-values.yaml, and waits for every Deployment and StatefulSet to roll out.

Doing it by hand is the same four steps:

  1. kubectl apply -f ci/common/ — the schema-less stub CRD for OnePasswordItem
  2. kubectl apply -f ci/<chart>/fixtures.yaml — dummy secrets, throwaway postgres/redis, hostPath PVs
  3. helm install <release> <chart> -f ci/<chart>/test-values.yaml
  4. watch pod readiness and events, then read the container log

Note what the stub CRD does and does not do. It accepts OnePasswordItem objects so the render applies; it does not materialize any Secret. The fixtures supply the Kubernetes Secrets under the names the chart expects. A chart that works in CI therefore proves its wiring, not its 1Password paths.

What CI does that your machine does not

  • kubectl is pinned in the pipeline to stay within one minor of the k3s image. Locally, a client far from the API server changes what the test means — check kubectl version before trusting a surprising result.
  • The install-test job is containerised, which is why the workflow rewrites the kubeconfig server address. A local k3d cluster on your own Docker host needs none of that.
  • Timeouts differ per chart via ci/<chart>/settings.env (INSTALL_TIMEOUT). freesailarr needs 1200s — ten containers, several GB of images and a real Wireguard handshake.

What cannot be verified locally

  • The publish step. It uploads to the private registry; there is no dry run. The pipeline is the first place a packaging mistake shows.
  • 1Password resolution. Charts render OnePasswordItem CRs, and only a cluster with the operator turns them into Secrets. A wrong op:// path renders fine and fails as a pod in CreateContainerConfigError.
  • Anything about the real cluster's storage. RWX, storage classes and fsGroup behaviour differ between k3d's local-path provisioner and the production cluster. A chart that mounts an existing claim is only half-tested by CI.

Tooling you need

Tool Why
helm render, lint, package
k3d + docker throwaway clusters
kubectl everything after the install
python3 the quantity render test reads values.schema.json
tea issues and PRs — see Forge and tooling. The Coder workspace has no op, so tea is called directly and acts as the bot account
a working commit signer branch protection requires signed commits; the Coder workspace signs through coder-git-ssh-sign without a 1Password step