Skip to content

Establish CI/CD Pipeline for Helm Chart Repository #70

@anchapin

Description

@anchapin

Establishing a Reliable CI/CD Pipeline for Helm Chart Repository

Based on industry best practices for 2026, we need to implement a structured, multi-tiered CI/CD pipeline for our Helm chart repository that covers linting, static analysis, unit testing, integration testing on ephemeral clusters, and secure automated packaging/releasing.

Recommended Repository Structure

We should restructure our repository to segregate workflows, global configurations, and individual charts:

my-helm-charts/
├── .github/
│   └── workflows/
│       ├── lint-test.yml        # Runs on PRs (Linting, Unit & Integration Tests)
│       └── release.yml          # Runs on Merges/Tags (Packages & Publishes OCI/Pages)
├── charts/
│   ├── app-one/
│   │   ├── Chart.yaml
│   │   ├── values.yaml
│   │   ├── values.schema.json   # Strongly recommended for strict input validation
│   │   ├── templates/
│   │   └── tests/               # Helm test templates (integration probes)
│   │       └── connection-test.yaml
├── ct.yaml                      # Chart-testing CLI configuration
├── lintconf.yaml                # yamllint rule configuration
└── README.md

Multi-Tiered Testing Strategy

Our testing should be divided into four distinct layers:

Layer 1: Schema & Static Validation

  • Helm Schema Validation: Maintain a values.schema.json file in each chart directory. This enforces parameter types and prevents invalid configurations before templates are even rendered.
  • YAML Linting: Use yamllint to check all configuration files.
  • Chart-Testing CLI (ct): The standard tool for multi-chart repositories. It detects modified files (to avoid testing unchanged charts) and enforces Semantic Versioning (SemVer) increments on modified charts.

Layer 2: Template & Unit Testing

  • Helm Unit Test Plugin (helm-unittest): This plugin allows you to write test suites in YAML to mock values and verify that templates render with the exact expected Kubernetes manifests. It catch typos or copy-paste errors early without requiring a live cluster.

Layer 3: Integration Testing (Ephemeral Cluster)

  • KinD (Kubernetes in Docker): Spin up a lightweight local Kubernetes cluster directly inside the GitHub Actions runner.
  • ct install: Installs the changed chart onto the ephemeral cluster to verify that Kubernetes accepts the rendered manifests without runtime schema validation errors.
  • helm test Probes: Include container probes (e.g., curl/ping checks) inside the templates/tests/ directory. Run these post-installation to verify that the deployed pods can communicate and start up properly.

Layer 4: Security & Supply Chain Security

  • Vulnerability & Misconfiguration Scanning: Use tools like Trivy or Checkov to scan rendered templates for Kubernetes security risks (like running as root, missing resource limits, or using vulnerable base images).

GitHub Actions Workflows

We need to implement two standard workflows:

Pipeline A: Lint, Unit Test, and Ephemeral Install (Run on Pull Requests)

This workflow handles checking out code, running static analysis, executing unit tests, booting a KinD cluster, and verifying live installation.

# .github/workflows/lint-test.yml
name: Lint and Test Charts

on:
  pull_request:
    paths:
      - 'charts/**'
      - '.github/workflows/lint-test.yml'

permissions:
  contents: read

jobs:
  lint-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Essential for 'ct' to compare history against target branch

      - name: Set up Helm
        uses: azure/setup-helm@v4
        with:
          version: v3.14.0

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      - name: Set up Chart-Testing
        uses: helm/chart-testing-action@v2

      - name: Run ct list-changed
        id: list-changed
        run: |
          changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }})
          if [[ -n "$changed" ]]; then
            echo "changed=true" >> "$GITHUB_OUTPUT"
          fi

      - name: Run ct lint
        if: steps.list-changed.outputs.changed == 'true'
        run: ct lint --config ct.yaml

      - name: Install helm-unittest plugin
        if: steps.list-changed.outputs.changed == 'true'
        run: |
          helm plugin install https://github.com/quintush/helm-unittest

      - name: Run Helm Unit Tests
        if: steps.list-changed.outputs.changed == 'true'
        run: |
          for chart in $(ct list-changed --target-branch ${{ github.event.repository.default_branch }}); do
            if [ -d "$chart/tests" ]; then
              helm unittest "$chart"
            fi
          done

      - name: Create KinD Cluster
        if: steps.list-changed.outputs.changed == 'true'
        uses: helm/kind-action@v1.12.0

      - name: Run ct install (Integration Test)
        if: steps.list-changed.outputs.changed == 'true'
        run: ct install --target-branch ${{ github.event.repository.default_branch }}

Pipeline B: Automate Releases (Run on Merge to main)

In 2026, publishing Helm charts relies on two primary channels: GitHub Pages (legacy standard) or OCI Registries (modern standard) using GitHub Container Registry (GHCR).

# .github/workflows/release.yml
name: Release Charts

on:
  push:
    branches:
      - main
    paths:
      - 'charts/**'

permissions:
  contents: write # Required for publishing to GH Pages releases
  packages: write # Required for publishing to GHCR

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Configure Git
        run: |
          git config user.name "$GITHUB_ACTOR"
          git config user.email "$GITHUB_ACTOR@users.noreply.github.com"

      - name: Set up Helm
        uses: azure/setup-helm@v4

      # OPTION A: Release to OCI Registry (GitHub Container Registry)
      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Package & Push OCI Charts
        run: |
          for chart in charts/*/; do
            helm package "$chart"
            helm push *.tgz oci://ghcr.io/${{ github.repository_owner }}/charts
            rm *.tgz
          done

      # OPTION B (Alternative): Release using Chart Releaser (GitHub Pages/Releases)
      - name: Run Chart Releaser
        uses: helm/chart-releaser-action@v1.6.0
        env:
          CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

Key Best Practices for 2026

  1. Prefer OCI Registries over Classic HTTP Repositories: Use OCI (like GHCR, AWS ECR, or Docker Hub) to store and version your Helm charts. They offer better access controls, benefit from native container vulnerability scanning, and align with modern deployment tools.
  2. Auto-Generate Documentation: Avoid manually editing your README.md values tables. Use helm-docs in a pre-commit hook or GitHub Action to automatically generate comprehensive documentation directly from comments in your values.yaml.
  3. Always Set Strict Resource Limits: KinD clusters running inside free GitHub runners have limited CPU and memory. Make sure your charts use reasonable resource requests/limits, and configure generous startup timeouts (--timeout 600s) on the install step to avoid pipeline failures caused by slow runner environments.
  4. Sign Your Charts: To secure your software supply chain, integrate Cosign into your release pipeline to cryptographically sign your packaged charts, allowing consumers to verify chart authenticity.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions