Helm chart release
This is the recipe gocdnext itself uses for charts/gocdnext.
Lint on every push, package + publish only when a tag fires.
Per-job when: filtering isn’t enforced today. The clean
separation is two pipeline files — .gocdnext/chart-lint.yaml
runs every push to validate the chart; .gocdnext/chart-release.yaml
fires only on tag pushes and does the publish.
Layout assumed
repo/├── charts/│ └── myapp/│ ├── Chart.yaml│ ├── values.yaml│ └── templates/...└── .gocdnext/ ├── chart-lint.yaml └── chart-release.yamlLint pipeline (every push)
name: chart-lint
when: event: [push, pull_request]
stages: [lint]
jobs: lint: stage: lint uses: ghcr.io/klinux/gocdnext-plugin-helm@v1 with: command: lint charts/myapp
template: stage: lint uses: ghcr.io/klinux/gocdnext-plugin-helm@v1 with: command: template myapp charts/myapp --debughelm lint catches schema errors; helm template actually
renders and catches YAML / template-logic errors lint misses
(unbalanced if, undefined .Values.foo, etc.). Running both
catches more before the chart ever leaves the build host.
Release pipeline (tag only)
name: chart-release
when: event: [tag] # tag pushes only
stages: [package, publish]
jobs: package: stage: package uses: ghcr.io/klinux/gocdnext-plugin-helm@v1 with: # Stamp version + appVersion from the tag (CI_BRANCH carries # the tag ref on a tag push). The plugin's `command:` is # word-split — keep flag pairs on one line. command: package charts/myapp --version ${CI_BRANCH} --app-version ${CI_BRANCH} --destination dist/ artifacts: paths: ["dist/*.tgz"]
publish-oci: stage: publish uses: ghcr.io/klinux/gocdnext-plugin-helm-push@v1 needs: [package] needs_artifacts: - from_job: package paths: ["dist/"] secrets: [GHCR_USERNAME, GHCR_TOKEN] with: chart-dir: charts/myapp version: ${CI_BRANCH} app-version: ${CI_BRANCH} backend: oci oci-repo: oci://ghcr.io/klinux/charts username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}What’s worth highlighting:
helm-push backends
The plugin supports oci (default — GHCR, Docker Hub, ECR,
GAR), chartmuseum, and nexus. The choice is one backend:
input — same helm package step regardless, the publish flow
swaps transport.
GHCR: backend: oci, oci-repo: oci://ghcr.io/<owner>/charts.
ChartMuseum: backend: chartmuseum, repo-url: https://charts.internal/. Nexus: backend: nexus, repo-url: https://nexus.corp/repository/helm/.
The gh-pages HTTP-repo pattern (chart-releaser-style index
merge) isn’t supported by this plugin — use GitHub Actions for
that publish path or migrate consumers to OCI.
Version from the tag
The plugin doesn’t strip a v prefix from CI_BRANCH on tag
pushes — so a tag v0.6.4 becomes the chart version v0.6.4.
Most consumers tolerate that; if you require strict semver
(0.6.4), strip the prefix yourself in a pre-step that rewrites
Chart.yaml before the package job.
Tag fires both pipelines
A vX.Y.Z push fires chart-lint.yaml (lint + template) AND
chart-release.yaml (package + publish). The lint pipeline
catches a broken render before the release pipeline pushes a
broken artefact. If the lint pipeline fails, the release
pipeline’s package job still runs (they’re independent
pipelines) — guard the publish step on lint success via an
upstream: material:
materials: - upstream: pipeline: chart-lint stage: lint status: successVariations
Sign the chart with cosign
OCI charts can be signed the same way as OCI images. Consumers
verify with cosign verify.
sign-chart: stage: publish uses: ghcr.io/klinux/gocdnext-plugin-cosign@v1 needs: [publish-oci] secrets: [COSIGN_PRIVATE_KEY, COSIGN_PASSWORD, GHCR_USERNAME, GHCR_TOKEN] with: # `key-content:` accepts the inline PEM from secrets; the # plugin writes it to a 0600 tempfile internally and a # `trap` wipes it on exit. The `key:` input is a FILE PATH # and the plugin refuses PEM-like values via a guard. No # `docker: true` — cosign signs via registry API. image: ghcr.io/klinux/charts/myapp:${CI_BRANCH} action: sign key-content: ${{ COSIGN_PRIVATE_KEY }} key-password: ${{ COSIGN_PASSWORD }} registry: ghcr.io username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}Auto-bump appVersion from a sibling pipeline
If your container image is built by a different pipeline (and
the chart’s appVersion should match the pushed image tag),
gate the chart release on the image’s release as an upstream
material:
materials: - upstream: pipeline: release stage: publish status: successThe chart pipeline now waits for the image release to land. Useful when the image and chart live in the same repo but ship in separate cycles.
Common pitfalls
helm pushto OCI requires login:GHCR_TOKENshould be a PAT withwrite:packages. The plugin runshelm registry loginwith the secret before the push.Chart.yamlstays at the in-repo version: the package step stamps the artefact’s version from the tag via--version/--app-version, butChart.yamlin main stays at whatever was last committed. Convention is to bumpChart.yamlto the next planned release in a chore commit before tagging.v-prefix in OCI tags: GHCR acceptsv0.6.4as a chart version tag, but some clients pin via plain SemVer (helm install --version 0.6.4). Choose one convention for the org and stick to it.- gh-pages publish migration: the chart-releaser-style HTTP
publish isn’t supported by
gocdnext/helm-push. Operators migrating off that flow either move consumers to OCI or keep publishing via GitHub Actions until OCI is universal.