From 365e43cdea02c4c008c74a563c2181a0dbdb9fe4 Mon Sep 17 00:00:00 2001 From: Keith Jackson Date: Fri, 30 Oct 2026 01:23:36 -0500 Subject: [PATCH 01/10] fix(renovate): run Renovate as the job container; the action needs a Docker daemon jobs do not get renovatebot/github-action is a wrapper around `docker run`. On this runner a job has no Docker daemon by decision (decisions/2026-10-29-image-pipeline.md), so every run, including dispatch run 343, died before Renovate started: failed to connect to the docker API at unix:///var/run/docker.sock ... no such file or directory The pinned image ghcr.io/renovatebot/renovate:44.167.0 is now the job container, and a run step execs renovate-entrypoint.sh. actions/checkout is dropped, since Renovate clones the repo itself. An optional RENOVATE_GITHUB_COM_TOKEN secret feeds GITHUB_COM_TOKEN. Local dry run (--platform=local, uid 12021, the step body verbatim): 179 deps extracted, 42 updates, and all 4 own-image digest lookups against the Gitea registry succeed. Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/renovate.yaml | 69 +++++++++---- decisions/2026-10-30-renovate-first-run.md | 113 +++++++++++++++++++++ renovate.json5 | 7 +- 3 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 decisions/2026-10-30-renovate-first-run.md diff --git a/.gitea/workflows/renovate.yaml b/.gitea/workflows/renovate.yaml index f635ebf..3e984e3 100644 --- a/.gitea/workflows/renovate.yaml +++ b/.gitea/workflows/renovate.yaml @@ -116,15 +116,37 @@ concurrency: jobs: renovate: runs-on: ubuntu-latest + # RENOVATE RUNS AS THE JOB CONTAINER, NOT THROUGH renovatebot/github-action. FIXED 2026-10-30. + # That action does exactly one thing: `docker run ghcr.io/renovatebot/renovate:`. On + # this runner a job has NO Docker daemon, by decision (decisions/2026-10-29-image-pipeline.md: + # act_runner sets container.docker_host to `-` when DOCKER_HOST is tcp://, and the DinD daemon + # is privileged, so a job that could reach it would be root on the node). So every run of the + # action died before Renovate started. Dispatch run 343 (2026-10-30), verbatim: + # [command]/usr/bin/docker run --env LOG_LEVEL ... --env RENOVATE_TOKEN=*** ... + # failed to connect to the docker API at unix:///var/run/docker.sock; ... no such file or directory + # ::error::Error: The process '/usr/bin/docker' failed with exit code 1 + # No auth, scope, config or network question was ever reached. Making the Renovate image the + # job container needs no daemon: act starts it through the runner's own DinD exactly as it + # starts the default ubuntu image, then `exec`s each step inside it. + # + # PINNED here (was the action's `renovate-version: 44.167.0`). renovate.json5 is validated + # against this exact tag — keep them in step. Renovate's github-actions manager reads + # `.gitea/workflows` and extracts `container.image`, so this pin is still self-maintained. + # + # The image runs as uid 12021 (`ubuntu`), and that is fine under act: the step command files + # ($GITHUB_STEP_SUMMARY etc.) are created mode 0666 (gitea/act v0.261.2 pkg/runner/step.go), + # HOME comes from the image env, and no step writes the workspace. There is deliberately NO + # actions/checkout: Renovate clones the repo itself from RENOVATE_ENDPOINT, and as uid 12021 + # checkout could not `git init` the root-owned workspace volume anyway. + container: + image: ghcr.io/renovatebot/renovate:44.167.0 # Renovate scans every manager across the whole repo; 40m is generous for this repo's size and # well under a runner's patience. Without a timeout a wedged run holds the `renovate` # concurrency group and silently blocks every subsequent scheduled run. timeout-minutes: 40 steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Preflight - the token must exist, and its absence must be LOUD + shell: bash env: RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} run: | @@ -147,25 +169,17 @@ jobs: exit 1 fi echo "RENOVATE_TOKEN is present (value not printed)." + # Proves the step is running INSIDE the pinned Renovate image, not the default job image. + echo "renovate $(renovate --version)" - name: Run Renovate - uses: renovatebot/github-action@v41.0.9 - with: - # Pinned, and Renovate maintains this pin itself: renovate.json's enabledManagers - # includes `github-actions`, and `helpers:pinGitHubActionDigests` is in its `extends`. - # This is the one dependency in the repo whose updater is its own updater. - # MUST match the file's real name. #79 renamed it to renovate.json5 and left this line - # saying `renovate.json`; every scheduled run from 2026-10-18 then died in ~1s with - # ENOENT before Renovate started, and nothing in the gate noticed. ci/lint.sh now - # asserts that every `configurationFile:` here names a file that exists. - configurationFile: renovate.json5 - token: ${{ secrets.RENOVATE_TOKEN }} - # PINNED 2026-10-29. Unset, this action version runs Renovate `39` (action.yml - # `renovate-version` default at v41.0.9), i.e. 39.264.0, and renovate.json5 uses - # `managerFilePatterns` (>= 40.2.0): under 39.x validation fails with five errors and - # the run stops at CONFIG_VALIDATION. #79 validated against `latest`, not this. - renovate-version: 44.167.0 + shell: bash + # The repo config is discovered by Renovate from the cloned repo (renovate.json5, + # RENOVATE_REQUIRE_CONFIG=required). The action used to ALSO load it as the global config + # via RENOVATE_CONFIG_FILE; that double-loading is gone. ci/lint.sh's `configurationFile:` + # check therefore has nothing to resolve in this file, and passes. env: + RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} # Self-hosted Gitea. `platform` + `endpoint` were the two values renovate.json's own # header said were "set at runtime by the operator's renovate runner" — this is that # runner, so they are set here rather than in the repo config, which keeps renovate.json @@ -189,6 +203,23 @@ jobs: # token; it therefore needs scope `read:package` in addition to the ones listed in the # preflight above. Gitea ignores the username for token auth (services/auth/basic.go). RENOVATE_HOST_RULES: '[{"hostType":"docker","matchHost":"gitea.home.keithjackson.dev","username":"renovate","password":"${{ secrets.RENOVATE_TOKEN }}"}]' + # OPTIONAL, ADDED 2026-10-30. A github.com token (a fine-grained PAT with NO permissions, + # public-repo read only) for lookups of GitHub-hosted dependencies. Without it Renovate + # SKIPS them with `github-token-required` — measured in a local dry run: 17 deps, i.e. + # every `uses: actions/*` here plus kubevirt/kubevirt, kubevirt/containerized-data-importer + # and intel/intel-device-plugins-for-kubernetes. Unset is safe: the step below drops the + # empty variable, and those deps simply stay unmaintained, with a WARN in the log. + GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }} + run: | + set -euo pipefail + # An expression that evaluates to '' still EXPORTS the variable, as an empty string. + # Drop those so an unset value means "Renovate's default", never "set to empty". + for v in RENOVATE_DRY_RUN GITHUB_COM_TOKEN; do + [ -n "${!v:-}" ] || unset "$v" + done + # Through the image's entrypoint, as `docker run` would have gone: it sources the + # containerbase env and initialises the bundled tools before exec'ing Renovate. + exec renovate-entrypoint.sh renovate - name: Summary if: always() diff --git a/decisions/2026-10-30-renovate-first-run.md b/decisions/2026-10-30-renovate-first-run.md new file mode 100644 index 0000000..347e37d --- /dev/null +++ b/decisions/2026-10-30-renovate-first-run.md @@ -0,0 +1,113 @@ +--- +date: 2026-10-30 +branch: fix/renovate-first-run +title: Renovate never started because its action needs a Docker daemon jobs here do not get; it now runs as the job container +--- + +## What changed + +**`.gitea/workflows/renovate.yaml`**: the job no longer uses `renovatebot/github-action`. The +pinned Renovate image (`ghcr.io/renovatebot/renovate:44.167.0`, the same version the action was +pinned to) is now the job's `container:`. A `run:` step calls `renovate-entrypoint.sh renovate` +inside it. `actions/checkout` is gone, because Renovate clones the repo itself. +`GITHUB_COM_TOKEN` is wired from a new **optional** secret, `RENOVATE_GITHUB_COM_TOKEN`. The step +unsets it, and `RENOVATE_DRY_RUN`, when they evaluate to the empty string. The preflight also +prints `renovate --version`, which proves the step ran inside the pinned image. + +**`renovate.json5`**: one comment now names `container.image` as the version pin, in place of +`renovate-version`. The config itself is unchanged. + +### Why: the root cause + +Every run of the action died before Renovate started. That includes the operator's dispatch, +run 343 (task 555, 2026-10-30 06:14 UTC), and the 11 scheduled runs before it (312–322). From +run 343's log: + + RENOVATE_TOKEN is present (value not printed). + [command]/usr/bin/docker run --env LOG_LEVEL ... --env RENOVATE_TOKEN=*** --env RENOVATE_CONFIG_FILE=/github-action/renovate.json5 ... + failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory + Error: The process '/usr/bin/docker' failed with exit code 1 + +The action is only a wrapper around `docker run`. On this runner a job deliberately gets no +Docker daemon (`decisions/2026-10-29-image-pipeline.md`): act_runner maps `docker_host` to `-` when +`DOCKER_HOST` is `tcp://`, and the DinD daemon is privileged. So this was a +platform-compatibility failure. The run never reached authentication, token scope, config or the +network. **The re-minted token is not implicated, and nothing in this log can say whether its +scopes are right.** + +A job `container:` needs no daemon. act creates it through the runner's DinD the same way it +creates the default `ubuntu-latest` image, then `exec`s each step inside it. I checked this +against gitea/act v0.261.2: + +- The job container's entrypoint is `/bin/sleep` (`run_context.go`). The Renovate image has + `/usr/bin/sleep`. +- The step command files are written `0o666` (`step.go`), so the image's uid 12021 can write + `$GITHUB_STEP_SUMMARY`. +- `HOME` comes from the image env. + +## What I verified + +- **The root cause.** I read all of run 343's job log (162 lines, job 0; job 1 is 404). It has one + error, quoted above, and no Renovate output at all. Runs 312–322 are all `failure` from the + `schedule` event. +- **The config validates against the pinned tag.** `podman run ... ghcr.io/renovatebot/renovate:44.167.0 + renovate-config-validator /tmp/renovate.json5` printed `Config validated successfully`. It also + printed the known migration diff for the top-level `docker` block. +- **A dry run with the fix extracts and looks up dependencies.** This was a local reproduction. I + made a clean snapshot of this branch's tracked files. I ran it in a podman container of the pinned + image with entrypoint `/bin/sleep`, as the image's own uid 12021 with `HOME=/home/ubuntu`. + Inside it I `exec`ed the workflow step's `run:` body, extracted verbatim from the YAML, with: + - `RENOVATE_PLATFORM=local`, `RENOVATE_DRY_RUN=full` and `GITHUB_COM_TOKEN=` (empty, to + exercise the unset); + - a docker host rule for `gitea.home.keithjackson.dev` using the `gitea-admin` credential from + `~/.git-credentials`, passed through env and never printed. + + The run exited 0 and logged `Dependency extraction complete`: + - flux 15 deps / 13 files + - github-actions 24 / 4 + - kubernetes 131 / 67 + - kustomize 2 / 2 + - regex (both custom managers) 7 / 6 + - **total 179 deps / 92 files** + + It found **42 updates**: 29 kubernetes, 6 flux, 2 kustomize, 1 github-actions and 4 regex + `digest`. 7 of them are `major`. +- **The `# renovate-digest:` lookups against the Gitea registry succeed.** All three + `/v2/token?scope=repository:home-network/:pull` requests succeeded, and so did + `HEAD /v2/home-network//manifests/main`. They proposed `digest` updates for + `conformance/versions.env`, `infra/agent/versions.env`, `platform/pm-agent/configmap.yaml` and + `platform/pm-agent/deployment.yaml`, all of which carry the zero placeholder digest today. The + `kubernetes` manager's copies of those images report `disabled`, which is the intended effect of + the packageRule. +- **The new pin is still maintained by Renovate.** The github-actions manager extracts + `ghcr.io/renovatebot/renovate:44.167.0` from `renovate.yaml` as depType `container`, and proposes + 44.198.0. +- **The `regex` vs `custom.regex` question from #73/#77 is closed.** Both custom managers produced + dependencies. +- **Missing GitHub token.** The local run logged `GitHub token is required for some dependencies`. + That accounts for 17 deps skipped with `github-token-required`, which is why + `RENOVATE_GITHUB_COM_TOKEN` is offered as an optional secret. + +## What I did NOT verify + +- **The workflow on the real runner. UNVERIFIED.** My reproduction ran with podman, not + act_runner + DinD, and used `--platform=local`. The act behaviour (sleep entrypoint, `exec` as the + image user, 0666 command files) comes from reading the gitea/act v0.261.2 source, not from a run. + The first real evidence will be a `workflow_dispatch` after merge. +- **The Gitea platform path and `RENOVATE_TOKEN`'s scopes. UNVERIFIED.** I have no access token, only + the `gitea-admin` password, which Renovate's Gitea platform cannot use. So these were not + exercised: platform init (`GET /user`, which needs `read:user`), repo clone, and the registry + lookups using the **token** with `read:package` rather than a password. +- **Behaviour inside the schedule window.** A dispatch outside 03:00–06:00 America/New_York + (07:00–10:00 UTC during EDT) will extract and look up, but will log "not within schedule" + instead of `DRY-RUN: Would create ...` lines. I did not change this. +- **Renovate as uid 12021 doing artifact updates** (for example helm lockfiles) on a real, + non-dry run. `--dry-run=full` does not run the tools. +- **The self-bump.** Renovate will propose `ghcr.io/renovatebot/renovate` 44.167.0 → 44.198.0 as a + minor, which the default rule auto-merges. `.gitea/**` is a sensitive path, so it will in fact + wait for an administrator override. Nothing re-runs `renovate-config-validator` on that bump, + and no gate check runs the validator. +- **No alert for "Renovate red every day".** That gap is still open. This failure ran for 12 days + without anyone noticing. +- **Sensitive path.** `.gitea/**` is in `infra/gitea/approval-policy.json`, so CI's + `approval-policy` stage will block until the operator posts `/approve-override` as a PR review. diff --git a/renovate.json5 b/renovate.json5 index 48e5ee4..898c75e 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -29,10 +29,11 @@ // `managerFilePatterns` key ("Custom Manager contains disallowed fields", "Invalid configuration // option: flux.managerFilePatterns", ...), because 39.x only knows `fileMatch`; the rename landed // in 40.2.0. So even after the workflow found this file, every run would have stopped at -// CONFIG_VALIDATION. renovate.yaml now pins `renovate-version` to the exact release this file is -// validated against. VALIDATE WITH THAT SAME TAG: +// CONFIG_VALIDATION. renovate.yaml now pins the exact release this file is validated against — +// since 2026-10-30 as the job's `container.image` tag (the action is gone: it needed a Docker +// daemon jobs here do not get). VALIDATE WITH THAT SAME TAG: // podman run --rm -v "$PWD/renovate.json5:/tmp/renovate.json5:ro,Z" \ -// ghcr.io/renovatebot/renovate: \ +// ghcr.io/renovatebot/renovate: \ // renovate-config-validator /tmp/renovate.json5 // (Not `--strict`: that also fails on a pending migration of the pre-existing top-level // `"docker": {"enabled": true}` block at the bottom, which is a warning, not an error.) From 0c6ee4921d853052c8764fae2fa3e86a598b2f3c Mon Sep 17 00:00:00 2001 From: Keith Jackson Date: Fri, 30 Oct 2026 01:55:39 -0500 Subject: [PATCH 02/10] fix(renovate): document and probe the five token scopes Renovate needs; lint daemon-bound actions Run 350 cleared platform init (/user 200, /version 200, /repos 200), then stopped at GET /api/v1/orgs/Home-Network with Gitea's own 403: token does not have at least one of required scope(s), required=[read:organization], token scope=read:package,write:repository,read:user The workflow's header listed only those three scopes, and the operator minted exactly that. Renovate's initRepo -> isOrg needs read:organization, and Gitea files /issues and /labels under the issue scope, which the dashboard and PR labels need: write:issue. - renovate.yaml: corrects the scope list (read:user, read:organization, write:repository, write:issue, read:package), with the call each serves. A new preflight step probes them with GETs, the token passed to curl on stdin, and names a missing scope instead of Renovate's bare "Authentication failure". - ci/lint.sh: rejects workflow `uses:` lines that need a Docker daemon (docker://, renovatebot/github-action, docker/*-action), the class fixed in #84. Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/renovate.yaml | 95 ++++++++++++++++++++-- ci/lint.sh | 16 ++++ decisions/2026-10-30-renovate-auth.md | 108 ++++++++++++++++++++++++++ 3 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 decisions/2026-10-30-renovate-auth.md diff --git a/.gitea/workflows/renovate.yaml b/.gitea/workflows/renovate.yaml index 3e984e3..50d57d6 100644 --- a/.gitea/workflows/renovate.yaml +++ b/.gitea/workflows/renovate.yaml @@ -60,9 +60,21 @@ # entry's "What I did NOT verify" — that gap is stated, not papered over. # # OPERATOR PREREQUISITE — ONE SECRET, NOT COMMITTED HERE: -# `RENOVATE_TOKEN` — a Gitea access token for the bot account, scopes `write:repository`, -# `read:user` and (since 2026-10-29) `read:package`. Set it as an Actions secret on this -# repository. Nothing in this repo mints, +# `RENOVATE_TOKEN` — a Gitea access token for the bot account (`renovate-bot`) with FIVE scopes. +# Each is required by a call Renovate 44.x makes against Gitea 1.24.3, and Gitea refuses a +# token missing one with `403 token does not have at least one of required scope(s)`: +# read:user GET /user (platform init: initPlatform) +# read:organization GET /orgs/{owner}, /orgs/{o}/labels (initRepo -> isOrg, then org labels) +# write:repository clone/push branches, /pulls, /statuses +# write:issue /issues, /labels, /issues/{n}/labels (dependency dashboard, PR labels; +# Gitea files all of these under the ISSUE scope, not repository) +# read:package the Gitea container registry (RENOVATE_HOST_RULES below) +# CORRECTED 2026-10-30. This list used to name only write:repository + read:user (+ read:package), +# and the operator minted exactly that. Run 350 then failed at +# `GET /api/v1/orgs/Home-Network 403 ... required=[read:organization], token scope=read:package, +# write:repository,read:user`; write:issue would have been the next wall. The preflight below now +# PROBES these scopes, so a short token fails in seconds with the missing scope named. +# Set it as an Actions secret on this repository. Nothing in this repo mints, # stores or references its value; it is consumed by name only. # Until it exists, this workflow FAILS LOUDLY on every scheduled run. That is intended: a # visible red is the correct state for "automation configured but not credentialed". @@ -161,10 +173,10 @@ jobs: That is the failure mode that let renovate.json sit inert in this repo for months while seven files claimed "Renovate maintains this pin". - TO FIX: create a Gitea access token for the bot account with scopes - `write:repository`, `read:user` and `read:package` (the last one lets Renovate read - this repo's own images in the Gitea registry), then add it to this repository as the - Actions secret RENOVATE_TOKEN. Do not commit it anywhere. + TO FIX: create a Gitea access token for the bot account (renovate-bot) with scopes + read:user, read:organization, write:repository, write:issue and read:package (the + header of .gitea/workflows/renovate.yaml says which call needs which), then add it to + this repository as the Actions secret RENOVATE_TOKEN. Do not commit it anywhere. MSG exit 1 fi @@ -172,6 +184,75 @@ jobs: # Proves the step is running INSIDE the pinned Renovate image, not the default job image. echo "renovate $(renovate --version)" + - name: Preflight - the token has every scope Renovate needs + shell: bash + env: + RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} + # ADDED 2026-10-30. Renovate reports EVERY platform-init failure as the bare + # "Authentication failure" (lib/modules/platform/gitea/index.ts initPlatform swallows the + # HTTP error), and a later scope gap surfaces as "Repository has unknown error". Two + # dispatches were spent learning one missing scope at a time. These probes are GETs only, + # and they name the missing scope. The token never reaches argv: curl reads it from stdin + # (`--config -`), so it is not in `ps` output or in the log. + run: | + set -uo pipefail + GITEA=https://gitea.home.keithjackson.dev + REPO=Home-Network/home-network-k3s-impl + fail=0 + body=$(mktemp) + probe() { # + local code + if [ "$3" = basic ]; then + code=$(printf 'user = "renovate:%s"\n' "$RENOVATE_TOKEN" \ + | curl -sS --config - -o "$body" -w '%{http_code}' "$GITEA$2") || code=000 + else + code=$(printf 'header = "Authorization: Bearer %s"\n' "$RENOVATE_TOKEN" \ + | curl -sS --config - -o "$body" -w '%{http_code}' "$GITEA$2") || code=000 + fi + if [ "$code" = 200 ]; then + echo "ok $1 (GET ${2%%\?*} -> 200)" + return + fi + fail=1 + echo "::error::scope $1: GET ${2%%\?*} -> $code" + case "$code" in + 401) echo " 401 = Gitea rejected the token VALUE (wrong, expired or revoked)." ;; + 403) if grep -q 'Access denied' "$body"; then + echo " 403 'Access denied' = a Cilium/Envoy policy denial, not Gitea." + else + echo " Gitea: $(jq -r '.message // empty' "$body" 2>/dev/null | head -c 300)" + fi ;; + *) echo " body: $(head -c 200 "$body")" ;; + esac + } + probe read:user "/api/v1/user" bearer + probe read:organization "/api/v1/orgs/${REPO%%/*}" bearer + probe read:repository "/api/v1/repos/$REPO" bearer + probe read:issue "/api/v1/repos/$REPO/labels?limit=1" bearer + probe read:package "/v2/token?service=container_registry&scope=repository:home-network/toolbox:pull" basic + # WRITE scopes cannot be proven by a GET. Gitea's scope-refusal message, however, lists + # the token's scopes (names only, never the value): ask for an admin route this bot + # cannot have, and read `token scope=` from the 403. Advisory if the format changes. + printf 'header = "Authorization: Bearer %s"\n' "$RENOVATE_TOKEN" \ + | curl -sS --config - -o "$body" "$GITEA/api/v1/admin/orgs?limit=1" || true + scopes=$(jq -r '.message // empty' "$body" 2>/dev/null | sed -n 's/.*token scope=//p') + if [ -n "$scopes" ]; then + echo "token scopes (from Gitea): $scopes" + for want in write:repository write:issue; do + case ",$scopes," in + *",$want,"*|*",all,"*) echo "ok $want" ;; + *) echo "::error::scope $want is missing (token scope=$scopes)"; fail=1 ;; + esac + done + else + echo "::warning::could not read the token's scope list; write:repository and write:issue are UNVERIFIED" + fi + rm -f "$body" + if [ "$fail" -ne 0 ]; then + echo "::error::RENOVATE_TOKEN failed the checks above (a 401 is the value, a 403 is a scope). Re-mint it for renovate-bot with read:user, read:organization, write:repository, write:issue, read:package and overwrite the secret." + exit 1 + fi + - name: Run Renovate shell: bash # The repo config is discovered by Renovate from the cloned repo (renovate.json5, diff --git a/ci/lint.sh b/ci/lint.sh index e5307fe..39aaf86 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -42,3 +42,19 @@ while IFS=: read -r wf _ value; do done < <(grep -nE '^[[:space:]]*configurationFile:' .gitea/workflows/*.y*ml 2>/dev/null || true) [ "$missing" -eq 0 ] || exit 1 ok "workflow configurationFile references resolve" + +# A workflow must not use an action that needs a Docker daemon: jobs on this runner get NONE, by +# decision (decisions/2026-10-29-image-pipeline.md; act_runner maps docker_host to `-`). Such an +# action is valid YAML and passes every other stage, then dies at runtime with +# "failed to connect to the docker API at unix:///var/run/docker.sock". renovatebot/github-action +# did exactly that on every Renovate run until 2026-10-30 (run 343). Run the tool's image as the +# job `container:` instead (see .gitea/workflows/renovate.yaml). Container (`docker://`) actions +# and Docker's own daemon-bound actions are rejected for the same reason. Comments are ignored. +daemon_uses='uses:[[:space:]]*["'\'']?(docker://|renovatebot/github-action|docker/(build-push|setup-buildx|setup-qemu|login)-action)' +bad=0 +while IFS= read -r hit; do + err "${hit%%:*}: needs a Docker daemon, which jobs here do not get: $(echo "${hit#*:*:}" | sed -E 's/^[[:space:]]+//')" + bad=1 +done < <(grep -HnE "^[^#]*${daemon_uses}" .gitea/workflows/*.y*ml 2>/dev/null || true) +[ "$bad" -eq 0 ] || exit 1 +ok "no workflow uses a Docker-daemon-bound action" diff --git a/decisions/2026-10-30-renovate-auth.md b/decisions/2026-10-30-renovate-auth.md new file mode 100644 index 0000000..b03b2c3 --- /dev/null +++ b/decisions/2026-10-30-renovate-auth.md @@ -0,0 +1,108 @@ +--- +date: 2026-10-30 +branch: fix/renovate-auth +title: Renovate's token needs five scopes, not three; the workflow documented the wrong list, and now probes it +--- + +## What changed + +**The fix is operator-side.** `RENOVATE_TOKEN` lacks `read:organization`, and it will next lack +`write:issue`. The operator must re-mint it with five scopes: + +- `read:user` +- `read:organization` +- `write:repository` +- `write:issue` +- `read:package` + +The git half fixes the two things that sent the operator round this loop twice. + +**`.gitea/workflows/renovate.yaml`**: +- **The header's scope list is corrected**, with the call each scope serves. The old list named + only `write:repository`, `read:user` and `read:package`, and the operator minted exactly that. + The preflight's "TO FIX" text is corrected the same way. +- **A new step, "Preflight - the token has every scope Renovate needs".** + - It probes read-only GETs: `/user` for read:user, `/orgs/Home-Network` for read:organization, + `/repos/{repo}` for read:repository, `/repos/{repo}/labels` for read:issue, and registry + `/v2/token` for read:package. + - It reads the token's own scope list out of Gitea's 403 from `/admin/orgs` (names only), and + requires `write:repository` and `write:issue` in it. + - It says what a failure means: 401 is the value; a Gitea 403 is a scope, and the probe prints + Gitea's message; an "Access denied" 403 is a Cilium/Envoy denial. + - The token is fed to curl on stdin (`--config -`), never in argv. + +**`ci/lint.sh`**: a new guard rejects any non-comment `uses:` in `.gitea/workflows` that needs a +Docker daemon: `docker://`, `renovatebot/github-action` and Docker's daemon-bound +`docker/*-action`s. That is the class of bug fixed in #84. + +### Root cause, with evidence + +Run 350 (task 567, 06:49 UTC) **got further than 348 and 349.** Gitea's router log, filtered to +the act-runner pod `10.0.0.38`: + + 06:49:31 GET /api/v1/user 200 OK + 06:49:31 GET /api/v1/version 200 OK + 06:49:31 GET /api/v1/repos/Home-Network/home-network-k3s-impl 200 OK + 06:49:31 GET /api/v1/orgs/Home-Network 403 Forbidden + +There were no `/v2/` requests: Renovate stopped before extraction. From the job log, verbatim: + + ERROR: Repository has unknown error (repository=Home-Network/home-network-k3s-impl) + "url": "https://gitea.home.keithjackson.dev/api/v1/orgs/Home-Network", + "message": "token does not have at least one of required scope(s), required=[read:organization], token scope=read:package,write:repository,read:user" + "x-envoy-upstream-service-time": "27", "server": "envoy" + INFO: Repository finished ... "result": "unknown-error", "exitCode": 8 + +**This is not Cilium.** The body is Gitea's JSON scope message, Gitea logged the request, and +`x-envoy-upstream-service-time` shows that Envoy forwarded it upstream. + +**The secret WAS updated.** Runs 348 and 349 got `GET /user 403` from `tokenRequiresScopes`. Run 350 +got `/user 200`, and its token scope list now includes `read:user`. + +**Why `read:organization` is needed.** Renovate 44.167.0, `lib/modules/platform/gitea/index.ts` +`initRepo`, calls `helper.isOrg` → `GET /orgs/{owner}` and rethrows on error. Gitea 1.24.3, +`routers/api/v1/api.go`, puts `/orgs/{org}` behind +`tokenRequiresScopes(AccessTokenScopeCategoryOrganization)`. + +**Why `write:issue` is needed next.** In the same `api.go`, `/repos/{o}/{r}/issues…`, `/labels` +and `/milestones` sit in a group with `tokenRequiresScopes(AccessTokenScopeCategoryIssue)` +(lines 1466–1582). Renovate's gitea-helper uses `/issues`, `/issues/{n}/labels`, `/labels` and +`/issues/{n}/comments`, for the dependency dashboard (`:dependencyDashboard` is extended) and for +PR labels. A dry run only reads them; a scheduled run writes them. + +The other causes the coordinator listed are ruled out: +- **Schedule window and dry-run.** The run never reached branch processing. +- **Repository config.** The run never reached the clone (`"cloned": undefined`). +- **The Summary step.** It is not involved; the failure is Renovate's own exit 8. + +## What I verified + +- I read the whole run 350 job log (204 lines), and the router lines above. Runs 348 and 349 are + as recorded in `decisions/2026-10-30-renovate-first-run.md`'s follow-up: `/user` got 403 from + `tokenRequiresScopes`. +- **The probe step, in the pinned image.** I extracted the step body from the YAML and ran it in + `ghcr.io/renovatebot/renovate:44.167.0` (`curl` 8.5.0 and `jq` are present) with a deliberately + **bogus** token. Every probe reported 401 with the "token VALUE" diagnosis, the scope-list read + fell back to a warning, and the step exited 1. +- **The scope-list parser.** Against run 350's exact message format it reports `write:issue` + MISSING. Against a five-scope list it reports both OK. Against `all` it reports both OK. +- **The lint guard.** It flags `renovatebot/github-action` in the pre-#84 `renovate.yaml` + (dfc432c, line 152). It reports nothing on the current workflows, and it ignores commented + lines. `bash -n ci/lint.sh` passes. +- **Account facts, read-only.** `renovate-bot` is a collaborator with `write`, and the org is + public. + +## What I did NOT verify + +- **The probe with a real token. UNVERIFIED.** I never had the token. In particular, I have not + observed that `GET /api/v1/admin/orgs` gives renovate-bot's token a 403 with a `token scope=` + list. The source says the scope check runs before `reqSiteAdmin`. If the message format + differs, the step warns and does not fail. +- **That five scopes are enough for a full non-dry run.** They cover every helper endpoint Renovate + 44.167.0 calls, mapped to Gitea 1.24.3's route categories by reading source. Branch pushes go + over git-over-HTTPS with the token; I believe that needs write:repository, but I have not + observed it. +- **Registry reads as renovate-bot.** `read:package` on a public org is expected to work. The new + probe is the first real test of it. +- **Sensitive paths.** `.gitea/**` and `ci/**` are both sensitive. CI's `approval-policy` will block + until the operator posts `/approve-override` as a PR review. From d9571fc6d8732cd64d33c2babebfaa0d8abe5ded Mon Sep 17 00:00:00 2001 From: Keith Jackson Date: Fri, 30 Oct 2026 02:35:33 -0500 Subject: [PATCH 03/10] fix(renovate): edit catalog sources, track CNPG imageName, honour update.range, guard real-run auto-merges Audit of the first completed dry runs (353/354) plus a debug-level local reproduction: - App-image updates edited only the generated apps/wave-*. A ServiceDefinition has no apiVersion, so the kubernetes manager extracts nothing from catalog/, and every such PR would fail render-golden (reproduced: GOLDEN DRIFT). New regex managers for catalog source.image and render.py OAUTH2_PROXY_IMAGE put the source and generated edits in one branch (render-golden rc 0, reproduced). - CNPG Cluster imageName was untracked (3 Clusters), while the grant Job was tracked. New manager. - allowedVersions mirror catalog update.range: this drops gitea 28.x, qbittorrent 20.x, radarr 6, nextcloud 35 and postgresql 18. - Never auto-merge: gitea, act_runner, postgresql (blast radius), flux range widening, python minors. Majors wait for a Dependency Dashboard tick. - velero plugin RC: semver versioning (v1.14.3-rc.2 -> v1.14.4 now proposed). - flux docker duplicates disabled (the "Cannot find replaceString" line). Two false comments corrected. Local dry run: regex deps 7 -> 20, branches 33 -> 29. The validator passes on 44.167.0. Co-Authored-By: Claude Opus 5.5 --- decisions/2026-10-30-renovate-run-hygiene.md | 128 +++++++++++++++++++ renovate.json5 | 109 +++++++++++++++- 2 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 decisions/2026-10-30-renovate-run-hygiene.md diff --git a/decisions/2026-10-30-renovate-run-hygiene.md b/decisions/2026-10-30-renovate-run-hygiene.md new file mode 100644 index 0000000..1425691 --- /dev/null +++ b/decisions/2026-10-30-renovate-run-hygiene.md @@ -0,0 +1,128 @@ +--- +date: 2026-10-30 +branch: fix/renovate-run-hygiene +title: Renovate's first completed dry runs, audited; it would have opened red PRs for every app and missed the databases. Config fixed +--- + +## What changed + +**`renovate.json5` only.** Each change carries a comment quoting the measurement behind it. + +1. **Three new regex managers, so updates edit the SOURCE.** + - `catalog/*.yaml` `source.image`. + - `catalog/render.py` `OAUTH2_PROXY_IMAGE`. + - CNPG `imageName:` under `platform/` and `apps/`. + + The first two match the rendered line's depName, so the catalog edit and the generated + `apps/wave-*` edit land in **one** branch. +2. **`allowedVersions` mirroring each ServiceDefinition's `update.range`:** + - gitea/gitea `<2.0.0` + - nextcloud `<32.0.0` + - qbittorrent `<6.0.0` + - radarr `<6.0.0` + - sonarr `<5.0.0` + - semaphore `<3.0.0` + + Plus CNPG postgresql `<17.0.0`, because a Postgres major is a migration, not a bump. +3. **The blast-radius never-automerge list gains** `gitea/gitea`, `gitea/act_runner` and + `ghcr.io/cloudnative-pg/postgresql`. +4. **HelmRelease range widening** (`matchManagers: flux`, ranged currentValue) is never + auto-merged and is labelled `range-widen`. +5. **Majors need a Dependency Dashboard tick** (`dependencyDashboardApproval`) before a PR opens. +6. **`flux` manager docker deps are disabled.** These are duplicates of `kubernetes` manager + extractions. +7. **`velero/velero-plugin-for-aws` uses `semver` versioning**, so its RC pin can reach a stable + release. +8. **`python` minors are not auto-merged.** +9. **Two false comments are corrected.** + - Ranges "cap Renovate at patches": false, Renovate widens them. + - `nextcloud:31-apache` and `docker:27-dind` "cannot be maintained": false, they were. + +PR #85 is merged, so nothing here overlaps an open branch. + +### The audit: dispatch runs 353 (07:12 UTC) and 354 (07:23 UTC), on 593d0fa + +Both runs ended `success`, Renovate `"result": "done"`, `exitCode 0`. Both were at `info` level, so +I reproduced the per-dependency detail with a debug-level local dry run of the same commit (see +below). + +The runs were inside the 03:00–06:00 America/New_York window, so branches were processed: +- run 353: 37 "Would commit" branches; +- run 354: 40, because the operator had added `RENOVATE_GITHUB_COM_TOKEN` 9 s before it. + +Gitea's router log for the act-runner pod during both runs shows 317 2xx responses. The only +non-2xx were all expected: +- `/v2/` 401: the registry challenge; +- `/admin/orgs` 403: the #85 scope probe, by design; +- 6× `HEAD …/manifests/sha256:000…` 404: the placeholder digests. + +| # | Class | Count | Cause | Severity | Fix | +|---|---|---|---|---|---| +| 1 | App-image PRs edit only generated `apps/wave-*` | 9 branches | A ServiceDefinition has no `apiVersion`, so the `kubernetes` manager extracts 0 deps from `catalog/`. The oauth2-proxy sidecar is a Python constant (`catalog/render.py:132`) | **Real problem.** Every such PR fails `ci/render-golden.sh` (reproduced: rc 1 "GOLDEN DRIFT"), stays red and holds a PR slot | git (1) | +| 2 | CNPG `imageName` untracked | 3 Clusters | The `kubernetes` manager reads `image:` only. The one tracked postgres line was the grant Job (`nextcloud-db.yaml:303`, "keep in step with spec.imageName") | **Real problem.** A real run bumps the Job and leaves the databases behind | git (1) | +| 3 | Bogus or out-of-policy majors | 5 branches: gitea 28.2.0, qbittorrent 20.04.1, radarr 6.x, nextcloud 35, postgresql 18.6 | Odd legacy tags, plus `update.range` in `catalog/` not being read by Renovate | Risk if run for real: unmergeable PRs, which hold slots | git (2) | +| 4 | Auto-merge of the merge gate's own host | gitea 1.24.3→1.27.3, act_runner 0.2.11→0.6.1, both "minor" | Not in the blast-radius list. A 0.x minor is breaking by semver | **Risk if run for real** | git (3) | +| 5 | Range widening auto-merged | external-dns, headlamp (minor); longhorn (blast radius); kube-prometheus-stack (major) | Renovate widens `>=a .yaml` + `apps/wave-*/.yaml`. + - **The oauth2-proxy branch** carries `catalog/render.py` plus the 5 rendered apps and + `platform/crafty`. + - **The postgresql-16.x branch** carries all three Clusters. + - The 2 flux docker duplicates report `disabled`. +- **render==golden holds for the new branch shape.** + - In the toolbox, I applied the gitea branch edit (catalog + apps) and the oauth2-proxy branch + edit (`render.py` constant + 5 apps) textually. `./ci/render-golden.sh` reported + `all 10 onboarded service golden(s) reproduce`, rc 0. + - Negative control, the old shape with only `apps/wave-0/gitea.yaml` bumped: rc 1, + `GOLDEN DRIFT`. + - Both edits were reverted afterwards. + +## What I did NOT verify + +- **The effect of the automerge, label and dashboard-approval rules (3, 4, 5, 8). UNVERIFIED in a + run.** `--platform=local` stops before branch processing, so I saw only which updates exist, not + their merged flags. The matchers are standard (`matchPackageNames`, `matchManagers`, + `matchCurrentValue`, `matchUpdateTypes`). The next dispatch's debug log shows them per branch. +- **`allowedVersions` under docker versioning** used Renovate's "Falling back to npm semver + syntax" path (16 debug lines). That it filters is shown by the five removed branches. +- **render==golden for the other app branches** (qbittorrent, radarr, sonarr, semaphore, seerr). I + proved it only for gitea and oauth2-proxy. They have the same shape: one `source.image` line ↔ + one rendered `image:` line. +- **The `allowedVersions` rules duplicate `catalog/*/update.range`** and can drift from it. Nothing + checks them against each other. A lint comparing the two is the obvious follow-up. +- **Any real (non-dry) run.** None has happened. PR creation, labels, auto-merge through Gitea's + API and the Dependency Dashboard issue are all unobserved. +- **Sensitive path?** `renovate.json5` is not in `infra/gitea/approval-policy.json`, as far as I + know. CI's `approval-policy` result on the PR is the measurement. diff --git a/renovate.json5 b/renovate.json5 index 898c75e..08c9d2d 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -149,6 +149,50 @@ ], "datasourceTemplate": "docker", "versioningTemplate": "docker" + }, + // ADDED 2026-10-30 — THE CATALOG IS THE SOURCE; apps/wave-*/*.yaml IS GENERATED FROM IT. + // First real dry run (dispatch runs 353/354): EVERY app-image update edited only the rendered + // apps/wave-*/*.yaml and NONE edited catalog/. The `kubernetes` manager lists catalog/ above + // but extracts nothing there: a ServiceDefinition has no `apiVersion`, so it is not a + // Kubernetes manifest to that manager (measured: 0 deps from catalog/). Every such PR would + // fail ci/render-golden.sh (the canonical render reverts the bump), sit red and hold a + // prConcurrentLimit slot. These two managers read the SOURCES, under the SAME depName as the + // rendered line, so both land in ONE branch and render==golden holds: `source.image` in each + // ServiceDefinition, and render.py's OAUTH2_PROXY_IMAGE (the six auth sidecars). + { + "customType": "regex", + "description": "ServiceDefinition `source.image: :` (catalog/*.yaml) — the source of the rendered apps/wave-* image.", + "managerFilePatterns": ["/(^|/)catalog/[^/]+\\.ya?ml$/"], + "matchStrings": [ + "\\n\\s+image:\\s*(?[^\\s:@#\"']+):(?[^\\s@#\"']+)" + ], + "datasourceTemplate": "docker" + }, + { + "customType": "regex", + "description": "catalog/render.py OAUTH2_PROXY_IMAGE — the per-app oauth2-proxy sidecar every forward-auth app is rendered with.", + "managerFilePatterns": ["/(^|/)catalog/render\\.py$/"], + "matchStrings": [ + "\\nOAUTH2_PROXY_IMAGE\\s*=\\s*\"(?[^\":@]+):(?[^\"@]+)\"" + ], + "datasourceTemplate": "docker" + }, + // ADDED 2026-10-30 — CNPG `Cluster.spec.imageName`. The `kubernetes` manager reads `image:` only, + // so the three database Clusters (platform/keycloak/postgres-cluster.yaml, apps/wave-0/ + // nextcloud-db.yaml, apps/wave-0/semaphore-db.yaml) were untracked, while the grant Job in + // nextcloud-db.yaml — whose comment says "keep the tag in step with spec.imageName" — WAS. A real + // run would have bumped the Job and left the database behind. Same depName, so one branch. + { + "customType": "regex", + "description": "CloudNativePG Cluster `imageName: :`.", + "managerFilePatterns": [ + "/(^|/)platform/.+\\.ya?ml$/", + "/(^|/)apps/.+\\.ya?ml$/" + ], + "matchStrings": [ + "\\n\\s+imageName:\\s*(?[^\\s:@#\"']+):(?[^\\s@#\"']+)" + ], + "datasourceTemplate": "docker" } ], "packageRules": [ @@ -200,7 +244,15 @@ "longhorn", "cilium", "cloudnative-pg", - "kube-prometheus-stack" + "kube-prometheus-stack", + // ADDED 2026-10-30. gitea and act_runner ARE the merge gate: a bad auto-merged bump takes + // down the very check that would have to approve its revert (the dry run proposed gitea + // 1.24.3 -> 1.27.3 and act_runner 0.2.11 -> 0.6.1, both as auto-mergeable "minor"; a 0.x + // minor is a breaking line by semver). The postgresql image restarts every CNPG database, + // Keycloak's (the IdP) included, now that imageName is tracked. + "gitea/gitea", + "gitea/act_runner", + "ghcr.io/cloudnative-pg/postgresql" ], "automerge": false, "addLabels": [ @@ -227,6 +279,51 @@ "gitea.home.keithjackson.dev/home-network/conformance-runner" ], "addLabels": ["own-image"] + }, + { + "description": "ADDED 2026-10-30. The `flux` manager also extracts `image:` lines inside HelmRelease values, which the `kubernetes` manager already owns. Measured: exactly two (alpine in platform/oauth2-proxy/helmrelease.yaml, velero-plugin-for-aws in platform/velero/helmrelease.yaml), each extracted twice; the second rewrite of the same line logged `Cannot find replaceString in current file content` in runs 353/354. Flux keeps its charts (datasource helm).", + "matchManagers": ["flux"], + "matchDatasources": ["docker"], + "enabled": false + }, + { + "description": "ADDED 2026-10-30. MIRRORS catalog/.yaml `update.range` — the operator's declared per-service policy (schemas/servicedefinition.schema.json), which Renovate does not read. Without it the dry run proposed gitea 28.2.0, qbittorrent 20.04.1 (both stale/odd tags, not real successors), radarr 6.x and nextcloud 35 (four majors past 31; Nextcloud cannot skip majors). KEEP IN STEP with the catalog ranges.", + "matchPackageNames": ["gitea/gitea"], + "allowedVersions": "<2.0.0" + }, + { "matchPackageNames": ["nextcloud"], "allowedVersions": "<32.0.0" }, + { "matchPackageNames": ["lscr.io/linuxserver/qbittorrent"], "allowedVersions": "<6.0.0" }, + { "matchPackageNames": ["lscr.io/linuxserver/radarr"], "allowedVersions": "<6.0.0" }, + { "matchPackageNames": ["lscr.io/linuxserver/sonarr"], "allowedVersions": "<5.0.0" }, + { "matchPackageNames": ["semaphoreui/semaphore"], "allowedVersions": "<3.0.0" }, + { + "description": "ADDED 2026-10-30. A CNPG Postgres MAJOR is a migration (new cluster + import), not an image bump; the dry run proposed 16.4 -> 18.6. Minors stay (and are blast-radius, above).", + "matchPackageNames": ["ghcr.io/cloudnative-pg/postgresql"], + "allowedVersions": "<17.0.0" + }, + { + "description": "ADDED 2026-10-30. A HelmRelease semver RANGE is a policy ceiling, and Renovate does not respect it — it WIDENS it (dry run: external-dns `<1.22.0` -> `<1.24.0`, headlamp `<0.42.0` -> `<0.47.0`), and the default rule would then auto-merge that as a 'minor', silently letting Flux adopt every new minor. Raising a ceiling is deliberate per-component work: propose it, never auto-merge it.", + "matchManagers": ["flux"], + "matchCurrentValue": "/^[<>=~^]|\\.x$/", + "automerge": false, + "addLabels": ["needs-review", "range-widen"] + }, + { + "description": "ADDED 2026-10-30. velero-plugin-for-aws is pinned to a RELEASE CANDIDATE (v1.14.3-rc.2) and Renovate proposed NOTHING for it, although v1.14.3 and v1.14.4 are stable on Docker Hub. Docker versioning treats everything after the first `-` as a compatibility SUFFIX, so only other `*-rc.2` tags qualify and the RC is a fossil forever. Semver versioning reads it as a prerelease, and a prerelease upgrades to the stable release.", + "matchPackageNames": ["velero/velero-plugin-for-aws"], + "versioning": "semver" + }, + { + "description": "ADDED 2026-10-30. A python MINOR is a language-runtime release (the dry run proposed 3.13 -> 3.15 for the Keycloak reconcilers), and the gate does not execute those scripts. Review it; patches still auto-merge.", + "matchPackageNames": ["python"], + "matchUpdateTypes": ["minor"], + "automerge": false, + "addLabels": ["needs-review"] + }, + { + "description": "ADDED 2026-10-30. MAJORS WAIT FOR A TICK ON THE DEPENDENCY DASHBOARD instead of opening a PR. They are never auto-merged anyway, and with prConcurrentLimit 5 the dry run's 10 majors would hold every PR slot open indefinitely, starving the patch/minor/digest PRs that can merge on green.", + "matchUpdateTypes": ["major"], + "dependencyDashboardApproval": true } ], // WHAT RENOVATE STILL CANNOT FIX, so nobody mistakes a working bot for full coverage. A @@ -235,10 +332,12 @@ // lscr.io/linuxserver/jackett:latest, pihole/pihole (NO TAG AT ALL), quay.io/keycloak/keycloak // (no tag), nextcloud:31-apache and docker:27-dind. Those must be pinned BY HAND to a concrete // version before any automation can maintain them, and until then they drift on every pull with - // nothing recording what is running. Separately, the HelmRelease minor-locking ranges (9 of 12, - // e.g. `>=88.0.0 <89.0.0` on kube-prometheus-stack) cap Renovate at patches inside a line it - // has been told not to leave; raising those ceilings is deliberate per-component work and is - // NOT part of enabling the bot. Both lists are in decisions/2026-10-17-automated-upgrades.md. + // nothing recording what is running. CORRECTED 2026-10-30: nextcloud:31-apache and docker:27-dind + // ARE maintained (the first dry run proposed 35-apache and 29-dind; the suffix is kept). + // Separately, the HelmRelease minor-locking ranges (9 of 12, e.g. `>=88.0.0 <89.0.0` on + // kube-prometheus-stack): THIS FILE USED TO SAY they cap Renovate at patches. They do not — + // Renovate WIDENS the range (runs 353/354). The `range-widen` rule above keeps that a reviewed + // PR rather than an auto-merge. Both lists are in decisions/2026-10-17-automated-upgrades.md. // platform/kyverno/tests/** ADDED 2026-10-17: those Kyverno policy fixtures pin `alpine:latest` // and a `conformance-runner:latest`, which are test inputs whose whole point is to be a fixed // shape, not software to keep current. `conformance/**` was already ignored for the same From df96be98bff26648157db86dce33d42e53f85fe2 Mon Sep 17 00:00:00 2001 From: Keith Jackson Date: Fri, 30 Oct 2026 02:54:51 -0500 Subject: [PATCH 04/10] fix(renovate): never auto-merge the oauth2-proxy sidecar or Renovate's own version Operator decision. Both are held at every update type and labelled needs-review: - quay.io/oauth2-proxy/oauth2-proxy (render.py constant, rendered sidecars, crafty). Since v7.13.0 oauth2-proxy validates refreshed OIDC sessions against the access token, every sidecar uses --cookie-refresh, and that path is untested against this Keycloak. - ghcr.io/renovatebot/renovate (and renovatebot/github-action if ever reintroduced). Nothing re-validates renovate.json5 against the new version before it takes effect. The validator passes on 44.167.0. Evaluated with Renovate's own packageRules engine: exactly 2 of 29 branch rows change (automerge true -> false, +needs-review); the other 27 are identical. Co-Authored-By: Claude Opus 5.5 --- .../2026-10-30-renovate-hold-auth-and-self.md | 52 +++++++++++++++++++ renovate.json5 | 14 +++++ 2 files changed, 66 insertions(+) create mode 100644 decisions/2026-10-30-renovate-hold-auth-and-self.md diff --git a/decisions/2026-10-30-renovate-hold-auth-and-self.md b/decisions/2026-10-30-renovate-hold-auth-and-self.md new file mode 100644 index 0000000..26ce70f --- /dev/null +++ b/decisions/2026-10-30-renovate-hold-auth-and-self.md @@ -0,0 +1,52 @@ +--- +date: 2026-10-30 +branch: fix/renovate-hold-auth-and-self +title: Operator decision — the oauth2-proxy sidecar and Renovate's own version never auto-merge +--- + +## What changed + +`renovate.json5` gains two packageRules. Both set `automerge: false` and `addLabels: ["needs-review"]`. +They sit after the auto-merge rule, so they override it at every update type. Each rule's +`description` carries the operator's reason. + +1. **`quay.io/oauth2-proxy/oauth2-proxy`.** This is the single depName emitted for: + - `catalog/render.py` `OAUTH2_PROXY_IMAGE` (custom regex manager); + - the five rendered `apps/wave-*` sidecars; + - `platform/crafty/deployment.yaml` (kubernetes manager). + + **Reason:** since v7.13.0, oauth2-proxy validates refreshed OIDC sessions against the access + token, not the id_token. Every sidecar sets `--cookie-refresh`, and that path is untested against + this Keycloak: the platform oauth2-proxy runs v7.15.5 without `--cookie-refresh`. + `render-golden` and `--selftest` pass on the bump, but both check rendered text, not the binary. +2. **`ghcr.io/renovatebot/renovate`.** This is the job `container.image` in + `.gitea/workflows/renovate.yaml`. The rule also lists `renovatebot/github-action`, so a + reintroduction would be held too. It is not used anywhere today; `ci/lint.sh` rejects it. + + **Reason:** nothing re-validates `renovate.json5` against the new version before it takes + effect. The 39.x `CONFIG_VALIDATION` failure is the precedent. + +## What I verified + +- **The config validates on the pinned 44.167.0.** `renovate-config-validator` printed + `Config validated successfully`. The migration diff is only the pre-existing top-level `docker` + block. +- **The flag change is exactly the intended one.** I used Renovate 44.167.0's own + `resolveConfigPresets` and `applyPackageRules` to evaluate b4ff287's config and this branch's + config against the same updates (the local dry run of the #86 config). Exactly **2 of 29** + branch rows differ: + - `ghcr.io-renovatebot-renovate-44.x`: `automerge=true labels=` → `automerge=false labels=needs-review`; + - `quay.io-oauth2-proxy-oauth2-proxy-7.x`: the same change, and every upgrade in the branch + resolves `false`, the `catalog/render.py` one included. + + The other 27 rows are byte-identical. + +## What I did NOT verify + +- **The three GitHub-hosted branches** in run 357 (`pin-dependencies`, `kubevirt-cdi`, + `intel-intel-device-plugins-for-kubernetes-0.x`) are not in the harness table: my local run has + no github.com token. Neither rule's package names can match them. +- **A live Renovate run with this config.** The next debug-level dispatch should show + `needs-review` on both branches. +- **The oauth2-proxy refresh path itself** remains untested. Exercising it on one app is the + review the label asks for. diff --git a/renovate.json5 b/renovate.json5 index 08c9d2d..35d5a3c 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -320,6 +320,20 @@ "automerge": false, "addLabels": ["needs-review"] }, + // OPERATOR DECISION 2026-10-30: these two never auto-merge, at ANY update type. Both sit after + // the auto-merge rule (rule 1) so they override it; `needs-review` is added, not replaced. + { + "description": "ADDED 2026-10-30 (operator decision). The oauth2-proxy image, under the ONE depName every source of it emits: catalog/render.py OAUTH2_PROXY_IMAGE (custom regex manager), the rendered apps/wave-* sidecars and platform/crafty/deployment.yaml (kubernetes manager). WHY: since v7.13.0 oauth2-proxy validates REFRESHED OIDC sessions against the ACCESS token instead of the id_token (upstream CHANGELOG, v7.13.0), every sidecar sets --cookie-refresh, and that refresh path has never run against this Keycloak (the platform oauth2-proxy runs v7.15.5 WITHOUT --cookie-refresh). render-golden and render.py --selftest pass on the bump, but both check rendered text, not the binary, so a green gate is not evidence here.", + "matchPackageNames": ["quay.io/oauth2-proxy/oauth2-proxy"], + "automerge": false, + "addLabels": ["needs-review"] + }, + { + "description": "ADDED 2026-10-30 (operator decision). Renovate's OWN version: the job `container.image` pin in .gitea/workflows/renovate.yaml (ghcr.io/renovatebot/renovate), and renovatebot/github-action should it ever be reintroduced (ci/lint.sh rejects it today). WHY: nothing re-validates renovate.json5 against the NEW version before it takes effect. The gate does not run renovate-config-validator, and this exact gap has bitten before: the action's default 39.x rejected this file (managerFilePatterns needs >= 40.2.0) and every run stopped at CONFIG_VALIDATION. Run the validator from the new tag, then merge.", + "matchPackageNames": ["ghcr.io/renovatebot/renovate", "renovatebot/github-action"], + "automerge": false, + "addLabels": ["needs-review"] + }, { "description": "ADDED 2026-10-30. MAJORS WAIT FOR A TICK ON THE DEPENDENCY DASHBOARD instead of opening a PR. They are never auto-merged anyway, and with prConcurrentLimit 5 the dry run's 10 majors would hold every PR slot open indefinitely, starving the patch/minor/digest PRs that can merge on green.", "matchUpdateTypes": ["major"], From 24db269ea8e44f752e638cb68b743774c8bb1a37 Mon Sep 17 00:00:00 2001 From: Keith Jackson Date: Fri, 30 Oct 2026 03:02:25 -0500 Subject: [PATCH 05/10] feat(renovate): track the Keycloak operator (the real Keycloak version), never auto-merge it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Keycloak CR has no spec.image, so the operator's RELATED_IMAGE_KEYCLOAK picks the server image; the three keycloak-k8s-resources release URLs in platform/keycloak/kustomization.yaml are therefore THE Keycloak version, and Renovate could not see them (no `# renovate:` marker). They now carry markers under one depName (one branch, groupName keycloak-operator), with a packageRule: automerge false, labels needs-review + blast-radius; majors still wait on the dashboard. The untagged quay.io/keycloak/keycloak in catalog/keycloak.yaml renders apps/wave-1/keycloak.yaml, which no kustomization lists. It is pinned to the operator's 26.0.6 as documentation, re-rendered, and Renovate is disabled for that image so it cannot raise PRs against a file nothing deploys. The render is kept (not deleted): it is the fixture for apps/wave-1/tests/app-slis.test.yaml's keycloak SLI rule tests. README: an "Upgrading Keycloak" section — an operator bump must be checked against passkey-reconciler.yaml's 26.0.6 Admin API assumptions. Co-Authored-By: Claude Opus 5.5 --- apps/wave-1/keycloak.yaml | 2 +- catalog/keycloak.yaml | 9 ++++++- platform/keycloak/README.md | 36 +++++++++++++++++++++++++++- platform/keycloak/kustomization.yaml | 9 +++++++ renovate.json5 | 31 ++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 3 deletions(-) diff --git a/apps/wave-1/keycloak.yaml b/apps/wave-1/keycloak.yaml index a21e8ff..29a38d7 100644 --- a/apps/wave-1/keycloak.yaml +++ b/apps/wave-1/keycloak.yaml @@ -75,7 +75,7 @@ spec: spec: containers: - name: keycloak - image: quay.io/keycloak/keycloak + image: quay.io/keycloak/keycloak:26.0.6 ports: - containerPort: 8080 volumeMounts: diff --git a/catalog/keycloak.yaml b/catalog/keycloak.yaml index 30b5dd5..06dcc08 100644 --- a/catalog/keycloak.yaml +++ b/catalog/keycloak.yaml @@ -5,8 +5,15 @@ # Validates against schemas/servicedefinition.schema.json. name: keycloak kind: container +# DOCUMENTATION-ONLY IMAGE. The running server is NOT chosen here: the Keycloak operator +# (platform/keycloak/kustomization.yaml, three pinned release URLs) picks it via its +# RELATED_IMAGE_KEYCLOAK, and the Keycloak CR has no spec.image. This SD's render, +# apps/wave-1/keycloak.yaml, is not listed in apps/wave-1/kustomization.yaml and deploys nothing +# (ci/render-golden.sh RENDER_GOLDEN_NOT_ONBOARDED). The tag records the operator's version for +# readers; renovate.json5 disables Renovate for this image so it cannot raise PRs against a file +# nothing deploys. The Keycloak version moves with the operator — update this tag by hand then. source: - image: quay.io/keycloak/keycloak + image: quay.io/keycloak/keycloak:26.0.6 exposure: public # login/redirect flows for internet-exposed services; hardened (docs/15; R-DNS/R-NET) egress: internet # normal WAN egress (docs/03; R-NET-05) storage: diff --git a/platform/keycloak/README.md b/platform/keycloak/README.md index 8b6c6c9..032fa98 100644 --- a/platform/keycloak/README.md +++ b/platform/keycloak/README.md @@ -19,7 +19,9 @@ Docker Hub images were purged (DECISIONS-LOG **F-KY1**) → that path ImagePullB 3-replica **`longhorn`** StorageClass (DR-critical, R-IDP-05), with declarative backup hooks. quay.io + ghcr.io are unaffected by the Bitnami purge. Operator manifests are pinned upstream release -files pulled as remote kustomize bases (the kubevirt-stub pattern); Renovate maintains the version. +files pulled as remote kustomize bases (the kubevirt-stub pattern). Renovate tracks the version via +`# renovate:` markers on those URLs (since 2026-10-30 — before that it could not see them) and never +auto-merges it; see [Upgrading Keycloak](#upgrading-keycloak-the-operator-is-the-version). ## Layout (controllers / configs split — F-MON1) @@ -360,6 +362,38 @@ it on the BOUND flow, after go-live, in order: A failed passkey ceremony is usually silent in the server log — the browser refuses before Keycloak is involved — so step 2 failing with no log entry is the expected signature of a wrong RP ID. +## Upgrading Keycloak: the operator IS the version + +The `Keycloak` CR has no `spec.image`, so the server image is whatever the operator's +`RELATED_IMAGE_KEYCLOAK` names — the same tag as the operator (prod, 2026-10-30: operator +`26.0.6`, `keycloak-0/1` on `quay.io/keycloak/keycloak:26.0.6`). Operator, CRDs and server are +released in lockstep from `keycloak/keycloak-k8s-resources`, and the three URLs in +`kustomization.yaml` are the one knob. Renovate raises them as ONE PR (`keycloak-operator` group, +labels `needs-review` + `blast-radius`), **never auto-merged**; majors wait for a dashboard tick. +`catalog/keycloak.yaml`'s image tag is documentation only (its render deploys nothing) and +Renovate is disabled for it — update it by hand to match after an operator bump merges. + +**An operator bump requires re-checking the passkey reconciler's assumptions.** +`passkey-reconciler.yaml` (and `ci/render-keycloak-passkey-policy.py`, which generates its desired +state) is written against **Keycloak 26.0.6 Admin API behaviour read from that tag's source** — +grep it for `26.0.6`. The gate cannot catch a break: it is static and never runs the reconciler +against a server. Before merging a bump, diff the new tag against 26.0.6 for at least: + +- `AuthenticationManagementResource.recurseExecutions` — does a sub-flow row still carry its name + in `displayName` (not `alias`)? The reconciler's flow matching depends on it. +- `updateExecutions` / execution-requirement updates — same request shape and id resolution? +- the WebAuthn Passwordless policy — is it still gated all-or-nothing on a non-empty + `webAuthnPolicyPasswordlessRpEntityName`, and does a realm `PUT` still REPLACE rather than merge + `requiredActions`? (The strict read-modify-write exists because of both.) +- authenticator-config alias handling for copied flows. + +Then, after the merge reconciles (operator actions): watch the `Keycloak` CR reach Ready and both +pods roll; run the reconciler once **with `--audit`** (see "Passkeys" above for the one-off Job) +and confirm it reports convergence with no write; confirm the client-secret and client-URL +reconcilers' next runs succeed; log in to one SSO app and run one `kubectl --context k3s-prod` +OIDC command. Roll back by reverting the PR — but a server minor may migrate the database +schema on start, so take/confirm a fresh CNPG backup before merging. + ## Exceptions that keep native auth (do not force SSO) — ADR-0014 Plex/Seerr (Plex auth), Minecraft (Mojang/Microsoft), UniFi (native). diff --git a/platform/keycloak/kustomization.yaml b/platform/keycloak/kustomization.yaml index de861be..8b58b52 100644 --- a/platform/keycloak/kustomization.yaml +++ b/platform/keycloak/kustomization.yaml @@ -21,8 +21,17 @@ namespace: keycloak resources: - namespace.yaml # --- Keycloak Operator v26.0.6 (pinned upstream release manifests) --- + # The operator, its CRDs and the SERVER IMAGE move in lockstep: the Keycloak CR has no + # spec.image, so the operator's RELATED_IMAGE_KEYCLOAK (quay.io/keycloak/keycloak:) + # is what runs. These three URLs are therefore THE Keycloak version. Each carries a Renovate + # marker (custom.regex manager, renovate.json5) under ONE depName, so all three bump in ONE + # branch, never auto-merged (packageRule "keycloak-operator"). Before merging a bump, re-check + # passkey-reconciler.yaml's 26.0.6 Admin API assumptions — see README.md "Upgrading Keycloak". + # renovate: datasource=github-tags depName=keycloak/keycloak-k8s-resources - https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.0.6/kubernetes/keycloaks.k8s.keycloak.org-v1.yml + # renovate: datasource=github-tags depName=keycloak/keycloak-k8s-resources - https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.0.6/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml + # renovate: datasource=github-tags depName=keycloak/keycloak-k8s-resources - https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.0.6/kubernetes/kubernetes.yml # --- Admin (break-glass) credential — SOPS-encrypted, referenced by name (ADR-0007, R-IDP-04) --- - keycloak-admin-credentials.sops.yaml diff --git a/renovate.json5 b/renovate.json5 index 08c9d2d..712d6d0 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -260,6 +260,35 @@ "blast-radius" ] }, + // ADDED 2026-10-30 — THE KEYCLOAK OPERATOR IS THE KEYCLOAK VERSION. The operator, its CRDs + // and the server image are released in lockstep from keycloak/keycloak-k8s-resources, and the + // Keycloak CR carries no spec.image, so the operator's RELATED_IMAGE_KEYCLOAK picks the server + // (measured on prod 2026-10-30: operator 26.0.6 -> keycloak-0/1 run quay.io/keycloak/keycloak: + // 26.0.6). The three release URLs in platform/keycloak/kustomization.yaml carry `# renovate:` + // markers under this ONE depName, so they move together in one branch (groupName makes that + // explicit). NEVER AUTO-MERGED, not even a patch: Keycloak is the IdP that gates SSO for every + // app AND kubectl OIDC, and platform/keycloak/passkey-reconciler.yaml encodes Keycloak 26.0.6 + // Admin API shapes read from that tag's source (sub-flow rows named by displayName, the + // RpEntityName all-or-nothing guard, requiredActions REPLACE-not-merge, ...). The gate is + // static and does not run that reconciler against a server, so a green check says nothing + // about whether a bump breaks it. Majors still wait on the dashboard (rule at the bottom). + { + "description": "Keycloak operator + CRDs + server (lockstep, one branch). IdP blast radius; passkey-reconciler.yaml encodes 26.0.6 Admin API shapes — re-check it against the new tag before merging. Never auto-merge.", + "matchDepNames": ["keycloak/keycloak-k8s-resources"], + "groupName": "keycloak-operator", + "automerge": false, + "addLabels": ["needs-review", "blast-radius"] + }, + // ADDED 2026-10-30 — quay.io/keycloak/keycloak IS NOT A DEPENDENCY RENOVATE MAY MOVE. Its only + // occurrences are catalog/keycloak.yaml `source.image` and the apps/wave-1/keycloak.yaml it + // renders, which is NOT listed in apps/wave-1/kustomization.yaml and deploys nothing (the + // operator above runs the server). A PR bumping it would change no running pod while looking + // like a Keycloak upgrade, and could drift from the operator's real version. + { + "description": "quay.io/keycloak/keycloak appears only in the documentation-only catalog/keycloak.yaml and its undeployed render; the operator (keycloak/keycloak-k8s-resources) chooses the real server image.", + "matchPackageNames": ["quay.io/keycloak/keycloak"], + "enabled": false + }, { "description": "ADDED 2026-10-29. This repo's own images are followed ONLY through the `# renovate-digest:` markers above. The `kubernetes` manager also sees platform/pm-agent/deployment.yaml and infra/agent/worker-job-template.yaml, where the image has a digest and no tag; left on, it would look those up as `latest` (a tag CI never pushes) and, for the template, re-open the self-referential loop the custom manager deliberately avoids.", "matchManagers": ["kubernetes"], @@ -334,6 +363,8 @@ // version before any automation can maintain them, and until then they drift on every pull with // nothing recording what is running. CORRECTED 2026-10-30: nextcloud:31-apache and docker:27-dind // ARE maintained (the first dry run proposed 35-apache and 29-dind; the suffix is kept). + // CORRECTED 2026-10-30: quay.io/keycloak/keycloak was never a running reference — the operator + // picks the server image — and the Keycloak version is now tracked via the operator URLs. // Separately, the HelmRelease minor-locking ranges (9 of 12, e.g. `>=88.0.0 <89.0.0` on // kube-prometheus-stack): THIS FILE USED TO SAY they cap Renovate at patches. They do not — // Renovate WIDENS the range (runs 353/354). The `range-widen` rule above keeps that a reviewed From 8ed4e30201783ab5f572feb05a09ae8ae32b988d Mon Sep 17 00:00:00 2001 From: Keith Jackson Date: Fri, 30 Oct 2026 03:06:56 -0500 Subject: [PATCH 06/10] docs(decisions): handoff for feat/renovate-track-keycloak-operator Co-Authored-By: Claude Opus 5.5 --- ...-10-30-renovate-track-keycloak-operator.md | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 decisions/2026-10-30-renovate-track-keycloak-operator.md diff --git a/decisions/2026-10-30-renovate-track-keycloak-operator.md b/decisions/2026-10-30-renovate-track-keycloak-operator.md new file mode 100644 index 0000000..822ea18 --- /dev/null +++ b/decisions/2026-10-30-renovate-track-keycloak-operator.md @@ -0,0 +1,113 @@ +--- +date: 2026-10-30 +branch: feat/renovate-track-keycloak-operator +title: Renovate tracks the Keycloak operator (the real Keycloak version), never auto-merged; the catalog's phantom image is pinned as documentation and disabled +--- + +## What changed + +- **`platform/keycloak/kustomization.yaml`**: each of the three `keycloak-k8s-resources/26.0.6` + release URLs (two CRDs and `kubernetes.yml`) now has a + `# renovate: datasource=github-tags depName=keycloak/keycloak-k8s-resources` marker on the line + above it. That is the format the existing `custom.regex` marker manager parses (the kubevirt + pattern). A comment explains that these URLs are the Keycloak version. The `Keycloak` CR has no + `spec.image`, so the operator's `RELATED_IMAGE_KEYCLOAK` picks the server image. +- **`renovate.json5`** has two new packageRules: + - **`keycloak/keycloak-k8s-resources`**: `groupName: keycloak-operator`, `automerge: false`, + `addLabels: [needs-review, blast-radius]`. A comment explains why. The operator, the CRDs and + the server are released in lockstep. The IdP gates SSO for every app and for kubectl. And + `passkey-reconciler.yaml` encodes Keycloak 26.0.6 Admin API shapes that the static gate never + exercises. Majors still go through the existing dashboard-approval rule. + - **`quay.io/keycloak/keycloak`**: `enabled: false`. Its only occurrences are in files that + deploy nothing. + + The "STILL CANNOT FIX" comment also gains a one-line correction for keycloak. +- **`catalog/keycloak.yaml`**: `source.image` changed from `quay.io/keycloak/keycloak` (no tag) to + `quay.io/keycloak/keycloak:26.0.6`. A comment says the image is DOCUMENTATION-ONLY and that the + operator chooses the real one. `apps/wave-1/keycloak.yaml` was re-rendered, which changes one + line. +- **`platform/keycloak/README.md`**: + - Corrects the false claim "Renovate maintains the version". Until now Renovate could not see + the version. + - Adds a new section, **"Upgrading Keycloak: the operator IS the version"**. It lists the 26.0.6 + assumptions in the passkey reconciler to diff against a new tag, and the post-merge checks. + +**The phantom: pin and disable, not delete.** `apps/wave-1/keycloak.yaml` is not listed in +`apps/wave-1/kustomization.yaml`, and render-golden excludes it (`RENDER_GOLDEN_NOT_ONBOARDED`). +Deleting it would still not be a no-op: + +- It is the fixture for the keycloak cases in `apps/wave-1/tests/app-slis.test.yaml` (the + StatefulSet-based `keycloak-down` rule). Those cases are discovered by the + every-PrometheusRule-in-apps/wave-1 convention. +- Removing it means moving that rule and its tests to `platform/keycloak`. That is a separate + observability change, not a Renovate one. + +Pinning plus `enabled: false` changes one line in a file nothing applies. It cannot affect the +cluster, and render-golden stays green. + +## What I verified + +- **Live state, prod, read-only kubectl, 2026-10-30.** + - Keycloak CR `keycloak` has an empty `spec.image`. + - The operator Deployment runs `quay.io/keycloak/keycloak-operator:26.0.6`, imageID + `sha256:8e3a1a56346f…`, with `RELATED_IMAGE_KEYCLOAK=quay.io/keycloak/keycloak:26.0.6`. + - `keycloak-0` and `keycloak-1` both run `quay.io/keycloak/keycloak:26.0.6`, imageID + `sha256:a93d22e13b86…`. + - There is no PrometheusRule containing "keycloak" in any namespace. This confirms the phantom + deploys nothing, including its `keycloak-down` rule (see the next section). +- **`renovate-config-validator`** on the pinned `ghcr.io/renovatebot/renovate:44.167.0`: + `Config validated successfully`. The only migration note is the pre-existing `docker` block. +- **Local dry run** (`RENOVATE_PLATFORM=local`, `RENOVATE_DRY_RUN=full`, debug, the same pinned + image, this worktree, with a GitHub token passed by environment variable; the token string + occurs 0 times in the log): + - The regex manager went to 21 files / 24 deps. + - `keycloak/keycloak-k8s-resources` was extracted three times from + `platform/keycloak/kustomization.yaml`, each at `currentValue 26.0.6`, datasource + `github-tags`, versioning `semver`. + - Each extraction has one update, `26.8.1`, updateType `minor`, and all three share **one** + `branchName: renovate/keycloak-operator`. + - `quay.io/keycloak/keycloak` was extracted from `catalog/keycloak.yaml` (regex) and from + `apps/wave-1/keycloak.yaml` (kubernetes) at 26.0.6, with `skipReason: disabled` and no + updates. +- **Rule effects.** `--platform=local` does not print each branch's merged flags. So I ran + Renovate's own `applyPackageRules` (from `dist/util/package-rules` in the same 44.167.0 image) + over this `renovate.json5` for each case: + + | Update | Result | + |---|---| + | operator minor 26.0.6 → 26.8.1 | `automerge:false`, `groupName:keycloak-operator`, labels `dependencies, automated-update, needs-review, blast-radius` | + | operator patch | same as the minor | + | operator major | `automerge:false`, `dependencyDashboardApproval:true`, labels add `breaking` | + | `quay.io/keycloak/keycloak`, catalog and rendered | `enabled:false` | + | control: sonarr patch | `automerge:true`, so the rule did not disable auto-merge globally | +- **render-golden, host:** `render.py --selftest` passed, and `all 10 onboarded service golden(s) + reproduce`. Before the pin, the committed phantom was byte-identical to the canonical render. I + re-rendered it after the pin. +- **Toolbox gate** (commit 24db269, worktree with the ENGINE shim mounting `.git` and the spec + repo, `DEV_KUBECONFIG=/dev/null`): **rc 3**. + - The only stage that did not run is **`gitea-settings`**. + - spec-immutability, decisions-log, lint, l0, render-golden, l1 and approval-policy all passed. + - L2/L3 `conformance` scaffold-skipped, as expected. + - **Not green.** +- **Sensitive paths:** none touched (checked against `infra/gitea/approval-policy.json`), so no + `/approve-override` is needed. + +## What I did NOT verify + +- **A real Renovate run.** No PR was opened by Renovate. The labels and `automerge:false` are + proven by Renovate's own matcher, not by an opened PR. +- **That 26.8.1 is safe to adopt.** I did not diff 26.0.6 against 26.8.1. That includes the + reconciler's assumptions: `recurseExecutions` displayName, `updateExecutions`, the + `RpEntityName` guard, `requiredActions` REPLACE, and config aliases. The README section lists + them for whoever reviews the first bump PR. It also says to take or confirm a fresh CNPG backup + first, because a server minor may migrate the schema. +- **FINDING, not fixed: Keycloak has no deployed "IdP down" alert.** The `keycloak-down` SLI rule + exists only inside the undeployed `apps/wave-1/keycloak.yaml`. promtool tests it, but it is not + on the cluster: there is no keycloak PrometheusRule live. Separately, because that file exists, + `render.py --emit-waves` would treat keycloak as onboarded. It would then add a Deployment named + `keycloak` beside the operator's StatefulSet (the known `--emit-waves` hazard). The proper fix is + a separate PR: move the rule and its tests into `platform/keycloak`, then delete the phantom. +- **The catalog tag can drift.** Nothing ties `catalog/keycloak.yaml`'s documentation tag to the + operator URLs. It must be updated by hand after an operator bump, and the README says so. +- **Concurrent branches** `fix/renovate-hold-auth-and-self` and `feat/pin-pihole-jackett-plex` + also edit `renovate.json5`. Neither had merged when I checked. A rebase may be needed. From 47eb6afa02c562045d9740c609eaaf46a3bf0949 Mon Sep 17 00:00:00 2001 From: renovate-bot Date: Fri, 30 Oct 2026 08:20:40 +0000 Subject: [PATCH 07/10] chore(deps): Pin dependencies --- .gitea/workflows/gate.yml | 2 +- .gitea/workflows/images.yaml | 2 +- .gitea/workflows/merge-gate.yaml | 20 ++++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.gitea/workflows/gate.yml b/.gitea/workflows/gate.yml index 127dc99..b738fb2 100644 --- a/.gitea/workflows/gate.yml +++ b/.gitea/workflows/gate.yml @@ -80,7 +80,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 # full history so ci/touched-requirements.sh can diff vs main diff --git a/.gitea/workflows/images.yaml b/.gitea/workflows/images.yaml index bbe700c..a259fb3 100644 --- a/.gitea/workflows/images.yaml +++ b/.gitea/workflows/images.yaml @@ -107,7 +107,7 @@ jobs: timeout-minutes: 75 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 # the per-image change detection diffs against the base commit diff --git a/.gitea/workflows/merge-gate.yaml b/.gitea/workflows/merge-gate.yaml index a9713db..3667e5e 100644 --- a/.gitea/workflows/merge-gate.yaml +++ b/.gitea/workflows/merge-gate.yaml @@ -60,9 +60,9 @@ jobs: spec-immutability: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 # the implementation repo + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 # the implementation repo - name: Check out the read-only spec (sibling path) - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: repository: ${{ vars.SPEC_REPO_SLUG }} # e.g. Home-Network/home-network-k3s (set in Gitea) path: ../home-network-k3s @@ -74,7 +74,7 @@ jobs: runs-on: ubuntu-latest needs: [spec-immutability] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: { fetch-depth: 0 } # full history so touched-requirement diff works - name: Install pinned tools run: ./.gitea/workflows/scripts/install-tools.sh lint @@ -85,7 +85,7 @@ jobs: runs-on: ubuntu-latest needs: [lint] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: { fetch-depth: 0 } - name: Install pinned tools run: ./.gitea/workflows/scripts/install-tools.sh l0 @@ -96,7 +96,7 @@ jobs: runs-on: ubuntu-latest needs: [l0] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: { fetch-depth: 0 } - name: Install pinned tools run: ./.gitea/workflows/scripts/install-tools.sh l1 @@ -107,7 +107,7 @@ jobs: runs-on: ubuntu-latest needs: [render-golden] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: { fetch-depth: 0 } - name: Install pinned tools run: ./.gitea/workflows/scripts/install-tools.sh l1 @@ -115,7 +115,7 @@ jobs: run: make -C ci l1 - name: Upload L1 artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: l1-junit-coverage path: | @@ -131,7 +131,7 @@ jobs: needs: [l1] if: ${{ vars.CLUSTER_AVAILABLE == 'true' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: { fetch-depth: 0 } - name: Install pinned tools run: ./.gitea/workflows/scripts/install-tools.sh conformance @@ -142,7 +142,7 @@ jobs: run: make -C ci conformance - name: Upload L2/L3 artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: conformance-junit-coverage path: | @@ -160,7 +160,7 @@ jobs: needs: [lint, l0, render-golden, l1, conformance] if: always() steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Decide env: R_LINT: ${{ needs.lint.result }} From 4ae648f6d306518b46824f725b63236e060d9c14 Mon Sep 17 00:00:00 2001 From: renovate-bot Date: Sat, 31 Oct 2026 08:16:08 +0000 Subject: [PATCH 08/10] chore(deps): Update gitea.home.keithjackson.dev/home-network/agent-runner:main Docker digest to c4eba5b --- platform/pm-agent/configmap.yaml | 2 +- platform/pm-agent/deployment.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/platform/pm-agent/configmap.yaml b/platform/pm-agent/configmap.yaml index 61104a1..7dda08a 100644 --- a/platform/pm-agent/configmap.yaml +++ b/platform/pm-agent/configmap.yaml @@ -59,7 +59,7 @@ data: # Until the first publish the dispatcher starts, fails to pull, and says so — which is the # visible failure, not a silent one. # renovate-digest: image=gitea.home.keithjackson.dev/home-network/agent-runner tag=main - PM_WORKER_IMAGE: "gitea.home.keithjackson.dev/home-network/agent-runner@sha256:0000000000000000000000000000000000000000000000000000000000000000" + PM_WORKER_IMAGE: "gitea.home.keithjackson.dev/home-network/agent-runner@sha256:c4eba5b3c508bf456a03391ac76f8275e7ea28a829da1450d99f6f6f25c6eb42" # --- the model ------------------------------------------------------------- # The PM only reads a thread and answers with one JSON object, so it holds no tools and diff --git a/platform/pm-agent/deployment.yaml b/platform/pm-agent/deployment.yaml index fdd4adb..a6a665c 100644 --- a/platform/pm-agent/deployment.yaml +++ b/platform/pm-agent/deployment.yaml @@ -72,7 +72,7 @@ spec: # of agent-runner's `main` tag (.gitea/workflows/images.yaml) — the all-zero # placeholder below is impossible on purpose (see configmap.yaml PM_WORKER_IMAGE). # renovate-digest: image=gitea.home.keithjackson.dev/home-network/agent-runner tag=main - image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:0000000000000000000000000000000000000000000000000000000000000000 + image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:c4eba5b3c508bf456a03391ac76f8275e7ea28a829da1450d99f6f6f25c6eb42 imagePullPolicy: IfNotPresent command: - /usr/local/bin/pm-dispatch From c6cfaf62b6232cd943f1b5be8a61ec701604b633 Mon Sep 17 00:00:00 2001 From: renovate-bot Date: Sat, 31 Oct 2026 08:16:10 +0000 Subject: [PATCH 09/10] chore(deps): Update gitea.home.keithjackson.dev/home-network/conformance-runner:main Docker digest to 65cc7c1 --- conformance/versions.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conformance/versions.env b/conformance/versions.env index 8325387..9b88153 100644 --- a/conformance/versions.env +++ b/conformance/versions.env @@ -61,7 +61,7 @@ CONFORMANCE_RUNNER_IMAGE_TAG="chainsaw0.2.12-kubectl1.36.4-fc44" # chainsaw-job.yaml's @@CONFORMANCE_RUNNER_IMAGE_DIGEST@@ takes the 64 hex digits WITHOUT the # `sha256:` prefix this value carries — the trigger must strip it. # renovate-digest: image=gitea.home.keithjackson.dev/home-network/conformance-runner tag=main -CONFORMANCE_RUNNER_IMAGE_DIGEST="sha256:0000000000000000000000000000000000000000000000000000000000000000" +CONFORMANCE_RUNNER_IMAGE_DIGEST="sha256:65cc7c1ab576fe8cdf45e7f68c7cf4eb5ce7e0db46e62191f6190c51610b20d9" CONFORMANCE_RUNNER_IMAGE="${CONFORMANCE_RUNNER_IMAGE_REPO}@${CONFORMANCE_RUNNER_IMAGE_DIGEST}" # # AFTERWARDS, TIGHTEN THE BOUNDARY: the header of conformance-trigger-job-shape.yaml asks for From 0b6282aba42f4ef53fd3f4025a22f6fa638fc823 Mon Sep 17 00:00:00 2001 From: renovate-bot Date: Sat, 31 Oct 2026 08:16:11 +0000 Subject: [PATCH 10/10] chore(deps): Update gitea.home.keithjackson.dev/home-network/toolbox:main Docker digest to d435fab --- infra/agent/versions.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/agent/versions.env b/infra/agent/versions.env index 676d66c..72bf881 100644 --- a/infra/agent/versions.env +++ b/infra/agent/versions.env @@ -18,7 +18,7 @@ # digest. For a LOCAL build on a local toolbox, override it: # infra/devcontainer/dev.sh build && AGENT_BASE_IMAGE=home-k3s-dev:local infra/agent/build-image.sh # renovate-digest: image=gitea.home.keithjackson.dev/home-network/toolbox tag=main -AGENT_BASE_IMAGE=gitea.home.keithjackson.dev/home-network/toolbox@sha256:0000000000000000000000000000000000000000000000000000000000000000 +AGENT_BASE_IMAGE=gitea.home.keithjackson.dev/home-network/toolbox@sha256:d435fabd6fc43bf5bb4de0c1ea8c4b19d8cea5c858679bd1675ccbed471b528a # Node.js — the runtime for the Claude Code CLI, installed from the Fedora base's module # stream. A major version, not a patch: the CLI supports the active LTS line and the