renovate: align the config with cluster-config
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Skipped
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Skipped
Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request) Skipped
Validate Helm Charts / Resource quantities (pull_request) Successful in 8s

The shared jlab-renovate-bot instance serves this repo and cluster-config,
and two repos behaving differently under one bot is a source of surprises
rather than of safety. This adopts cluster-config's configuration:
config:best-practices in extends, labels ["renovate"], no PR caps
(prHourlyLimit 0, prConcurrentLimit 100), separateMinorPatch, an explicit
dependencyDashboard, and global automerge with platformAutomerge.

Two of those choices were previously wrong here and are now right, because
the branch protection on main has caught up: a review requirement and
validate-charts as a required check are both active. platformAutomerge
therefore arms the PR on the forge and Gitea merges it when the check
passes AND the review exists - the review is the gate, which is what makes
automerging majors something other than unattended merging.

Comments that asserted the opposite are rewritten rather than left to rot:
the platformAutomerge: false comment ("not (yet) a required check on either
side"), the "Majors stay manual" rule, the README PR policy bullets and the
required-check paragraph, and the CLAUDE.md forge notes.

config:best-practices pulls in docker:pinDigests, which is switched off
again for custom.regex and helm-values: an appVersion: is a tag by
construction and has nowhere to put a digest. Kept deliberately:
minimumReleaseAge of three days on the routine updates (every merge here
publishes a chart), the ignorePaths and ci/** opt-outs, every per-chart
manager, the nginx and qbittorrent constraints, and postUpgradeTasks.

Fixes #34
This commit was merged in pull request #35.
This commit is contained in:
jklein committed 2026-09-22 19:44:28 +00:00
1 parent ddbf82e0d4
commit 02b3c3ce75
3 files changed
+88 -35

No files matched your search

+2 -2
View File
@@ -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.) - **`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. - **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. - **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. - **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. Relevant whenever branch protection is touched. - **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. - **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. - **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.
+9 -5
View File
@@ -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`. **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: **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. 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. 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 ### 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. 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.
- **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. - **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. - 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 ## Creating a new chart
+77 -28
View File
@@ -16,15 +16,45 @@
// Commit subjects in this repo are plain sentences, not conventional // Commit subjects in this repo are plain sentences, not conventional
// commits. Renovate's own merged PRs already look like that. // commits. Renovate's own merged PRs already look like that.
":semanticCommitsDisabled", ":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", timezone: "Europe/Berlin",
labels: ["dependencies"], labels: ["renovate"],
dependencyDashboard: true,
// 21 charts plus the workflows: without a cap the first run after a quiet // No caps, as in cluster-config. The first run after a quiet week can open
// week opens a wall of PRs, and every chart PR spins up a k3d install-test. // many PRs at once and every chart PR starts a k3d install-test; on Gitea a
prConcurrentLimit: 10, // whole matrix runs inside one job on one runner, so those queue instead of
prHourlyLimit: 4, // 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 // 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 // ghcr.io/ORG/IMAGE and its appVersion is "xxx" — nothing to update, and the
@@ -80,26 +110,43 @@
}, },
{ {
description: [ description: [
"Patch, minor and digest updates merge themselves once the PR pipeline is green.", "Routine updates wait three days before Renovate even opens the PR. Automerge is",
"Minor is included because the sidecar images barely move in patch steps —", "global now (top of this file), so this rule no longer decides WHAT merges by itself —",
"alpine 3.22 -> 3.24 and nginx 1.30.4 -> 1.31.4 are both minor, so a patch-only", "it decides how fresh a release may be when it does. A merge here is never just a",
"rule would practically never fire. Majors stay manual.", "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"], 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", 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: [ description: [
"Chart app versions. Kept out of the automerge grouping decisions above only by label,", "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", "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", "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", "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", "fix included - ever reaches a cluster. Automerge carries that PR to main once it is",
"minor updates do that on their own once the pipeline is green, and", "reviewed and the check is green, and ci/renovate-bump-chart.sh bumps the chart patch",
"ci/renovate-bump-chart.sh bumps the chart patch that publishes it (appVersion is", "that publishes it (appVersion is unchanged, so it takes the patch branch). A major",
"unchanged, so it takes the patch branch). A major arrives as a PR to merge by hand.", "goes the same way - it is the review, not a separate policy, that gives it a look.",
], ],
matchManagers: ["helmv3"], matchManagers: ["helmv3"],
addLabels: ["subchart"], addLabels: ["subchart"],
@@ -230,7 +277,8 @@
"points at the moving 'beta'. The built-in helm-values manager reads repository/tag and", "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", "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", "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,", "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,", "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 // isGreaterThan("2026.8.22-777ba8fa4", current) -> false, equals -> true
// 777ba8fa4 was pushed at 03:06 and 9fea41204 at 05:38 the SAME day, so // 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 - // the build group makes Renovate offer the older image as an update -
// a downgrade, unattended, because date updates land in the patch/minor // a downgrade, and one that automerge would carry to main as soon as the
// automerge rule. With NaN the pair is neither equal nor ordered at all. // 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), // 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 // it just takes no part in the comparison: two builds of one day are