Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions go/internal/stack/postgres_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
48 changes: 48 additions & 0 deletions tools/renovate/config.json5
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<tag>@<digest>` docker reference.
customType: "regex",
managerFilePatterns: ["/^go/internal/stack/postgres_image\\.go$/"],
matchStrings: [
"postgres:(?<currentValue>\\d+)@(?<currentDigest>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) ──
Expand Down Expand Up @@ -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
Expand Down
145 changes: 145 additions & 0 deletions tools/renovate/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// <tag>@<digest> 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
Expand Down
Loading