Files
jklein e3f6256244
Validate Helm Charts / Detect changed charts (pull_request) Successful in 51s
Validate Helm Charts / Selector match (pull_request) Successful in 10s
Validate Helm Charts / Resource quantities (pull_request) Successful in 11s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 10s
Validate Helm Charts / Build apache-tika (pull_request) Successful in 5s
Validate Helm Charts / Trivy config apache-tika (pull_request) Successful in 13s
Validate Helm Charts / Build bentopdf (pull_request) Successful in 6s
Validate Helm Charts / Build calibre-web-automated (pull_request) Successful in 5s
Validate Helm Charts / Build garage (pull_request) Successful in 6s
Validate Helm Charts / Bump level (pull_request) Successful in 4s
Validate Helm Charts / YAML scalars (pull_request) Successful in 9s
Validate Helm Charts / NetworkPolicy (pull_request) Successful in 10s
Validate Helm Charts / Build bulwark (pull_request) Successful in 6s
Validate Helm Charts / Build cyberchef (pull_request) Successful in 6s
Validate Helm Charts / Build freesailarr (pull_request) Successful in 5s
Validate Helm Charts / Build gotenberg (pull_request) Successful in 6s
Validate Helm Charts / Build homeassistant (pull_request) Successful in 5s
Validate Helm Charts / Build keycloak (pull_request) Successful in 6s
Validate Helm Charts / Build matrix-synapse (pull_request) Successful in 5s
Validate Helm Charts / Build minecraft-router (pull_request) Successful in 6s
Validate Helm Charts / Build minecraft-server (pull_request) Successful in 5s
Validate Helm Charts / Build outline (pull_request) Successful in 5s
Validate Helm Charts / Build paperless-ngx (pull_request) Successful in 5s
Validate Helm Charts / Build rallly (pull_request) Successful in 5s
Validate Helm Charts / Build samba (pull_request) Successful in 5s
Validate Helm Charts / Build satisfactory (pull_request) Successful in 5s
Validate Helm Charts / Build searxng (pull_request) Successful in 6s
Validate Helm Charts / Build stalwart (pull_request) Successful in 5s
Validate Helm Charts / Build sure (pull_request) Successful in 6s
Validate Helm Charts / Build teamspeak-server (pull_request) Successful in 5s
Validate Helm Charts / Build patchmon (pull_request) Successful in 5s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 6s
Validate Helm Charts / Build template-chart (pull_request) Successful in 5s
Validate Helm Charts / Build urlaubsverwaltung (pull_request) Successful in 5s
Validate Helm Charts / Build valkey (pull_request) Successful in 5s
Validate Helm Charts / Build voucher-vault (pull_request) Successful in 6s
Validate Helm Charts / Build ytdlp-web-player (pull_request) Successful in 6s
Validate Helm Charts / Build zipline (pull_request) Successful in 6s
Validate Helm Charts / Trivy config bulwark (pull_request) Successful in 19s
Validate Helm Charts / Trivy config calibre-web-automated (pull_request) Successful in 16s
Validate Helm Charts / Trivy config cyberchef (pull_request) Successful in 13s
Validate Helm Charts / Trivy config freesailarr (pull_request) Successful in 20s
Validate Helm Charts / Trivy config bentopdf (pull_request) Successful in 11s
Install-test / Install-test apache-tika (pull_request) Successful in 1m31s
Validate Helm Charts / Trivy config garage (pull_request) Successful in 21s
Validate Helm Charts / Trivy config gotenberg (pull_request) Successful in 12s
Validate Helm Charts / Trivy config homeassistant (pull_request) Successful in 12s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 13s
Validate Helm Charts / Trivy config matrix-synapse (pull_request) Successful in 18s
Install-test / Install-test bentopdf (pull_request) Successful in 55s
Validate Helm Charts / Trivy config minecraft-router (pull_request) Successful in 14s
Validate Helm Charts / Trivy config minecraft-server (pull_request) Successful in 13s
Validate Helm Charts / Trivy config outline (pull_request) Successful in 15s
Validate Helm Charts / Trivy config paperless-ngx (pull_request) Successful in 21s
Install-test / Install-test bulwark (pull_request) Successful in 1m20s
Validate Helm Charts / Trivy config patchmon (pull_request) Successful in 15s
Validate Helm Charts / Trivy config rallly (pull_request) Successful in 11s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 12s
Validate Helm Charts / Trivy config samba (pull_request) Successful in 14s
Validate Helm Charts / Trivy config satisfactory (pull_request) Successful in 26s
Validate Helm Charts / Trivy config searxng (pull_request) Successful in 23s
Validate Helm Charts / Trivy config stalwart (pull_request) Successful in 13s
Install-test / Install-test calibre-web-automated (pull_request) Successful in 1m52s
Validate Helm Charts / Trivy config sure (pull_request) Successful in 15s
Validate Helm Charts / Trivy config teamspeak-server (pull_request) Successful in 12s
Validate Helm Charts / Trivy config template-chart (pull_request) Successful in 14s
Validate Helm Charts / Trivy config urlaubsverwaltung (pull_request) Successful in 21s
Install-test / Install-test cyberchef (pull_request) Successful in 1m2s
Validate Helm Charts / Trivy config valkey (pull_request) Successful in 15s
Validate Helm Charts / Trivy config voucher-vault (pull_request) Successful in 13s
Validate Helm Charts / Trivy config ytdlp-web-player (pull_request) Successful in 13s
Validate Helm Charts / Trivy config zipline (pull_request) Successful in 14s
Install-test / Install-test freesailarr (pull_request) Successful in 3m42s
Install-test / Install-test garage (pull_request) Successful in 1m14s
Install-test / Install-test gotenberg (pull_request) Successful in 1m32s
Install-test / Install-test homeassistant (pull_request) Successful in 2m24s
Install-test / Install-test keycloak (pull_request) Successful in 2m34s
Install-test / Install-test matrix-synapse (pull_request) Successful in 1m31s
Install-test / Install-test minecraft-router (pull_request) Successful in 48s
Install-test / Install-test minecraft-server (pull_request) Successful in 1m52s
Install-test / Install-test outline (pull_request) Successful in 2m7s
Install-test / Install-test paperless-ngx (pull_request) Successful in 2m56s
Install-test / Install-test patchmon (pull_request) Successful in 1m22s
Install-test / Install-test rallly (pull_request) Successful in 1m38s
Install-test / Install-test rss-bridge (pull_request) Successful in 42s
Install-test / Install-test samba (pull_request) Successful in 1m12s
Install-test / Install-test searxng (pull_request) Successful in 1m13s
Install-test / Install-test stalwart (pull_request) Successful in 1m18s
Install-test / Install-test sure (pull_request) Successful in 2m15s
Install-test / Install-test teamspeak-server (pull_request) Successful in 1m19s
Install-test / Install-test urlaubsverwaltung (pull_request) Successful in 1m33s
Install-test / Install-test valkey (pull_request) Successful in 1m14s
Install-test / Install-test voucher-vault (pull_request) Successful in 1m54s
Validate Helm Charts / Install-test (pull_request) Successful in 45m59s
Install-test / Install-test ytdlp-web-player (pull_request) Successful in 54s
Install-test / Install-test zipline (pull_request) Successful in 1m57s
feat(gotenberg): a named switch for remote assets, off by default
The maintainer's decision, and it keeps restrictive-by-default intact: not an
opening but networkPolicy.egress.remoteAssets, whose purpose is in its name
instead of being an unplaceable [80, 443] in extraPorts. Additive inside the
major this chart already carries in this PR, so no second version step.

WHAT IT BUYS, measured rather than assumed. paperless-ngx splits a mail into
two steps with two separate cluster-internal endpoints: the .eml goes to TIKA
for parsing (PAPERLESS_TIKA_ENDPOINT) and the resulting HTML comes here for
rendering (PAPERLESS_TIKA_GOTENBERG_ENDPOINT). Of the images in that HTML,
EMBEDDED ones are MIME parts inside the file and need no network from either
service - which is why apache-tika stays DNS-only and is unchanged - while
REMOTE ones (<img src="https://...">) are fetched by Chromium here, at render
time. With the switch off they are silently dropped: the PDF is produced and is
simply missing them, nothing fails, nothing is logged, no probe notices. That
invisibility is the reason the switch exists.

NOT CHECKED, and written down as not checked rather than implied: whether
paperless-ngx re-embeds the images Tika extracted before handing the HTML over.
If it does not, embedded images would be lost too - but that would be
pre-existing paperless behaviour and not this policy, so the switch is not a
fix for it.

The SSRF argument is unchanged and is why the default is off: Gotenberg drives
a headless Chromium and 3000 is its probe port, so the service is reachable
from the whole cluster, and outbound web access turns anything that can POST
here into a way of fetching arbitrary URLs.

Two things the implementation needed, and one of them was a real defect. The
NOTES.txt branch read .Values.networkPolicy.egress.remoteAssets directly, which
fails the whole render with "nil pointer evaluating interface
{}.remoteAssets" when an operator erases the egress block - the schema permits
null there, and issue #99's rule is that an erased block means "the default
applies", not a broken render. It is null-safe now, and an erased block falls
back to DNS-only, which is the restrictive direction. And ci/networkpolicy/
gotenberg.yaml makes the switch's ON state permanent in CI rather than checked
once by hand: it renders both the switch's egress rule and the
source-restricted probe branch, so all five assertions run over the toggle in
both positions on every pull request. Worth having precisely because assertions
2 and 3 look at INGRESS rules - an egress mistake has no other mechanical
check.

Also, three things that were load-bearing and written nowhere:

- NO CHART SHIPS allowedNamespaces PRE-FILLED. A chart naming mc-router or
  stalwart in its defaults would bake in an assumption about this cluster's
  namespace layout, which a chart cannot keep true. Restrictive is the default
  and filling it is the DEPLOYMENT adaptation - so the release-side one-liners
  this sweep produces are the expected procedure, not a defect list, and the
  major on each chart is what announces them. ingressController is the one
  exception, because it names a cluster component rather than a tenant.
- A POLICY APPLIES TO TRAFFIC THAT CAME THROUGH A SERVICE, and that is why the
  ingressController rule works at all. A Service is only destination NAT and
  the policy acts at the pod's interface. What the Service changes is the
  SOURCE ADDRESS: pod -> ClusterIP -> pod preserves it, so a podSelector can
  still match Traefik; NodePort traffic with the default externalTrafficPolicy:
  Cluster is masqueraded to a node, which is why those rules are left open.
  stalwart's template already carried the second half; the first half is the
  one everything rests on.
- AND THE ONE CLUSTER SETTING THIS CONVENTION DEPENDS ON AND CANNOT CHECK:
  Traefik must not run with hostNetwork. Measured 2026-10-14 - the DaemonSet
  has none and its pods carry real pod IPs (10.42.3.120, 10.42.4.94,
  10.42.0.187 in the pod CIDR 10.42.0.0/16, nodes on entirely different
  addresses), and only because of that does a podSelector match it. On host
  networking those pods share the node's namespace, stop being selectable,
  and EVERY WEB CHART GOES DARK AT ONCE while every pod stays Ready, because
  the probe path does not run through the controller. Same class as
  --enable-certificate-owner-ref in #126: undetectable from here. It is in
  README and in the header of template-chart's policy, so whoever changes the
  controller's networking meets the consequence on the thing they are changing.

Part of #5
2026-10-14 21:19:34 +00:00

22 KiB
Raw Permalink Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Working on issues — mandatory workflow

The main Claude session acts as the task manager and does not implement issues itself:

  1. Plan first, get approval: The manager presents a plan for the issue and waits for the user's explicit approval. No implementation before approval.
  2. Document the final plan on the issue: Once approved, the plan is posted as a comment on the issue before implementation starts (skip if the issue body already contains the full plan; later decisions go there as comments too).
  3. Delegate implementation: After approval, the manager launches a dedicated development agent. The agent implements the issue completely on its own branch and opens a PR.
  4. PR comment handling by the agent: After opening the PR, the development agent watches it for review comments. New comments are addressed automatically (code changes, reply on the thread), and addressed threads are marked as resolved.
  5. Update the wiki — mandatory, and before the merge is requested. The wiki is this repo's knowledge base; a change that lands in main while the wiki still describes the old state has not been finished, it has been half-finished in a way nothing fails on. The development agent edits the affected pages as part of its issue, so code and documentation are reviewed together. See "The wiki" below for what obliges an edit and what does not.
  6. Agent lifetime: The development agent terminates itself once its PR has been merged (or closed).
  7. Only after the PR has been merged by the user may work on the next issue begin. The user merges; never merge yourself.
  8. After the merge, verify the linked issue was closed (auto-close via "Fixes #n"); close it manually if not. Gitea auto-closes on the same keywords, but only for an issue in the same repository — a migrated issue whose number changed is closed by hand. Check in the same step that the wiki edit really landed.

Additional rules for all commits, PRs, issues, and comments:

  • Do not add a Co-Authored-By trailer and do not append "Generated with Claude Code" footers.
  • Commits must be signed, and in the Coder workspace they are: signing runs through its own SSH signer (gpg.format = ssh, gpg.ssh.program = coder-git-ssh-sign, commit.gpgsign = true), so no 1Password step is involved. Verify before opening the PR — git log --format='%G?' -1 must print G — and if signing fails, stop and report rather than pushing unsigned work, because branch protection rejects it at merge time.
  • A PR that does not finish its issue writes Part of #n, never Fixes #n. The keyword closes the issue on merge, so a Fixes on a PR that leaves work behind marks an open task as done — and reopening by hand is the only repair, which only happens if someone remembers to look. This is not hypothetical: Gitea PR #48 carried Fixes #40 while rss-bridge had been deliberately left out of the sweep, and the merge closed Gitea #40 with one occurrence still open. Step 8 above is the other half of the same rule: the auto-close it verifies is only the right outcome when the keyword was the right one.

The wiki

The knowledge base is the Gitea wiki at https://git.jk-dev.de/jlab-cluster/helm-charts/wiki — one page per chart (installation, required 1Password fields, every configurable value, the chart's traps) plus the long form of this workflow, the pipelines, the style guide and the incidents each rule was bought with. It is a separate git repository:

git clone ssh://git@git.jk-dev.de:30022/jlab-cluster/helm-charts.wiki.git

Its default branch is main, not master.

Flat layout, one Markdown file per page, page name = file name, links are relative page names without the extension ([CI/CD](CI-CD)). A new page must also be listed in _Sidebar.md, or it is reachable only by search.

There is no PR flow here, so fetch and rebase before every push and never force-push — two agents have already edited the same page minutes apart, and a force push deletes the other edit silently, with nothing left to report it.

What obliges a wiki edit (step 5 of the workflow above, same round of work, never a follow-up issue):

Change Page
a value added, removed, renamed, or its default changed the chart's page — values table, and the operating notes if behaviour moved
runtime behaviour changed (probes, hardening, restart semantics, a value becoming required) the chart's page, plus Lessons-Learned if the reason could be repeated elsewhere
a new chart a new chart page, a row in Charts, an entry in _Sidebar.md
a convention added or changed Chart-Style-Guide (and README, which stays the code-side copy)
pipeline, fixture or runner behaviour changed CI-CD
this issue workflow changed Workflow — and this file, which is the binding copy
a Renovate rule added or changed Renovate
something broke in a way a rule now prevents Lessons-Learned

What does not oblige an edit: an app-version bump, at any level. Renovate automerges those continuously and never touches the wiki, which is why chart pages carry no version numbers at all and name Chart.yaml as the source of truth. Since Gitea #128 "plain" is mechanically defined instead of being a judgement: the chart version now states the size of the jump, because ci/renovate-bump-chart.sh takes that size from Renovate's update type (app minor → chart minor). An app update that does change values or behaviour is not a plain bump and the table applies — but nothing obliges anyone to go and find out. That is the deliberate limit of #128, not an oversight: the reading duty was rejected, so the sonarr incident in Lessons-Learned stays repeatable and a bigger number is the only warning that is given.

Wiki commits follow the same rules as repo commits: no Co-Authored-By, no "Generated with Claude Code" footer.

Which forge, and with which tool

Everything runs on the Gitea instance JLab-Git: issues, pull requests, CI and Renovate. github.com is a push mirror with Actions and Renovate switched off, and it lags until the mirror runs. README, "Where this repo lives", holds the measured differences between a Gitea runner and a GitHub one.

  • tea, not gh. Work happens in a Coder workspace, and it has no op — that is the normal case here, not a broken environment, so do not wait for the 1Password CLI or propose installing it. Call tea directly; it carries the workspace's stored login and opens PRs and issues under the bot account, which is accepted. Say so in the report when something is authored that way. (op plugin run -- tea, the older instruction, applies only where op actually exists.)
  • The remote is origin, and it points at Gitea (ssh://git@git.jk-dev.de:30022/jlab-cluster/helm-charts.git). The checkout has exactly that one, and so does every worktree cut from it, so a bare git push goes to the leading copy; the github.com mirror is fed by Gitea itself and is never pushed to by hand. A clone elsewhere may carry the mirror as a second remote named github — wherever more than one exists, say which one you mean.
  • Cut every worktree from a freshly fetched origin/main. git fetch origin main first, then branch from origin/main — never from the local main ref, and never assume the shared checkout is current: both can be many merges behind. The staleness is invisible where it matters most, and it outlives the fix: the project-instructions block a subagent receives is a snapshot of the spawning session's working directory, taken when that session started — not a live read. Fetching mid-session does not refresh it, so the file on disk can be correct while every agent started from then on is still handed the superseded rules. Measured on 2026-09-27: an agent whose own worktree was identical to origin/main (git rev-list --left-right --count origin/main...HEAD returned 0 0) received a block from before Gitea #47 and reported a contradiction in this file that two PRs had already fixed, quoting the stale copy as binding. Two consequences, and only these two help: the spawning session's directory must be current before the session starts, and an agent checks a rule against the file in its own worktree plus a diff against origin/main — never against main, and never against its own context block, because the first consequence cannot be enforced from inside an agent and the second can.
  • Branch protection with a signature requirement is active, and signatures verify on Gitea. A commit that is not signed does not reach main — check before opening a PR, do not discover it at merge time.
  • No merge queue. Gitea has none, and its place is taken by auto-merge on green checks: the PR is armed once and merges itself when the required check passes and the required review exists — both are active on main, so an armed PR without a review sits there instead of merging. Rule 7 above is unchanged — the user merges, and an agent never merges its own PR or arms auto-merge on it.
  • A required check per chart is not possible. Gitea runs a whole matrix as one job under one status context, so the install-test reports once for all charts. What is required on main is validate-charts, as that single context; per chart is the part that cannot be had. Relevant whenever branch protection is touched.
  • Issue numbers changed with the move, and #n in this repo's files means the GitHub tracker. The issues open at the time are Gitea #5–#21 and #24; the mapping is a comment on Gitea #14. Closed ones were not migrated and exist only on GitHub. Beware that Gitea silently resolves a bare #n against its own counter, so an old reference can render as a link to something unrelated. When you write a new reference, use the Gitea number.
  • Renovate runs outside this repo, as the shared jlab-renovate-bot instance; it reads our renovate.json5 and its postUpgradeTasks hook is allow-listed there. Do not add a Renovate workflow here — a second runner means duplicate PRs and two schedulers rewriting each other's branches. README, "Renovate on Gitea", has the evidence.

Keep the docs current

  • Changes to this issue workflow always go into this CLAUDE.md file.
  • General repository information (charts, CI pipelines, conventions, tooling) always lives in README.md. Any PR that changes repo-wide behavior must update README.md in the same PR.
  • Operator-facing knowledge lives in the wiki, not in README.md: how a chart is installed, what every value does, what its traps are. The division is CLAUDE.md binds the agent, README.md explains the repository to someone reading the code, the wiki explains the system to whoever runs it, and a chart's NOTES.txt explains one release. Where README and the wiki cover the same rule, both are updated in the same PR — see "The wiki" above.
  • Runtime verification of chart changes is done in a local, throwaway k3d cluster (k3d is installed on the maintainer's machine): stub CRD for OnePasswordItem, dummy secrets, throwaway infrastructure (postgres/redis) where needed.

What this repo is

A collection of self-made Helm charts, one directory per chart, published to a private, classic (index-based) Helm registry by Gitea Actions — an HTTP POST of the packaged .tgz, not an OCI helm push; the target URL and its credentials live in 1Password and never in this repository. Validation happens on three levels: locally with Helm, in the PR pipeline (validate-charts.yml: strict lint, package, version-bump check), and in the PR install-test stage (k3d, fixtures under ci/ — see README):

helm lint --strict <chart-name>
helm template <chart-name> --set ingress.domain=example.com --set ingress.clusterIssuer=letsencrypt   # render locally; required values vary per chart

Creating a new chart

Copy template-chart/ and replace every xxx placeholder — in file names (templates/xxx.deployment.yaml → templates/<app>.deployment.yaml), in Chart.yaml, in the values.yaml keys and their .Values.xxx.* references inside templates, and in values.schema.json (both the values paths and the xxx. helper prefixes). The schema and the null-safe fallback helpers come with the template but their content is chart-specific — adapt them along the six points of the README section "Values that must not lie", and validate the result against the chart's real bundle values, not only against its defaults. Then wire up CI:

  1. Add .github/workflows/publish-<chart-name>.yml (copy an existing one, e.g. publish-outline.yml) — it triggers on pushes to main touching <chart-name>/** and calls the reusable publish-helm-chart.yml, which lives in workflow-util/pipelines and is bound to a tag. Copying an existing hull gets the uses: path, registry-owner: ${{ github.repository_owner }} and secrets: inherit right; writing one from memory does not.
  2. Add the chart name to the chart-name choice list in .github/workflows/change-app-version.yml.
  3. Add install-test fixtures under ci/<chart-name>/ (test-values.yaml, fixtures.yaml) or an entry with justification in ci/excluded-charts.txt — the PR install-test fails if both are missing.
  4. Add a customManagers block for the chart's main image in renovate.json5 (one per chart, since the image repository lives in the template and the version in Chart.yaml) — without it Renovate never updates that chart's app version. See README section "Dependency updates (Renovate)".

Versioning scheme

Chart.yaml versions follow version: <chart-semver>+up<appVersion> (e.g. 1.4.0+up1.9.2), with appVersion matching the suffix (dashes in the app version are normalized to dots in the suffix). Any push to main touching a chart directory publishes that chart, so the version must be bumped in the same change.

Semver conventions for the chart part. The chart jump is the greater of the chart's own classification and the jump of what it ships (Gitea #128) — a maximum, never a replacement, so a value that becomes required stays a major even under an app patch:

  • Patch: bug fixes without behavior or values-schema change; an app patch or digest update.
  • Minor: additive, backward-compatible changes (new optional values with unchanged defaults); an app minor update.
  • Major: runtime behavior changes or breaking values changes (e.g. probes, security hardening, computed limits, values that become required); an app major update.

The app half of that is mechanical: ci/renovate-bump-chart.sh takes the level from Renovate's update type, passed in as {{{updateType}}}, and not from comparing appVersion — freesailarr pins every tag in values.yaml and its appVersion never moves, so a comparison is blind to exactly the case the rule exists for. Non-semver app versions (CalVer, date tags, four-part and suffixed tags — seven charts) stay at patch for a minor, because their second position is a month or a flavour and says nothing about the jump; an app major still produces a chart major. It obliges nobody to read the release notes — that variant was rejected in #128 — so an app minor that changes a default can still reach main unremarked; the number stops understating, that is all it does. See the wiki, Lessons-Learned.

App-version updates go through PRs (so the validate and install-test pipelines run), not through the change-app-version.yml dispatch workflow (which commits directly to main and bypasses both). For app major jumps, update stepwise: first to the last release of the old major line (own PR, merge, publish), then to the newest major.

Chart conventions

The binding style guide for all current and future charts lives in README.md ("Chart style guidelines"); template-chart/ is the reference implementation. Compact summary:

  • Resources named {{ .Release.Name }}-{{ .Chart.Name }}-<kind>; template files <app>.<kind>.yaml; stateful apps use a StatefulSet (<app>.sfs.yaml). Documented exception: historical PVC names stay (renames would orphan data); StatefulSet renames only with a --cascade=orphan migration note in a major PR.
  • Main image tag from {{ .Chart.AppVersion }}; auxiliary/sidecar/init images pinned via values and reviewed on app-update rounds.
  • Ingress: ingress.domain and ingress.clusterIssuer required without defaults; ingress.className defaults to nginx; optional ingress.annotations merged with the cert-manager annotation; TLS secret tls-<release>-<chart>-ingress. Documented exception: homeassistant keeps its flexible upstream ingress style (className default nginx).
  • Secrets are 1Password OnePasswordItem CRs: values expose secrets.<name>SecretRef holding an op:// item path; the operator materializes the Kubernetes Secret.
  • Config changes must restart the workload (#82): every pod template consuming a ConfigMap carries checksum/config: {{ include (print $.Template.BasePath "/<chart>.configmap.yaml") . | sha256sum }} (one line per ConfigMap). Without it a changed ConfigMap restarts nothing — subPath mounts are never refreshed and apps read config only at start. Adding it to an existing chart is a major bump. Secrets from OnePasswordItem cannot be hashed (Helm never sees their content); charts mounting them document the limitation in values.yaml.
  • Every container offers an additive env block rendered through tpl with value, secretKeyRef, and configMapKeyRef forms.
  • Hardening: automountServiceAccountToken: false, non-root, readOnlyRootFilesystem: true, caps dropped, allowPrivilegeEscalation: false; contexts values-configurable (securityContext.pod / securityContext.container); writable paths as emptyDirs; image-forced deviations MUST be documented with a comment in the chart's values.yaml.
  • Probes: liveness/readiness/startup on every workload (exception: paperless consume CronJob), values-configurable with calibrated defaults.
  • Every chart renders a NetworkPolicy, networkPolicy.enabled defaults to true (#5); adding one to an existing chart is a major. Values: enabled, allowedNamespaces, probeSources, ingressController, egress.extraPorts — and only the ones that actually reach a rule, because a value that does nothing reads as a control. Each port falls in one of four classes and the class decides the rule: a port a network probe addresses stays open (no from) until probeSources is set, because the kubelet probes from the worker node's IP and no chart can know it; a NodePort-published port stays open (the source is already masqueraded); anything else is restricted to the namespace, allowedNamespaces and — for HTTP — ingressController; a replica-to-replica bus gets a bare podSelector. exec probes are not network probes (no packet, so the port can be restricted — valkey, minecraft-server). allowedNamespaces is never shipped pre-filled — a chart must not bake in an assumption about this cluster's namespace layout, so restrictive is the default and filling it is the deployment adaptation (which is why a sweep produces release-side one-liners: that is the procedure, not a defect list). A policy applies to traffic that came through a Service — a Service is only destination NAT and the policy acts at the pod interface; what changes is the source address, preserved for pod→ClusterIP→pod (this is why the ingressController rule works) and masqueraded to a node for NodePort traffic. And the whole convention depends on one setting it cannot check: Traefik must not run with hostNetwork — measured, its pods have real pod IPs in 10.42.0.0/16; on host networking they stop being selectable as pods and every web chart goes dark at once while every pod stays Ready. Pod ports, never NodePorts. Never from: []. Reasoning in Go template comments, never # (those ship to the cluster), and such a comment must not spell out its own closing delimiter. Egress is by port, never destination — Canal has no FQDN rule and Calico-specific objects are out of scope — so most dependency ports are the standard ones and a dependency elsewhere is blocked; policyTypes names only what there is something to restrict (valkey, homeassistant, satisfactory are Ingress-only, each argued in its own file). ci/run-networkpolicy-test.sh is the gate. Full rules: README, "NetworkPolicies".
  • Resources: requests as values defaults; limits auto-computed by the _helpers.tpl helper (limitFactor default 3; no CPU limits unless set explicitly).
  • Replicas per workload (explicit 0 allowed) plus a chart-global scaleDown flag that overrides them.
  • icon: mandatory in every Chart.yaml; helm lint --strict without findings; CI fixtures under ci/<chart>/ or a justified exclusion.
  • Chart dependencies are admissible but expensive (the former "no dependencies" rule fell with #214; ytdlp-web-player was the first to use one, stalwart the second, paperless-ngx the third). A dependency has to be optional and off by default via condition:, and upstream's chart is consumed as it comes — where its defaults fall short of these conventions, that is documented in our values.yaml, never patched. The price to know before adding the next one: a subchart is invisible to everything downstream that watches Helm release versions, so a new upstream version — a security fix included — reaches a cluster only when the outer chart is republished (Renovate's built-in helmv3 manager plus ci/renovate-bump-chart.sh is the whole safety net, and since #128 that republish carries the subchart's own level — an upstream minor is a chart minor, which is the only place that jump becomes visible at all; it belongs in a comment at the pinned version); and every helm command exits 1 while charts/ is unfilled, which is why ci/helm-dependencies.sh runs before lint, package, render and install — including the render test that covers all charts on every PR. Full rules and the rest of the cost: README, "Chart dependencies".