Compare commits

..
Author SHA1 Message Date
jakman85andClaude Opus 5.5 b548224db8 feat(gitea-actions): act_runner capacity 2, dind sized for two concurrent jobs
gate / gate (pull_request) Successful in 2m59s
gate / conformance (pull_request) Skipped
The in-cluster runner ran at the default capacity 1 with no config file, so
gate / images / renovate runs on different refs queued behind each other
(29-759s waits; run 472 waited 759s behind a renovate burst).

- ConfigMap act-runner-config with ONLY runner.capacity: 2, selected via
  CONFIG_FILE (honoured by the v0.6.1 image's run.sh). v0.6.1 LoadDefault()
  applies the same defaults to a partial file as to no file; the
  generate-config example is deliberately not used (its labels would override
  the registered ones, force_pull differs from the default).
- Pod-template config-revision annotation so ConfigMap-only edits can roll it.
- dind: cpu request 100m -> 2 (no limit), memory 256Mi/2Gi -> 1Gi/4Gi.
  Measured at capacity 1: peak 0.69 core, 447Mi working set (30d), 0 OOMs.
- runner: memory 128Mi/512Mi -> 256Mi/1Gi (peak 314Mi at capacity 1).

apps/wave-1/gitea-actions.yaml is a hand-owned EXTRA_UNITS file (the
renderer only places it in a wave), so it is the source, not generated output.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:09:08 -06:00
5 changed files with 235 additions and 138 deletions

No files matched your search

+69 -5
View File
@@ -77,6 +77,42 @@ spec:
requests:
storage: 1Gi
---
# act_runner config file — DELTAS ONLY. act_runner v0.6.1 LoadDefault()
# (internal/pkg/config/config.go) unmarshals this file onto a ZERO Config and then fills every
# unset field with the same default it uses when there is no file at all, so a file holding
# only `runner.capacity` changes exactly that and nothing else. Do NOT paste in the output of
# `act_runner generate-config`: that example is NOT the defaults. Its `runner.labels` would
# OVERRIDE the registered labels in /data/.runner (daemon.go: config labels win when non-empty)
# and repoint ubuntu-latest at a different job image; its `force_pull: true` differs from the
# no-file default (false); and its sample `envs` would be injected into every job.
#
# Using a file also makes act_runner IGNORE the legacy GITEA_RUNNER_CAPACITY / _FILE /
# _ENVIRON / _ENV_FILE / GITEA_DEBUG / GITEA_TRACE env vars (deprecated.go). None of them is
# set on this Deployment; the GITEA_INSTANCE_URL / _REGISTRATION_TOKEN / _NAME / _LABELS env
# below is consumed by the image's run.sh for registration, not by that legacy path.
#
# A ConfigMap edit alone does NOT restart the runner (it reads the file once at start). If a
# future change touches ONLY this ConfigMap, also bump the Deployment's pod template (e.g. the
# config-revision annotation below) or the new value stays inert until the next restart.
apiVersion: v1
kind: ConfigMap
metadata:
name: act-runner-config
namespace: gitea-actions
labels:
app.kubernetes.io/name: act-runner
data:
config.yaml: |
runner:
# Two concurrent jobs (default 1). Measured 2026-11-08: queue wait ~1s normally, but
# 29-759s whenever gate / images / renovate runs collide (run 472 waited 759s behind a
# renovate burst). Both jobs share the ONE dind daemon below; act_runner names each job's
# containers GITEA-ACTIONS-TASK-<task id> and creates a network per job, so they do not
# collide. Workflow `concurrency:` groups are enforced by Gitea server-side and are
# unaffected: gate.yml's gate-<ref> group still cancels a superseded run on the SAME ref,
# and renovate's group still serialises renovate. The dind resources are sized for TWO jobs.
capacity: 2
---
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -95,6 +131,10 @@ spec:
metadata:
labels:
app.kubernetes.io/name: act-runner
annotations:
# Bump when act-runner-config changes without any other pod-template change, so the
# Deployment rolls and act_runner re-reads its config (see the ConfigMap header).
homelab.keithjackson.dev/act-runner-config-revision: "1" # capacity 2
spec:
# CI holds no cluster credentials — do not mount a SA token into the runner or
# (via the shared netns) the job containers (R-SEC-02, docs/14).
@@ -146,12 +186,23 @@ spec:
mountPath: /certs
- name: dind-storage
mountPath: /var/lib/docker
# Sized for runner.capacity: 2. Every job's containers run UNDER this daemon, so this
# container's cgroup carries the whole CI workload. Measured 2026-11-08 (Prometheus,
# capacity 1): peak CPU 0.69 core (1m rate, 30d), peak working set 447Mi (30d), 0 OOMs.
# cpu request 2: two jobs at today's peak is ~1.4 core, and the gate is being
# parallelised, so per-job demand rises. The request is what the CFS share is
# weighted by under contention (100m was a 1/20 share); NO cpu limit, deliberately,
# so an idle node's spare cores are still usable. k3s-prod-2 (8 CPU) had 2945m
# requested; +1.9 core = ~4.85 (61%), and prod-1/prod-3 fit too if it reschedules.
# memory request 1Gi / limit 4Gi: two jobs at today's peak is ~0.9Gi. An OOM kill here
# takes down dockerd and with it BOTH running jobs, so the limit carries ~4x headroom
# over that for the parallelised gate. Nodes have ~31Gi allocatable.
resources:
requests:
cpu: 100m
memory: 256Mi
cpu: "2"
memory: 1Gi
limits:
memory: 2Gi
memory: 4Gi
startupProbe:
exec:
command:
@@ -195,23 +246,36 @@ spec:
value: /certs/client
- name: DOCKER_TLS_VERIFY
value: "1"
# The image's run.sh passes `--config <this>` to BOTH `act_runner register` and
# `act_runner daemon` when CONFIG_FILE is set (act_runner v0.6.1 scripts/run.sh).
- name: CONFIG_FILE
value: /etc/act_runner/config.yaml
volumeMounts:
- name: data
mountPath: /data
- name: certs
mountPath: /certs
readOnly: true
- name: config
mountPath: /etc/act_runner
readOnly: true
# Measured 2026-11-08 at capacity 1: peak CPU 0.24 core, peak working set 314Mi (30d)
# against the old 512Mi limit. It holds per-task state, log buffers and the embedded
# actions/cache server for every concurrent task, so the limit is doubled for capacity 2.
resources:
requests:
cpu: 100m
memory: 128Mi
memory: 256Mi
limits:
memory: 512Mi
memory: 1Gi
volumes:
- name: data
persistentVolumeClaim:
claimName: act-runner-data
- name: certs
emptyDir: {}
- name: config
configMap:
name: act-runner-config
- name: dind-storage
emptyDir: {}
@@ -1,65 +0,0 @@
---
date: 2026-11-08
branch: chore/renovate-config-cleanup
title: Renovate config — hand-migrate the docker key, track gitea/runner, keep HelmRelease range lower bounds
---
## What changed
- **`renovate.json5`: the deprecated top-level `"docker": {"enabled": true}` is hand-migrated** to
`packageRules[0] = {"matchCategories": ["docker"], "enabled": true}`, the exact shape Renovate's
own migration produced on `origin/renovate/migrate-config` (PR #139). Its comment moved with it.
This replaces #139, which was closed because its rewrite deleted every comment in the file. No
other comment was removed or reflowed (diff is additions plus the moved block).
- **`gitea/runner` added to the blast-radius rule** beside `gitea/act_runner`, with a comment:
act_runner was renamed upstream to gitea/runner after 0.6.1; Renovate does not follow renames,
so the actual image migration is a separate manual PR.
- **Two new packageRules for flux/helm `>=A <B` ranges:** `rangeStrategy: "bump"`, plus
`matchUpdateTypes: ["bump"] -> enabled: false`, so in-range releases (which Flux adopts on its
own) do not become PRs. Cause of the bug, from the 44.167.0 source: rangeStrategy is unset
(`auto`); the flux manager has no `getRangeStrategy`/`updateLockedDependency`, so
`lib/modules/manager/index.ts getRangeStrategy` resolves `auto` to `replace`; helm versioning is
the npm api verbatim, and npm `getNewValue` under `replace` turns `>=1.12.0 <1.13.0` into
`<1.14.0` (what PRs #131, #130 and #115 did).
- **`platform/headlamp/helmrelease.yaml`: `version: "<0.47.0"` -> `">=0.46.0 <0.47.0"`**, comment
refreshed. The live chart is 0.46.0, so the set of installable charts is unchanged.
## What I verified
- **Semantic diff of #139:** I parsed `renovate.json5` on `origin/renovate/migrate-config` and on its
parent `5b484f6` with json5 (the copy inside the renovate image). With `docker` removed from the
parent and `packageRules[0]` removed from the migrated file, the two are identical. So the only
change was `{"matchCategories":["docker"],"enabled":true}`, inserted at packageRules[0].
- **Validator, `ghcr.io/renovatebot/renovate:44.167.0`** (the tag that `.gitea/workflows/renovate.yaml`
pins as `container.image`): `renovate-config-validator` gives `Config validated successfully`
(rc 0), and so does `--strict` (rc 0), both as a file path and in repo mode. **`--strict` on
origin/main's file gives rc 1** and prints the `docker` migration diff. Nothing is left to migrate.
- **`getNewValue` called in the 44.167.0 image** (helm versioning), currentValue
`>=1.12.0 <1.13.0`, newVersion 1.13.0: replace `<1.14.0`, widen `>=1.12.0 <1.14.0`,
bump `>=1.13.0 <1.14.0`, pin/update-lockfile `<1.14.0`. On `<0.47.0`, bump returns the value
unchanged, so it would never propose anything for that range. That is why the rule is scoped to
the `>=A <B` shape.
- **Local dry run** (`--platform=local`, `RENOVATE_DRY_RUN=lookup`, 44.167.0, live chart repos)
against a fixture repo of four HelmReleases under `platform/`:
- With this branch's config: `No config migration necessary`. longhorn `>=1.9.0 <1.10.0` ->
`>=1.13.0 <1.14.0` (minor). kube-prometheus-stack `>=88.0.0 <89.0.0` -> `>=93.1.0 <94.0.0`
(major). The in-range bump `>=88.6.5 <89.0.0` was dropped (`Filtered out 1 disabled update(s)`).
headlamp `>=0.46.0 <0.47.0` and `<0.47.0`: no update.
- Control run with origin/main's config: `Config migration necessary`, longhorn -> `<1.14.0`,
kube-prometheus-stack -> `<94.0.0`. This reproduces the reported bug.
- Live headlamp HelmRelease (read-only kubectl, k3s-prod): spec `<0.47.0`, history chartVersion
`0.46.0`, Ready `True`.
## What I did NOT verify
- **I have not seen a real scheduled Renovate run against this repo with this config.** The claim that
Renovate stops reopening the migrate-config PR rests on the validator and on `No config migration
necessary` in the local dry run, not on the Gitea bot. After merge, watch the next run, and if
`renovate/migrate-config` is still on the remote, delete it.
- **Under bump, the new lower bound is the release Renovate proposes**, e.g. `>=1.13.2 <1.14.0`, not
`>=1.13.0`. Flux still installs the newest release in the window. Ranges with other shapes keep the
default `replace`: `3.9.x`, `~`, ceiling-only, and exact pins.
- **The open PRs #131, #115 and any others still carry unbounded `<X` values.** They were generated
under the old strategy, and Renovate should rebase or regenerate them under bump on its next run.
I have not checked that it does.
- The gitea/runner rename itself (a new image reference and any config changes) is not done here.
+155
View File
@@ -0,0 +1,155 @@
---
date: 2026-11-08
branch: feat/runner-capacity-2
title: act_runner runs two jobs at once, and dind is sized for two
---
## What changed
`apps/wave-1/gitea-actions.yaml` only. That file is a **hand-owned** non-catalog unit
(`EXTRA_UNITS["gitea-actions"]` in `catalog/render.py`): the renderer only decides which wave
it lands in, it does not generate its bytes, and `ci/render-golden.sh` compares only files the
catalog renders. So it is edited in place, and no re-render was needed or run.
1. **New ConfigMap `act-runner-config`** holding a config file with ONE key,
`runner.capacity: 2`, mounted read-only at `/etc/act_runner/` and selected with
`CONFIG_FILE=/etc/act_runner/config.yaml`. The image's `run.sh` (act_runner v0.6.1) turns
that into `--config` for both `register` and `daemon`.
- Deltas only, on evidence: v0.6.1 `LoadDefault()` unmarshals the file onto a zero Config and
then applies the same defaults as the no-file path, so the other settings are unchanged.
- The `generate-config` example was deliberately NOT used. Its `runner.labels` would override
the registered labels in `/data/.runner` and repoint `ubuntu-latest` at
`docker.gitea.com/runner-images`. Its `force_pull: true` is not the default. Its sample `envs`
would be injected into every job.
- With a file present, act_runner ignores the legacy `GITEA_RUNNER_CAPACITY` /
`_FILE` / `_ENVIRON` / `_ENV_FILE` / `GITEA_DEBUG` / `GITEA_TRACE` env vars. None of
them is set on this Deployment.
2. **Pod-template annotation `homelab.keithjackson.dev/act-runner-config-revision: "1"`.**
act_runner reads its config once, at start, so a change to the ConfigMap alone would never
take effect. Bump this annotation whenever you change only the ConfigMap.
3. **dind resources:** requests `cpu 100m -> 2` and `memory 256Mi -> 1Gi`; limit
`memory 2Gi -> 4Gi`. There is still no CPU limit, on purpose.
4. **runner resources:** request `memory 128Mi -> 256Mi`, limit `memory 512Mi -> 1Gi`. CPU stays
at `100m`.
Why these sizes. Measured 2026-11-08 through Prometheus, at capacity 1:
| container | peak CPU (1m rate, 30d) | peak working set (30d) | OOM events (7d) |
|---|---|---|---|
| dind | 0.69 core | 447 MiB | 0 |
| runner | 0.24 core | 314 MiB | 0 |
At the moment of measurement the pod was idle (`top`: dind 1m/138Mi, runner 1m/83Mi), on
k3s-prod-2.
- **CPU request 2.** Two jobs at today's peak is about 1.4 cores. The gate is being parallelised
in other work, so each job will use more than that. The request sets dind's CFS weight under
contention; at 100m it got a 1/20 share. With no limit, dind can still use spare cores on an
idle node.
- **Memory 1Gi / 4Gi.** Two jobs at today's peak is about 0.9 GiB. If dind is OOM-killed,
dockerd goes down and takes both running jobs with it, so the limit is about 4x that.
- **Runner limit 512Mi -> 1Gi.** Its 30-day peak was already 61% of the old limit, and with
capacity 2 it holds state for two tasks.
Scheduling headroom: CPU requests per node were measured with `describe node`. The pod's total
CPU request goes from 200m to 2100m (+1.9 cores).
| node | CPU requested now | after the change | allocatable |
|---|---|---|---|
| prod-2 (current) | 2945m | ~4845m (61%) | 8 |
| prod-1 | 3175m | ~5075m | 8 |
| prod-3 | 3840m | ~5740m (72%) | 8 |
The pod fits on any of the three if the RWO PVC moves it.
Concurrency semantics. Workflow `concurrency:` groups are enforced by Gitea on the server, not by
the runner, so capacity does not change them:
- `gate.yml` `gate-${{ github.ref }}` with cancel-in-progress still cancels a superseded run on
the same ref.
- `renovate` (group `renovate`, no cancel) and `images-${{ github.ref }}` still serialise
themselves.
What capacity 2 does change: runs on *different* refs (a PR gate next to a renovate or images
run, or two PRs) now run side by side instead of queueing. The required check
`gate / gate (pull_request)` is still one job per PR head, and gate.yml's other job
(`conformance`) is `if: false`, so it never runs.
Both jobs share one dockerd. act_runner gives each task its own container prefix
(`GITEA-ACTIONS-TASK-<task id>`) and its own network, and no workflow uses fixed container names
or host ports.
## What I verified
- **Source of truth.** `catalog/render.py` `EXTRA_UNITS` places `gitea-actions.yaml` by
anchor/offset only. `ci/render-golden.sh` iterates over rendered files only, so this file is
outside render==golden. No `--emit-waves` was run.
- **act_runner v0.6.1 behaviour, read in its source** (tag v0.6.1):
- `config.LoadDefault()` treats a partial file as described above.
- `daemon.go`: config labels override `.runner` labels only when non-empty.
- `poller.go`: a semaphore of `cfg.Runner.Capacity` bounds concurrent tasks.
- `runner.go`: per-task container name prefix.
- The image's `run.sh`, read from `gitea/act_runner:0.6.1`, honours `CONFIG_FILE`.
- **The live pod has no config file today.** `/data` holds only `.runner` and `lost+found`.
`act_runner --version` reports v0.6.1. No LimitRange or ResourceQuota exists in
`gitea-actions`.
- **Resource numbers.** All figures above come from read-only `kubectl top`, `describe node` and
Prometheus queries against k3s-prod.
- **dind disk.** The emptyDir sits on the node's 11.8G ephemeral filesystem: 4.0G used, 7.2G
free. Image cache is 2.18GB, which is the one `catthehacker/ubuntu:act-22.04` image. Two
concurrent jobs share that image.
- **Embedded config.** It parses as YAML to `{'runner': {'capacity': 2}}`. The only `$` in the
manifest is the existing `${BASE_DOMAIN}`, so nothing new is exposed to Flux postBuild
envsubst.
- **Approval policy.** `apps/wave-1/` matches no `sensitive_paths` glob in
`infra/gitea/approval-policy.json`, so this PR merges on a green gate alone.
- **Local gate.** Pinned toolbox, run from the worktree on the committed tree. The rc and skipped
stages are given in the PR description.
## What I did NOT verify
- **UNVERIFIED: that the runner actually starts with the config and runs two jobs at once.**
Nothing is applied until Flux reconciles `main`.
- **Capacity cannot be seen through Gitea's API.** act_runner v0.6.1 does not report capacity to
Gitea: `Declare` sends only version and labels. `GET /api/v1/admin/actions/runners` therefore
cannot show "capacity 2". Concurrency has to be observed instead.
- **UNVERIFIED: whether the parallelised gate fits in these sizes.** That gate has not landed.
Re-measure after it does, with
`max_over_time(sum by (container)(rate(container_cpu_usage_seconds_total{namespace="gitea-actions"}[1m]))[7d:30s])`.
- **Rollout cancels in-flight jobs.** The rollout restarts the runner pod (strategy Recreate,
`shutdown_timeout: 0s`), and any job running at that moment is cancelled, possibly including
the `push`-to-main gate run triggered by this very merge. Re-run it if that happens.
- **dind disk is not bounded by a limit.** With two jobs it holds two jobs' workspaces and layers
on the same 11.8G node filesystem. I did not add an `ephemeral-storage` limit, because an
eviction would kill both jobs. Watch kubelet DiskPressure on the runner's node.
- **Stale comments in sensitive paths.** `.gitea/workflows/gate.yml` (line ~52) and
`.gitea/workflows/README.md` (line ~86) still say "`act_runner` runs at capacity 1". I left
them alone because `.gitea/**` is a sensitive path and editing them would put this PR behind
human approval. Fix them in a separate PR.
### Operator post-merge checklist
1. Flux applied it:
`kubectl --context k3s-prod -n flux-system get kustomization apps-wave-1 -o jsonpath='{.status.lastAppliedRevision}'`
equals the merge commit.
2. The pod restarted with the config:
- `kubectl --context k3s-prod -n gitea-actions get pod -l app.kubernetes.io/name=act-runner`
shows a new pod, 2/2 Running.
- `kubectl ... exec deploy/act-runner -c runner -- cat /etc/act_runner/config.yaml` shows
`capacity: 2`.
- `kubectl ... exec deploy/act-runner -c runner -- ps` shows `act_runner daemon --config /etc/act_runner/config.yaml`.
- The logs show "declare successfully" with the two expected labels (`ubuntu-latest`,
`homelab` -> `catthehacker/ubuntu:act-22.04`). That confirms the labels did not change.
3. The runner is registered and online:
- `GET /api/v1/admin/actions/runners` lists `incluster-dind`, online, with the same ID as
before (no duplicate row).
- Capacity is NOT shown there; see above.
4. Concurrency is real: trigger two runs on different refs (e.g. two PR gates, or a PR gate plus a
`workflow_dispatch` of renovate). Then either:
- in `GET /api/v1/repos/Home-Network/home-network-k3s-impl/actions/tasks`, confirm their
`run_started_at` .. `updated_at` windows overlap; or
- check
`docker --host=tcp://127.0.0.1:2376 --tlsverify ... ps` in the dind container lists two
`GITEA-ACTIONS-TASK-*` container sets at once.
5. Resources: `kubectl --context k3s-prod -n gitea-actions describe pod` shows dind requests
cpu 2 / memory 1Gi, limit memory 4Gi, and runner limit memory 1Gi.
+1 -5
View File
@@ -40,11 +40,7 @@ spec:
# values (-session-ttl + the -oidc-* args from the externalSecret; useCookie stays default-false
# so `-oidc-use-cookie` is NOT emitted) is supported by the v0.41.0 backend. Renovate maintains
# within 0.41.x; the appVersion continues to pin the Headlamp image.
# 2026-11-08: NOW 0.46.x. PR #130 (Renovate, default rangeStrategy `replace`) rewrote
# `>=0.41.0 <0.42.0` to the unbounded-below `<0.47.0`; the lower bound is restored here at the
# chart that was live when it merged (0.46.0). renovate.json5 now bumps `>=A <B` ranges as a window,
# so the next widen PR proposes `>=0.47.x <0.48.0` rather than `<0.48.0`.
version: ">=0.46.0 <0.47.0"
version: "<0.47.0"
sourceRef:
kind: HelmRepository
name: headlamp
+10 -63
View File
@@ -37,8 +37,6 @@
// 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.)
// RESOLVED 2026-11-08: that block is now hand-migrated to the first packageRule (`matchCategories:
// ["docker"]`), the exact shape Renovate's own migration produced, so no migration is pending.
//
// `regex` vs `custom.regex` in enabledManagers, RESOLVED FROM SOURCE (Renovate 39.264.0 and
// later): both work. lib/config/migrations/custom/enabled-managers-migration.ts rewrites
@@ -198,22 +196,6 @@
}
],
"packageRules": [
// The `docker` manager (Dockerfiles, docker-compose) and the `kubernetes` manager above were
// BOTH previously `enabled: false`, solely so Renovate would not fight Flux image-automation
// over a running tag. That controller was never enabled and never will be here (see _comment),
// so the only thing those two flags were achieving was leaving every plain `image:` tag in the
// repo unmanaged. `docker` is left out of enabledManagers because this repo contains no
// Dockerfile that ships to the cluster; `kubernetes` is now ON and is the one that matters.
// MOVED HERE 2026-11-08 from a top-level `"docker": {"enabled": true}` key, which Renovate
// deprecated (the top-level language objects became `matchCategories` rules). Hand-migrated to
// the exact output of Renovate's own config migration (branch renovate/migrate-config, PR #139:
// a json5 semantic diff of that file against its parent shows THIS rule, inserted at
// packageRules[0], as the ONLY change). #139 itself was closed unmerged because its rewrite
// dropped every comment in this file. With nothing left to migrate, Renovate stops reopening it.
{
"matchCategories": ["docker"],
"enabled": true
},
{
"description": "Auto-merge the allowed low-risk classes on green CI \u2014 no human authoring (R-UPD-02). The Gitea merge gate (make -C ci gate) is the required check.",
"matchUpdateTypes": [
@@ -270,11 +252,6 @@
// Keycloak's (the IdP) included, now that imageName is tracked.
"gitea/gitea",
"gitea/act_runner",
// ADDED 2026-11-08. act_runner was RENAMED upstream to gitea/runner after 0.6.1
// (https://gitea.com/gitea/runner/releases). Renovate does not follow a rename — it will
// never propose gitea/act_runner -> gitea/runner — so that migration is a manual PR. Listed
// now so the new name is blast-radius from its first reference, not after an auto-merge.
"gitea/runner",
"ghcr.io/cloudnative-pg/postgresql"
],
"automerge": false,
@@ -391,45 +368,6 @@
"automerge": false,
"addLabels": ["needs-review", "range-widen"]
},
// ADDED 2026-11-08 — KEEP THE LOWER BOUND WHEN A MINOR-LOCKED RANGE MOVES. MEASURED PROBLEM:
// PRs #131 (longhorn), #130 (headlamp) and #115 (kube-prometheus-stack) turned
// `">=1.12.0 <1.13.0"` into `"<1.14.0"` — an UNBOUNDED-below range that also admits every
// older chart (and #130 merged: platform/headlamp/helmrelease.yaml carried `<0.47.0`). WHY,
// from the 44.167.0 source this file is validated against: no rangeStrategy is set, so it is
// `auto`; the flux manager has no getRangeStrategy and no updateLockedDependency, so
// lib/modules/manager/index.ts getRangeStrategy resolves `auto` -> `replace`. The helm
// datasource's defaultVersioning is `helm`, which is the npm versioning api verbatim
// (lib/modules/versioning/helm/index.ts: `api = { ...npm }`), and npm getNewValue under
// `replace` rewrites `>=A <B` to just `<C`. Calling that getNewValue in the 44.167.0 image for
// currentValue `>=1.12.0 <1.13.0`, newVersion 1.13.0:
// replace -> `<1.14.0` widen -> `>=1.12.0 <1.14.0` bump -> `>=1.13.0 <1.14.0`
// Only `bump` keeps a one-minor window. (widen keeps the bound but grows the window by a minor
// on every PR — the opposite of minor-locking.) Under bump the new lower bound is the release
// proposed (e.g. 1.13.2 -> `>=1.13.2 <1.14.0`); Flux still picks the newest that satisfies it.
// Scoped to the bounded `>=A <B` shape only: on a ceiling-only range like `<0.47.0` bump
// returns the value unchanged, so Renovate would silently propose NOTHING for it, ever.
{
"description": "ADDED 2026-11-08. HelmRelease `version: \">=A <B\"` ranges move as a whole window (rangeStrategy bump), so a range-widen PR yields `>=1.13.0 <1.14.0`, not `<1.14.0`. See the comment above for the measured cause.",
"matchManagers": ["flux"],
"matchDatasources": ["helm"],
"matchCurrentValue": "/^>=\\S+ <\\S+$/",
"rangeStrategy": "bump"
},
// ...AND DO NOT LET bump ADD NOISE. Under bump Renovate also raises the lower bound for a
// release ALREADY INSIDE the range (lib/workers/repository/process/lookup/generate.ts sets
// isBump; e.g. 1.12.5 against `>=1.12.0 <1.13.0` -> `>=1.12.5 <1.13.0`), which `replace` never
// proposed. Flux already installs that release with no PR, and the range-widen rule above
// would hold every one for review. util/package-rules/update-types.ts matches isBump as
// updateType `bump`, so exactly those updates are switched off here; out-of-range moves are
// ordinary minor/major updates and are unaffected.
{
"description": "ADDED 2026-11-08. No PR for a chart release the HelmRelease range already admits — Flux adopts it on its own.",
"matchManagers": ["flux"],
"matchDatasources": ["helm"],
"matchCurrentValue": "/^>=\\S+ <\\S+$/",
"matchUpdateTypes": ["bump"],
"enabled": false
},
{
"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"],
@@ -501,5 +439,14 @@
"conformance/tests/**",
"platform/image-automation/per-service/**",
"platform/kyverno/tests/**"
]
],
// The `docker` manager (Dockerfiles, docker-compose) and the `kubernetes` manager above were
// BOTH previously `enabled: false`, solely so Renovate would not fight Flux image-automation
// over a running tag. That controller was never enabled and never will be here (see _comment),
// so the only thing those two flags were achieving was leaving every plain `image:` tag in the
// repo unmanaged. `docker` is left out of enabledManagers because this repo contains no
// Dockerfile that ships to the cluster; `kubernetes` is now ON and is the one that matters.
"docker": {
"enabled": true
}
}