renovate: align the config with cluster-config #35
No files matched your search
@@ -54,8 +54,8 @@ Everything runs on the Gitea instance JLab-Git: issues, pull requests, CI and Re
|
||||
- **`tea`, not `gh`.** Work happens in a **Coder workspace, and it has no `op`** — that is the normal case here, not a broken environment, so do not wait for the 1Password CLI or propose installing it. Call `tea` directly; it carries the workspace's stored login and opens PRs and issues under the **bot account**, which is accepted. Say so in the report when something is authored that way. (`op plugin run -- tea`, the older instruction, applies only where `op` actually exists.)
|
||||
- **Remotes are named `github` and `JLab-Git`** — there is no `origin`. `JLab-Git/main` is the leading copy; say which remote you mean, because `git push` without one is ambiguous here.
|
||||
- **Branch protection with a signature requirement is active**, and signatures verify on Gitea. A commit that is not signed does not reach `main` — check before opening a PR, do not discover it at merge time.
|
||||
- **No merge queue.** Gitea has none, and its place is taken by **auto-merge on green checks**: the PR is armed once and merges itself when the required checks pass. Rule 6 above is unchanged — the user merges, and an agent never merges its own PR or arms auto-merge on it.
|
||||
- **A required check per chart is not possible.** Gitea runs a whole matrix as one job under one status context, so the install-test reports once for all charts. Relevant whenever branch protection is touched.
|
||||
- **No merge queue.** Gitea has none, and its place is taken by **auto-merge on green checks**: the PR is armed once and merges itself when the required check passes **and the required review exists** — both are active on `main`, so an armed PR without a review sits there instead of merging. Rule 6 above is unchanged — the user merges, and an agent never merges its own PR or arms auto-merge on it.
|
||||
- **A required check per chart is not possible.** Gitea runs a whole matrix as one job under one status context, so the install-test reports once for all charts. What is required on `main` is `validate-charts`, as that single context; per chart is the part that cannot be had. Relevant whenever branch protection is touched.
|
||||
- **Issue numbers changed with the move, and `#n` in this repo's files means the GitHub tracker.** The issues open at the time are Gitea #5–#21 and #24; the mapping is a comment on Gitea #14. Closed ones were not migrated and exist only on GitHub. Beware that Gitea silently resolves a bare `#n` against its own counter, so an old reference can render as a link to something unrelated. When you write a *new* reference, use the Gitea number.
|
||||
- **Renovate runs outside this repo**, as the shared `jlab-renovate-bot` instance; it reads our `renovate.json5` and its `postUpgradeTasks` hook is allow-listed there. **Do not add a Renovate workflow here** — a second runner means duplicate PRs and two schedulers rewriting each other's branches. README, "Renovate on Gitea", has the evidence.
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ Everything below was measured on this instance, not taken from documentation, an
|
||||
|
||||
**Runner labels mean different machines than on GitHub.** `ubuntu-latest` here has **no Docker socket**; `ubuntu-docker` has one. Exactly one job cares — the install-test, because k3d is a Docker client — and its `runs-on` is therefore an expression rather than a fixed label, so the same file still works on a GitHub runner where `ubuntu-docker` does not exist. Every other job (lint/package, the quantity render, the Trivy scan) needs no socket and stays on `ubuntu-latest`.
|
||||
|
||||
**A matrix runs concurrently inside ONE job, under ONE status context.** This is the single most consequential difference and it has two halves. Operationally: all legs share one Docker daemon, which is what makes a per-leg resource name mandatory (see the cluster name below). For branch protection: **a required check per chart is not possible**, because there is only one context for the whole matrix — and its name carries the *unexpanded* matrix expression, e.g. `Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request)`. Required-check names have to be read off a real run; they cannot be copied from GitHub.
|
||||
**A matrix runs concurrently inside ONE job, under ONE status context.** This is the single most consequential difference and it has two halves. Operationally: all legs share one Docker daemon, which is what makes a per-leg resource name mandatory (see the cluster name below). For branch protection: **a required check per chart is not possible**, because there is only one context for the whole matrix — and its name carries the *unexpanded* matrix expression, e.g. `Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request)`. Required-check names have to be read off a real run; they cannot be copied from GitHub. What *is* required on `main` today is **`validate-charts`**, as the single context it reports under, alongside a **review**: per chart is the part that is impossible, not required checks as such. Both are what makes Renovate's `platformAutomerge: true` safe — see "PR policy".
|
||||
|
||||
**Expected runtimes**, so that a later deviation is visible as one:
|
||||
|
||||
@@ -159,7 +159,7 @@ What Renovate keeps current:
|
||||
|
||||
Each chart renders its main image as `<repo>:{{ .Chart.AppVersion }}` — repository in the template, version in `Chart.yaml`. Renovate cannot join two files, so the mapping lives in `renovate.json5`, one block per chart. It is deliberately *not* a `# renovate:` comment inside each `Chart.yaml`: that would touch all chart directories and force a version bump plus a publish per chart for a pure comment change. Charts whose tags carry a flavour or date suffix (`apache-tika`, `minecraft-server`, `rss-bridge`, `searxng`, `teamspeak-server`) override `versioning` in their block; the reasoning sits next to each one.
|
||||
|
||||
One rule those overrides share, learned from `searxng` and worth knowing before writing the next one: **a capture group is a claim about how the value is compared.** Renovate's regex versioning parses `major`, `minor`, `patch`, `build` and `revision` with `Number.parseInt` (`modules/versioning/regex/index.js`, `_parse`), so a non-numeric commit hash captured as `build` becomes whatever digits lead it — `9fea41204` is `9`, `777ba8fa4` is `777`, and a hash starting with a letter is `NaN`. Measured on renovate 41.173.1 and 44.48.0, that makes Renovate offer an image pushed *earlier the same day* as an update to a newer one, unattended under the patch/minor automerge rule. Parts of a tag that carry no ordering belong **outside** every group: the anchored pattern still requires them to be there, they simply take no part in the comparison. `prerelease` is not the way out either — it sorts correctly but marks every tag `isStable === false`, which hands the whole chart to `ignoreUnstable`. `freesailarr` has no **app-version** block on purpose — see the images exception above; a block would either duplicate the `helm-values` updates or rewrite an `appVersion` that selects no image. It does carry one `customManagers` block, for `mediathekarr`: that image is pinned by digest because its tags carry no version (`latest` is a 2024 build, `beta` moves), and the built-in `helm-values` manager reads only `repository`/`tag` and skips it (`skipReason: invalid-value`), so the two cannot collide. Digest rules need the same guard as versioning rules and for a sharper reason: while the pin is current, a matching rule and a broken one produce the identical empty result. Verified with two dry-run fixtures — the real digest yields no update, a deliberately stale one yields exactly one `digest` update back to the pinned digest.
|
||||
One rule those overrides share, learned from `searxng` and worth knowing before writing the next one: **a capture group is a claim about how the value is compared.** Renovate's regex versioning parses `major`, `minor`, `patch`, `build` and `revision` with `Number.parseInt` (`modules/versioning/regex/index.js`, `_parse`), so a non-numeric commit hash captured as `build` becomes whatever digits lead it — `9fea41204` is `9`, `777ba8fa4` is `777`, and a hash starting with a letter is `NaN`. Measured on renovate 41.173.1 and 44.48.0, that makes Renovate offer an image pushed *earlier the same day* as an update to a newer one — a downgrade wearing the clothes of a routine date bump, which is exactly what a review waves through and automerge then carries to `main`. Parts of a tag that carry no ordering belong **outside** every group: the anchored pattern still requires them to be there, they simply take no part in the comparison. `prerelease` is not the way out either — it sorts correctly but marks every tag `isStable === false`, which hands the whole chart to `ignoreUnstable`. `freesailarr` has no **app-version** block on purpose — see the images exception above; a block would either duplicate the `helm-values` updates or rewrite an `appVersion` that selects no image. It does carry one `customManagers` block, for `mediathekarr`: that image is pinned by digest because its tags carry no version (`latest` is a 2024 build, `beta` moves), and the built-in `helm-values` manager reads only `repository`/`tag` and skips it (`skipReason: invalid-value`), so the two cannot collide. Digest rules need the same guard as versioning rules and for a sharper reason: while the pin is current, a matching rule and a broken one produce the identical empty result. Verified with two dry-run fixtures — the real digest yields no update, a deliberately stale one yields exactly one `digest` update back to the pinned digest.
|
||||
|
||||
A consequence of pinning every tag in `values.yaml`: `ci/renovate-bump-chart.sh` sees an unchanged `appVersion` and bumps the chart **patch**. That holds for all ten images alike, the digest-pinned one included — none of them is special — and it is the intended outcome, since `appVersion` here is the chart's own marker rather than a tracked upstream version.
|
||||
|
||||
@@ -202,9 +202,13 @@ One item is easy to miss and is named in "The version-bump hook" above: the inst
|
||||
|
||||
### PR policy
|
||||
|
||||
- **Automerge** for `patch`, `minor` and `digest` updates, after a `minimumReleaseAge` of 3 days and only once the PR pipeline is green — install-test included, which is why that job no longer runs with `continue-on-error`. Minor is in scope because the sidecar images barely move in patch steps (`alpine 3.22` → `3.24`, `nginx 1.30.4` → `1.31.4` are both minor), so a patch-only rule would practically never fire. Renovate merges itself (`platformAutomerge: false`) rather than handing the PR to the forge's own auto-merge, which on **both** GitHub and Gitea would merge immediately as long as `main` has no required status checks. That is a statement about the branch protection, not about the platform: once required checks exist on `main`, `platformAutomerge: true` becomes the better setting on either side. Majors are merged by hand.
|
||||
- **Majors** arrive one line at a time (`separateMultipleMajor`): a v1 → v3 jump produces a PR for the latest v2 first and then one for v3 — the stepwise procedure the versioning section requires.
|
||||
- `prConcurrentLimit: 10` / `prHourlyLimit: 4` keep the k3d install-tests from all starting at once.
|
||||
The config is aligned with the one in `cluster-config`, because the same `jlab-renovate-bot` instance serves both and two repos that behave differently under one bot is a source of surprises rather than of safety.
|
||||
|
||||
- **Automerge is global** (`automerge: true`, `automergeType: "pr"`, `platformAutomerge: true`) — every update type, **majors included**. The safeguard is not the absence of automerge, it is the branch protection: `main` requires a **review** and requires `validate-charts` to pass. `platformAutomerge` arms the PR on the forge, and Gitea merges it when *both* conditions are met, so an armed PR without a review simply sits there. This is the setting the older `platformAutomerge: false` comment in `renovate.json5` named as the better one "once required checks exist on `main`" — they now do. Same reasoning as `cluster-config`, which retired its dashboard-approval step for it.
|
||||
- **`minimumReleaseAge: "3 days"`** for `patch`, `minor` and `digest` updates. Renovate does not even open the PR before then. A merge here is never just a merge — a chart publish follows it straight away — so a release that gets yanked hours after it appeared must not reach the registry. Majors carry no age gate: they are held at the review, not by a delay.
|
||||
- **Majors** arrive one line at a time (`separateMultipleMajor`): a v1 → v3 jump produces a PR for the latest v2 first and then one for v3 — the stepwise procedure the versioning section requires. `separateMinorPatch: true` likewise splits minor and patch into separate PRs, so one red install-test does not block unrelated updates.
|
||||
- **No PR caps** (`prConcurrentLimit: 100`, `prHourlyLimit: 0`), as in `cluster-config`. The known cost, written down rather than discovered: the first run after a quiet week can open many PRs at once, and every chart PR starts a k3d install-test — which on a Gitea runner queues inside one job rather than fanning out (see "Where this repo lives"). That queue is the accepted trade for not having updates trickle in four per hour.
|
||||
- **`config:best-practices`** is in `extends`. Two of its members matter here: `helpers:pinGitHubActionDigests` moves the workflow files from `actions/checkout@v7` to SHA pins (wanted), and `docker:pinDigests` is switched off again for `custom.regex` and `helm-values` — an `appVersion:` is a tag by construction (`<repo>:{{ .Chart.AppVersion }}`, and the `+up` suffix is built from the same string), so a digest has nowhere to go. Sidecar digests in `values.yaml` *could* be pinned; that is a separate decision, since each digest change would be a chart patch plus a publish.
|
||||
- The manually dispatched `change-app-version.yml` still exists for one-off bumps, but the normal path for app updates is now a Renovate PR.
|
||||
|
||||
## Creating a new chart
|
||||
|
||||
+77
-28
@@ -16,15 +16,45 @@
|
||||
// Commit subjects in this repo are plain sentences, not conventional
|
||||
// commits. Renovate's own merged PRs already look like that.
|
||||
":semanticCommitsDisabled",
|
||||
// Same preset as cluster-config, so both repos behave identically on the
|
||||
// shared jlab-renovate-bot instance. Two of the things it pulls in are
|
||||
// load-bearing here: `helpers:pinGitHubActionDigests` (wanted — the
|
||||
// workflow files move from `actions/checkout@v7` to SHA pins) and
|
||||
// `docker:pinDigests`, which is turned off again for the two managers that
|
||||
// cannot carry a digest — see the pinDigests rule below. The
|
||||
// npm-flavoured members (`:pinDevDependencies`,
|
||||
// `security:minimumReleaseAgeNpm`, `:maintainLockFilesWeekly`) are inert
|
||||
// in a repo without a package manager.
|
||||
"config:best-practices",
|
||||
],
|
||||
|
||||
timezone: "Europe/Berlin",
|
||||
labels: ["dependencies"],
|
||||
labels: ["renovate"],
|
||||
dependencyDashboard: true,
|
||||
|
||||
// 21 charts plus the workflows: without a cap the first run after a quiet
|
||||
// week opens a wall of PRs, and every chart PR spins up a k3d install-test.
|
||||
prConcurrentLimit: 10,
|
||||
prHourlyLimit: 4,
|
||||
// No caps, as in cluster-config. The first run after a quiet week can open
|
||||
// many PRs at once and every chart PR starts a k3d install-test; on Gitea a
|
||||
// whole matrix runs inside one job on one runner, so those queue instead of
|
||||
// fanning out. That queue is the accepted price for not having updates
|
||||
// trickle in four per hour — nothing merges without a review anyway.
|
||||
prConcurrentLimit: 100,
|
||||
prHourlyLimit: 0,
|
||||
|
||||
// Minor and patch arrive as separate PRs rather than one combined branch.
|
||||
// With 23 charts a shared branch means one red install-test blocks unrelated
|
||||
// updates, and the separation is what lets a patch be reviewed in seconds.
|
||||
separateMinorPatch: true,
|
||||
|
||||
// Every update automerges once the PR is mergeable, majors included, and
|
||||
// `platformAutomerge` hands that decision to the forge instead of letting
|
||||
// Renovate poll: Gitea arms the PR and merges it when the required check
|
||||
// (`validate-charts`) passes AND the required review exists. Both are active
|
||||
// on `main`, which is why this is not unattended merging — the review is the
|
||||
// gate, and without it the armed PR simply sits there. Same reasoning as
|
||||
// cluster-config, where the dashboard approval was retired for it.
|
||||
automerge: true,
|
||||
automergeType: "pr",
|
||||
platformAutomerge: true,
|
||||
|
||||
// template-chart is the scaffold. Its image is the literal placeholder
|
||||
// ghcr.io/ORG/IMAGE and its appVersion is "xxx" — nothing to update, and the
|
||||
@@ -80,26 +110,43 @@
|
||||
},
|
||||
{
|
||||
description: [
|
||||
"Patch, minor and digest updates merge themselves once the PR pipeline is green.",
|
||||
"Minor is included because the sidecar images barely move in patch steps —",
|
||||
"alpine 3.22 -> 3.24 and nginx 1.30.4 -> 1.31.4 are both minor, so a patch-only",
|
||||
"rule would practically never fire. Majors stay manual.",
|
||||
"Routine updates wait three days before Renovate even opens the PR. Automerge is",
|
||||
"global now (top of this file), so this rule no longer decides WHAT merges by itself —",
|
||||
"it decides how fresh a release may be when it does. A merge here is never just a",
|
||||
"merge: every chart change publishes a chart straight away, and a release that is",
|
||||
"yanked hours after it appeared should not reach the registry at all.",
|
||||
"cluster-config spends the same three days on the dependencies where a merge is",
|
||||
"expensive (authentik, rancher); here that is every one of them.",
|
||||
"",
|
||||
"Majors deliberately carry no age gate: they are not the updates that slip through",
|
||||
"unnoticed, they are the ones held at the review, and separateMultipleMajor makes",
|
||||
"them arrive one line at a time anyway.",
|
||||
],
|
||||
matchUpdateTypes: ["patch", "minor", "digest"],
|
||||
automerge: true,
|
||||
automergeType: "pr",
|
||||
// Renovate waits for the checks itself instead of handing the PR to the
|
||||
// forge's own auto-merge. Both of them — GitHub's, and the auto-merge
|
||||
// that takes the merge queue's place on Gitea, which has none — merge
|
||||
// immediately when the branch has no required status checks configured,
|
||||
// and validate-charts is not (yet) a required check on either side. This
|
||||
// is a statement about the branch protection, not about the platform:
|
||||
// once required checks exist on `main`, true is the better value.
|
||||
platformAutomerge: false,
|
||||
// A release that is hours old should not walk into main unattended; a
|
||||
// chart publish follows the merge straight away.
|
||||
minimumReleaseAge: "3 days",
|
||||
},
|
||||
{
|
||||
description: [
|
||||
"`config:best-practices` brings `docker:pinDigests`, and two of this repo's managers",
|
||||
"have nowhere to put a digest. custom.regex reads `appVersion:` out of a Chart.yaml,",
|
||||
"and that value is a TAG by construction: the templates render",
|
||||
"`<repo>:{{ .Chart.AppVersion }}`, and the chart version's `+up<appVersion>` suffix is",
|
||||
"built from the same string — an `@sha256:...` fits into neither. cluster-config makes",
|
||||
"the identical opt-out for its fleet manager, for the identical reason.",
|
||||
"",
|
||||
"helm-values is opted out too, but as a decision rather than a necessity: sidecar",
|
||||
"images there COULD be pinned by digest, and every digest change would then be a chart",
|
||||
"patch plus a publish. Whether that is worth it is its own question, not part of this",
|
||||
"alignment.",
|
||||
"",
|
||||
"Unaffected: the freesailarr/mediathekarr block below, which extracts a digest that is",
|
||||
"written in values.yaml on purpose (pinDigests only ADDS digests where none exists),",
|
||||
"and the GitHub Actions pinning from `helpers:pinGitHubActionDigests` — a different",
|
||||
"datasource, and wanted.",
|
||||
],
|
||||
matchManagers: ["custom.regex", "helm-values"],
|
||||
pinDigests: false,
|
||||
},
|
||||
{
|
||||
description: [
|
||||
"Chart app versions. Kept out of the automerge grouping decisions above only by label,",
|
||||
@@ -121,10 +168,10 @@
|
||||
"This rule adds only a label. It exists because the update matters more here than",
|
||||
"its size suggests: a subchart is invisible to everything downstream that watches",
|
||||
"Helm release versions, so this PR is the ONLY way a new proxy version - a security",
|
||||
"fix included - ever reaches a cluster. Under the automerge rule above, patch and",
|
||||
"minor updates do that on their own once the pipeline is green, and",
|
||||
"ci/renovate-bump-chart.sh bumps the chart patch that publishes it (appVersion is",
|
||||
"unchanged, so it takes the patch branch). A major arrives as a PR to merge by hand.",
|
||||
"fix included - ever reaches a cluster. Automerge carries that PR to main once it is",
|
||||
"reviewed and the check is green, and ci/renovate-bump-chart.sh bumps the chart patch",
|
||||
"that publishes it (appVersion is unchanged, so it takes the patch branch). A major",
|
||||
"goes the same way - it is the review, not a separate policy, that gives it a look.",
|
||||
],
|
||||
matchManagers: ["helmv3"],
|
||||
addLabels: ["subchart"],
|
||||
@@ -230,7 +277,8 @@
|
||||
"points at the moving 'beta'. The built-in helm-values manager reads repository/tag and",
|
||||
"would have nothing to compare, so this manager captures all three keys and lets Renovate",
|
||||
"propose digest updates for the beta tag (currentDigest + currentValue = a digest-only",
|
||||
"lookup; automerged under the 'digest' update type in the rule above).",
|
||||
"lookup; it arrives as a 'digest' update, which the three-day age gate above covers and",
|
||||
"automerge then carries once the PR is reviewed and green).",
|
||||
"",
|
||||
"The pattern is anchored on the three keys in their values.yaml order: repository, tag,",
|
||||
"digest. Reordering or renaming them does not fail anything — it silently stops matching,",
|
||||
@@ -407,8 +455,9 @@
|
||||
// isGreaterThan("2026.8.22-777ba8fa4", current) -> false, equals -> true
|
||||
// 777ba8fa4 was pushed at 03:06 and 9fea41204 at 05:38 the SAME day, so
|
||||
// the build group makes Renovate offer the older image as an update -
|
||||
// a downgrade, unattended, because date updates land in the patch/minor
|
||||
// automerge rule. With NaN the pair is neither equal nor ordered at all.
|
||||
// a downgrade, and one that automerge would carry to main as soon as the
|
||||
// PR is waved through as the routine date bump it looks like. With NaN
|
||||
// the pair is neither equal nor ordered at all.
|
||||
//
|
||||
// Uncaptured, the hash still has to be THERE (the pattern is anchored),
|
||||
// it just takes no part in the comparison: two builds of one day are
|
||||
|
||||
Reference in new issue
Block a user