Tags give the ability to mark specific points in history as being important
-
4.6.0
protectedRelease: pipeline 4.6.003c0206b · ·dunn.dev/pipeline 4.6.0 `ci-runtime-go` now builds on UBI10 (`ubi10/ubi:10.2` builder, `ubi10/ubi-micro:10.2` final), completing the estate's UBI10 train: consumers pin this catalog at fixed tags, so this release is what actually moves them off ubi9. A **minor**: no template input added, removed, or renamed. - **`ci-runtime-go`: base moved ubi9 → ubi10.** Builder `ubi10/ubi:10.2`, final `ubi10/ubi-micro:10.2`; the OCI `base.name` label now names the real base (!211). Zero ubi9 references remain in any Containerfile. - **`renovate-config.json`: UBI9→UBI10 `replacement` rules added; the cairn `platformAutomerge: false` interim override removed** (!240) — its premise died twice: the branch-head starvation was fixed in the cairn repos themselves, and Renovate's own merge `PUT` cannot satisfy this estate's sha-required merges. - Routine tracking: golang 1.26, `buildah:stable`, renovate 44, `anchore/syft`, ubi10 digests. - **`signal-digest`: `_request()` retries transient read timeouts** (3 attempts, short backoff; !251) — a single slow gitlab.com answer no longer kills the digest job. Real HTTP status answers propagate unchanged.
-
4.5.1
protectedRelease: pipeline 4.5.19328a177 · ·pipeline 4.5.1 `ci-go` now builds on go1.26.6, closing the five stdlib advisories below. Consumers pin `ci-go` at a fixed catalog tag rather than `:latest`, so this tag is what actually delivers the fix — each consumer still has to bump its pinned ref and re-run `audit` before govulncheck goes quiet. A **patch**: no template input added, removed, or renamed. - **`ci-go`: Go toolchain digest bumped past go1.26.5 to close five stdlib CVEs.** `containers/ci-go.Containerfile`'s `mirror.gcr.io/library/golang:1.26` digest pin landed on go1.26.6-equivalent content via the routine digest-track lane (renovate!213, merged 2026-08-15; folded into !219). Fixes, as reported by `go-audit`'s govulncheck against consumers on the affected toolchain: - **GO-2026-5972** / CVE-2026-33818 — `encoding/asn1` `Unmarshal` lacked a recursion limit, allowing stack exhaustion on deeply-nested input. - **GO-2026-5026** / CVE-2026-39821 — `net/http` (via `golang.org/x/net/idna`) accepted ASCII-only Punycode labels it should have rejected, a hostname validation bypass. - **GO-2026-6218** (`net/url` quadratic complexity), **GO-2026-6090** (`crypto/tls` post-handshake message limit), **GO-2026-6088** (`encoding/xml` recursion depth) — same go1.26.6 fix, caught in the same scan. The fix reached `origin/main` before this entry did: the `ci-go` Containerfile is pinned by digest, tracks the upstream `golang:1.26` tag, and Renovate's digest-update lane (`matchUpdateTypes: [digest, pinDigest]`, `minimumReleaseAge: null`) auto-merges those on green CI regardless of cause — it is not the `vulnerabilityAlerts`-specific security lane, which targets manifest-declared package advisories, not a Containerfile `FROM` digest. The gap was cadence, not policy: the fixed image published upstream 2026-08-13T20:27Z; the estate's next scheduled Renovate scan didn't run against it until 2026-08-15T03:00Z, roughly a day and a half later. This CHANGELOG entry is the only thing this fix needed that automerge couldn't supply. **Not yet propagated**: consumers pin `ci-go` at a fixed catalog tag (`bairn@4.4.1`, `tutti@2.1.2`, `qobuz@2.4.0`, all pre-dating this fix) — none will see it until a new catalog tag ships and each consumer bumps its pinned ref. The `go-audit` job is `allow_failure: true`, so none of this blocked anything; it has been failing silently since well before this CVE pair (bairn's `audit` job has red history back to at least 2026-05-18). -
4.5.0
protectedRelease: pipeline 4.5.02213983b · ·pipeline 4.5.0 Two threads. The `validate-*` self-validation harness had never run — every component was included at `@$CI_COMMIT_SHA`, `$[[ component.version ]]` resolves empty for a SHA ref, so each job pulled `ci-rust:` and died at container attach, masked by `when: manual` + `allow_failure: true`. It runs now, and immediately caught a stale Claude model id, a `container-image` publishing path bug, and an unbound-variable class in `pipeline.just`. It is deliberately still not gating: the repo has no language fixtures, so every go/rust/node validate job would fail on that alone, and a permanently-red gate on the L1 catalog is worse than none. The second thread is supply-chain hygiene: Fedora and UBI10 repoints, and an end to the recurring class where GitLab garbage-collects its own published analyzer digest and reddens a pipeline we never touched. A **minor**: no template input added, removed, or renamed. - **Renovate: UBI9 to UBI10 replacement rules.** `replacement` is its own updateType, separate from `major` — the human-review lane never saw these. - **`validate-*` harness is playable.** Image-tag interpolation fixed and wiring corrected. Stays `manual` + `allow_failure` by deliberate decision until language fixtures exist. - **`ci-runtime-node` on UBI10** (`ubi10/nodejs-22-minimal`), removing ten unfixable HIGH curl/libcurl CVEs — 0 of 107 measured findings had a Red Hat fix, so the base moved rather than the finding being waived. - **Fedora containers repoint** `quay.io` to `registry.fedoraproject.org`. - **hadolint checksum** reads the combined `checksums.sha256` (upstream 2.15.0+). - **Rust jobs build, test and clippy `--locked`.** - **`secret-verdict` no longer pins a digest.** GitLab GC'd its own published `security-products/secrets` digest, which reddened this pipeline twice on an image we never changed; it now tracks the immutable version tag. - **`claude-drift-triage`** default model id corrected. - **`lint-includes`** tolerates an `image:` override on suppressed jobs. - **Eleven `pipeline.just` recipes guarded against unbound `CI_COMMIT_TAG`** under `set -u`. Release paths hard-fail with a clear message; artifact paths skip cleanly. A silent `exit 0` in a release path would report green while publishing nothing. - **`signal-digest.py`** discovers group runners dynamically instead of checking a hardcoded id pair, and its wedged-MR predicate now catches conflict-plus-armed. The old check reported a permanent false WARN on a dead runner while covering neither runner that mattered. - **`platformAutomerge: false` scoped to `dunn.dev/cairn/**`** (was estate-wide), moving the decision into Renovate's own retry loop so it self-heals. - **Stop digest-pinning `registry.gitlab.com/security-products/**`.** Pinning a digest we do not control against a registry that GCs it converts vendor housekeeping into a hard red pipeline.
-
4.4.0
protectedRelease: pipeline 4.4.0f59386d7 · ·dunn.dev/pipeline 4.4.0 Minor: secret detection that can actually fail a pipeline. One new component (secret-verdict) and one new optional input on automerge-gate (secret_job). Nothing removed or renamed, and "secret" is deliberately absent from the required-checks default, so an existing consumer that upgrades renders an identical materialized job set and an identical gate verdict. Adoption is opt-in, per repo. Added - secret-verdict: GitLab's own Security/Secret-Detection template cannot block a leak. Its analyzer shells out to gitleaks with --exit-code 0, so the job exits clean even when it finds a credential -- the findings go into gl-secret-detection-report.json and nowhere else. Flipping that template's allow_failure to false does not fix it; it only makes operational faults blocking. This component runs the same analyzer over the same scan surface, so migration is a swap of the include rather than a change in what gets flagged, then parses the report itself and derives its own verdict. It emits CHECK_SECRET=pass|fail as a dotenv for automerge-gate and also exits nonzero on a finding -- unlike osv-verdict, which always exits 0 and leaves blocking to the gate. That divergence is deliberate: a verdict-only component would leave a repo with no automerge-gate still unable to fail, reproducing the exact bug. Fail-closed throughout: a missing, zero-length, schema-foreign, or non-array report, and any nonzero analyzer exit, all emit fail and exit 1 -- absence is never read as cleanliness. Scope control is excluded_paths, visible in the diff. There is deliberately no severity knob and no allow_failure input. Proven with a planted-secret probe. Changed - automerge-gate: new secret_job input plus a matching needs: entry, handled exactly like osv_job. Consumers opt in by adding "secret" to required-checks once they include the component. Because secret-verdict reddens the pipeline itself, the gate is skipped rather than run-and-blocking when a leak is found -- GitLab skips the dependents of a failed needs: job. The pipeline is red, and automerge blocked, either way. - The catalog meta-pipeline dogfoods the new slot: secret-verdict runs for real alongside osv-verdict, and this repo's own automerge-gate sets required-checks "osv,secret". - check-principle-image-tracking-version gains a second documented exception for the GitLab secret-detection analyzer image, pinned tag@digest and Renovate-tracked. Fixed - Renovate: security-fix automerge now requires a real pipeline rather than a green dashboard park. vulnerabilityAlerts keeps automerge true but now also sets platformAutomerge false, so the merge runs through Renovate's own checkAutoMerge instead of GitLab native automerge, which registers a branch carrying zero status checks as mergeable immediately. Scoped to vulnerabilityAlerts only; every other automerge lane is untouched. No consumer migration ships with this tag. patinate, kits, iac and qobuz still include the fail-open upstream template; moving them is a separate decision. Tag convention unchanged since 2.0.0: bare semver, no v-prefix. See CHANGELOG.md for the full entry.
-
4.3.1
protectedRelease: pipeline 4.3.1025295a0 · ·dunn.dev/pipeline 4.3.1 Patch: reference-check migrates off the magic-name GITLAB_TOKEN group variable to ESTATE_READ_API_TOKEN, with a fallback during the dual-variable migration window. No new inputs, no removed/renamed inputs. Fixed - reference-check: GITLAB_TOKEN (provisioned in 4.2.0) is a magic name glab prioritizes over CI_JOB_TOKEN autologin for every glab call across every job in the dunn.dev group -- not just this recipe's own reads. A read_api-only token squatting on that name silently broke dunn.dev/cairn/pipeline's create-release job (403 insufficient_scope: glab picked up the read-only group token instead of the write-capable CI_JOB_TOKEN). Migrated the recipe to read ESTATE_READ_API_TOKEN instead, falling back to GITLAB_TOKEN during the migration window, scoped inside the recipe's own subshell only -- nothing exported process-wide. - reference-check: no longer exports an empty GITLAB_TOKEN. A naive GITLAB_TOKEN="${ESTATE_READ_API_TOKEN:-${GITLAB_TOKEN:-}}" would export an empty GITLAB_TOKEN when neither variable is set, and an empty magic-name variable shadows CI_JOB_TOKEN autologin exactly like a wrong-scoped one -- the identical failure mode this release exists to fix, reintroduced one layer down. Now unsets GITLAB_TOKEN when the resolved value is empty, so autologin sees a genuinely clean environment. The GITLAB_TOKEN fallback stays in place only for the dual-variable migration window and should be dropped once the group GITLAB_TOKEN variable is deleted. Tag convention unchanged since 2.0.0: bare semver, no v-prefix. See CHANGELOG.md for the full entry. -
4.3.0
protectedRelease: pipeline 4.3.07a8b682c · ·4.3.0 — cloudflare-worker: deploy-time script deps + worker_secret_variables Graduated from dunn.dev/gals after a live deploy failure: the deploy job now honors needs_npm_install (wrangler bundles the worker script at deploy time), and an opt-in worker_secret_variables input populates Worker secrets from CI variables via wrangler secret bulk.
-
4.2.0
protectedRelease: pipeline 4.2.055316958 · ·dunn.dev/pipeline 4.2.0 Four correctness fixes, no new inputs, no removed/renamed inputs -- follows 4.1.0's minor-not-patch precedent. Fixed - container-image: builder image (quay.io/buildah/stable) repinned off a :latest that quay.io proved (2026-08) rebuilds in place across v1.43.1, v1.43, v1, and latest alike -- untracked drift, not "always current." Repinned to v1.43.1-immutable@sha256:fc649e18..., a real non-rebuilt tag Renovate can track and bump. - renovate.json templates/*.yml customManager: the sole matchString required a quoted default: value, so container-image.yml's bare image: quay.io/buildah/stable:latest was invisible to it. Added an image:/name: key variant (quoted or bare, digest optional). - reference-check: every API-backed assertion in dunn.dev/reference's primitives (check_gitlab_setting, check_registry_cleanup_coverage) was a silent no-op in CI since introduction -- ci never installed glab, the recipe never set PROJECT_ID. Fixed: ci now bakes glab (checksum-verified, version-pinned to harness/mise.toml's gitlab-org/cli pin); the recipe exports PROJECT_ID from CI_PROJECT_ID plus GLAB_ENABLE_CI_AUTOLOGIN=true. That alone still 403'd on the general Projects API (CI_JOB_TOKEN's job-token-access table doesn't cover GET /projects/:id). Closed by a read_api group access token, wired as group CI variable GITLAB_TOKEN (masked, unprotected, glab's own higher-precedence-than-CI_JOB_TOKEN env var -- confirmed via `glab auth login --help`, and empirically: a CI_JOB_TOKEN-authenticated call would 403 on this endpoint rather than return real settings). Proven end-to-end against a real job (dunn.dev/reference, throwaway branch, this catalog's own ci image): check_registry_cleanup_coverage reads and reports a real live value (PASS ... cleanup disabled (enabled=false), job 15676948764), and a throwaway deliberate-violation assertion reports a real FAIL against real data (FAIL ... default_branch = main (expected /this-branch-does-not-exist/), job 15677067064). fail_on_violation stays false estate-wide: all 9 current consumers were audited against live state and every one fails at least one contract today (pre-existing, unrelated gaps). Fix violations first, then enforce. - renovate-config preset: the cloudflare workers ecosystem group matched almost nothing -- matchUpdateTypes:["major"] excluded wrangler's peer bump to @cloudflare/workers-types ^5 at 4.108.0, a minor, so the group MR only ever carried the types half (the exact ERESOLVE the rule existed to prevent). Dropped matchUpdateTypes entirely. Tag convention unchanged since 2.0.0: bare semver, no v-prefix. See CHANGELOG.md for the full entry.
-
4.1.0
protectedRelease: pipeline 4.1.0277024d5 · ·dunn.dev/pipeline 4.1.0 Minor: additive plus fixes. No input removed or renamed, no existing default behaviour changed. Consumers pick this up by bumping their component pins. Added - model-id-drift (new, opt-in): fails when a consumer's tracked working tree carries a hardcoded claude-(opus|sonnet|haiku) major below the declared current_ga_major (default "5"). No network access; pure regex over the already-checked-out tree. No repo is auto-enrolled. There is no Renovate datasource for Claude model ids, so a hardcoded id rots invisibly -- confirmed stale on dunn.dev/pi and dunn.dev/skald.md while Opus 5 was GA. - ci-go bakes osv-scanner, ci bakes hadolint (both checksum-verified release binaries). osv-verdict now runs on ci-go, so the gate binary is no longer fetched fresh on every MR across the estate. Changed -- renovate-config preset - go.mod indirect dependencies now propose an MR only when they carry an active OSV/GHSA alert (security:gomodIndirectSecurityUpdates). Surfaced by bairn's kin-openapi indirect dep needing a hand-diagnosed fix. - Internal-artifact MAJORs exempt from minimumReleaseAge; automerge stays false (age delay and human review are separate knobs). Closed two coverage gaps while widening: the bare-catalog depName form was never matched, and the gitlab-releases exemption is now scoped by package name so it no longer silently covered upstream gitlab-org/cli. - mergeConfidence:all-badges, display-only, for the human majors-review queue. - minimumReleaseAge:null for four evidenced timestamp-less packages (envoy via mirror.gcr.io, two immich images, @dunn.dev/reference on GitLab's npm registry) -- a strand fix by exact package name, with explicit automerge:false, not a registry-wide trust exemption. Fixed -- osv-verdict - Go call analysis runs for real now that the job is on ci-go. The gate blocks on called=true or called=unknown; a finding call analysis proves unreachable is still logged and kept in the artifact, just not gate-blocking. - Default-branch pushes diff against CI_COMMIT_BEFORE_SHA instead of full-scanning, closing the green-MR-then-red-main asymmetry. - The MR diff base was a silent no-op: git stash swept up the untracked head-scan outputs and nothing ever popped it, so the verdict was pass regardless of whether a new finding existed. Fail-open under a fail-closed gate, on every diff-mode run. Head scan outputs now survive outside the tree. Fixed -- automerge-gate, osv-verdict - Both jobs now instantiate on a bare branch pipeline with no open MR yet. The prior rules matched only merge_request_event or the default branch; audited across 97 consumer projects, ~25 had zero gate coverage in exactly that gap, where the job never emitted and needs:optional read absent rather than failed. - The clause shipped in a two-rule form and was then consolidated to one (CI_COMMIT_BRANCH && CI_OPEN_MERGE_REQUESTS == null). That is a simplification, not a behaviour change -- both forms render identically. Verified pre-tag: a synthetic consumer POSTed to ci/lint at three refs with components pinned by SHA, carrying probes that prove the rendered variable context. Gate jobs instantiate on a no-MR branch, are correctly suppressed on a branch with an open MR, and main is unchanged. A grandparent control at the commit before the branch clause existed confirms the harness discriminates: the gate jobs are absent there on the same ref and present after. Also: baked tool versions advanced (cosign 3.1.2, syft 1.49.0, just 1.57.0; ci-iac uv 0.11.32, sops 3.13.3), image ARG and template spec-input default moving in lockstep.
-
4.0.1
protectedRelease: pipeline 4.0.120226a3f · ·dunn.dev/pipeline 4.0.1 Fix: ci-runtime-go ships the CA trust bundle + tzdata. The UBI-micro base shipped neither, so a static (CGO-off) Go binary doing outbound TLS failed x509 verification on its first call -- caddy's Let's Encrypt ACME + Cloudflare DNS-01, and bairn's Famly API (total-failure for both). Every other ci-* image already installed ca-certificates on purpose; ci-runtime-go was the outlier. Staged from a throwaway full-UBI builder; final image stays package-manager-free (+3.5MB). ci-runtime-node unaffected (Node uses its own compiled-in root store). Adversarially verified pre-tag: the built image carries 146 valid PEM certs (incl. ISRG Root X1/X2) at Go's x509 paths.
-
4.0.0
protectedRelease: pipeline 4.0.054188f8b · ·dunn.dev/pipeline 4.0.0 Breaking major (dead-surface removal + supply-chain hardening). Removed (zero-consumer, estate-wide verified): - cloudflare-pages component + recipes/validate slots (use cloudflare-worker) - release-create `extra_sbom_jobs` input + upload-extra-sbom job/recipe Supply chain: - ci image bakes osv-scanner + cosign (checksum-verified) → osv-verdict gate and node-publish signing install nothing at runtime - syft installer pinned to a release tag (was mutable main) - template tool-version defaults are Renovate-tracked (customManager) Lint: lint-includes (supersedes lint-suppression) — structural YAML bijection + suppression coverage. cosign/syft fallback defaults aligned to baked versions; go-sbom skips .sha256 sidecars. Merged via !96 (rebased onto current main; pipeline green incl. osv-verdict).
-
3.1.0
protectedRelease: pipeline 3.1.0b2d0bfc4 · ·3.1.0 - pin-audit + preset improvements Adds the pin-audit component (I1 enforcement: fails the build on an image pin Renovate cannot track forward -- bare @sha256 / commit-SHA tag) + the ADOPTION.md adoption profile, now adoptable by consumers (pin-audit postdated the 3.0.0 tag, so pin-audit@3.0.0 never resolved). Plus renovate-config preset improvements accumulated since 3.0.0 (abandonment signal, automerge-pin, astro/vitest ecosystem grouping, rebaseWhen=behind-base-branch, lockFileMaintenance release-age exemption, trusted-upstream fast-track, mise-gitlab manager de-dup) and ci/ci-iac fixes. No breaking change: every component's inputs and behavior are unchanged from 3.0.0. Minor bump.
-
3.0.0
protectedRelease: pipeline 3.0.058c93002 · ·3.0.0 - baked-recipe component model BREAKING: every component's logic now lives in pipeline.just, baked into the ci-* images at /opt/dunn-pipeline/justfile. Templates are thin `just` wrappers and inputs pass via variables:. The `image:`-override escape hatch (run a component on a vanilla base and install tools on-demand) is removed: overriding `image:` to a base without the baked justfile will fail. Pin a ci-* image (the default) or fork the recipe. Migrated families: go, rust, node, the L0 gate (osv-verdict + automerge-gate), the ops/deploy family (cloudflare-pages/worker/r2, reference-check, slsa-attest, release-create, claude-drift-triage), and container-sbom. container-image stays inline by design (foreign buildah image, no baked justfile). No consumer-facing contract change: every component's inputs, artifacts, and behavior are identical to 2.7.0. Only the internal execution model changed.
-
2.7.0
protectedRelease: pipeline 2.7.01097b747 · ·2.7.0: ship osv-verdict + automerge-gate with fail-open guard First tagged release of the osv-verdict and automerge-gate components (previously in the Unreleased section). The gate blocks (exit 1) when zero required checks were evaluated, so an empty or typo'd required-checks no longer fail-OPENs to GATE GREEN.
-
2.6.0
protectedRelease: pipeline 2.6.0ece52b17 · ·pipeline 2.6.0 Image-only release; templates unchanged since 2.5.1. ci image: - ship just alongside make (estate repos converging on justfile; consumers can drop per-job dnf installs) - sigstore/cosign 3.x Repo CI: - containers rebuild on MRs that touch their Containerfile
-
2.5.1
protectedRelease: pipeline 2.5.1b3046463 · ·renovate preset: lift UBI X.Y allowedVersions estate-wide
-
2.5.0
protectedRelease: pipeline 2.5.06ee131f3 · ·2.5.0 - extra_sbom_jobs array on release-create release-create now accepts an `extra_sbom_jobs:` type-array input backing a new `upload-extra-sbom-assets` job, so consumers shipping N SBOMs per release can link all of them to the GitLab Release page. Backward compat: omitting `extra_sbom_jobs` keeps the prior single- SBOM behavior; the new job runs tag-only as a cheap no-op when the input is defaulted. GitLab CI components only support whole-value array substitution, so the multi-SBOM path lives in a second job rather than expanding the existing upload-release-assets needs: block.
-
2.3.3
protectedRelease: pipeline 2.3.3585aa3d5 · ·2.3.3 fix(container-sbom): sbom_filename input. Multi-image consumers that collocate SBOMs under one package_name can now give each job a distinct output filename so they don't overwrite each other. Default behaviour unchanged.
-
2.4.0
protectedRelease: pipeline 2.4.021750b33 · ·2.4.0 — Renovate preset, validator gate, daily cadence This release introduces the estate-wide Renovate preset as a public catalog surface, alongside the per-tag rebuild of ci-* images and the component versions that have always shipped on tag. What consumers gain ------------------- A central preset published from this repo. Every consumer renovate.json can shrink to a single line: { "extends": ["gitlab>dunn.dev/pipeline:renovate-config"] } The preset carries: platformAutomerge: true (GitLab merges minor+patch on green CI without waiting for the next Renovate run), prHourlyLimit: 0 (no per-hour cap; daily run drains the queue), assignees set to the maintainer accounts (so manual-review MRs land in the inbox), the minor+patch automerge + major manual-review packageRules, and the shared Containerfile ARG customManager used by every Pattern A repackaging consumer (caddy-estate, pipeline's own ci-* images, future auth upstream-software containers). No `schedule` field in the preset. Cadence is owned by the runner's CI cron schedule alone, with RENOVATE_FORCE overriding any stale per-repo schedule at scan time. Operational changes in this repo -------------------------------- - New `validate-renovate` job runs `renovate-config-validator --strict` on every MR touching renovate.json or renovate-config.json. A malformed preset cannot reach consumers. - Renovate runner job drops $CI_PIPELINE_SOURCE == "web". Entry points are `schedule` (daily cron) and `api` only. - Pipeline's own renovate.json now extends the preset and keeps only the two catalog-specific managers: UBI date-stamped-tag filter and the templates/*.yml Docker pin regex. - README gains an Architecture section with the end-to-end Mermaid diagram of the single-maintainer estate. Container images ---------------- ci, ci-go, ci-rust, ci-runtime-go, ci-runtime-node all published at :2.4.0 alongside this tag, matching every consumer's @2.4.0 pin under the v2.0.0 triple-coupling contract. Migration --------- No breaking changes for existing consumers. Per-repo renovate.json files continue to work; migration to the central preset is hygiene and can be done one repo at a time. The fan-out lands as a sequence of small per-repo MRs starting with dunn.dev/reference. -
2.3.2
protectedRelease: pipeline 2.3.2b570d1f3 · ·2.3.2 chore: UBI runtime bases on clean X.Y tags. - ci-runtime-go: ubi9/ubi-micro:9.8 (was 9.7-1778461406) - ci-runtime-node: ubi9/nodejs-22-minimal:9.8 (was :1) Plus renovate.json package rule constraining UBI image tag matching to /^[0-9]+\.[0-9]+$/, preventing drift back to date-stamped tags. Same UBI under the hood; cleaner pin, less PR churn for consumers.