Releasing Charts to OCI Registries in CI

Modern Helm pushes charts to any OCI registry with helm push. Automate package, version-bump discipline, and publish in your pipeline.

The old world of a static index.yaml on a web server is fading. Since Helm 3.8, OCI registries are first-class — the same registry that stores your container images (GHCR, ECR, Docker Hub, Harbor, Artifactory) stores your charts too. One credential, one place, built-in auth and retention.

The publish flow

  1. helm package ./mychart produces a versioned .tgz from Chart.yaml's version.
  2. helm registry login authenticates to the registry (in CI, use a token from the pipeline's secrets).
  3. helm push mychart-1.4.0.tgz oci://<registry>/charts uploads it. The chart name and version become the OCI repository and tag automatically.

Consumers then install straight from the registry: helm install my-api oci://ghcr.io/acme/charts/my-api --version 1.4.0 — no helm repo add required.

Versioning discipline is the real work

The commands are easy; the discipline is what separates a senior pipeline. Bump version (the chart's SemVer) on every change — patch for template tweaks, minor for new values, major for breaking value renames. Keep appVersion tracking the application image separately. Tag immutably: never overwrite an already-published chart version, because someone somewhere has pinned to it.

Sign what you publish. helm push can attach a provenance signature, and consumers can helm install --verify. In a supply-chain-conscious org this is table stakes, not a nice-to-have.

Example

Example · bash
# CI step: package and publish a chart to an OCI registry (GitHub Actions style)
set -euo pipefail

# Fail early if the chart version was not bumped for this change
ct lint --charts ./charts/my-api --check-version-increment=true

# Package -> the .tgz is named from Chart.yaml's version
helm package ./charts/my-api --destination ./dist

# Authenticate using a CI-provided token, never a hard-coded password
echo "$REGISTRY_TOKEN" | helm registry login ghcr.io \
  --username "$GITHUB_ACTOR" --password-stdin

# Push -- chart name + version become the OCI repo and tag
helm push ./dist/my-api-1.4.0.tgz oci://ghcr.io/acme/charts

# Consumers install directly, no 'helm repo add' needed:
#   helm install my-api oci://ghcr.io/acme/charts/my-api --version 1.4.0

When to use it

  • A CI pipeline packages and pushes a versioned chart to GitHub Container Registry on every release tag so users can install it with a helm install oci:// command.
  • A platform team uses OCI registries to store charts alongside container images in the same ECR repository, simplifying access control to a single IAM policy.
  • A developer pins a chart version from an OCI registry in Chart.yaml dependencies so builds are reproducible regardless of which charts are currently pushed to the registry.

More examples

Package and push in GitHub Actions

Authenticates to GitHub Container Registry, reads the chart version from Chart.yaml, and pushes the package as an OCI artifact.

Example · bash
# In a GitHub Actions step:
echo "${{ secrets.GITHUB_TOKEN }}" | \
  helm registry login ghcr.io -u ${{ github.actor }} --password-stdin

VERSION=$(grep '^version:' Chart.yaml | awk '{print $2}')
helm package ./myapp
helm push myapp-${VERSION}.tgz oci://ghcr.io/${{ github.repository_owner }}/charts

Install from OCI registry

Installs a specific chart version directly from an OCI registry without adding a repository first.

Example · bash
helm install my-app \
  oci://ghcr.io/myorg/charts/myapp \
  --version 1.3.0 \
  -f values-prod.yaml \
  -n production

OCI dependency in Chart.yaml

Declares an OCI-hosted chart as a dependency using an oci:// repository URL, supported since Helm 3.8.

Example · yaml
# Chart.yaml
dependencies:
  - name: mylib
    version: "1.0.0"
    repository: oci://ghcr.io/myorg/charts

Discussion

  • Be the first to comment on this lesson.