Bazel rules to package, lint, publish and deploy Helm charts, plus a few companions used around a deployment: decrypting sops secrets, creating Kubernetes namespaces (with GKE Workload Identity) and uploading files to Google Cloud Storage.
Every external tool (helm, sops, kubectl, gcloud) is fetched by Bazel as a toolchain, so builds do not depend on what is installed on the host.
- Requirements
- Installation
- Quick start
- Rules
- Toolchains
- Stamping values
- Examples
- Development
- Releases
- License
| Bazel | 7.x, 8.x and 9.x (bazel_compatibility = [">=7.0.0"]). The test suite runs on Bazel 8.3.1 in CI; the examples/ module is built against 7.x, 8.x and 9.x by the BCR presubmit. |
| Dependency management | bzlmod (recommended) or a legacy WORKSPACE. |
| Host platforms | Linux and macOS, amd64 and arm64 (the toolchains also ship Windows binaries, untested). |
| Container images | helm_chart can take the digest of an image built with rules_oci. |
Check the releases page for the latest version: every release carries a ready-to-paste snippet for both setups.
# MODULE.bazel
bazel_dep(name = "masorange_rules_helm", version = "1.8.2")The module registers the helm, sops, kubectl and gcloud toolchains for you. See Toolchains to pin different versions.
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "masorange_rules_helm",
sha256 = "<sha256 from the release notes>",
strip_prefix = "masorange_rules_helm-1.8.2",
urls = ["https://github.com/masmovil/masorange_rules_helm/releases/download/v1.8.2/masorange_rules_helm-v1.8.2.tar.gz"],
)
load("@masorange_rules_helm//:repositories.bzl", "masorange_rules_helm_repositories")
masorange_rules_helm_repositories()
load("@masorange_rules_helm//:config.bzl", "masorange_rules_helm_configure")
masorange_rules_helm_configure()masorange_rules_helm_repositories() fetches the dependencies (bazel_skylib, rules_pkg, rules_oci, aspect_bazel_lib) and masorange_rules_helm_configure() sets them up and registers the toolchains. WORKSPACE support is legacy: Bazel 8 disables it by default and Bazel 9 removes it, so prefer bzlmod.
Package a chart from its sources, stamp the digest of the image it deploys into values.yaml, lint it, publish it and install it:
load("@masorange_rules_helm//helm:defs.bzl", "helm_chart", "helm_lint_test", "helm_push", "helm_release", "helm_uninstall")
load("@rules_oci//oci:defs.bzl", "oci_image")
oci_image(
name = "image",
base = "@distroless_java",
entrypoint = ["java", "-jar", "/app.jar"],
)
helm_chart(
name = "chart",
chart_name = "my-service",
srcs = glob(["chart/**"]),
version = "1.4.0",
app_version = "2.3.1",
# writes the image digest into .image.tag and appends "@" to .image.repository
image = ":image",
values = {
"replicaCount": "3",
"ingress.host": "my-service.example.com",
},
deps = ["//charts/base:chart"],
)
# bazel test //my-service:lint
helm_lint_test(
name = "lint",
chart = ":chart",
)
# bazel run //my-service:push
helm_push(
name = "push",
chart = ":chart",
repository_url = "oci://europe-docker.pkg.dev/my-project/helm-charts",
)
# bazel run //my-service:install
helm_release(
name = "install",
chart = ":chart",
release_name = "my-service",
namespace = "my-namespace",
values = ["values-prod.yaml"],
kubernetes_context = "my-cluster",
)
# bazel run //my-service:uninstall
helm_uninstall(
name = "uninstall",
release_name = "my-service",
namespace = "my-namespace",
kubernetes_context = "my-cluster",
)bazel build //my-service:chart produces my-service-1.4.0.tgz. Rules that talk to a registry or a cluster (helm_push, helm_release, helm_uninstall, k8s_namespace, gcs_upload) build an executable: run them with bazel run.
Load from @masorange_rules_helm//helm:defs.bzl.
| Rule | What it does |
|---|---|
helm_chart |
Packages a chart into a reproducible <chart_name>-<version>.tgz. Overrides Chart.yaml fields (version, app_version, api_version, description, also from a JSON/YAML version file), sets values by YAML path, adds templates, embeds chart dependencies under charts/ and stamps an oci_image digest into the values. Sources can be checked-in files or outputs of other rules; a chart can even be declared without sources. The macro is a wrapper around the chart_srcs rule, which documents every attribute. |
helm_lint_test |
Test target running helm lint on a packaged chart. |
helm_push |
Publishes a packaged chart to an OCI registry (oci://, with helm push) or to an HTTP chart repository (ChartMuseum-style API). Credentials come from the host helm config or from HELM_USER/HELM_PASSWORD. |
helm_pull |
Repository rule (and utils.pull bzlmod extension) that downloads a chart from a remote registry and exposes it as @<repo>//:chart, ready to be used in deps. Needs helm on the host PATH. |
helm_release |
Installs or upgrades a release (helm upgrade --install) from a packaged chart or a remote chart, with values files (including decrypted secrets), --set overrides, namespace creation and kube context selection. |
helm_uninstall |
Uninstalls a release. |
helm_chart also exposes a ChartInfo provider (chart name, version, sources and archive) for rules that want to consume charts.
Load from @masorange_rules_helm//sops:defs.bzl.
| Rule | What it does |
|---|---|
sops_decrypt |
Decrypts sops-encrypted files with an age key file or a .sops.yaml config. Its outputs can be fed to helm_release as values files. |
Load from @masorange_rules_helm//k8s:defs.bzl.
| Rule | What it does |
|---|---|
k8s_namespace |
Creates a namespace with kubectl, optionally annotating a service account and binding it to a GCP service account through GKE Workload Identity (gcloud). Use it as namespace_dep of helm_release. |
Load from @masorange_rules_helm//gcs:defs.bzl.
| Rule | What it does |
|---|---|
gcs_upload |
Uploads a single file to a gs:// bucket. |
The binaries are downloaded from their official release channels and pinned by sha256:
| Tool | Default version | Available versions |
|---|---|---|
| helm | v3.16.3 | v3.16.3, v3.13.2, v3.13.1 |
| sops | v3.8.1 | v3.8.1 |
| kubectl | v1.28.2 | v1.28.2 |
| gcloud | 502.0.0 | 502.0.0, 473.0.0, 450.0.0 |
With bzlmod the module registers the default versions. To pin another one, declare your own installation with a distinct name and register it; toolchains registered by the root module take precedence:
# MODULE.bazel
tools = use_extension("@masorange_rules_helm//:extensions.bzl", "toolchains")
tools.install(
helm_name = "helm_pinned",
helm_version = "v3.13.2",
)
use_repo(tools, "helm_pinned_toolchains")
register_toolchains("@helm_pinned_toolchains//:all")The same tag accepts sops_name/sops_version, kubectl_name/kubectl_version and gcloud_name/gcloud_version. Versions must be one of the listed ones (each entry carries its checksums in */private/*_toolchain.bzl; adding a version is a small PR).
With WORKSPACE, masorange_rules_helm_configure() registers the defaults; the register_*_toolchains(name, version, register = True) helpers in toolchains.bzl let you register a different version.
values accepts Bazel workspace status variables. Enable stamping on the target and pass --stamp together with a --workspace_status_command:
helm_chart(
name = "chart",
chart_name = "my-service",
srcs = glob(["chart/**"]),
stamp = -1, # follow --stamp / --nostamp
values = {
"deployment.branch": "${STABLE_GIT_BRANCH}",
"deployment.buildTime": "${BUILD_TIMESTAMP}",
},
)bazel build //my-service:chart --stamp --workspace_status_command=./stamp.shBoth stable and volatile variables are supported. Stamped charts are rebuilt whenever the status changes, so keep stamp off for charts that must stay cacheable.
The examples/ directory is a standalone Bazel module that consumes these rules the way a user would; it is built by CI and by the BCR presubmit on Bazel 7, 8 and 9.
| Example | Shows |
|---|---|
simple_chart |
Package, lint and push a chart (also with the retro-compatible helm/helm.bzl API). |
chart_override |
Override Chart.yaml fields from the rule. |
chart_version_file |
Take the chart version or appVersion from a JSON or YAML file. |
chart_multi_version |
Package the same sources under two versions. |
chart_with_image |
Stamp an oci_image digest into the values. |
chart_with_deps |
Embed another helm_chart as a dependency. |
empty_chart |
Declare a chart with no source files at all. |
release_local_chart, release_remote_chart |
Install a packaged or a remote chart with helm_release. |
release_with_secrets |
Decrypt sops secrets with age and pass them as values. |
bazel test //... # rules and integration tests (uses stamp.sh in CI: --config=ci)
(cd examples && bazel test //...) # the examples module, against the local checkout
bazel run //docs:write_docs_md # regenerate docs/*.md with Stardoc after changing a docstringRule implementations live under <area>/private/; the public API is re-exported from <area>/defs.bzl (and helm/helm.bzl for the pre-1.0 load path). Tests for helm_chart are in helm/tests/helm_chart and unpack the produced archive to compare it with the expected files.
Pushing a tag vX.Y.Z runs release.yaml: it runs the tests, builds the source archive, creates the GitHub release with the installation snippet and opens the publication PR against the Bazel Central Registry through publish.yaml.