<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>GitLab CI on Matteo ZINUTTI</title><link>https://mzinutti.fr/en/categories/gitlab-ci/</link><description>Recent content in GitLab CI on Matteo ZINUTTI</description><generator>Hugo -- gohugo.io</generator><language>en-US</language><lastBuildDate>Mon, 12 Oct 2026 18:43:52 +0200</lastBuildDate><atom:link href="https://mzinutti.fr/en/categories/gitlab-ci/index.xml" rel="self" type="application/rss+xml"/><item><title>Ensuring the quality of Helm Charts</title><link>https://mzinutti.fr/en/posts/helm-chart-quality-pipeline/</link><pubDate>Mon, 11 May 2026 14:00:00 +0000</pubDate><guid>https://mzinutti.fr/en/posts/helm-chart-quality-pipeline/</guid><description>&lt;img src="https://mzinutti.fr/posts/helm-chart-quality-pipeline/img/helm-flow.svg" alt="Featured image of post Ensuring the quality of Helm Charts" /&gt;&lt;h2 id="introduction"&gt;Introduction
&lt;/h2&gt;&lt;p&gt;As an SRE / Platform Engineer, one of my responsibilities is to provide reliable Helm charts to the development teams. These charts are the foundation for deploying their applications on our Kubernetes platforms. A faulty chart means impacted applications, hours of investigation, and eroded trust in the components we provide.&lt;/p&gt;
&lt;p&gt;The trigger for this work was an avoidable incident: undocumented &lt;em&gt;breaking changes&lt;/em&gt; were introduced and broke every deployment using the chart, which forced us to ask ourselves a fundamental question: &lt;strong&gt;how do we guarantee the quality of the Helm charts we provide?&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="context"&gt;Context
&lt;/h2&gt;&lt;h3 id="the-problem"&gt;The problem
&lt;/h3&gt;&lt;p&gt;Managing Helm charts by hand and without automated validation carries several risks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Undocumented breaking changes&lt;/strong&gt;: incompatible changes can be introduced without the consuming teams being informed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Syntax errors&lt;/strong&gt;: typos or YAML errors can go unnoticed until the production deployment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invalid templates&lt;/strong&gt;: templates that do not render correctly or generate invalid Kubernetes manifests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Functional regressions&lt;/strong&gt;: changes that break existing features without being detected.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No traceability&lt;/strong&gt;: hard to understand the history of changes and their impact.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="the-goal"&gt;The goal
&lt;/h3&gt;&lt;p&gt;I therefore needed a framework able to automatically validate each change before it is merged into the main branch, explicitly flag &lt;em&gt;breaking changes&lt;/em&gt;, check the Kubernetes compliance of the generated manifests, test the expected behaviours and track changes in a usable &lt;em&gt;changelog&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;But it was not only about adding tools. The answer also had to be organisational: better frame contributions, make collaboration easier to follow, and make sure the quality of the chart no longer relies only on individual attention.&lt;/p&gt;
&lt;h2 id="components"&gt;Components
&lt;/h2&gt;&lt;p&gt;The workflow is built around several tools and practices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitLab CI/CD&lt;/strong&gt;: orchestrates the whole validation and publication pipeline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Helm &amp;amp; plugins&lt;/strong&gt;: the package manager for Kubernetes, with its native validation tools and its plugins.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Git Flow&lt;/strong&gt;: branching strategy and Merge Request process.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SemVer&lt;/strong&gt;: versioning to clearly communicate the type of changes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep a Changelog&lt;/strong&gt;: a standard format to document every change.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="pipeline-architecture"&gt;Pipeline architecture
&lt;/h2&gt;&lt;p&gt;The GitLab CI pipeline is organised in three main stages, each with a specific goal.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://mzinutti.fr/posts/helm-chart-quality-pipeline/img/helm-flow.svg"
 alt="GitLab CI pipeline in three stages: lint with helm lint, validate with helm template, helm kubeconform and helm unittest, then release with helm package and the upload to the registry" width="800px"&gt;&lt;figcaption&gt;
 &lt;h4&gt;Pipeline overview&lt;/h4&gt;
 &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id="linting"&gt;Linting
&lt;/h3&gt;&lt;p&gt;The first level of validation in the pipeline checks the syntax and overall structure of the chart with Helm&amp;rsquo;s native &lt;code&gt;helm lint&lt;/code&gt; command. It gives fast, immediately understandable feedback. Although it does not cover every case, it removes gross errors very early, before they reach the next steps, which avoids running deeper validations and wasting time.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;helm lint .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;blockquote class="alert alert-note"&gt;
 &lt;div class="alert-header"&gt;
 &lt;span class="alert-icon"&gt;📝&lt;/span&gt;
 &lt;span class="alert-title"&gt;Note&lt;/span&gt;
 &lt;/div&gt;
 &lt;div class="alert-body"&gt;
 &lt;p&gt;&lt;strong&gt;What Helm lint checks:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Correct YAML syntax&lt;/li&gt;
&lt;li&gt;Chart structure (Chart.yaml, templates/, etc.)&lt;/li&gt;
&lt;li&gt;Formatting issues&lt;/li&gt;
&lt;li&gt;Warnings on best practices&lt;/li&gt;
&lt;/ul&gt;
 &lt;/div&gt;
 &lt;/blockquote&gt;
&lt;p&gt;Once the structure of the chart is confirmed, we can move on to more specific validation steps.&lt;/p&gt;
&lt;h3 id="template-validation"&gt;Template validation
&lt;/h3&gt;&lt;p&gt;To answer the question &amp;ldquo;does the chart actually render correctly with the provided values?&amp;rdquo;, I chose &lt;code&gt;helm template&lt;/code&gt;, which renders the templates. This is often where you find missing variables, badly written conditions, or errors in Go Template functions.&lt;/p&gt;
&lt;p&gt;It works simply: Helm interprets the templates and produces the final Kubernetes manifests, without applying them to a cluster. You get a concrete view of what would be deployed, which makes it a particularly useful step to detect rendering errors as early as possible.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;helm template .&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;blockquote class="alert alert-note"&gt;
 &lt;div class="alert-header"&gt;
 &lt;span class="alert-icon"&gt;📝&lt;/span&gt;
 &lt;span class="alert-title"&gt;Note&lt;/span&gt;
 &lt;/div&gt;
 &lt;div class="alert-body"&gt;
 &lt;p&gt;&lt;strong&gt;What Helm template checks:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Correct rendering of Go templates (variables, functions, pipelines)&lt;/li&gt;
&lt;li&gt;Resolution of values and of the helpers defined in &lt;code&gt;_helpers.tpl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Correct syntax of &lt;code&gt;{{- if }}&lt;/code&gt; conditions and &lt;code&gt;{{- range }}&lt;/code&gt; loops&lt;/li&gt;
&lt;li&gt;No undefined or wrongly referenced variables&lt;/li&gt;
&lt;/ul&gt;
 &lt;/div&gt;
 &lt;/blockquote&gt;
&lt;h3 id="schema-compliance"&gt;Schema compliance
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;helm template&lt;/code&gt; guarantees that rendering works, but not that the result is acceptable to Kubernetes. That is precisely why I chose to use &lt;a class="link" href="https://github.com/yannh/kubeconform" target="_blank" rel="noopener"
 &gt;Kubeconform&lt;/a&gt;. This tool was selected because it is fast, easy to integrate into a CI, and very effective at validating manifests against known schemas.&lt;/p&gt;
&lt;p&gt;It compares the generated resources to the expected Kubernetes schemas. So it does not only check that the YAML is well formed: it also validates the structure of the manifest, the allowed fields, the expected types and some version incompatibilities. With the &lt;a class="link" href="https://github.com/datreeio/crds-catalog" target="_blank" rel="noopener"
 &gt;CRD catalog&lt;/a&gt; added, it remains useful even in Kubernetes environments extended by operators.&lt;/p&gt;
&lt;p&gt;Originally, this tool is a binary, but a Helm plugin was made so it can be used directly from the Helm CLI.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;helm plugin install https://github.com/jtyr/kubeconform-helm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;helm kubeconform .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Kubeconform configuration (.kubeconform):&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;schema-location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Default K8s resources schema&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;ignore-missing-schemas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Don&amp;#39;t fail on resources where schema is not found.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;blockquote class="alert alert-note"&gt;
 &lt;div class="alert-header"&gt;
 &lt;span class="alert-icon"&gt;📝&lt;/span&gt;
 &lt;span class="alert-title"&gt;Note&lt;/span&gt;
 &lt;/div&gt;
 &lt;div class="alert-body"&gt;
 &lt;p&gt;&lt;strong&gt;What kubeconform checks:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Invalid or deprecated fields&lt;/li&gt;
&lt;li&gt;Incorrect data types&lt;/li&gt;
&lt;li&gt;Missing values for required fields&lt;/li&gt;
&lt;li&gt;Compatibility issues with the targeted Kubernetes version&lt;/li&gt;
&lt;/ul&gt;
 &lt;/div&gt;
 &lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Example output:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stdin - Secret mariadb-cluster is valid
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stdin - StatefulSet mariadb-cluster is valid
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stdin - Cluster pg-cluster is valid
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stdin - MongoDBCommunity mongodb-cluster is valid
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Summary: 17 resources found parsing stdin - Valid: 17, Invalid: 0, Errors: 0, Skipped: 0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="unit-tests"&gt;Unit tests
&lt;/h3&gt;&lt;p&gt;I added &lt;code&gt;Helm Unittest&lt;/code&gt; because, beyond syntax and compliance, the functional behaviour of the chart had to be protected. It is the tool that lets me state explicit expectations: a name must follow a convention, a resource must exist in a given case, an image must be built with a given value, etc.&lt;/p&gt;
&lt;p&gt;The principle is close to a classic test framework, but applied to Helm rendering. You describe a set of input values, then express assertions on the produced manifest. This approach is particularly useful to avoid silent regressions when a chart evolves and starts handling several configuration variants.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;helm plugin install https://github.com/helm-unittest/helm-unittest.git&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;helm unittest .&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Unit tests check the expected behaviour of the chart in different configurations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example test (tests/deployment_test.yaml):&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;suite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;test deployment&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;templates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;deployment.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;it&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;should create a valid deployment&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image.tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;asserts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;isKind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Deployment&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;matchRegex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;metadata.name&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="l"&gt;my-chart$&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;spec.template.spec.containers[0].image&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nginx:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;blockquote class="alert alert-note"&gt;
 &lt;div class="alert-header"&gt;
 &lt;span class="alert-icon"&gt;📝&lt;/span&gt;
 &lt;span class="alert-title"&gt;Note&lt;/span&gt;
 &lt;/div&gt;
 &lt;div class="alert-body"&gt;
 &lt;p&gt;These tests check that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The default values produce valid manifests&lt;/li&gt;
&lt;li&gt;The different configurations work correctly&lt;/li&gt;
&lt;li&gt;Critical behaviours are preserved (non-regression)&lt;/li&gt;
&lt;/ul&gt;
 &lt;/div&gt;
 &lt;/blockquote&gt;
&lt;h3 id="release-management"&gt;Release management
&lt;/h3&gt;&lt;p&gt;Publication is only triggered when a Git tag is created on the &lt;code&gt;main&lt;/code&gt; branch. This process guarantees that only tagged versions are published, and makes it possible to accumulate several changes before creating a release.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key point: the CHANGELOG&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every Merge Request &lt;strong&gt;must&lt;/strong&gt; include an update of &lt;code&gt;CHANGELOG.md&lt;/code&gt;. This file follows the &lt;a class="link" href="https://keepachangelog.com/en/1.0.0/" target="_blank" rel="noopener"
 &gt;Keep a Changelog&lt;/a&gt; format and uses &lt;a class="link" href="https://semver.org/" target="_blank" rel="noopener"
 &gt;Semantic Versioning&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## [1.1.0] - 2026-01-26
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### Added
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Resource limits and requests for all containers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### Changed
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Updated default memory limit to 512Mi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[1.1.0]: https://gitlab.com/project/compare/1.0.9...1.1.0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Available categories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Added&lt;/strong&gt;: new features&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Changed&lt;/strong&gt;: changes to existing features&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fixed&lt;/strong&gt;: bug fixes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Removed&lt;/strong&gt;: removed features&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote class="alert alert-important"&gt;
 &lt;div class="alert-header"&gt;
 &lt;span class="alert-icon"&gt;📌&lt;/span&gt;
 &lt;span class="alert-title"&gt;Important&lt;/span&gt;
 &lt;/div&gt;
 &lt;div class="alert-body"&gt;
 &lt;p&gt;If a change introduces a breaking change, it must be:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Clearly stated in the CHANGELOG&lt;/li&gt;
&lt;li&gt;Explained in the Merge Request description&lt;/li&gt;
&lt;li&gt;Accompanied by a migration guide if needed&lt;/li&gt;
&lt;/ol&gt;
 &lt;/div&gt;
 &lt;/blockquote&gt;
&lt;h3 id="semantic-versioning"&gt;Semantic Versioning
&lt;/h3&gt;&lt;p&gt;We strictly follow Semantic Versioning (MAJOR.MINOR.PATCH):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MAJOR&lt;/strong&gt;: breaking changes (incompatible with previous versions)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MINOR&lt;/strong&gt;: new backward-compatible features&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PATCH&lt;/strong&gt;: backward-compatible bug fixes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1.0.9&lt;/code&gt; → &lt;code&gt;1.1.0&lt;/code&gt;: adding resource limits (backward-compatible) → MINOR&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1.1.0&lt;/code&gt; → &lt;code&gt;2.0.0&lt;/code&gt;: changing the structure of values.yaml (breaking) → MAJOR&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="observed-benefits"&gt;Observed benefits
&lt;/h2&gt;&lt;p&gt;Since this workflow was set up, we have observed:&lt;/p&gt;
&lt;h3 id="quality"&gt;Quality
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Zero undocumented breaking changes&lt;/strong&gt; in 6 months&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;90% fewer deployment errors&lt;/strong&gt; related to the charts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Early detection&lt;/strong&gt; of problems (before code review)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="collaboration"&gt;Collaboration
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A clear process&lt;/strong&gt; for all contributors (juniors and seniors)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More effective code reviews&lt;/strong&gt; (focus on logic, not syntax)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full traceability&lt;/strong&gt; of changes through the CHANGELOG&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="trust"&gt;Trust
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;The development teams &lt;strong&gt;trust&lt;/strong&gt; the stability of the charts&lt;/li&gt;
&lt;li&gt;Updates are &lt;strong&gt;less feared&lt;/strong&gt; because the process is transparent&lt;/li&gt;
&lt;li&gt;Rollbacks are &lt;strong&gt;easier&lt;/strong&gt; thanks to clear versioning&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="productivity"&gt;Productivity
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Less time&lt;/strong&gt; spent on manual validation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automation&lt;/strong&gt; of repetitive tasks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Focus&lt;/strong&gt; on added value rather than on checking syntax&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="sequence-diagram"&gt;Sequence diagram
&lt;/h2&gt;&lt;figure&gt;&lt;img src="https://mzinutti.fr/posts/helm-chart-quality-pipeline/img/flux-diagram.svg"
 alt="Sequence diagram: the merge request pipeline runs the lint, the rendering, the schema validation and the tests, then the tag pipeline packages the chart and publishes it to the registry" width="600px"&gt;&lt;figcaption&gt;
 &lt;h4&gt;Complete sequence diagram&lt;/h4&gt;
 &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="points-of-attention"&gt;Points of attention
&lt;/h2&gt;&lt;p&gt;A few lessons learned while setting this up:&lt;/p&gt;
&lt;h3 id="complete-unit-tests"&gt;Complete unit tests
&lt;/h3&gt;&lt;p&gt;Do not only test the nominal cases. Include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The default configurations&lt;/li&gt;
&lt;li&gt;Edge cases&lt;/li&gt;
&lt;li&gt;The different combinations of values&lt;/li&gt;
&lt;li&gt;Upgrade scenarios&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="changelog-documentation"&gt;CHANGELOG documentation
&lt;/h3&gt;&lt;p&gt;The CHANGELOG is &lt;strong&gt;the contract&lt;/strong&gt; with users. It must be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Complete&lt;/strong&gt;: every change must be documented&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clear&lt;/strong&gt;: understandable language, no unnecessary technical jargon&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Actionable&lt;/strong&gt;: for breaking changes, state what to do&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="protecting-the-main-branch"&gt;Protecting the main branch
&lt;/h3&gt;&lt;p&gt;The protection of &lt;code&gt;main&lt;/code&gt; must be strict:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No direct commits&lt;/li&gt;
&lt;li&gt;Mandatory pipeline&lt;/li&gt;
&lt;li&gt;Mandatory approval&lt;/li&gt;
&lt;li&gt;The CHANGELOG must be up to date &lt;strong&gt;before&lt;/strong&gt; the merge&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="protected-tags"&gt;Protected tags
&lt;/h3&gt;&lt;p&gt;Restrict tag creation to Maintainers to avoid accidental publications.&lt;/p&gt;
&lt;h3 id="communication"&gt;Communication
&lt;/h3&gt;&lt;p&gt;When a breaking change happens, even a well-documented one:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Notify the teams &lt;strong&gt;in advance&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Provide a migration guide&lt;/li&gt;
&lt;li&gt;Offer support during the transition&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="future-improvements"&gt;Future improvements
&lt;/h2&gt;&lt;p&gt;We are considering adding:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Non-regression tests&lt;/strong&gt;: compare the manifests generated between versions with &lt;code&gt;helm diff&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security scanning&lt;/strong&gt;: integrate tools such as Trivy to detect vulnerabilities&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Policy enforcement&lt;/strong&gt;: use tools such as OPA or Kyverno to validate security policies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment monitoring&lt;/strong&gt;: track the adoption of new versions&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Setting up a rigorous validation pipeline for our Helm charts has changed the way we work. The incident that pushed us to implement this workflow is now impossible thanks to the multiple levels of validation.&lt;/p&gt;
&lt;p&gt;This process not only guarantees the technical quality of the charts, it also sets a clear contract with the development teams: they can trust the charts we provide, and when a breaking change is needed, it is clearly documented and communicated.&lt;/p&gt;
&lt;p&gt;For an SRE team, providing a reliable platform is paramount. This workflow lets us keep that promise while keeping the ability to evolve and continuously improve our charts.&lt;/p&gt;
&lt;p&gt;If you manage Helm charts for a Kubernetes platform, I strongly encourage you to set up a similar process. The initial investment quickly pays off through fewer incidents and better collaboration.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Useful resources:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://helm.sh/docs/" target="_blank" rel="noopener"
 &gt;Helm Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/yannh/kubeconform" target="_blank" rel="noopener"
 &gt;Kubeconform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/helm-unittest/helm-unittest" target="_blank" rel="noopener"
 &gt;Helm Unittest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://keepachangelog.com/" target="_blank" rel="noopener"
 &gt;Keep a Changelog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://semver.org/" target="_blank" rel="noopener"
 &gt;Semantic Versioning&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>