agentleFS
community-chartscommunity-charts/helm-charts/.claude/skills/helm-chart-development/SKILL.md

helm-chart-development

Skill133 starsChanged 6 months agoMITView on GitHub

Trigger when a user is building, writing, or debugging Helm chart source files — not deploying or installing charts. Key scenarios: packaging an application as a Helm chart for the first time, writing or fixing Go templates (deployment, service, ServiceMonitor, ingress), authoring _helpers.tpl helper functions, configuring values.schema.json validation, diagnosing ct lint or ah lint schema errors, writing helm-unittest test suites, or setting up helm-docs with README.md.gotmpl. The core signal: the user is acting as a chart developer (creating or modifying chart files), not as a chart consumer operating a deployed release.

  • Reads credentials

1 other project uses a version of this, 1 of them edited.

Adopted by

Most recently taken up by

  • Other versions1 project uses a version of this.

    Word for word · 0

    None found.

    Edited · 1

    1. foxj77/claude-code-skills★ 1950% same

    Wrote their own version · 0

    None found.

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

  • In this repository2 other files it gives its agents.

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Use set: to override values per test.

    line 490
  2. Very specific:

    Never add a Co-Authored-By trailer.

    line 545

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Helm Chart Development

Quick Reference

TaskCommand
Lint (chart-testing)ct lint --debug --config ./.github/configs/ct-lint.yaml --lint-conf ./.github/configs/lintconf.yaml
Lint (ArtifactHub)ah lint -p charts/<chart-name>
Unit testshelm unittest --strict --file 'unittests/**/*.yaml' charts/<chart-name>
Unit tests (all charts)for chart in charts/*/; do helm unittest --strict --file 'unittests/**/*.yaml' "${chart%/}"; done
Update dependencieshelm dependency update charts/<chart-name>
Template dry-runhelm template myrelease charts/<chart-name> -f values.yaml
Install debughelm install myrelease charts/<chart-name> --debug --dry-run

Chart Structure

charts/<name>/
├── Chart.yaml          # Chart metadata, version, appVersion, ArtifactHub annotations
├── Chart.lock          # Locked dependency versions
├── values.yaml         # Default values
├── values.schema.json  # JSON Schema validation for values (additionalProperties: false)
├── values-kind.yaml    # Minimal values for kind cluster CI testing (no external deps, no PVCs)
├── README.md           # Auto-generated by helm-docs from README.md.gotmpl — never edit directly
├── README.md.gotmpl    # Template source for README generation
├── LICENSE
├── .skip-kind-test     # Optional: if present, ct install step is skipped in CI
├── files/              # Static files mounted into containers (e.g., scripts, configs)
├── templates/
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── serviceaccount.yaml
│   ├── hpa.yaml
│   ├── servicemonitor.yaml
│   ├── NOTES.txt
│   └── *_test.yaml     # Helm template test files (flat — no tests/ subdirectory)
└── unittests/
    └── *_test.yaml     # helm-unittest test suites (separate from templates/)

Chart.yaml

apiVersion: v2
name: myapp
description: A Helm chart for MyApp
type: application
version: 1.0.0
appVersion: "2.3.1"

home: https://github.com/org/myapp
icon: https://example.com/icon.png
sources:
  - https://github.com/org/myapp

maintainers:
  - name: Platform Team
    email: platform@company.com

keywords:
  - app
  - web

annotations:
  artifacthub.io/category: integration-delivery
  artifacthub.io/license: Apache-2.0
  artifacthub.io/prerelease: "false"
  artifacthub.io/containsSecurityUpdates: "false"
  artifacthub.io/changes: |
    - kind: added
      description: Initial release
  artifacthub.io/images: |
    - name: myapp
      image: myorg/myapp:2.3.1
  artifacthub.io/maintainers: |
    - name: Platform Team
      email: platform@company.com
  artifacthub.io/links: |
    - name: support
      url: https://github.com/org/myapp/issues

dependencies:
  - name: postgresql
    version: "12.x.x"
    repository: https://charts.bitnami.com/bitnami
    condition: postgresql.enabled

For the full list of ArtifactHub annotations with format details, read references/artifacthub-annotations.md.

Versioning Rules

  • Any change (including docs) requires a version bump in Chart.yaml following semver.
  • appVersion tracks the upstream application version.
  • Breaking changes bump MAJOR version and must add an "Upgrading" section to README.md.gotmpl.
  • artifacthub.io/changes must be populated on every release — it drives auto-generated release notes.

README Generation

README.md is auto-generated from README.md.gotmpl by helm-docs. Never edit README.md directly.

helm-docs --chart-search-root=charts --template-files=README.md.gotmpl

The pre-commit hook runs helm-docs automatically. If it modifies README.md, the commit fails — re-stage and retry:

git add charts/<chart-name>/README.md
git commit -s

Values Design

# -- Number of replicas
replicaCount: 1

image:
  # -- Image repository
  repository: myapp
  # -- Image pull policy
  pullPolicy: IfNotPresent
  # -- Image tag (defaults to appVersion)
  tag: ""

# -- Image pull secrets
imagePullSecrets: []
# -- Override chart name
nameOverride: ""
# -- Override full name
fullnameOverride: ""

serviceAccount:
  create: true
  annotations: {}
  name: ""
  automount: true

podAnnotations: {}
podLabels: {}

podSecurityContext:
  fsGroup: 1000
  runAsNonRoot: true

securityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  runAsNonRoot: true
  runAsUser: 1000
  capabilities:
    drop:
      - ALL

service:
  type: ClusterIP
  port: 80
  targetPort: 8080

ingress:
  enabled: false
  className: ""
  annotations: {}
  hosts:
    - host: chart-example.local
      paths:
        - path: /
          pathType: Prefix
  tls: []

resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 100m
    memory: 128Mi

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

nodeSelector: {}
tolerations: []
affinity: {}
extraEnv: []
extraVolumeMounts: []
extraVolumes: []

livenessProbe:
  httpGet:
    path: /healthz
    port: http
  initialDelaySeconds: 10
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /ready
    port: http
  initialDelaySeconds: 5
  periodSeconds: 5

postgresql:
  enabled: false
  auth:
    database: myapp
    username: myapp

Values Schema (values.schema.json)

Uses JSON Schema draft-07. The "additionalProperties": false at root is required — any value in values.yaml without a schema entry causes ct lint to fail.

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "additionalProperties": false,
  "required": ["image"],
  "properties": {
    "replicaCount": {
      "type": "integer",
      "minimum": 1
    },
    "image": {
      "type": "object",
      "additionalProperties": false,
      "required": ["repository"],
      "properties": {
        "repository": { "type": "string" },
        "tag": { "type": "string" },
        "pullPolicy": {
          "type": "string",
          "enum": ["Always", "IfNotPresent", "Never"]
        }
      }
    },
    "resources": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "limits": {
          "type": "object",
          "properties": {
            "cpu": { "type": "string" },
            "memory": { "type": "string" }
          }
        },
        "requests": {
          "type": "object",
          "properties": {
            "cpu": { "type": "string" },
            "memory": { "type": "string" }
          }
        }
      }
    }
  }
}

Template Helpers (_helpers.tpl)

{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "myapp.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{- define "myapp.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "myapp.labels" -}}
helm.sh/chart: {{ include "myapp.chart" . }}
{{ include "myapp.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{- define "myapp.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "myapp.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

{{- define "myapp.image" -}}
{{- $tag := .Values.image.tag | default .Chart.AppVersion -}}
{{- printf "%s:%s" .Values.image.repository $tag }}
{{- end }}

For string, math, list, dict, encoding, and date functions, read references/template-functions.md.

Template Patterns

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "myapp.fullname" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "myapp.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
        {{- with .Values.podAnnotations }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
      labels:
        {{- include "myapp.labels" . | nindent 8 }}
        {{- with .Values.podLabels }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "myapp.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: {{ include "myapp.image" . }}
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.service.targetPort }}
              protocol: TCP
          {{- with .Values.livenessProbe }}
          livenessProbe:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- with .Values.readinessProbe }}
          readinessProbe:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          {{- with .Values.extraEnv }}
          env:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- with .Values.extraVolumeMounts }}
          volumeMounts:
            {{- toYaml . | nindent 12 }}
          {{- end }}
      {{- with .Values.extraVolumes }}
      volumes:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}

Conditional resource (Ingress)

{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "myapp.fullname" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  {{- if .Values.ingress.className }}
  ingressClassName: {{ .Values.ingress.className }}
  {{- end }}
  rules:
    {{- range .Values.ingress.hosts }}
    - host: {{ .host | quote }}
      http:
        paths:
          {{- range .paths }}
          - path: {{ .path }}
            pathType: {{ .pathType }}
            backend:
              service:
                name: {{ include "myapp.fullname" $ }}
                port:
                  number: {{ $.Values.service.port }}
          {{- end }}
    {{- end }}
{{- end }}

Unit Tests (helm-unittest)

Tests live in unittests/ and use the helm-unittest syntax. Each file asserts on rendered template output.

# unittests/deployment_test.yaml
suite: deployment
templates:
  - deployment.yaml
tests:
  - it: should render with default values
    asserts:
      - isKind:
          of: Deployment
      - equal:
          path: spec.template.spec.containers[0].image
          value: myapp:1.0.0

  - it: should set replica count
    set:
      replicaCount: 3
    asserts:
      - equal:
          path: spec.replicas
          value: 3

  - it: should not set replicas when autoscaling is enabled
    set:
      autoscaling.enabled: true
    asserts:
      - notExists:
          path: spec.replicas

  - it: should disable deployment when enabled is false
    set:
      enabled: false
    asserts:
      - hasDocuments:
          count: 0

Common assertion types: isKind, equal, notEqual, contains, notContains, exists, notExists, hasDocuments, matchSnapshot.

Use set: to override values per test. Snapshot tests store expected output in unittests/__snapshot__/.

NOTES.txt Template

{{- $fullName := include "myapp.fullname" . -}}
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
  {{- range .paths }}
  http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
  {{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ $fullName }})
  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
  echo http://$NODE_IP:$NODE_PORT
{{- else if contains "ClusterIP" .Values.service.type }}
  kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ $fullName }} 8080:{{ .Values.service.port }}
  echo "Visit http://127.0.0.1:8080"
{{- end }}

Templating Tips

# Trim whitespace with {{- and -}}
{{- if .Values.enabled }}
key: value
{{- end }}

# Proper indentation
labels:
  {{- include "myapp.labels" . | nindent 2 }}

# Fail if required value is missing
image: {{ required "image.repository is required" .Values.image.repository }}

# Default value
port: {{ .Values.port | default 8080 }}

# Coalesce (first non-empty wins)
name: {{ coalesce .Values.name .Values.nameOverride .Chart.Name }}

# Range over map (env vars pattern)
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
  value: {{ $value | quote }}
{{- end }}

Contributing Requirements

  • Each PR must only change one chart at a time.
  • Commits require a Signed-off-by trailer: git commit -s
  • Never add a Co-Authored-By trailer.
  • YAML files: 2-space indent, no trailing spaces, LF line endings, newline at EOF.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.