From e73dd550f3ddf2aa51144e2ddc86b18245e33b9f Mon Sep 17 00:00:00 2001 From: mintaka Date: Thu, 27 Aug 2026 14:59:24 -0400 Subject: [PATCH 1/2] fix(renovate): mirror flake.nix Go vendorHash + cap wails/v3 below 3.1 (RIG-2852) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two Renovate gaps surfaced landing GTK4 T1 (RIG-2818, PR #667), both in tools/renovate/. Gap 1 (bug, all Go bumps): refresh-fod-hashes.ts refreshed the Go vendorHash in guest-image/default.nix but not the IDENTICAL one in flake.nix:52 — both are buildGoModule + proxyVendor over the same go/, so a gomod bump moves both and the stale flake.nix pin failed `nix flake check` (hash-mismatch), the RIG-2432 easy-dep-bump failure class. Add a `mirrorFiles` field to the Go FodEntry: the build vehicle realises guestd's FOD once, and the recomputed SRI is mirrored to flake.nix (equal by construction, no second realise). Add flake.nix to the top-level postUpgradeTasks.fileFilters so Renovate commits the mirror edit. Gap 2 (safety, wails): no cap on github.com/wailsapp/wails/v3, so Renovate would auto-open a v3.1 bump violating the GTK4-migration "Never v3.1" floor (compass-gtk4-migration/design.md §Global Constraints). Add a gomod packageRule capping it with a REGEX allowedVersions `/^v?3\.0\./` — not a semver range, because gomod's node-semver ranges exclude a prerelease at a different major.minor.patch, so `< 3.1.0-0` would wrongly reject the current v3.0.0-beta.0 pin. Admits v3.0.0-beta.N and future v3.0.x; rejects v3.1+ and v4+. Tests: refresh-fod-hashes.test.ts asserts the flake.nix mirror is refreshed to the same SRI on a go/go.mod bump (red-green verified against a neutered mirror loop); config.test.ts pins the flake.nix fileFilter and replays the wails regex against v3.0.0-beta.0 / v3.1.0 / v4.0.0 (red-green verified against a loosened cap). renovate:ci green (92 pass, typecheck + biome clean). Refs RIG-2852, RIG-2818, PR #667. --- tools/renovate/config.json5 | 30 +++++++++++++ tools/renovate/config.test.ts | 53 ++++++++++++++++++++++- tools/renovate/refresh-fod-hashes.test.ts | 38 ++++++++++++++++ tools/renovate/refresh-fod-hashes.ts | 31 +++++++++++-- 4 files changed, 146 insertions(+), 6 deletions(-) diff --git a/tools/renovate/config.json5 b/tools/renovate/config.json5 index e2e8a5f2d..977b538cc 100644 --- a/tools/renovate/config.json5 +++ b/tools/renovate/config.json5 @@ -441,6 +441,31 @@ matchDepNames: ["go"], enabled: false, }, + { + // ── wails/v3: hold below v3.1 (GTK4-migration floor, RIG-2818/RIG-2819) ── + // The GTK4 migration record freezes a hard "Never v3.1" floor + // (docs/designs/product/compass-gtk4-migration/design.md §Global Constraints + // "Wails floor"): wails v3.1 drops the legacy GTK3 build tag, so bumping to it + // before the GTK4 flip (RIG-2819) is proven would strand the app with no + // working native shell. go/go.mod pins v3.0.0-beta.0 today; Renovate would + // auto-open a v3.1.x bump the moment upstream ships it, violating the floor + // silently. Cap it here until the flip lands and every wails bump is gated by + // the gtk4-e2e lane. + // + // REGEX, not a semver range: gomod uses `semver` versioning, whose node-semver + // ranges admit a prerelease ONLY when a comparator shares its exact + // major.minor.patch tuple — so `< 3.1.0-0` would REJECT the current + // v3.0.0-beta.0 (its only comparator sits at tuple 3.1.0), opening zero PRs + // and freezing the pin. A slash-delimited regex is matched against the raw + // version string instead, sidestepping prerelease-range semantics entirely + // (mirrors the postgres-stack `/^18$/` rule above). `^v?3\.0\.` admits + // v3.0.0-beta.N and any future v3.0.x, rejects v3.1.x and v4+. config.test.ts + // replays it against v3.0.0-beta.0 / v3.1.0 / v4.0.0. Remove this rule + its + // guard once RIG-2819's GTK4 flip is merged. + matchManagers: ["gomod"], + matchDepNames: ["github.com/wailsapp/wails/v3"], + allowedVersions: "/^v?3\\.0\\./", + }, { // ── bunfig soak exemption for the bun types packages ── // bunfig.toml `minimumReleaseAgeExcludes` exempts `@types/bun` and `bun-types` @@ -645,6 +670,11 @@ "tools/toolchain/versions/node.nix", "tools/toolchain/versions/moon.nix", "guest-image/default.nix", + // flake.nix carries the SAME Go vendorHash as guest-image/default.nix + // (both buildGoModule + proxyVendor over go/); refresh-fod-hashes.ts mirrors + // the recomputed value into it, so it must be committable too or a gomod + // bump lands with flake.nix's vendorHash stale → `nix flake check` red. + "flake.nix", "agent-image/entrypoint.nix", ], executionMode: "branch", diff --git a/tools/renovate/config.test.ts b/tools/renovate/config.test.ts index bf67bf3ee..b21b8010c 100644 --- a/tools/renovate/config.test.ts +++ b/tools/renovate/config.test.ts @@ -281,10 +281,16 @@ describe("tools/renovate FOD-hash refresh wiring (PR #579)", () => { expect(topLevel?.executionMode).toBe("branch"); }); - test("the top-level task commits BOTH FOD files (fileFilters cover them)", () => { + test("the top-level task commits ALL THREE Go/bun FOD files (fileFilters cover them)", () => { // gomod branches + bun/npm-first branches inherit this slot; it must be able - // to commit both the Go vendorHash file and the bun outputHash file. + // to commit the Go vendorHash file, its flake.nix mirror (identical hash, + // same buildGoModule proxyVendor set over go/ — refresh-fod-hashes.ts mirrors + // the recomputed value into it), and the bun outputHash file. Renovate only + // commits files a task's fileFilters names, so a missing flake.nix here would + // silently drop the mirror edit → a gomod bump lands with flake.nix's + // vendorHash stale and `nix flake check` red (RIG-2852 Gap 1). expect(topLevel?.fileFilters).toContain("guest-image/default.nix"); + expect(topLevel?.fileFilters).toContain("flake.nix"); expect(topLevel?.fileFilters).toContain("agent-image/entrypoint.nix"); }); @@ -662,6 +668,49 @@ describe("tools/renovate postgres + gomod go disables", () => { }); }); +describe("tools/renovate wails/v3 floor cap (RIG-2852, GTK4 migration)", () => { + // The GTK4 migration record freezes a "Never v3.1" floor: wails v3.1 removes + // the legacy GTK3 build tag, so an auto-opened v3.1 bump before the GTK4 flip + // (RIG-2819) is proven would strand the app with no native shell. A gomod + // packageRule caps github.com/wailsapp/wails/v3 below v3.1 via a REGEX + // allowedVersions (not a semver range — gomod's node-semver ranges exclude a + // prerelease at a different major.minor.patch, so `< 3.1.0-0` would wrongly + // reject the current v3.0.0-beta.0). Find it by behavior, not index. + const wailsRule = cfg.packageRules.find( + (r) => + r.matchManagers?.includes("gomod") && + r.matchDepNames?.includes("github.com/wailsapp/wails/v3"), + ); + + test("a gomod cap rule exists for wails/v3 with a regex allowedVersions", () => { + expect(wailsRule).toBeDefined(); + expect(wailsRule?.matchManagers).toEqual(["gomod"]); + expect(wailsRule?.matchDepNames).toEqual(["github.com/wailsapp/wails/v3"]); + const allowedVersions = wailsRule?.allowedVersions ?? ""; + // Slash-delimited regex form (matched against the raw version string), + // mirroring the postgres-stack /^18$/ rule. + expect(allowedVersions.startsWith("/")).toBe(true); + expect(allowedVersions.endsWith("/")).toBe(true); + }); + + test("the cap admits v3.0.0-beta.N and future v3.0.x, rejects v3.1.x and v4+", () => { + // Compile the shipped regex from its /.../ delimiters and replay it against + // the real pinned version and the floor boundary — so a fat-fingered cap + // that still admitted v3.1 (e.g. `^v?3\.`) fails here, not just a changed + // literal. go/go.mod pins v3.0.0-beta.0 today; it MUST stay admitted or the + // cap opens zero PRs and freezes the pin (the SEA-1220 silent-no-updates + // shape). + const allowedVersions = wailsRule?.allowedVersions ?? ""; + const matcher = new RegExp(allowedVersions.slice(1, -1)); + expect(matcher.test("v3.0.0-beta.0")).toBe(true); // the current pin + expect(matcher.test("v3.0.0-beta.7")).toBe(true); // a newer beta + expect(matcher.test("v3.0.1")).toBe(true); // a future v3.0 patch + expect(matcher.test("v3.1.0")).toBe(false); // the frozen floor + expect(matcher.test("v3.1.0-beta.0")).toBe(false); // a v3.1 prerelease + expect(matcher.test("v4.0.0")).toBe(false); // a future major + }); +}); + 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 diff --git a/tools/renovate/refresh-fod-hashes.test.ts b/tools/renovate/refresh-fod-hashes.test.ts index d47f751b4..2e38dc4a1 100644 --- a/tools/renovate/refresh-fod-hashes.test.ts +++ b/tools/renovate/refresh-fod-hashes.test.ts @@ -78,6 +78,16 @@ const GO_NIX_FIXTURE = `let }; in guestd `; +// The flake.nix mirror carries the SAME Go vendorHash marker; the script writes +// the recomputed value here too (FodEntry.mirrorFiles). A distinct placeholder so +// a missing-mirror regression is unmistakable (it would stay at this value). +const PLACEHOLDER_GO_MIRROR = "sha256-PLACEHOLDERflakemirror00000000000000000="; +const GO_MIRROR_FIXTURE = `let + compass-app = pkgs.buildGoModule { + ${GO_ENTRY.marker}${bodyOf(PLACEHOLDER_GO_MIRROR)}"; + }; +in compass-app +`; const BUN_NIX_FIXTURE = `let nodeModules = pkgs.stdenv.mkDerivation { ${BUN_ENTRY.marker}${bodyOf(PLACEHOLDER_BUN)}"; @@ -133,6 +143,12 @@ async function buildBaselineRepo(): Promise { // Pin files at the paths the TABLE declares. await write(GO_ENTRY.file, GO_NIX_FIXTURE); await write(BUN_ENTRY.file, BUN_NIX_FIXTURE); + // Mirror pin files the Go entry declares (same vendorHash, refreshed in + // lockstep — derived from the shipped table so a rebase that edits mirrorFiles + // keeps this honest with no second edit). + for (const mirror of GO_ENTRY.mirrorFiles ?? []) { + await write(mirror, GO_MIRROR_FIXTURE); + } // Trigger manifests (content is irrelevant; only their diff-vs-base matters). for (const trigger of [...GO_ENTRY.triggers, ...BUN_ENTRY.triggers]) { await write(trigger, "baseline\n"); @@ -198,6 +214,28 @@ describe("tools/renovate/refresh-fod-hashes.ts gate (PR #579)", () => { ); }); + // RIG-2852 Gap 1: the SAME go/go.mod bump must refresh the flake.nix MIRROR to + // the identical value — not just guest-image/default.nix. Before this fix the + // mirror was never touched, so an auto-opened Go bump landed with flake.nix's + // vendorHash stale and `nix flake check` red. Assert every declared mirror got + // the go-modules SRI and none kept its distinct placeholder. + test("refreshes every flake.nix mirror to the same SRI on a go/go.mod bump", async () => { + await Bun.write(join(repo, "go/go.mod"), "bumped\n"); + + const res = await runRefresh(repo); + expect(res.exitCode).toBe(0); + + const mirrors = GO_ENTRY.mirrorFiles ?? []; + expect(mirrors.length).toBeGreaterThan(0); // the Go entry declares flake.nix + for (const mirror of mirrors) { + const text = await readFile(join(repo, mirror), "utf8"); + expect(hashOnMarker(text, GO_ENTRY.marker)).toBe( + stubSriForFragment("go-modules"), + ); + expect(text).not.toContain(bodyOf(PLACEHOLDER_GO_MIRROR)); + } + }); + // A gomod-only bump refreshes the Go vendorHash and leaves the bun outputHash // pin byte-for-byte untouched — the per-FOD self-gate granularity. test("a go/go.mod bump leaves the bun outputHash pin untouched", async () => { diff --git a/tools/renovate/refresh-fod-hashes.ts b/tools/renovate/refresh-fod-hashes.ts index 81b9f2116..fe02ad1b3 100755 --- a/tools/renovate/refresh-fod-hashes.ts +++ b/tools/renovate/refresh-fod-hashes.ts @@ -4,12 +4,19 @@ // red on a `hash mismatch in fixed-output derivation` build break (the RIG-2432 // easy-dep-bump goal, PR #579's failure class). // -// Compass pins exactly two FOD hashes outside the vendored forks/ trees, each -// content-addressing a fetched dependency set that MOVES when a manifest bumps: +// Compass pins two FOD hash VALUES outside the vendored forks/ trees, each +// content-addressing a fetched dependency set that MOVES when a manifest bumps. +// The Go vendorHash is pinned in TWO files that share it by design (below): // // guest-image/default.nix vendorHash compass-guestd's Go module set -// (buildGoModule; no vendor/ dir) — -// invalidated by a go/go.mod|go.sum bump +// flake.nix vendorHash compass-app + cmd-binaries' module set +// — the SAME proxyVendor hash over go/ +// (flake.nix:46-52 documents the equality), +// both invalidated by a go/go.mod|go.sum +// bump. The build vehicle realises ONLY +// guestd's FOD; flake.nix is refreshed as a +// MIRROR — the identical value, no second +// realise — see FodEntry.mirrorFiles. // agent-image/entrypoint.nix outputHash compass-agent's installed node_modules // tree (recursive FOD of `bun install`) — // invalidated by a bun.lock bump @@ -85,6 +92,13 @@ export type FodEntry = { // Manifests whose change invalidates this FOD (repo-root-relative). The // per-entry self-gate fires when any of these differs from the base branch. triggers: string[]; + // Extra files carrying the IDENTICAL pinned hash (same `marker`), equal to + // `file`'s by construction — e.g. a second buildGoModule with the same + // proxyVendor set over the same go/. They are NOT separately realised (the + // build vehicle content-addresses only `file`'s FOD, so a faked mirror pin + // would never surface in its output); each is rewritten to the SRI `file`'s + // realise reports. Absent for a lone pin. + mirrorFiles?: string[]; }; export const FOD_ENTRIES: FodEntry[] = [ @@ -93,6 +107,7 @@ export const FOD_ENTRIES: FodEntry[] = [ marker: 'vendorHash = "sha256-', drvFragment: "go-modules", triggers: ["go/go.mod", "go/go.sum"], + mirrorFiles: ["flake.nix"], }, { file: "agent-image/entrypoint.nix", @@ -264,6 +279,14 @@ async function main(): Promise { rewriteInlineHash(origText, entry.marker, got, entry.file), ); console.log(`renovate-fod: ${entry.file} -> ${got}`); + for (const mirror of entry.mirrorFiles ?? []) { + const mirrorText = await Bun.file(mirror).text(); + await Bun.write( + mirror, + rewriteInlineHash(mirrorText, entry.marker, got, mirror), + ); + console.log(`renovate-fod: ${mirror} (mirror) -> ${got}`); + } } console.log("renovate-fod: FOD hashes refreshed."); From 97f71bfd342e55d7f92ed154070ea49bcead66a5 Mon Sep 17 00:00:00 2001 From: mintaka Date: Thu, 27 Aug 2026 15:16:21 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix(renovate):=20address=20RIG-2852=20revie?= =?UTF-8?q?w=20=E2=80=94=20de-stale=20wails=20comment,=20tie=20cap=20test?= =?UTF-8?q?=20to=20live=20pin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 1 (0 high / 0 medium / 2 low) doc-accuracy nits, both fixed: - config.json5: the wails-cap comment said go/go.mod pins v3.0.0-beta.0, but it now pins v3.0.0-beta.14 (Renovate bumped it via RIG-2818). No behavioral change — the regex admits both — but the comment was stale. Made it version-agnostic ("a v3.0.0 prerelease today") so it can't drift on the next beta bump. - config.test.ts: the cap-replay test asserted hard-coded v3.0.0-beta.0/beta.7, never the actual pin. Now it reads the wails require line from go/go.mod and asserts the shipped regex admits whatever is pinned — tying the cap to ground truth (mirrors the postgres-stack live-extraction test), so a future pin/regex pairing that would open zero PRs fails here. Red-green verified: a cap that rejects the live pin reds the ground-truth assertion. renovate:ci green (92 pass, typecheck + biome clean). Refs RIG-2852. Co-authored-by: Matt Wilkinson --- tools/renovate/config.json5 | 9 +++++---- tools/renovate/config.test.ts | 26 +++++++++++++++++--------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/tools/renovate/config.json5 b/tools/renovate/config.json5 index 977b538cc..8682ffcd6 100644 --- a/tools/renovate/config.json5 +++ b/tools/renovate/config.json5 @@ -447,20 +447,21 @@ // (docs/designs/product/compass-gtk4-migration/design.md §Global Constraints // "Wails floor"): wails v3.1 drops the legacy GTK3 build tag, so bumping to it // before the GTK4 flip (RIG-2819) is proven would strand the app with no - // working native shell. go/go.mod pins v3.0.0-beta.0 today; Renovate would + // working native shell. go/go.mod pins a v3.0.0 prerelease today; Renovate would // auto-open a v3.1.x bump the moment upstream ships it, violating the floor // silently. Cap it here until the flip lands and every wails bump is gated by // the gtk4-e2e lane. // // REGEX, not a semver range: gomod uses `semver` versioning, whose node-semver // ranges admit a prerelease ONLY when a comparator shares its exact - // major.minor.patch tuple — so `< 3.1.0-0` would REJECT the current - // v3.0.0-beta.0 (its only comparator sits at tuple 3.1.0), opening zero PRs + // major.minor.patch tuple — so `< 3.1.0-0` would REJECT the current v3.0.0 + // prerelease pin (its only comparator sits at tuple 3.1.0), opening zero PRs // and freezing the pin. A slash-delimited regex is matched against the raw // version string instead, sidestepping prerelease-range semantics entirely // (mirrors the postgres-stack `/^18$/` rule above). `^v?3\.0\.` admits // v3.0.0-beta.N and any future v3.0.x, rejects v3.1.x and v4+. config.test.ts - // replays it against v3.0.0-beta.0 / v3.1.0 / v4.0.0. Remove this rule + its + // replays it against the live go.mod pin plus the v3.0.x / v3.1.x / v4.0.0 + // boundaries. Remove this rule + its // guard once RIG-2819's GTK4 flip is merged. matchManagers: ["gomod"], matchDepNames: ["github.com/wailsapp/wails/v3"], diff --git a/tools/renovate/config.test.ts b/tools/renovate/config.test.ts index b21b8010c..88a16f86b 100644 --- a/tools/renovate/config.test.ts +++ b/tools/renovate/config.test.ts @@ -675,7 +675,7 @@ describe("tools/renovate wails/v3 floor cap (RIG-2852, GTK4 migration)", () => { // packageRule caps github.com/wailsapp/wails/v3 below v3.1 via a REGEX // allowedVersions (not a semver range — gomod's node-semver ranges exclude a // prerelease at a different major.minor.patch, so `< 3.1.0-0` would wrongly - // reject the current v3.0.0-beta.0). Find it by behavior, not index. + // reject the current v3.0.0 prerelease pin). Find it by behavior, not index. const wailsRule = cfg.packageRules.find( (r) => r.matchManagers?.includes("gomod") && @@ -693,16 +693,24 @@ describe("tools/renovate wails/v3 floor cap (RIG-2852, GTK4 migration)", () => { expect(allowedVersions.endsWith("/")).toBe(true); }); - test("the cap admits v3.0.0-beta.N and future v3.0.x, rejects v3.1.x and v4+", () => { - // Compile the shipped regex from its /.../ delimiters and replay it against - // the real pinned version and the floor boundary — so a fat-fingered cap - // that still admitted v3.1 (e.g. `^v?3\.`) fails here, not just a changed - // literal. go/go.mod pins v3.0.0-beta.0 today; it MUST stay admitted or the - // cap opens zero PRs and freezes the pin (the SEA-1220 silent-no-updates - // shape). + test("the cap admits the LIVE go.mod pin + future v3.0.x, rejects v3.1.x and v4+", () => { + // Compile the shipped regex from its /.../ delimiters and replay it. The + // load-bearing assertion reads the ACTUAL wails require line from go/go.mod + // and asserts the cap admits whatever is pinned — so a future pin/regex + // pairing that would open zero PRs (the cap silently rejecting the real pin, + // the SEA-1220 freeze shape) fails HERE, tied to ground truth rather than a + // hard-coded literal. The boundary cases below then pin the reject edge so a + // fat-fingered cap (e.g. `^v?3\.`) that leaked v3.1 also fails. const allowedVersions = wailsRule?.allowedVersions ?? ""; const matcher = new RegExp(allowedVersions.slice(1, -1)); - expect(matcher.test("v3.0.0-beta.0")).toBe(true); // the current pin + + const goMod = readFileSync(join(repoRoot, "go", "go.mod"), "utf8"); + const pin = goMod.match( + /github\.com\/wailsapp\/wails\/v3\s+(?\S+)/, + )?.groups?.version; + expect(pin).toBeDefined(); // the require line must exist + expect(matcher.test(pin as string)).toBe(true); // the cap MUST admit it + expect(matcher.test("v3.0.0-beta.7")).toBe(true); // a newer beta expect(matcher.test("v3.0.1")).toBe(true); // a future v3.0 patch expect(matcher.test("v3.1.0")).toBe(false); // the frozen floor