Helm & Kubernetes Packaging

Publishing & Versioning Charts

18 min Lesson 8 of 28

Publishing & Versioning Charts

A chart that lives only on your laptop is just a template library. A chart that is published to a registry becomes a versioned artifact that your entire organisation — and CI/CD pipelines, GitOps controllers, and on-call engineers at 3 a.m. — can pull, inspect, and deploy with a single command. This lesson covers everything from the classic HTTP chart repository protocol to modern OCI registries, semantic versioning discipline, and cryptographic provenance that makes auditors happy.

Two Distribution Models: HTTP Repos vs. OCI Registries

Helm supports two fundamentally different transport mechanisms for distributing charts.

Classic HTTP Repositories follow a simple contract: an HTTP server hosts a directory of .tgz chart archives and an index.yaml that lists every chart with its version, checksum, and download URL. Any static file server — S3, GCS, GitHub Pages, Nginx — can act as a Helm repo. You interact with it via helm repo add, helm repo update, and helm search repo.

OCI Registries (introduced as stable in Helm 3.8, released February 2022) store charts as OCI artifacts alongside container images in the same registry infrastructure. ECR, GCR, ACR, Harbor, and Docker Hub all support OCI artifacts. Instead of helm repo add you push and pull with helm push and helm pull oci://. No index.yaml is needed — the registry itself handles discovery and metadata.

Industry direction: Google, AWS, and Microsoft have all standardised on OCI registries as the preferred Helm distribution mechanism. If you are starting a new chart repository today, use OCI. HTTP repos are still common in open-source ecosystems (ArtifactHub, Bitnami) but new internal tooling at big-tech companies predominantly uses OCI.
HTTP Repo vs OCI Registry distribution flows HTTP Chart Repository helm package myapp-1.2.0.tgz S3 / GCS index.yaml + .tgz upload helm repo add helm install pull OCI Registry helm package myapp-1.2.0.tgz ECR / GCR / ACR OCI artifact layer helm push helm pull oci:// helm install oci:// pull
Left: classic HTTP repo flow — package, upload, register, install. Right: OCI flow — package, push directly, install with oci:// URI.

Semantic Versioning for Charts

Every Helm chart carries two independent version strings in Chart.yaml:

  • version — the chart version, semver-formatted. Bump this when the chart's templates, values schema, or defaults change.
  • appVersion — the version of the application the chart deploys (e.g. the Docker image tag). Changing appVersion without changing version is a common mistake that prevents meaningful rollbacks.

Strict semver discipline is non-negotiable in production:

  • PATCH (1.2.3 → 1.2.4) — bug fix in templates, documentation update, non-breaking default change.
  • MINOR (1.2.3 → 1.3.0) — new optional value added, new template feature, backwards-compatible change.
  • MAJOR (1.2.3 → 2.0.0) — breaking change: renamed value keys, removed template output, changed required field types. A major bump is a signal to consumers: your existing values.yaml overrides may break.
Production pitfall — the frozen appVersion: A team updates the Docker image tag in their deployment pipeline by passing --set image.tag=v2.3.1 at upgrade time but never bumps appVersion or version in Chart.yaml. After six months, helm list shows chart version 1.0.0 in every environment, making it impossible to correlate a cluster state with a Git commit. Always treat appVersion and version as part of your release artifact — automate their update in CI.
# Chart.yaml — two independent version fields apiVersion: v2 name: myapp description: Production-grade microservice chart type: application version: 1.4.2 # chart version — bump on template/values changes appVersion: "2.3.1" # app version — matches Docker image tag # In CI, automate version bumps with yq (a YAML processor) # Bump chart version (minor) and sync appVersion with the image tag: IMAGE_TAG="$(git rev-parse --short HEAD)" yq e ".appVersion = \"${IMAGE_TAG}\"" -i charts/myapp/Chart.yaml yq e ".version = \"$(semver bump minor $(yq e .version charts/myapp/Chart.yaml))\"" \ -i charts/myapp/Chart.yaml

Publishing to an HTTP Repository (S3 Pattern)

The S3 + helm repo index pattern is battle-tested for internal repos. The workflow is: package the chart, regenerate the index, and sync to S3. The Helm plugin helm-s3 automates this.

# One-time setup: install the helm-s3 plugin and initialise the bucket helm plugin install https://github.com/hypnoglow/helm-s3.git helm s3 init s3://my-company-charts/charts # Add the repo locally (once per developer machine / CI runner) helm repo add company s3://my-company-charts/charts # Package and push a chart helm package charts/myapp --destination /tmp/ helm s3 push /tmp/myapp-1.4.2.tgz company # Verify helm repo update helm search repo company/myapp --versions # NAME CHART VERSION APP VERSION DESCRIPTION # company/myapp 1.4.2 2.3.1 Production-grade microservice chart # company/myapp 1.4.1 2.2.8 ... # Install a specific version (pinning is mandatory in production) helm upgrade --install myapp company/myapp \ --version 1.4.2 \ --values prod-values.yaml \ --namespace myapp \ --atomic

Publishing to an OCI Registry (ECR Pattern)

AWS ECR is the most common OCI chart registry in enterprise environments. The workflow mirrors container image pushes — authenticate, package, push with a URI.

# Authenticate Helm to ECR (runs before every push in CI) AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) AWS_REGION=us-east-1 aws ecr get-login-password --region ${AWS_REGION} \ | helm registry login \ --username AWS \ --password-stdin \ ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com # Create the ECR repository for the chart (once) aws ecr create-repository \ --repository-name helm-charts/myapp \ --region ${AWS_REGION} # Package and push helm package charts/myapp helm push myapp-1.4.2.tgz \ oci://${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/helm-charts # Install directly from OCI (no helm repo add needed) helm upgrade --install myapp \ oci://${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/helm-charts/myapp \ --version 1.4.2 \ --values prod-values.yaml \ --namespace myapp \ --atomic # Inspect chart metadata without installing helm show chart \ oci://${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/helm-charts/myapp \ --version 1.4.2
Immutability is a feature: Configure your OCI registry (or S3 bucket) to make published chart versions immutable — once myapp:1.4.2 is pushed, it cannot be overwritten. This mirrors Docker image best practices. If a bug is found, the fix goes into 1.4.3, never a silent overwrite of 1.4.2. ECR supports immutable image tags at the repository level; enable it with --image-tag-mutability IMMUTABLE in the create-repository command above.

Chart Provenance and Signing

For regulated industries (fintech, healthcare, government) and any supply-chain-conscious organisation, a chart's SHA digest is not enough — you need a cryptographic signature that proves the chart came from a trusted author and was not tampered with in transit. Helm supports signing via GPG provenance files.

When you run helm package --sign, Helm produces a .prov file alongside the .tgz. The provenance file contains the SHA-256 of the chart archive and a PGP-clearsigned block. Consumers run helm verify or pass --verify to helm install to validate the signature before deploying anything.

# Generate a GPG key for chart signing (do this once per org / team) gpg --full-generate-key # Choose: RSA 4096, no expiry (or a long expiry), name = "Helm Charts CI" # Export the public key and distribute it to consumers (via keyserver or internal PKI) gpg --export --armor "Helm Charts CI" > helm-signing-pub.asc # Package and sign in CI helm package charts/myapp \ --sign \ --key "Helm Charts CI" \ --keyring ~/.gnupg/pubring.gpg \ --destination /tmp/ # This produces two files: # /tmp/myapp-1.4.2.tgz # /tmp/myapp-1.4.2.tgz.prov # Push both files to the chart repo helm s3 push /tmp/myapp-1.4.2.tgz company helm s3 push /tmp/myapp-1.4.2.tgz.prov company # Consumers install with signature verification helm install myapp company/myapp \ --version 1.4.2 \ --verify \ --keyring /etc/helm/helm-signing-pub.gpg
Sigstore / Cosign for OCI charts: GPG provenance works only with HTTP repos. For OCI registries, the emerging standard is Cosign (from the Sigstore project, backed by Google, Red Hat, and Chainguard). Cosign can sign OCI artifacts (including Helm charts pushed as OCI layers) with keyless OIDC-based signatures tied to a CI identity rather than a long-lived GPG key. This is the model used by major open-source projects (Kubernetes, cert-manager) today. Flux and ArgoCD both support Cosign verification of OCI chart sources.

CI/CD Integration: Automating the Full Publish Pipeline

The following GitHub Actions workflow ties together everything above: version bumping, packaging, pushing to ECR, and optionally signing — triggered on every push to main when the charts/ directory changes.

# .github/workflows/helm-publish.yml name: Publish Helm Chart on: push: branches: [main] paths: ["charts/**"] env: AWS_REGION: us-east-1 ECR_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com jobs: publish: runs-on: ubuntu-latest permissions: id-token: write # needed for OIDC-based AWS auth contents: read steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # full history for version detection - name: Configure AWS credentials (OIDC — no stored secrets) uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} aws-region: ${{ env.AWS_REGION }} - name: Install Helm uses: azure/setup-helm@v4 with: version: "3.15.x" - name: Authenticate Helm to ECR run: | aws ecr get-login-password --region ${{ env.AWS_REGION }} \ | helm registry login \ --username AWS \ --password-stdin \ ${{ env.ECR_REGISTRY }} - name: Detect changed charts and publish run: | set -euo pipefail CHANGED=$(git diff --name-only HEAD~1 HEAD -- charts/ \ | awk -F/ '{print $2}' | sort -u) for CHART in $CHANGED; do echo "=== Publishing charts/${CHART} ===" helm dependency update charts/${CHART} helm package charts/${CHART} --destination /tmp/charts/ TARBALL=$(ls /tmp/charts/${CHART}-*.tgz | tail -1) helm push ${TARBALL} \ oci://${{ env.ECR_REGISTRY }}/helm-charts done
Chart Releaser for open-source projects: If you maintain a public chart repository on GitHub Pages, the chart-releaser action (helm/chart-releaser-action) automates packaging, GitHub Release creation, and index.yaml updates. It is the canonical setup for community chart repositories and is used by the Helm project itself.

Dependency Version Constraints

When your chart depends on subchart libraries (covered in lesson 6), the Chart.lock file pins exact versions of each dependency. Treat Chart.lock like package-lock.json — commit it, and run helm dependency update only when you intentionally upgrade a dependency. Never delete Chart.lock in CI and let it re-resolve freely; that is how you get a different dependency version in prod than in staging.

By the end of this lesson you have the tooling to close the gap between "chart on disk" and "versioned, signed, auditable artifact available to your entire organisation." The next lesson covers Helm hooks and chart tests — the mechanisms that let you validate deployments and perform database migrations atomically within a release.