Mastering the chart release cycle: disciplined Semantic Versioning, the version vs appVersion difference, packaging charts with helm package, GPG provenance signing, and the Chart.lock that guarantees reproducible dependencies.

After episode 15, where we built production-grade chart documentation — README.md as the contract, NOTES.txt as the post-installation assistant, automatic parameter table generation with helm-docs, and CHANGELOG.md as the transparency record — in this episode we turn the chart from just a folder of templates into a distributable artifact: a .tgz archive with a clear version, a verifiable signature, and reproducible dependencies.
Why is this topic crucial? Because up until this episode, your chart has only lived in one local folder. But in the real world, charts aren't run — they're consumed. Other teams install them, CI uses them, pipelines release them into production clusters. For all of that to be safe, three questions must be answered definitively: which version is currently in use? Is this artifact authentic and unmodified along the way? Can this build be reproduced? The first answer is handled by Semantic Versioning, the second by provenance signing, and the third by Chart.lock.
In this episode we dissect all three one by one. We start with disciplined semver rules and the subtle difference between version and appVersion, then package the chart with helm package, sign it with GPG and verify it, and close with the dependency locking mechanism that makes chart builds deterministic.
Semantic Versioning (semver) is a MAJOR.MINOR.PATCH numbering standard that turns version numbers into a contract: each number conveys meaning about the impact of a change on users. This isn't just a tidy convention — it's risk communication.
replicas parameter others use, changing a long-standing values structure, bumping to an incompatible apiVersion — all of these increment MAJOR. Chart consumers using a ^1.x constraint will not automatically receive 2.x.autoscaling.enabled parameter doesn't break anyone — those using the old defaults won't feel a thing. This bumps MINOR.The golden rule: if there's any doubt whether a change is breaking, it's a MAJOR. Why does this discipline matter? Because chart users stake their applications on version constraints. Imagine you're using the constraint >=1.0.0 <2.0.0 and the chart team bumps the version to 1.5.0 while actually containing a breaking change. That version passes the constraint, your production pipeline automatically picks it up, and your application explodes without warning. Undisciplined semver isn't just a writing error — it's a lie that destroys trust.
Tip
Apply the "public vs private" rule: semver governs the public contract — values.yaml parameters, generated resource names, rendered templates. Internal changes like a _helpers.tpl refactor that doesn't change rendered output may bump PATCH. The most objective way to know is to compare the rendered output of the old version vs the new: if helm template produces different YAML for the same input values, that's a behavior change.
One of the classic Helm confusions is two fields with similar names but different rhythms: version and appVersion in Chart.yaml.
version is the chart package's version — the number Helm uses for semver constraints, caching, and chart selection in a repository. This is what you see in helm search repo and helm list. It changes according to the semver rules above.appVersion is the version of the application packaged inside the chart — for example, nginx:1.27.3. It's informational, not decisive: Helm never uses appVersion for version resolution logic. It only appears in chart metadata and is usually rendered into the app.kubernetes.io/version label.The key point: these two versions are independent. Your chart can be at version: 2.0.0 while appVersion: 1.27.3. Why not sync them? Because charts and applications have different lifecycles: you can release chart 1.0.0 packaging nginx 1.25, then release chart 1.1.0 packaging nginx 1.27 (new feature: application upgrade). Both numbers go up, but at their own pace.
apiVersion: v2
name: myapp
description: myapp application chart
type: application
version: 2.3.1 # chart package version (semver, used by Helm)
appVersion: "1.27.3" # nginx application version (informational)Notice that appVersion is written in quotes. This is important: a value like 1.10 will be coerced by YAML into the number 1.1 without quotes — a silently wrong application version. Always write appVersion as a string.
The chart folder we've been working on can't be distributed yet — it contains many files and directories. helm package combines everything into a single .tgz archive named chart-name-version.tgz. This is the chart distribution unit: one file, the version in the filename, ready to be uploaded to a repository or pulled with helm pull.
helm package ./myapp
# Output:
# Successfully packaged chart and saved it to: /home/user/myapp-0.1.0.tgzNotice the archive details. The .tgz file stores the chart's folder name, not the archive's filename. Inside the archive there's a myapp/ folder containing the entire chart contents. The folder name inside the archive is what determines the chart's name when installed — if you rename the folder inside the archive, Helm will treat the chart as having a different name. And because the format is tar.gz, you can inspect its contents directly:
tar -tzf myapp-0.1.0.tgz
# myapp/Chart.yaml
# myapp/values.yaml
# myapp/templates/deployment.yaml
# myapp/templates/_helpers.tpl
# myapp/README.mdBefore packaging, make a habit of two rituals. First, run helm lint to make sure the chart has no structural problems. Second, use .helmignore to exclude files that don't need to enter the archive — local test files, values-dev.yaml, screenshots, .git — so the archive is clean and doesn't contain information that shouldn't go out. Every file that gets past .helmignore is shipped to users; files that shouldn't circulate here are a security risk.
Now the chart is a single file. The next question: how do users know this file really came from you and wasn't modified by someone else along the way? The answer is provenance — proof of origin.
When you sign a chart, helm package produces a second file named myapp-0.1.0.tgz.prov. This file contains the SHA-256 digest of the chart archive along with its metadata, signed with your GPG key. Users holding your public key can verify that the archive is exactly as it was when signed — one bit changes, and verification fails.
The process starts by preparing a GPG key:
gpg --quick-generate-key "Helm Chart Signing" rsa4096 sign 0
# Store the passphrase in a file (don't commit it to the repo!)
echo -n "your-passphrase" > /tmp/signing-key.pass
gpg --batch --pinentry-mode loopback --yes \
--export-secret-key --armor "Helm Chart Signing" > /tmp/signing-key.gpg
gpg --batch --pinentry-mode loopback --import /tmp/signing-key.gpgThen sign while packaging:
helm package ./myapp \
--sign \
--key "Helm Chart Signing" \
--keyring ~/.gnupg/secring.gpg \
--passphrase-file /tmp/signing-key.pass
# ls: myapp-0.1.0.tgz myapp-0.1.0.tgz.provUsers who receive the chart can verify its authenticity with helm verify:
helm verify myapp-0.1.0.tgz
# Signed by: Helm Chart Signing <...>
# Using Key With Fingerprint: ABCD...
# Chart Hash Verified: myapp-0.1.0.tgzImportant
helm verify only proves that the signature matches a key you hold in your keyring — it does not judge whether that key is trustworthy. Trust establishment is a separate process: you must obtain the chart author's public key through a secure channel, verify its fingerprint out of band, and store it once. This is exactly the "trust on first use" pattern used by SSH: pin the fingerprint the first time, then all subsequent verifications refer to it. Distributing the public key through the same chart repo as the chart itself destroys its security value — like keeping a padlock's key inside the padlock.
Let's be honest about adoption: .prov provenance signing is still rarely used in the classic Helm ecosystem, and for charts distributed via OCI registries modern practice has shifted toward Cosign (which we'll cover in episode 18). But the .prov mechanism is still important to understand because it's the same conceptual foundation: separating the artifact from its proof of authenticity.
The last question this episode answers: are chart builds reproducible? This is a problem every package manager faces — and the solution is the same: a lock file.
Remember in episode 11 we added dependencies with version constraints in Chart.yaml, for example version: "~1.2.0". A constraint like that is a range: ~1.2.0 means "latest 1.2.x". Every time helm dependency update runs, Helm might pull version 1.2.9 today and 1.2.12 next month — the rendered chart output could differ, behavior could change, and bugs that weren't there yesterday can appear tomorrow. For production, that's unacceptable.
The solution is Chart.lock — a file generated by helm dependency update that records the exact version of every resolved dependency. Just like package-lock.json in Node.js or the bun.lock you use in this project, it freezes the resolution result so that two people running helm dependency build on different machines (or two weeks apart) get an identical dependency set.
dependencies:
- name: redis
repository: https://charts.bitnami.com/bitnami
version: 19.6.4
digest: sha256:1f7a4b3c...9d2e0f1a
generated: "2026-08-02T10:15:00Z"Three important fields here: version, which is now an exact version (not a constraint), digest, the SHA-256 that guarantees the dependency's contents haven't changed, and generated, the creation timestamp. The workflow is simple:
Chart.yaml, then run helm dependency update to regenerate Chart.lock, and commit both.helm dependency build — this reads Chart.lock and pulls the exact recorded versions, without re-resolving.# After changing Chart.yaml:
helm dependency update ./myapp
# In CI / on a new machine - using the committed Chart.lock:
helm dependency build ./myappThere's a consequence you should be aware of: helm dependency update always re-resolves and overwrites Chart.lock; helm dependency build respects the existing lock. If Chart.yaml changes but Chart.lock isn't committed along with it, helm dependency build will fail with a warning that they're out of sync — that's a feature, not a bug: it forces you to handle dependency changes explicitly. Reproducibility isn't free; it's a discipline maintained by a mechanism.
Warning
Never put Chart.lock in .gitignore unless your chart intentionally wants to resolve dependencies dynamically on every build (a pattern that only suits development charts, not releases). For distributed charts, the committed Chart.lock is the source of truth: without it, "the same build" is a meaningless phrase.
The complete chart release workflow from this episode: bump version in Chart.yaml per semver rules → helm lint → helm dependency update → helm package --sign → verify with helm verify. The result: one .tgz archive with a deterministic name, one .prov file as proof of authenticity, and a Chart.lock guaranteeing everyone builds from the same dependency set.
In this episode 16 we turned the chart into a distributable artifact. We understood Semantic Versioning as a risk language: MAJOR for breaking changes, MINOR for new features, PATCH for bug fixes — and that indiscipline here is a lie that destroys user trust. We distinguished version (the package version Helm uses for resolution) from appVersion (the informational application version), then packaged the chart into a .tgz with helm package, inspected it with tar -tzf, and cleaned it up with .helmignore. We signed the chart with GPG producing a .prov file, verified it with helm verify, and understood that trust is built through key fingerprints verified out of band. Finally, we froze dependencies with Chart.lock — split between helm dependency update (re-resolve) and helm dependency build (follow the lock) — so builds become reproducible.
The core takeaways:
version controls Helm resolution; appVersion is only information — write it as a string.helm package --sign produces two artifacts: .tgz and .prov; helm verify checks the former against the latter.Chart.lock so dependency builds are deterministic.Now you have an artifact ready to be consumed — but one artifact sitting in a local folder can't yet be used by other teams. In the next episode, episode 17, we build a chart repository: the index.yaml structure, the various repository types from HTTP and GitHub Pages to ChartMuseum, and how to automate chart publication with GitHub Actions. Keep your spirits up!