DESIGN 0005: Helm Chart for repo-guardian
Status: Implemented Author: Donald Gifford Date: 2026-03-14
Overview
Replace the existing Kustomize-based deployment (deploy/base/ + overlays) with
a Helm chart that packages repo-guardian for Kubernetes. The chart will live in
charts/repo-guardian/ and use the same tooling ecosystem as
server-price-tracker:
helm-cr for releases, helm-ct for chart testing, helm-diff for upgrade previews,
and helm-docs for auto-generated documentation.
Goals and Non-Goals
Goals
- Package all repo-guardian Kubernetes resources into a single Helm chart.
- Support all current deployment scenarios: dev (dry-run), prod (live), and Tailscale Funnel sidecar.
- Integrate helm-cr, helm-ct, helm-diff, and helm-docs into the repo tooling.
- Add Makefile targets and CI workflows for chart linting, testing, and release.
- Provide
values.yamldefaults that match the current Kustomize base configuration. - Support optional Tailscale sidecar injection via values toggle.
- Generate chart README automatically with helm-docs.
Non-Goals
- Remove Kustomize overlays immediately (deprecate, then remove later).
- Support Helm Operator or Flux HelmRelease CRDs (plain
helm installfirst). - Package third-party dependencies as subcharts (e.g., Prometheus, Grafana).
- Multi-tenant or multi-instance deployment patterns.
Background
repo-guardian currently deploys via Kustomize with a base and overlays for dev, prod, and Tailscale environments. While Kustomize works for simple patching, the project needs:
- Parameterized configuration -- environment-specific values without maintaining separate overlay patches for each field.
- Release management -- versioned chart packages hosted on GitHub Pages via chart-releaser, consistent with the server-price-tracker pattern.
- Chart testing -- structured lint and install tests via chart-testing (ct) with kind clusters in CI.
- Self-documenting values -- helm-docs generates a README from value annotations, reducing drift between docs and code.
The server-price-tracker repo provides a proven reference for this tooling stack.
Detailed Design
Chart Structure
charts/repo-guardian/
Chart.yaml # apiVersion v2, type: application
values.yaml # Default values with helm-docs annotations
README.md # Auto-generated by helm-docs
.helmignore # Ignore patterns
ci/
ci-values.yaml # Minimal values for ct install (stub image, no probes)
templates/
_helpers.tpl # Name, fullname, labels, selectorLabels helpers
deployment.yaml # Main deployment (app + optional tailscale sidecar)
service.yaml # ClusterIP service (http + metrics ports)
serviceaccount.yaml # Optional ServiceAccount
configmap.yaml # Template files (CODEOWNERS, dependabot, renovate)
secret.yaml # GitHub App credentials (app-id, webhook-secret, private-key)
servicemonitor.yaml # Optional Prometheus ServiceMonitor
NOTES.txt # Post-install instructions
tests/
deployment_test.yaml # helm-unittest cases
service_test.yaml
configmap_test.yaml
secret_test.yaml
Chart.yaml
apiVersion: v2
name: repo-guardian
description: GitHub App that automates repository onboarding and compliance
type: application
version: 0.1.0
appVersion: "0.6.0"
version tracks the chart independently from appVersion (the Go binary
version). chart-releaser bumps version on release.
Values Design
# -- Number of replicas
replicaCount: 1
image:
# -- Container image repository
repository: ghcr.io/donaldgifford/repo-guardian
# -- Image pull policy
pullPolicy: IfNotPresent
# -- Overrides the image tag (default: appVersion)
tag: ""
# -- Image pull secrets
imagePullSecrets: []
# -- Override the chart name
nameOverride: ""
# -- Override the full release name
fullnameOverride: ""
serviceAccount:
# -- Create a ServiceAccount
create: true
# -- Annotations for the ServiceAccount
annotations: {}
# -- Override the ServiceAccount name
name: ""
# -- Pod annotations
podAnnotations: {}
# -- Pod labels
podLabels: {}
# -- Pod security context
podSecurityContext: {}
# -- Container security context
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65534
service:
# -- Service type
type: ClusterIP
# -- Webhook HTTP port
httpPort: 80
# -- Metrics port
metricsPort: 9090
# -- Container resource requests and limits
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 15
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 5
periodSeconds: 10
# -- repo-guardian application configuration (env vars)
config:
# -- GitHub App ID
appId: ""
# -- GitHub organization name
org: ""
# -- Webhook listen port
port: 8080
# -- Metrics listen port
metricsPort: 9090
# -- Log level (debug, info, warn, error)
logLevel: "info"
# -- Dry run mode
dryRun: false
# -- Worker count for check queue
workerCount: 5
# -- Queue size for check queue
queueSize: 100
# -- Reconciliation schedule interval (Go duration)
scheduleInterval: "168h"
# -- Skip forked repositories
skipForks: true
# -- Skip archived repositories
skipArchived: true
# -- GitHub App secrets
secrets:
# -- Create secret resource (false = use existing secret)
create: true
# -- Name of existing secret (when create=false)
existingSecret: ""
# -- GitHub webhook secret
webhookSecret: ""
# -- GitHub App private key (PEM format)
privateKey: ""
# -- Mount private key as file (true) or env var (false)
privateKeyAsFile: true
# -- Template overrides for CODEOWNERS, dependabot, renovate
templates:
# -- Custom CODEOWNERS template (empty = use embedded default)
codeowners: ""
# -- Custom dependabot template
dependabot: ""
# -- Custom renovate template
renovate: ""
# -- Webhook IP allowlist configuration
webhookIPAllowlist:
# -- Enable GitHub IP allowlist middleware
enabled: true
# -- Allow requests when allowlist unavailable
failOpen: false
# -- Trust X-Forwarded-For proxy headers
trustProxyHeaders: false
# -- Tailscale Funnel sidecar
tailscale:
# -- Enable Tailscale sidecar container
enabled: false
# -- Tailscale container image
image: ghcr.io/tailscale/tailscale:latest
# -- Tailscale hostname (becomes <hostname>.<tailnet>.ts.net)
hostname: repo-guardian
# -- Use userspace networking (no CAP_NET_ADMIN needed)
userspace: true
# -- Name of existing secret containing 'authkey'
authKeySecret: tailscale-auth
# -- Create RBAC for Tailscale state management
rbac:
create: true
serviceMonitor:
# -- Create Prometheus ServiceMonitor
enabled: false
# -- Scrape interval
interval: 30s
# -- Additional labels for ServiceMonitor
labels: {}
# -- Additional environment variables
extraEnv: []
# -- Additional volumes
extraVolumes: []
# -- Additional volume mounts
extraVolumeMounts: []
# -- Node selector
nodeSelector: {}
# -- Tolerations
tolerations: []
# -- Affinity rules
affinity: {}
Comment annotations (# --) are consumed by helm-docs to generate the README
values table automatically.
Private Key Handling
The chart supports two modes for the GitHub App private key, matching the current Kustomize base:
-
File mount (default):
secrets.privateKeyAsFile: truemounts the private key from the Secret as a PEM file at/etc/repo-guardian/private-key/private-key.pem. SetsGITHUB_PRIVATE_KEY_PATHenv var. -
Environment variable:
secrets.privateKeyAsFile: falseinjects the private key directly viaGITHUB_PRIVATE_KEYenv var. Simpler but less secure (visible inkubectl describe pod).
Tailscale Sidecar
When tailscale.enabled: true, the chart injects:
- A sidecar container running
ghcr.io/tailscale/tailscale:latestwith userspace networking. - A ConfigMap with the Funnel serve config (proxy 443 -> 127.0.0.1:8080).
- An emptyDir volume for Tailscale state.
- Optional RBAC (Role + RoleBinding) for Tailscale state management.
This replaces the current deploy/overlays/tailscale/ Kustomize overlay with a
single tailscale.enabled: true toggle.
Template Helpers (_helpers.tpl)
Standard helpers following the server-price-tracker pattern:
| Helper | Output |
|---|---|
repo-guardian.name |
Chart name (truncated to 63 chars) |
repo-guardian.fullname |
Release-qualified name |
repo-guardian.labels |
Standard Kubernetes labels |
repo-guardian.selectorLabels |
Selector labels (name + instance) |
repo-guardian.serviceAccountName |
ServiceAccount name |
repo-guardian.secretName |
Secret resource name |
OCI Registry Support
In addition to the default GitHub Pages chart repository (via chart-releaser), the chart supports pushing to OCI-compliant registries such as Amazon ECR. This is useful for production environments where charts are consumed from a private registry rather than a public GitHub Pages endpoint.
Push workflow:
# Login to ECR (or any OCI registry)
aws ecr get-login-password --region us-east-1 | \
helm registry login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
# Package the chart
helm package charts/repo-guardian
# Push to OCI registry
helm push repo-guardian-0.1.0.tgz oci://123456789012.dkr.ecr.us-east-1.amazonaws.com/helm-charts
Install from OCI registry:
helm install repo-guardian \
oci://123456789012.dkr.ecr.us-east-1.amazonaws.com/helm-charts/repo-guardian \
--version 0.1.0 \
-f values-prod.yaml
Makefile targets:
| Target | Description |
|---|---|
helm-push |
Push packaged chart to OCI registry (HELM_REGISTRY var) |
CI workflow (.github/workflows/chart-release.yml addition):
An optional job publishes the chart to an OCI registry after the GitHub Pages
release succeeds. The registry URL is configured via the HELM_OCI_REGISTRY
repository secret. When the secret is not set, the OCI push step is skipped.
helm-oci-push:
name: Push to OCI Registry
needs: chart-release
if: vars.HELM_OCI_REGISTRY != ''
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@v6
- uses: azure/setup-helm@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-arn: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Login to ECR
run: |
aws ecr get-login-password | \
helm registry login --username AWS --password-stdin "${{ vars.HELM_OCI_REGISTRY }}"
- name: Package and push
run: |
helm package charts/repo-guardian
helm push repo-guardian-*.tgz "oci://${{ vars.HELM_OCI_REGISTRY }}"
ECR repository setup:
The ECR repository must be created before the first push. This is a one-time setup step:
aws ecr create-repository \
--repository-name helm-charts/repo-guardian \
--region us-east-1
Tooling Setup
mise.toml additions
# helm
helm = "3.19.0"
helm-cr = "latest"
helm-ct = "latest"
helm-diff = "latest"
helm-docs = "latest"
ct.yaml (chart-testing config)
chart-dirs:
- charts
target-branch: main
check-version-increment: false
validate-maintainers: false
lint-conf: charts/.yamllint.yml
charts/.yamllint.yml
Separate yamllint config for charts that ignores template directories:
extends: default
rules:
line-length:
max: 150
level: warning
indentation:
spaces: 2
indent-sequences: true
comments:
min-spaces-from-content: 1
document-start:
present: true
truthy:
allowed-values: ["true", "false", "yes", "no", "on", "off"]
check-keys: false
empty-values: disable
ignore: |-
charts/repo-guardian/templates/
Makefile Targets
Add targets to the existing Makefile matching the server-price-tracker pattern:
| Target | Description |
|---|---|
helm-lint |
helm lint charts/repo-guardian |
helm-template |
Render templates with default values |
helm-template-ci |
Render templates with CI values |
helm-package |
Package chart to .tgz |
helm-unittest |
Run helm-unittest plugin tests |
helm-test |
helm-lint + helm-unittest |
helm-ct-lint |
ct lint --config ct.yaml --all |
helm-ct-list-changed |
List changed charts since target branch |
helm-ct-install |
Install and test in kind cluster |
helm-docs |
Generate chart README with helm-docs |
helm-diff-check |
Show diff between installed release and local chart |
helm-cr-package |
Package chart with chart-releaser |
helm-push |
Push packaged chart to OCI registry (HELM_REGISTRY var) |
CI Workflows
Chart Release (.github/workflows/chart-release.yml)
Triggered manually (workflow_dispatch). Uses helm/chart-releaser-action@v1
to package and publish the chart to GitHub Pages.
name: Chart Release
on:
workflow_dispatch: {}
permissions:
contents: write
jobs:
chart-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
persist-credentials: true
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Set up Helm
uses: azure/setup-helm@v4
- name: Run chart-releaser
uses: helm/chart-releaser-action@v1
with:
charts_dir: charts
skip_existing: true
env:
CR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CI Additions (.github/workflows/ci.yml)
Add two jobs to the existing CI workflow:
Helm Unit Tests:
helm-unittest:
name: Helm Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Run helm-unittest
uses: d3adb5/helm-unittest-action@v2
with:
charts: charts/repo-guardian
flags: --color
Helm Chart Test (ct lint + install):
helm-test:
name: Helm Chart Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: azure/setup-helm@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: helm/chart-testing-action@v2
- name: List changed charts
id: list-changed
run: |
changed=$(ct list-changed --config ct.yaml)
if [[ -n "$changed" ]]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Lint charts
if: steps.list-changed.outputs.changed == 'true'
run: ct lint --config ct.yaml
- name: Create kind cluster
if: steps.list-changed.outputs.changed == 'true'
uses: helm/kind-action@v1
- name: Install and test charts
if: steps.list-changed.outputs.changed == 'true'
run: ct install --config ct.yaml
Helm Lint (add to existing lint-repo job):
- name: Lint Helm chart
run: helm lint charts/repo-guardian/
Testing Strategy
helm-unittest
Unit tests in charts/repo-guardian/tests/ validate template rendering:
- deployment_test.yaml -- replica count, image tag, env vars, volume mounts, Tailscale sidecar injection, probe configuration.
- service_test.yaml -- port mappings, service type.
- configmap_test.yaml -- template file content, custom template overrides.
- secret_test.yaml -- secret creation vs existing secret reference, private key mount mode.
- servicemonitor_test.yaml -- conditional creation, interval, labels.
ct lint
Chart-testing lints the chart YAML, validates Chart.yaml fields, and runs
yamllint with the chart-specific config.
ct install
Chart-testing creates a kind cluster, installs the chart with ci/ci-values.yaml
(stub image, disabled probes), and runs helm test to verify the release is
healthy.
ci/ci-values.yaml
Minimal values that allow the chart to install in a kind cluster without real GitHub credentials:
image:
repository: busybox
tag: latest
config:
appId: "12345"
dryRun: true
secrets:
webhookSecret: "test-secret"
privateKey: "test-key"
privateKeyAsFile: false
livenessProbe: {}
readinessProbe: {}
resources: {}
helm-diff
Used locally to preview changes before upgrading a release:
make helm-diff-check RELEASE=repo-guardian
Migration / Rollout Plan
- Phase 1: Create chart -- Add
charts/repo-guardian/with all templates, values, and tests. Kustomize overlays remain untouched. - Phase 2: Add tooling -- Add helm, helm-cr, helm-ct, helm-diff, helm-docs
to
mise.toml. Add Makefile targets and CI workflows. - Phase 3: Validate -- Deploy to Talos cluster using
helm installand verify parity with Kustomize deployment. - Phase 4: Deprecate Kustomize -- Mark
deploy/as deprecated in docs. Keep for one release cycle. - Phase 5: Remove Kustomize -- Delete
deploy/directory after confirming all deployments use the Helm chart.
Open Questions
All resolved.
-
~~Kustomize removal timeline~~: Resolved. Keep Kustomize alongside Helm for now. Revisit removal after the Helm chart is validated on the Talos cluster and at least one tagged release has shipped with it.
-
~~GitHub Pages for chart repo~~: Resolved. Use
gh-pagesbranch via chart-releaser as the default. Additionally, support pushing charts to an OCI-compliant registry (e.g., ECR) for production environments. See the OCI Registry Support section under Detailed Design. -
~~helm-unittest plugin installation~~: Resolved. Already added to
mise.toml. Local developers get it automatically viamise install. -
~~Makefile structure~~: Resolved. Keep helm targets in the existing single
Makefile. No modular split needed at this time.
Decisions
-
Chart location:
charts/repo-guardian/(notdeploy/helm/orchart/). Matches the server-price-tracker convention and chart-releaser expectations. -
Tailscale as a values toggle, not a subchart: The Tailscale sidecar is simple enough to template directly. A subchart adds unnecessary complexity for a single container.
-
helm-docs comment annotations: Use
# --comment style invalues.yamlfor auto-generated documentation. This is the helm-docs default format.
References
- server-price-tracker Helm chart -- reference implementation for tooling setup
- DESIGN-0004: GitHub Webhook IP Allowlist Middleware
- INV-0001: Tailscale Funnel for Webhook Testing
- helm-cr (chart-releaser)
- helm-ct (chart-testing)
- helm-diff
- helm-docs
- helm-unittest