Onboarding handshake: Read in this order:
Projects/CLAUDE.md(global standards, workspace-local)tcwlab/CLAUDE.md(toolchain context, workspace-local)- This file (helm-specific)
helm is the container image that bundles a pinned Helm CLI, hardened on Alpine 3.23, intended for Forgejo/GitHub Actions container: jobs that lint, template, or deploy Kubernetes Helm charts in CI. The image tag mirrors the Helm CLI version exactly (tcwlab/helm:3.20.2 contains Helm 3.20.2), so consumer pipelines can pin a concrete Helm version without surprises.
This image grew out of the adopt-not-build book examples (Buecher/adopt-not-build-examples/02-companion-helm/) and the broader pattern across TCW verticals where a Helm-chart wrapper sits in front of an OSS upstream (Zitadel, Authentik, Vault, Cilium, FluxCD, …). Those wrapper repos all need a deterministic, reproducible Helm CLI in their CI — without dragging in setup-helm actions, Node.js, or the host runner's pre-installed Helm binary.
Primary consumers are the Helm-chart-wrapper repos in K8Box, Atrium, Spectrum, and IdentServ — anywhere a Helm chart is linted (helm lint), template-rendered (helm template) or deployed in CI. Plus the adopt-not-build book companion examples, where the image carries the helm-lint job.
- Stage 1 —
base:alpine:3.23withcurl,tar,git,bash,ca-certificatesfrom apk. - Stage 2 —
dependencies: Architecture-aware download of the Helm binary fromhttps://get.helm.sh/. Picksarm64oramd64based onapk --print-arch. Installs to/usr/local/bin/helmand runshelm version --shortas a smoke test inside the build. - Stage 3 —
release: Inherits frombase, copies the Helm binary from the dependencies stage, sets OCI labels, creates a non-root userhelmusr, and pre-creates the Helm 3 cache/config directories under$HOMEso consumer pipelines do not need to write to$HOMEfrom read-only mounts.
Contents:
| Component | Version | Purpose |
|---|---|---|
| Helm CLI | 3.20.2 |
Kubernetes package manager |
curl |
Alpine 3.23 apk | Download support |
tar |
Alpine 3.23 apk | Archive extraction |
git |
Alpine 3.23 apk | Helm chart repos via git |
bash |
Alpine 3.23 apk | Shell compatibility |
ca-certificates |
Alpine 3.23 apk | TLS/SSL certificate validation |
ENTRYPOINT: helm. WORKDIR: /workspace. USER: helmusr (non-root).
Intentionally no Node.js — the image is meant for shell-based checkout (git init && git fetch), not Forgejo's JS-based actions/checkout. Saves image footprint and avoids Node.js version dependencies.
The image tag mirrors the Helm CLI version exactly: tcwlab/helm:3.20.2 contains Helm 3.20.2. There is no separate wrapper SemVer — the only version-relevant variable is the Helm CLI itself.
- Helm bump: PR with
ARG HELM_VERSION=<new-version>in the Dockerfile. The CI pipeline reads this ARG in thepublishstep and tags the image accordingly. New image tag = new Helm version. - Alpine major bump: PR with
FROM alpine:<n>in stage 1. No image SemVer change — Alpine is the base, not the application. - Helm major bump (3 → 4): Coordinate with consumer repos via the K8Box/Atrium platform CLAUDE.md — Helm 4 has breaking chart compatibility considerations. Roll out across the toolchain in lockstep, not piecemeal.
semantic-release as in the other image repos: auto-tag from Conventional Commits, Forgejo release, Docker Hub push as tcwlab/helm:<helm-version>-<semver> (immutable), tcwlab/helm:<helm-version> (float), and tcwlab/helm:latest (float).
Consumer pipelines pin the concrete <helm-version> (e.g. 3.20.2). latest is acceptable for local experiments but not for production CI — see tcwlab/CLAUDE.md for the consumer-side pinning discipline.
- PR with the Helm version bump in the Dockerfile (
ARG HELM_VERSION=<new>). - CI passes — smoke test (
helm version --short) must succeed. - Trivy scan must pass HIGH/CRITICAL — Helm releases occasionally bring transitive Go-stdlib CVEs that need a patched Helm release before we can publish.
- Consumer outreach on Helm minor/major bump: the consumer repos in
Buecher/adopt-not-build-examples/,Atrium/idp/,K8Box/*/etc. pinHELM_VERSIONin theirci.ymlenv:block. On bump, coordinate these values upward via PRs in those repos. - Update the top-level
tcwlab/versions.yaml.
- Helm plugins (
helm-diff,helm-secrets,helm-unittest, …). The image stays minimal — plugins are a consumer concern. If a consumer needs a plugin, it gets installed in a CI step or in a downstream image that derives fromtcwlab/helm. - kubectl. We have a separate image planning (currently in
legacy/kubectl/as reference) for that. Mixing kubectl into helm contradicts the "one tool per image" discipline. - A pre-configured kubeconfig. Consumers mount their own kubeconfig at runtime; the image must not assume any cluster context.
- Helm chart repository credentials. Configured at runtime via
helm repo addwith the consumer's secrets. - Node.js. Intentionally omitted — see "What's inside?" above.
helm-lint:
runs-on: ubuntu-22.04
container:
image: tcwlab/helm:3.20.2
steps:
- name: Checkout (shell)
env:
GITHUB_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
run: |
git config --global init.defaultBranch main
git init .
git remote add origin \
"https://oauth2:${GITHUB_TOKEN}@git.mon.k8b.co/${{ github.repository }}.git"
git fetch --depth=1 origin "${{ github.sha }}"
git checkout FETCH_HEAD
- name: helm lint
run: helm lint ./chart
- name: helm template (smoke test)
run: helm template ./chart --debug | head -40helm-render:
runs-on: ubuntu-22.04
container:
image: tcwlab/helm:3.20.2
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
- run: helm template ./chart > /tmp/rendered.yaml
trivy-scan:
runs-on: ubuntu-22.04
needs: [helm-render]
container:
image: tcwlab/trivy:0.70.0
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
- run: trivy config ./chartComplete pipeline pattern: see Buecher/adopt-not-build-examples/.forgejo/workflows/ci.yml for the canonical lint-template-scan combination.
- Cluster connectivity from CI. The image itself has no opinion about how the consumer reaches a real cluster — kubeconfig is mounted at runtime. The K8Box-internal practice is to use a short-lived
helm install --kube-token=…rather than mounting a static kubeconfig, but that is a consumer concern. - Helm cache between CI runs. The image pre-creates
~/.cache/helmso that consumer pipelines can mount a cache volume to persist chart dependencies between runs. There is no centralized TCW Helm cache backend — intentional, because setup and maintenance overhead do not fit current scale. apk add gituses the Alpine default version. On security vulnerabilities in git, we would have to push a base image update ourselves. Currentlygitis upgraded withapk upgrade, but not version-pinned — trade-off between reproducibility and security-patch velocity.- No plugin pre-installation. Consumers who need
helm-difforhelm-secretsinstall them in a CI step. This is a deliberate trade-off (image stays small, plugin versions stay in the consumer's hands), but creates duplication in pipelines that need the same plugin frequently. If a TCW-wide plugin combination crystallizes, it can graduate to a dedicatedtcwlab/helm-with-diffimage — but only when the duplication actually hurts.