From 34f791177c5362912b3735097344359a7b2dfb72 Mon Sep 17 00:00:00 2001 From: mintaka Date: Thu, 27 Aug 2026 00:49:04 -0400 Subject: [PATCH] feat(renovate): auto-bump DefaultPostgresImage digest within postgres:18 (RIG-2774) Surface go/internal/stack/postgres_image.go's DefaultPostgresImage Go const as a docker dep via a custom.regex manager in tools/renovate/config.json5 so upstream postgres:18 security rebuilds (same major, new digest) flow through a reviewable PR instead of the pin silently rotting. DL-260 freezes the postgres MAJOR at 18 for on-disk-format stability, so a paired packageRule pins allowedVersions to /^18$/: the digest moves, an 18->19 major bump stays a deliberate design action (re-run the T8 podman integration test), never an auto-PR. Two conventions the file's shape forces: - depName is 'postgres-stack', NOT 'postgres': the CI-service disable fence (matchDepNames ['postgres'], enabled false) is unscoped by manager/file, so a 'postgres' depName would inherit the disable and open zero PRs. The distinct name keeps the two postgres pins independently governed; a last-match-wins replay test asserts postgres-stack resolves ENABLED while 'postgres' stays disabled. - versioningTemplate is explicit 'docker': a custom.regex manager defaults to semver-coerced regardless of datasource, which mishandles a @ reference. The repo-wide 5-day minimumReleaseAge soak is kept: Docker Hub carries a tag_last_pushed timestamp for the digest, so a rebuild clears the window (no permanent-pending stall the git-refs channel dep needs nulled). config.test.ts gains a RIG-2774 block: manager shape, live regex extraction against the real Go file, the digest-only /^18$/ rule, and the fence- independence behavioral guard. Updates the const's bump-procedure comment to describe the automated digest path. Spec-impact: none Co-authored-by: Matt Wilkinson --- go/internal/stack/postgres_image.go | 16 +-- tools/renovate/config.json5 | 48 +++++++++ tools/renovate/config.test.ts | 145 ++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 6 deletions(-) diff --git a/go/internal/stack/postgres_image.go b/go/internal/stack/postgres_image.go index c2d3a4f9f..bbde69ced 100644 --- a/go/internal/stack/postgres_image.go +++ b/go/internal/stack/postgres_image.go @@ -17,10 +17,14 @@ package stack // major/minor-version-sensitive, and a mutable tag would ship an unreviewed // database under the installed stack. // -// Bump procedure: advance the digest below when the postgres minor/major moves, -// then re-run the T8 container integration test (up -> probe DSN -> fresh-process -// down -> container gone) against the new digest before landing. Keep the major -// aligned with pgtest.go's pin discipline so a dev-box stack and an installed -// stack never skew on-disk format. This is a Go const Renovate cannot see (like -// pgtest.go's pgImage), so it moves only via a reviewed manual PR. +// Bump procedure: a DIGEST bump within postgres:18 (an upstream security rebuild, +// same major) is automated — Renovate surfaces this const as a docker dep via a +// customManager in tools/renovate/config.json5 (depName `postgres-stack`) and +// opens a reviewable PR to advance the digest. A MAJOR bump (18 -> 19) is frozen +// by DL-260 for on-disk-format stability and stays manual: the customManager's +// packageRule pins allowedVersions to /^18$/, so 18 -> 19 is never auto-proposed. +// When you do move the major deliberately, re-run the T8 container integration +// test (up -> probe DSN -> fresh-process down -> container gone) against the new +// digest before landing, and keep the major aligned with pgtest.go's pin +// discipline so a dev-box stack and an installed stack never skew on-disk format. const DefaultPostgresImage = "docker.io/library/postgres:18@sha256:1957b2ff3137e4ef7f3bc813e74fff50b1e1ffddc85c8b9d6f14ade972be8687" diff --git a/tools/renovate/config.json5 b/tools/renovate/config.json5 index b59ac0bd0..e2e8a5f2d 100644 --- a/tools/renovate/config.json5 +++ b/tools/renovate/config.json5 @@ -251,6 +251,37 @@ depNameTemplate: "renovate", datasourceTemplate: "npm", }, + { + // ── Containerized postgres image (RIG-2774, DL-260) ── + // go/internal/stack/postgres_image.go pins the S4 container-backed postgres + // image (DefaultPostgresImage) as a Go const the native managers can't see. + // Surface it as a docker dep so upstream postgres:18 security REBUILDS (same + // major, new digest) flow through a reviewable PR instead of the pin silently + // rotting. This is a STANDALONE default the T8 adapter runs, with NO parity + // coupling — unlike pgtest.go's pgImage, disabled below because it is locked + // to ci.yml's service image. DL-260 freezes the MAJOR at 18 for on-disk- + // format stability; the packageRule below (allowedVersions /^18$/) moves only + // the digest, so an 18->19 major is a deliberate design action (re-run the T8 + // podman integration test), never an auto-PR. + // + // depName is `postgres-stack`, NOT `postgres`: the CI-service fence below is + // `matchDepNames: ["postgres"], enabled: false` with no manager/file scope, + // so a `postgres` depName here would inherit that disable and open zero PRs. + // The distinct name keeps the two postgres pins independently governed. + // + // versioningTemplate is explicit `docker`: a custom.regex manager defaults to + // `semver-coerced` regardless of datasource (see the catalog manager note + // above), which mishandles a `@` docker reference. + customType: "regex", + managerFilePatterns: ["/^go/internal/stack/postgres_image\\.go$/"], + matchStrings: [ + "postgres:(?\\d+)@(?sha256:[a-f0-9]{64})", + ], + depNameTemplate: "postgres-stack", + packageNameTemplate: "docker.io/library/postgres", + datasourceTemplate: "docker", + versioningTemplate: "docker", + }, ], // ── The github-actions manager: SHA-pin maintenance (RIG-2432) ── @@ -380,6 +411,23 @@ matchDepNames: ["postgres"], enabled: false, }, + { + // ── Containerized postgres image: digest-only within major 18 (RIG-2774) ── + // DL-260 freezes the postgres MAJOR at 18 (on-disk-format stability). Pin the + // allowed version to major 18 so only the digest moves; an 18->19 major bump + // is a deliberate design action (re-run the T8 podman integration test to + // verify on-disk-format compatibility), never an auto-PR. Scoped by the + // distinct `postgres-stack` depName (see the customManager above) so it never + // touches the `postgres` CI-service fence above. No matchUpdateTypes: the + // rule must apply to ALL update types so allowedVersions filters a major + // candidate too — scoping it to `digest` would leave a `19` unfiltered. The + // repo-wide 5-day minimumReleaseAge soak is KEPT: Docker Hub carries a + // tag_last_pushed timestamp for the digest, so a rebuild clears the window + // and opens a PR (no permanent-pending stall the git-refs channel dep has). + matchManagers: ["custom.regex"], + matchDepNames: ["postgres-stack"], + allowedVersions: "/^18$/", + }, { // ── gomod `go` directive: manual floor policy ── // The `go` directive in go/go.mod tracks the tools/toolchain/versions/go.nix diff --git a/tools/renovate/config.test.ts b/tools/renovate/config.test.ts index 3561e33cf..bf67bf3ee 100644 --- a/tools/renovate/config.test.ts +++ b/tools/renovate/config.test.ts @@ -662,6 +662,151 @@ describe("tools/renovate postgres + gomod go disables", () => { }); }); +describe("tools/renovate postgres-stack digest manager (RIG-2774, DL-260)", () => { + // DefaultPostgresImage (go/internal/stack/postgres_image.go) is a standalone Go + // const the native managers can't see; a custom.regex manager surfaces it as a + // docker dep so upstream postgres:18 rebuilds (same major, new digest) flow + // through a reviewable PR. DL-260 freezes the major at 18, so the paired + // packageRule pins allowedVersions to /^18$/ — the digest moves, an 18->19 + // major never auto-opens. Find both by behavior, not index. + const pgManager = cfg.customManagers?.find((m) => + m.managerFilePatterns?.some((p) => p.includes("postgres_image")), + ); + const pgRule = cfg.packageRules.find( + (r) => + r.matchManagers?.includes("custom.regex") && + r.matchDepNames?.includes("postgres-stack"), + ); + + test("a docker custom.regex manager surfaces the pin (postgres-stack, docker versioning)", () => { + expect(pgManager).toBeDefined(); + expect(pgManager?.customType).toBe("regex"); + expect(pgManager?.datasourceTemplate).toBe("docker"); + expect(pgManager?.depNameTemplate).toBe("postgres-stack"); + expect(pgManager?.packageNameTemplate).toBe("docker.io/library/postgres"); + // Explicit docker versioning: a custom.regex manager defaults to + // semver-coerced regardless of datasource, which mishandles a + // @ docker reference. + expect(pgManager?.versioningTemplate).toBe("docker"); + }); + + test("its regex extracts the tag + digest from the real postgres_image.go", () => { + const src = readFileSync( + join(repoRoot, "go", "internal", "stack", "postgres_image.go"), + "utf8", + ); + const pattern = pgManager?.matchStrings?.[0]; + expect(pattern).toBeDefined(); + // Exactly one qualifying pin: use matchAll (not exec) so a second + // accidental postgres:NN@sha256 string in the Go file — which Renovate + // would silently extract as a second dep — fails this build closed. + const matches = [...src.matchAll(new RegExp(pattern as string, "g"))]; + expect(matches).toHaveLength(1); + expect(matches[0]?.groups?.currentValue).toBe("18"); + expect(matches[0]?.groups?.currentDigest).toMatch(/^sha256:[a-f0-9]{64}$/); + }); + + test("the digest-only-within-18 rule exists (postgres-stack, allowedVersions /^18$/)", () => { + expect(pgRule).toBeDefined(); + const allowedVersions = pgRule?.allowedVersions ?? ""; + expect(allowedVersions).toBe("/^18$/"); + expect(pgRule?.matchDepNames).toEqual(["postgres-stack"]); + // No matchUpdateTypes: the version filter must apply to ALL update types so + // an 18->19 major candidate is filtered too — scoping to `digest` would + // leave a major unfiltered. + expect(pgRule?.matchUpdateTypes).toBeUndefined(); + // Semantic teeth: derive the matcher from the configured value (strip the + // /.../ delimiters) and assert it accepts 18 while rejecting a 19 major — + // so a fat-fingered allowedVersions (e.g. /^1[89]$/) that still admits 19 + // fails here, not just a changed literal. + const versionMatcher = new RegExp(allowedVersions.slice(1, -1)); + expect(versionMatcher.test("18")).toBe(true); + expect(versionMatcher.test("19")).toBe(false); + }); + + // Load-bearing behavioral guard: the CI-service disable fence + // (matchDepNames ["postgres"], enabled false) is unscoped by manager/file, so a + // `postgres` depName here would inherit the disable and open ZERO PRs. Replay + // Renovate's last-match-wins packageRule semantics (mirroring resolveGroupName's + // gates) for a synthetic postgres-stack docker dep and confirm it resolves + // ENABLED — this fails closed if the fence (or any future unscoped rule) ever + // swallows postgres-stack, silently defeating the automation. + const resolveEnabled = (dep: SyntheticDep): boolean => { + let enabled = true; + for (const rule of cfg.packageRules) { + if (rule.matchManagers && !rule.matchManagers.includes(dep.manager)) { + continue; + } + if ( + rule.matchUpdateTypes && + !(dep.updateType && rule.matchUpdateTypes.includes(dep.updateType)) + ) { + continue; + } + if ( + rule.matchDepTypes && + !(dep.depType && rule.matchDepTypes.includes(dep.depType)) + ) { + continue; + } + if ( + rule.matchDepNames && + !(dep.depName && rule.matchDepNames.includes(dep.depName)) + ) { + continue; + } + if ( + rule.matchPackageNames && + !(dep.packageName && rule.matchPackageNames.includes(dep.packageName)) + ) { + continue; + } + if ( + rule.matchFileNames && + !( + dep.fileName && + rule.matchFileNames.some((g) => + globToRegExp(g).test(dep.fileName as string), + ) + ) + ) { + continue; + } + if ( + rule.excludeDepNames && + dep.depName && + rule.excludeDepNames.includes(dep.depName) + ) { + continue; + } + if (typeof rule.enabled === "boolean") enabled = rule.enabled; + } + return enabled; + }; + + test("a postgres-stack docker dep resolves ENABLED (fence independence)", () => { + expect( + resolveEnabled({ + manager: "custom.regex", + depName: "postgres-stack", + packageName: "docker.io/library/postgres", + fileName: "go/internal/stack/postgres_image.go", + updateType: "digest", + }), + ).toBe(true); + // And the original `postgres` CI-service dep stays DISABLED — the two pins + // remain independently governed. + expect( + resolveEnabled({ + manager: "github-actions", + depName: "postgres", + fileName: ".github/workflows/ci.yml", + updateType: "digest", + }), + ).toBe(false); + }); +}); + describe("tools/renovate bun-types soak exemption ↔ bunfig excludes", () => { // The catalog-scoped soak-exemption packageRule governs ONLY catalog deps // (matchManagers custom.regex + matchDepTypes workspaces.catalog), so its