96dd1adbda89b20713c36c3aba1ef4f975410195
30
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a9ca5302de
|
feat(paperless-ngx): optional valkey subchart, password from the redis item
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 12s
Validate Helm Charts / Build paperless-ngx (pull_request) Successful in 6s
Validate Helm Charts / Trivy config paperless-ngx (pull_request) Successful in 22s
Install-test / Install-test paperless-ngx (pull_request) Successful in 3m31s
Validate Helm Charts / Install-test (pull_request) Successful in 3m31s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish Paperless-NGX Helm Chart / Publish Paperless-NGX Helm Chart (push) Successful in 10s
SonarQube Analysis / SonarQube Analysis (push) Successful in 33s
paperless-ngx cannot run without a Redis - PAPERLESS_REDIS is the Celery broker - so every installation needed a second Helm release whose address lived in a 1Password item and in neither chart. This adds our own published valkey chart as an optional dependency, off by default and gated on valkeySubchart.enabled (not valkey.enabled, which the subchart's strict schema rejects and which Helm would only have complained about for whoever first switched it on). Additive, default behaviour unchanged: minor, 12.1.3+up3.2.1 -> 12.2.0+up3.2.1. Configured as a QUEUE (PVC + noeviction, the subchart's defaults), unlike stalwart's cache. Read off the 3.2.1 source rather than assumed: one PAPERLESS_REDIS url serves five consumers - the Celery broker, the result backend (Redis in 3.x, the database before), the channels pub/sub layer and two Django caches. allkeys-lru would not be a trade here but actively wrong, because LRU evicts typeless across the whole keyspace and a backlog of waiting consume tasks is exactly what it reads as cold; a dropped task is a document silently never processed. The price, named in values.yaml rather than smoothed over: two of the five consumers are caches, so a full instance refuses writes - new tasks included - and the answer is memory sizing, not a policy change. All three cache-like consumers are TTL-bounded, which is what keeps that price small. The password comes from the SAME item as the Redis connection, in a third field auth-conf holding the complete line "requirepass <password>". It cannot DEFAULT to secrets.redisSecretRef, and that is a property of Helm, not an omission: a values.yaml is never templated and a parent cannot compute a subchart's value. Measured - a template expression there lands verbatim in the subchart's itemPath and fails as a YAML parse error naming neither the value nor the cause. Forgetting it is therefore caught in values.schema.json, by a conditional rule rather than a fail() in a template. Also measured, and the reason: Helm renders a subchart's templates BEFORE the parent's, so a guard in this chart loses the race to the subchart's own required(), which reports "secrets.authSecretRef" and cannot know the path belongs to the redis item. A check that cannot fire is the mistake found while testing #77, so the first draft of that guard was removed rather than shipped. The schema runs before any rendering and names /valkey/secrets/authSecretRef. One trap found while testing and closed here: "valkeySubchart: null" DELETES the key, Helm then cannot resolve the condition path and treats the dependency as ENABLED - so erasing the switch turned the subchart on with no password set. Unlike every other block in this chart (issue #99), that one is rejected by the schema instead of defaulting. The install-test now runs with the subchart ON and the throwaway redis Deployment removed: the url in the fixtures names ci-valkey-service, so the bundled Valkey is the broker the release actually uses. #65 covered this path by a render only - deliberately, since an unused Valkey would have made that test look like it covered clustering. This is the first real installation of the valkey-subchart path. Documented, not glossed: the password now sits in that item twice and a rotation has to touch both fields; with the subchart on the vault url is a mandatory step, not a hint; and a OnePasswordItem materializes every field of its item, so both Secrets carry all three keys - untidy, harmless, and written down so nobody "tidies it up". Fixes #78 |
||
|
|
780cb8eafd |
feat(stalwart): new chart for the Stalwart mail server
Cluster-capable from the start, with roles rather than symmetry: one StatefulSet per entry of the "nodes" map, the key passed in STALWART_ROLE. Since v0.16 Stalwart keeps its whole configuration inside the data store and manages it over JMAP, so the chart's scope is deliberately narrow: it renders config.json (exactly the DataStore object), the workloads, the Services, the NetworkPolicies, the Ingress and a cert-manager Certificate. Listeners, blob store, in-memory store, TLS object and cluster roles are operator work in the WebUI, which is why NOTES.txt carries an eight-step first install. config.json stays secret-free: authSecret uses the EnvironmentVariable variant (verified against the serde definition of PostgreSqlStore at v0.16.24), so the password arrives through secretKeyRef and checksum/config stays meaningful. The username cannot follow it - authUsername is a plain String with no secret form - so it is a public value and says so. Services are per PORT, not per group: a NodePort is cluster-unique, so "full" and "mx" can only accept SMTP behind one Service, selected through a per-port pod label. The http port gets a per-group ClusterIP Service instead, so the Ingress reaches exactly ingress.nodeGroup. Four traps, documented rather than explained away: - The role lookup FAILS OPEN. A node whose ClusterRole is missing from the database enables every listener and every task while staying Running with a green probe, and the log line arrives after the listeners are bound. Hence mx at replicas 0, per-port Services, and a NetworkPolicy that is a ceiling independent of what was bound. - NET_BIND_SERVICE is a documented deviation needed to EXEC, not to bind. The image sets setcap cap_net_bind_service=+ep, which makes the kernel fail execve when the bounding set is empty. The values comment says both why it is there and that it is never used, because the second half is what stops the next hardening pass from removing it. - externalTrafficPolicy must be Cluster - no pod runs on a control-plane node while the HA proxy delivers to them, so Local would drop every packet - and the source address that costs is restored only by the HA proxy's PROXY v2 header. - /healthz is rate-limited to 100 requests/60s per source IP, which is a node IP for kubelet probes, so all pods on a node share one bucket. The probe intervals are calculated (6 req/min/pod, ~16 pods per node) and the sum is a comment in values.yaml. A first install needs bootstrap.enabled: true: the setup wizard appears only when config.json is ABSENT, and this chart always renders one, so recovery mode is upstream's documented path for a templated config.json. Second chart in the repo with a dependency - the Valkey project's own chart, off by default behind condition:, because Stalwart's cluster coordinator rides on the in-memory store connection. The render aborts when more than one pod runs with no in-memory store address anywhere. Also wired up: publish-stalwart.yml through the shared block in workflow-util/pipelines, the change-app-version.yml choice list, a Renovate customManagers block anchored on the v prefix (which excludes the moving latest / v0.16 / -alpine tags), CI fixtures, and the README and CLAUDE.md lines that claimed ytdlp-web-player was the only chart with a dependency. Verified locally: ci/helm-dependencies.sh, helm lint --strict (0 failed), helm package, helm template against chart defaults and against ci/stalwart/test-values.yaml, the rendered config.json parsed as JSON with the field types the serde definition requires, ci/run-quantity-render-test.sh (0 failed, both nodes.*.resources paths exercised), NOTES.txt rendered through a scratch ConfigMap, and the null-safety and fail paths (securityContext=null keeps NET_BIND_SERVICE; a cluster without an in-memory store, an unknown port name, a group without http and an unknown ingress.nodeGroup all abort with a named message; the schema rejects timeout, a string timeoutMs and a list-shaped nodes). NO runtime verification was possible - there is no k3d or Docker daemon in this workspace. Part of #65 |
||
|
|
8878329f31
|
ci: publish charts through the shared block in workflow-util/pipelines
Validate Helm Charts / Detect changed charts (pull_request) Successful in 6s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Skipped
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Skipped
Validate Helm Charts / Resource quantities (pull_request) Successful in 13s
Validate Helm Charts / Install-test (pull_request) Skipped
The reusable publish workflow now lives in workflow-util/pipelines and is
called pinned to @v3. All 25 thin hulls switch over and our copy is deleted in
the same commit: a hull pointing at a deleted file, or an orphaned file nobody
calls, would both be worse than the state before.
Each hull gains one line, registry-owner: ${{ github.repository_owner }}. The
owner used to be derived inside the workflow (#63); as a required input without
a default it is evaluated here, in the calling repository, where the value is
measured. That is the house rule of pipelines - a shared block never guesses an
owner segment - and it keeps the block free of any context semantics. The
workflow_dispatch and push triggers of every hull are untouched; after the move
the dispatch is the only way to trigger a publish by hand.
The Connect host is no longer wired into the block. It reads the repository
variable OP_CONNECT_HOST, which the maintainer has set, and fails in its first
step naming the variable when it is empty.
Documentation follows the same split as the code. The README entry no longer
describes a workflow of this repository; it names where the block lives, what
each caller owes it, and points at the publish-helm-chart page of that
repository's wiki for the reasons behind the upload step, which are no longer
ours to keep current. Nothing of them was lost in the move - the upload step's
shell body went across unchanged apart from two error messages that name the
owner as an input.
Also corrected, because the move made them wrong rather than merely stale:
- the helm-dependencies.sh entry still counted the local publish workflow among
the five places it runs; it is still five, one of them now the shared block,
which is why that block names the script as a prerequisite of its callers;
- two comments in install-test.yml pointed at publish-helm-chart.yml as a
sibling file, one of them for a trap it documented "at length" - that long
form is now on the pipelines wiki, and the comments say so;
- the hull count read 24, and reusable workflows with secrets: inherit are now
confirmed to work across repositories too;
- the chart-creation step in CLAUDE.md said the reusable workflow is ours.
install-test.yml stays here, with the reason written down (decision 4 on the
issue): it is reusable in form but welded to the ci/ fixtures, the exclusion
parser and this repository's chart matrix. A block only one repository can use
is not shared, only moved. The note is there so the question is not asked again.
Fixes #66
|
||
|
|
61e28a1459
|
docs: the instructions block is a snapshot from session start
Validate Helm Charts / Detect changed charts (pull_request) Successful in 2s
Validate Helm Charts / Resource quantities (pull_request) Successful in 8s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Skipped
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Skipped
Validate Helm Charts / Install-test (pull_request) Skipped
The first wording said the project-instructions block "is read from the spawning session's working directory", which suggests that pulling in that directory fixes it. It does not. The block is a snapshot taken when the spawning session started, and nothing re-reads it afterwards. Measured: a session whose working directory had been fast-forwarded to the tip immediately before, and verified as up to date, still handed the next agent a block from before Gitea #47 — while that agent's own worktree was identical to origin/main (0 0). A current checkout and stale instructions at the same moment. Hence the two consequences that actually hold: the directory must be current before the session starts, and an agent checks a rule against the file in its own worktree and the diff against origin/main, because the first cannot be enforced from inside an agent and the second can. |
||
|
|
56bed94064
|
docs: cut every worktree from a freshly fetched origin/main
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Skipped
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Skipped
Validate Helm Charts / Install-test (pull_request) Skipped
Validate Helm Charts / Resource quantities (pull_request) Successful in 8s
A worktree branched from the local main ref can be arbitrarily old, and the staleness is invisible where it costs most: the project-instructions block a subagent receives at the head of its context is read from the spawning session's working directory, not from origin/main. An outdated worktree therefore hands every agent rules that were already corrected. Measured on 2026-09-27: an agent reported a contradiction in CLAUDE.md that two PRs had already fixed, and quoted the stale copy as binding. Nothing broke only because the same agent compared its diff against origin/main rather than against the local pointer. The same day, in the shared checkout, main..HEAD showed 105 differing files while the difference against origin/main was one line. Also name the wiki's default branch: it is main, not master. Since the rule to fetch and rebase before every wiki push, the name is required — an agent tried to rebase onto origin/master and the rebase failed. Fixes #57 |
||
|
|
5c513291a8
|
docs: a PR that does not finish its issue writes Part of #n, not Fixes #n
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Skipped
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Skipped
Validate Helm Charts / Install-test (pull_request) Skipped
Validate Helm Charts / Resource quantities (pull_request) Successful in 8s
|
||
|
|
733fea5d9e
|
docs: point the two workflow cross-references at the right steps
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Skipped
Validate Helm Charts / Resource quantities (pull_request) Successful in 7s
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Skipped
Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request) Skipped
Moving the wiki step from 7 to 5 shifted the two items behind it, and the references that name them by number stayed where they were. The wiki section still called itself step 7, and the auto-merge bullet cited rule 6 for "the user merges", which is rule 7 — rule 6 is the agent's lifetime. A number pointing at the wrong step is the kind of reference that passes for a reason on the next reading, so both are corrected here rather than in an issue of their own. |
||
|
|
1d3bacfb12
|
docs: name the remote that exists, and a push rule for the wiki
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Skipped
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Skipped
Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request) Skipped
Validate Helm Charts / Resource quantities (pull_request) Successful in 8s
The remote bullet described two remotes, github and JLab-Git, and denied that origin exists. The checkout has exactly one, origin, pointing at the Gitea instance, and so does every worktree cut from it; two agents lost time on the difference. The point the bullet made survives — Gitea is the leading copy, the mirror is fed from there, and where a second remote does exist the advice to name it still holds. The wiki has no PR flow, so parallel edits meet only at push time. During Gitea #16 and #18 two agents edited the same two pages minutes apart and the rebase happened to be clean; a force push would have deleted the other edit with nothing to report it. Fetch and rebase before every push, never force-push. The chart pages no longer quote versions, so the sentence that explained them as snapshots now says they carry no numbers at all. That change is already in the wiki. |
||
|
|
02b3c3ce75
|
renovate: align the config with cluster-config
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Skipped
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Skipped
Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request) Skipped
Validate Helm Charts / Resource quantities (pull_request) Successful in 8s
The shared jlab-renovate-bot instance serves this repo and cluster-config,
and two repos behaving differently under one bot is a source of surprises
rather than of safety. This adopts cluster-config's configuration:
config:best-practices in extends, labels ["renovate"], no PR caps
(prHourlyLimit 0, prConcurrentLimit 100), separateMinorPatch, an explicit
dependencyDashboard, and global automerge with platformAutomerge.
Two of those choices were previously wrong here and are now right, because
the branch protection on main has caught up: a review requirement and
validate-charts as a required check are both active. platformAutomerge
therefore arms the PR on the forge and Gitea merges it when the check
passes AND the review exists - the review is the gate, which is what makes
automerging majors something other than unattended merging.
Comments that asserted the opposite are rewritten rather than left to rot:
the platformAutomerge: false comment ("not (yet) a required check on either
side"), the "Majors stay manual" rule, the README PR policy bullets and the
required-check paragraph, and the CLAUDE.md forge notes.
config:best-practices pulls in docker:pinDigests, which is switched off
again for custom.regex and helm-values: an appVersion: is a tag by
construction and has nowhere to put a digest. Kept deliberately:
minimumReleaseAge of three days on the routine updates (every merge here
publishes a chart), the ignorePaths and ci/** opt-outs, every per-chart
manager, the nginx and qbittorrent constraints, and postUpgradeTasks.
Fixes #34
|
||
|
|
5b13569948
|
docs: the Coder workspace is the normal case, so tea runs without op
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Skipped
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Skipped
Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request) Skipped
Validate Helm Charts / Resource quantities (pull_request) Successful in 7s
Work on this repo happens in a Coder workspace that has no 1Password CLI. The rule that tea may only be called as 'op plugin run -- tea' therefore described an environment that no longer exists here, and following it meant either waiting for a tool that is not coming or ending with a pushed branch and no PR. tea is now called directly. It carries the workspace's stored login, so what it opens is authored by the bot account - accepted, and named in the report when it happens. The older instruction is kept for machines that do have op. Signing needed the same correction: it runs through the workspace's own SSH signer, not op-ssh-sign, and the check is 'git log --format=%G?' printing G. |
||
|
|
feb70cf1e4
|
docs: order the wiki step before the merge, not after it
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Skipped
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Skipped
Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request) Skipped
Validate Helm Charts / Resource quantities (pull_request) Successful in 8s
|
||
|
|
03bb454eb7
|
docs: make the wiki the knowledge base and its upkeep a required step
The wiki now carries one page per chart (prerequisites, installation, the required 1Password fields, the complete values reference and the chart's traps) plus the workflow, the pipelines, versioning, Renovate, the style guide and the incidents behind each rule. README points at it and keeps its own role: the overview for someone reading the code, while anything an operator needs lives in the wiki. CLAUDE.md gains the wiki edit as step 7 of the mandatory workflow, with a table of what obliges an edit - and the one thing that does not, a plain app-version bump, since Renovate automerges those and never touches the wiki. Also corrects the patchmon row: the 2.x server serves frontend and /api/* from one process, so the chart renders one Deployment plus an optional guacd sidecar, not two Deployments. |
||
|
|
60670182ff
|
docs: describe what is, now that the move is done
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Skipped
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Skipped
Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request) Skipped
Validate Helm Charts / Resource quantities (pull_request) Successful in 7s
The documentation still described a migration in progress, and the worst line claimed the install-test "cannot run on Gitea at all" - true two days ago, the opposite of true now, and exactly the sort of sentence someone builds a wrong assumption on. README and CLAUDE.md now state what holds: everything runs on Gitea, github.com is a mirror with Actions and Renovate off, branch protection with a signature requirement is active, and the tool is tea. The move itself is not retold - that is Gitea #14. "Where this repo lives" keeps its place but turns around: it is no longer a list of what blocks us, it is what a Gitea runner does differently from a GitHub one, with the measurements. Newly written down because it holds permanently and stood nowhere: a matrix runs concurrently in ONE job under ONE status context, so a required check per chart is impossible and the context name carries the unexpanded matrix expression; expected runtimes, so a later deviation is visible as one, with the note that the difference to GitHub is structural - one runner for both legs against a VM per leg - and not performance. The six assumptions a GitHub runner had been meeting quietly are split rather than listed. Five are environment (MTU, fs.inotify, missing kubectl and envsubst, kubeconfig on 0.0.0.0): they fail loudly, say nothing about our code, and are fixed on the node side. The sixth gets its own block because it is the one that does say something about our code - the hard-coded k3d cluster name was wrong on GitHub too and only never showed there, because a VM per matrix leg meant two legs never saw each other. A generous environment is no friend when you want to know whether something is correct. Two rules generalised out of this round, as rules rather than as a story: a check that a broken system passes is worse than no check (with the two vacuous forms that caused it), and green does not mean checked, pointing at Gitea #24 as an open case of the same kind - explicitly not claimed as solved. Also fixes a hazard the move created: most #n in these files name closed GitHub issues that were never migrated, while Gitea resolves a bare #n against its own counter - #22 there is an unrelated throwaway pull request, not the install-test issue the text means. Stated once at the top instead of rewriting sixteen references into numbers that do not exist. |
||
|
|
b9917505cd
|
Drop the issue-migration script: a one-off job is not repo content
The migration from GitHub issues to Gitea runs exactly once, by hand over the API. Checking a script in for it would leave permanently maintained code behind for a task that stops existing after its single run - and worse, it would read to every later visitor as a tool meant to be reused. A one-off move is carried out once and is history afterwards; it is not a part of the repository. The one reference in CLAUDE.md is rewritten rather than deleted: that the issue numbers do not survive the move is still true and still worth knowing, so the bullet now says a `#n` in this repo means the GitHub issue, and that what to do about the old references is open in #225. |
||
|
|
c3889a66b6
|
Drop the Renovate workflow: the Gitea side already has a shared instance
A measurement overtook this part of the change. A shared `jlab-renovate-bot` instance already runs on the Gitea side and already has this repository enrolled - its first run there opened PRs for searxng and gotenberg plus the dependency dashboard. A second runner in this repo would produce duplicate PRs and two schedulers rewriting each other's branches, so .github/workflows/renovate.yml is removed again. Two things fall out of that run, and both were open risks: - The instance reads our renovate.json5. searxng and gotenberg are customManagers targets of ours - no built-in manager finds an appVersion in a Chart.yaml - so the config is demonstrably in effect. - allowedCommands is already granted there. The searxng PR's diff carries `version: 1.0.4+up2026.9.11.ffe96f8a6 -> 1.0.5+up2026.9.13.96479fff6` next to the appVersion, and nothing but ci/renovate-bump-chart.sh writes `version:`. Had that allow-list been missing, postUpgradeTasks would have been skipped SILENTLY and every chart PR on Gitea would have been red on the version check with nothing naming the cause. With the workflow gone there is no container image to pin either; verified by dry run that the ghcr.io/renovatebot/renovate dependency is no longer extracted, while TRIVY_VERSION still is - that one belongs to the Trivy replacement and stays. README and CLAUDE.md now say Renovate runs on Gitea as an existing shared instance and that no workflow belongs here, so the next reader who finds none knows why instead of building one. |
||
|
|
a6c27ebef3
|
Prepare the Gitea move: measured CI deviations, self-hosted Renovate, issue migration script
Groundwork for #225, built on a throwaway pull request against the Gitea instance rather than on assumptions. The move itself is not part of this change - no Actions are switched off, no mirror is enabled, no branch protection is touched. What the probe run measured, and what follows from it: - Which jobs need a Docker socket: only the install-test. Measured per job - detect-changed-charts, build-chart, the quantity render and the Trivy config scan all pass on `ubuntu-latest`, which on that instance has no socket. The install-test dies there at "Create k3d cluster" with "Cannot connect to the Docker daemon", an error naming k3d and never the runner label. Its `runs-on` is now an expression: the label `ubuntu-docker` on Gitea, `ubuntu-latest` on GitHub, because the former does not exist there and a chart PR would otherwise queue forever during the handover. Gitea evaluates it - the probe job was taken by the docker runner. - The install-test still cannot pass on Gitea. That daemon is rootless with "Cgroup Driver: none"; the k3s node restarts immediately with an empty log because k3s needs cgroup delegation. The label is necessary and not sufficient, and the fix is the maintainer's. - aquasecurity/setup-trivy cannot be used on Gitea: it checks out aquasecurity/trivy from github.com with `github.token`, which there is a Gitea token, and ends on "could not read Username for https://github.com". Replaced by the release tarball - one request, same behaviour on both forges, green on the probe run. Also in this change: - .github/workflows/renovate.yml, Renovate in self-operation: a daily scheduled run in the official container, inert on github.com via a `github.server_url` guard. Every rule proved by dry run instead of assumed - the pinned Renovate image is tracked by the built-in github-actions manager (44.80.0 -> 44.87.1, minor), TRIVY_VERSION is still tracked after setup-trivy is gone (v0.72.0 -> v0.74.0, minor), and `ubuntu-docker` is not extracted by the github-runners manager at all, so nothing will quietly "correct" it. - ci/migrate-issues-to-gitea.sh, runnable, with --dry-run, and NOT run. Its header names what it cannot do: comment authorship, the #<n> references between issues, and attachments. - CLAUDE.md and README.md on the new flow: which forge and which tool, auto-merge on green checks in place of the merge queue Gitea does not have, and the measured differences of the two CIs. Refs #225 - deliberately no "Fixes": the move is not finished with this. |
||
|
|
485f4fa52b
|
satisfactory: game.enableSeasonalEvents says what it does (7.0.0+upv1.9.10)
game.seasonalEvents was handed to the container unchanged, into the image's DISABLESEASONALEVENTS. Its default true therefore wrote DISABLESEASONALEVENTS=true: the seasonal events were OFF in every install this chart has ever produced, while values.schema.json described the very same key as "true means events are enabled" (#215). Of the two ways out, this is the loud one. Inverting the template under the old key would have flipped the behaviour of every existing release in silence - the same class of mistake that created the issue. Renaming the key cannot be missed: game.seasonalEvents no longer exists, additionalProperties is false on the game block, and a values file that still carries it fails schema validation on upgrade instead of quietly meaning the opposite. The new key is positive (enableSeasonalEvents) to avoid a double negative, so the chart negates it on the way into the env var. Go's "not" cannot do that: it reads every non-empty string as true, and the schema deliberately accepts the string form of booleans, so "false" would have flipped the wrong way. The helper compares explicitly and fails on anything that is neither true nor false rather than filing it under "off". The default is enableSeasonalEvents: false, which renders the DISABLESEASONALEVENTS this chart has always rendered. At chart defaults the 6.1.0 and 7.0.0 manifests are identical, so upgrading alone moves no server; switching the events on is now a deliberate act with a key that means it. Separate finding from the same issue, fixed here because it lives in the block being touched: "--set game=null" - or a leftover "game:" whose sub-keys were deleted - aborted the render with a nil pointer at .Values.game.maxPlayers. The game block never got the null-safe treatment of #99 that probes, security contexts and resources have. It now falls back to the documented defaults, and the schema accepts the null accordingly. The same gap remains in the dev and server blocks; it is reported, not fixed here. Major bump: a values key is removed, and the schema rejects it. |
||
|
|
01f804a43a
|
ci: publish charts to a classic index-based Helm registry
The publish step no longer speaks OCI. "helm registry login", "helm push" and "helm registry logout" are replaced by a single HTTP POST of the packaged .tgz to <charts-url>/api/charts, with Basic auth per request. Credentials and the target URL come from a new 1Password item op://GitHub-Actions/jlab-registry-access (username, password, charts-url). The old item is left untouched, so a revert of this commit restores the previous pipeline without any further work. No registry address enters the repository. The three properties of the old step survive: - values are read via steps.<id>.outputs.* into the step's own env block and used as quoted shell variables, each guarded with ":?" - credentials stay out of argv: "curl --config -" instead of "--user" - a failed upload fails the job: "--fail-with-body", because curl exits 0 on HTTP 401/403/409 without it and the publish would go green with nothing uploaded The "<chart>-*.tgz" glob is replaced by the file name built from the chart name and the version already read from Chart.yaml; --upload-file takes exactly one file. Two small additions fall out of measuring the new step: the registry host is masked, because a curl connection error prints only "<host>:<port>" and the mask on the full URL does not cover that, and whitespace in the credentials is rejected up front, because an unquoted curl config value ends at the first space. No chart is touched and no chart version is bumped. |
||
|
|
6b0d8c9232
|
ytdlp-web-player: new chart with an optional oauth2-proxy subchart (1.0.0+upv1.2.0)
Deploys matszwe02/ytdlp_web_player, a Flask app that fetches videos with
yt-dlp, transcodes them with ffmpeg and serves them as HLS. It ships no
authentication of any kind, so the chart carries a preconfigured oauth2-proxy
as an optional subchart, in the data path in front of the app.
This is the first chart in the repo with a dependency, so the rule "charts
have no dependencies" falls here. It is replaced in CLAUDE.md and README.md by
a bar: a dependency has to be optional and off by default, upstream's chart is
consumed as it comes (documented where it falls short of our conventions,
never patched), and the price is written down - above all that a subchart is
invisible to everything downstream watching Helm release versions, so a new
proxy release only reaches a cluster when this chart is republished.
CI learns the dependency in five places, not the three the plan expected:
helm lint, package, template and install all exit 1 while charts/ is unfilled,
so ci/helm-dependencies.sh runs before lint and package in validate-charts.yml,
before package in publish-helm-chart.yml, and inside run-install-test.sh,
run-trivy-config.sh and run-quantity-render-test.sh - the last of which renders
every chart on every PR and would have gone red first.
Notable chart decisions, each measured rather than assumed:
- The image has no USER and runs as root by default, but not by necessity: it
runs under an arbitrary uid with readOnlyRootFilesystem and the full
hardening baseline. Only the pinned uid is a deviation, and it is documented.
- Two emptyDirs, each justified separately. /data holds the video cache
(DATA_PATH). /tmp is NOT mounted - measured across metadata extraction, HLS
transcoding, sprite generation and audio extraction, the image never writes
there. What it does write is $HOME/.cache, so /cache is mounted with HOME and
XDG_CACHE_HOME pointed at it; without it yt-dlp re-downloads its JavaScript
challenge-solver from github.com on every extraction.
- The proxy is wired through config.configFile, the only value the subchart
passes through tpl - config.upstreams is emitted with toJson and would carry
a literal {{ .Release.Name }} into the manifest.
- Rendering aborts when this chart's Ingress and the proxy's claim the same
host: ingress-nginx rejects the second one and a Fleet bundle then sits in
ErrApplied until forceSyncGeneration is bumped.
Fixes #214
|
||
|
|
6b25d082a7
|
renovate: chart-aware config with an automatic version bump
Renovate only ever rewrites appVersion or an image tag, so every PR it opened against a chart directory failed the build-chart version check (#138, #139). ci/renovate-bump-chart.sh recomputes version: <semver>+up<appVersion> as a postUpgradeTasks hook: chart major when the app major moves, chart patch otherwise. Each chart renders its main image as <repo>:{{ .Chart.AppVersion }}, so repository and version live in different files and Renovate cannot join them. The mapping is one customManager per chart in renovate.json5, deliberately not a comment inside each Chart.yaml - that would touch all 20 chart directories and force 20 publishes for a comment change. Patch and digest updates automerge after a 3-day minimumReleaseAge once the pipeline is green; majors arrive one line at a time to match the stepwise app-major procedure. ci/** stays excluded: the install-test fixtures pin throwaway infrastructure to major lines on purpose. |
||
|
|
b218f64eac
|
template-chart + README: values.schema.json, gate and null-safe helpers
Closes out the schema programme (#68) by moving what the 18 charts taught into the reference implementation and the binding guidelines. template-chart: - values.schema.json as the reference schema: additionalProperties false on every chart-owned level, pod-spec passthrough left open, and the reasoning inline in the descriptions so a copy carries it along. - defaultedDict and subBlock plus the per-block fallbacks for both security contexts, the three probes and resources, wired into both the Deployment and the StatefulSet. - the reference itself violated guideline 1: its env block reads ingress.domain, so with the Ingress gated off it rendered an empty value. That read is now required(), and the comment says why - the template is what people copy, so it has to teach the guard, not the hole. README, new section "Values that must not lie" with the five rules: 1. a value read outside its obvious template needs its own guard (matrix-synapse published '{"m.server": ":443"}' to federation without it, rendering cleanly and reporting healthy) 2. probes, contexts and resources through the null-safe helper, never behind an "if" - a guard removes the probe instead of showing a null 3. no liveness on an external dependency where a dependency-free path exists, otherwise document why on the helper 4. a fallback reproduces THIS chart's defaults, not the repo standard - calibre's readOnlyRootFilesystem: false is what keeps the runtime uid at 1000 instead of 911 and existing volume data readable 5. a probe default carries its context - port name, Host header, exec command - or the probe vanishes (teamspeak) or gets 403 (patchmon) Plus the two mechanics that are easiest to lose: null reaches schema validation only for keys WITHOUT a chart default (which is why the helper and the ["object","null"] types each cover a case the other cannot), and schemas must be validated against real bundle values - that step caught a draft of mine that would have broken three live paperless releases. CLAUDE.md: the new-chart checklist now names the schema and the adaptation step, since the copied file is chart-specific and wrong until it is adapted. freesailarr is not covered: it is not on main yet (#59). |
||
|
|
1bca98b9ef
|
Add checksum/config annotation to template-chart and make it binding
A changed ConfigMap did not restart any pod in this repo: no chart carries a checksum/config annotation, so the pod template stays identical and the running container keeps its old config. With subPath mounts (the usual form here) the kubelet never refreshes the file either, so the change is silently ineffective - see #82, where this caused a real outage while every bundle was green and the pod Ready. template-chart is the reference implementation, so it gets the annotation on both pod templates (Deployment and StatefulSet), and the README style guide gets a new binding section 9 covering the rule, the reason, and the limit for 1Password-backed Secrets. Major bump: runtime behavior changes - a config change now costs a restart. |
||
|
|
408232a36f
|
Document the consolidated chart style guidelines
README.md gains a comprehensive 'Chart style guidelines' section - the binding end state of the conventions overhaul (#32, batches 0-4) for all current and future charts: naming (incl. the PVC-name exception and the StatefulSet migration pattern), versioning scheme and semver rules, image pinning, the ingress standard (domain + clusterIssuer strictly required without defaults, className default nginx, annotations merge, TLS secret name, homeassistant exception), security hardening with mandatory documentation of deviations, probes, the auto-limits scheme, the secrets.<name>SecretRef convention, tpl-rendered env blocks, scaling, and the icon/lint/CI-fixture/no-dependencies rules. The Versioning section now points there and documents that app-version updates go through PRs. CLAUDE.md's conventions section is aligned as a compact summary referencing the README guide. |
||
|
|
f091c33205
|
Conventions batch 2: flexible ingress standard, configurable probes
Establish the flexible ingress standard from #32 in template-chart and document it in README/CLAUDE.md: ingress.domain and ingress.clusterIssuer stay required, new ingress.className value (default nginx) replaces the hardcoded ingressClassName, new optional ingress.annotations are merged with the cert-manager cluster-issuer annotation, TLS secret name unchanged (tls-<release>-<chart>-ingress). Apply the standard to cyberchef and patchmon (the two charts without an own batch-3 major PR) and make the patchmon probes values-configurable (server + guacd, rendered through tpl so the default Host header keeps following ingress.domain). Defaults render semantically identical to the previous hardcoded output. cyberchef 5.0.1 -> 5.1.0, patchmon 5.0.1 -> 5.1.0 (minor, additive). |
||
|
|
13d9abb6d2
|
Bring CLAUDE.md up to date with current validation and versioning practice
- validation happens via strict lint, PR pipeline and install-test stage - new charts need ci/ fixtures or an exclusion entry - codify semver conventions (patch/minor/major) - app updates via PRs with stepped major upgrades |
||
|
|
b5eba4cbf1
|
Harden pod and container security contexts across all workloads
Add securityContext hardening (runAsNonRoot, readOnlyRootFilesystem, capabilities drop ALL, allowPrivilegeEscalation false) to the 10 workloads that lacked it: apache-tika, cyberchef, gotenberg, homeassistant, matrix-delegation-nginx, paperless-server, paperless-consume cronjob (plus automountServiceAccountToken false), patchmon-frontend, urlaubsverwaltung and zipline. All contexts are configurable via securityContext.pod / securityContext.container in values.yaml; writable paths are provided as emptyDirs. Image-forced deviations (Home Assistant root start for s6-overlay + NET_RAW for discovery, paperless root init container for /run ownership) are documented in the charts' values.yaml. Major version bump for every changed published chart. |
||
|
|
f349428d0e
|
Keep workflow docs in CLAUDE.md and general repo info in README current
- CLAUDE.md: document plan-on-issue step, doc-currency rules, k3d verification convention - README: document validate-charts PR pipeline, branch-filtered publish trigger, strict lint, local k3d runtime verification |
||
|
|
0d3b722dd1
|
Update issue workflow: manager plans, dev agent implements and tends its PR | ||
|
|
0275c01a18
|
Document mandatory issue workflow in CLAUDE.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
116a0499c5
|
Add CLAUDE.md with repo guidance for Claude Code
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |