From 569b0ad0fc8806c7f6dc161666b3b5a116735575 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Fri, 2 Oct 2026 20:21:08 +0000 Subject: [PATCH] ci: let the adapter build in a release PR, and rehearse that state on every change The 0.6.0 release PR (#43) failed "kube adapter (go)": gateway/kube/go.mod requires gateway v0.6.0, whose tag does not exist until the PR merges, and workspace mode still reads the required version's go.mod ("unknown revision gateway/v0.6.0"). main never requires an untagged version, so nothing caught it. - go.work replaces the core at exactly the version the adapter requires (Go refuses an unversioned replace of a workspace module). The line carries x-release-please-version, and Release Please updates /go.work with gateway/kube/go.mod. - .github/scripts/release-rehearsal.mjs applies Release Please's own version regex to every marked line with a version that is never tagged (0.999.0), vets and builds the adapter, and restores the files. It fails if go.work and the adapter's requirement disagree. It runs in CI's kube job and in task verify. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/release-rehearsal.mjs | 65 +++++++++++++++++++++++++++ .github/workflows/ci.yml | 9 ++++ Taskfile.yml | 7 +++ docs/releasing.md | 5 ++- go.work | 8 ++++ release-please-config.json | 4 ++ 6 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/release-rehearsal.mjs diff --git a/.github/scripts/release-rehearsal.mjs b/.github/scripts/release-rehearsal.mjs new file mode 100644 index 0000000..3894587 --- /dev/null +++ b/.github/scripts/release-rehearsal.mjs @@ -0,0 +1,65 @@ +// Rehearse a release pull request: build the Go adapter the way it will be built in one. +// +// Release Please rewrites every line marked `x-release-please-version` to the version being released, +// and that version's tag does not exist until the release PR merges. Builds that pass on main can +// therefore fail on the release PR (0.6.0 did: workspace mode still read gateway@v0.6.0's go.mod). +// This applies Release Please's own version regex to the marked lines, with a version that can never +// be tagged, vets the adapter, and restores the files. +// +// It also refuses drift: go.work's replacement must name the version gateway/kube/go.mod requires. + +import { execFileSync } from "node:child_process"; +import { readFileSync, writeFileSync } from "node:fs"; + +// Verbatim from release-please src/updaters/generic.ts. +const VERSION_REGEX = /(?\d+)\.(?\d+)\.(?\d+)(-(?[\w.]+))?(\+(?[-\w.]+))?/; +const MARKER = "x-release-please-version"; +// A v0 version (the module path has no /vN suffix) that will never be tagged. +const REHEARSED = "0.999.0"; +const CORE = "github.com/ConfigButler/krm-stream/gateway v"; + +const files = ["gateway/kube/go.mod", "go.work"]; +const originals = new Map(files.map((f) => [f, readFileSync(f, "utf8")])); + +const coreVersion = (file) => { + const line = originals + .get(file) + .split("\n") + .find((l) => l.includes(CORE) && l.includes(MARKER)); + if (!line) throw new Error(`${file}: no ${MARKER} line for the core module`); + return line.slice(line.indexOf(CORE) + CORE.length).split(/\s/)[0]; +}; + +const required = coreVersion("gateway/kube/go.mod"); +const replaced = coreVersion("go.work"); +if (required !== replaced) { + console.error( + `::error file=go.work::go.work replaces core v${replaced}, but gateway/kube/go.mod requires v${required}. ` + + "They must name the same version; Release Please moves both.", + ); + process.exit(1); +} + +try { + for (const [file, text] of originals) { + const rehearsed = text + .split("\n") + .map((line) => (line.includes(MARKER) ? line.replace(VERSION_REGEX, REHEARSED) : line)) + .join("\n"); + writeFileSync(file, rehearsed); + } + if (!readFileSync("gateway/kube/go.mod", "utf8").includes(`${CORE}${REHEARSED}`)) { + throw new Error("the rehearsal did not rewrite the adapter's core requirement"); + } + const run = (...args) => execFileSync("go", args, { cwd: "gateway/kube", stdio: "inherit" }); + try { + run("vet", "-tags", "e2e", "./..."); + run("build", "./..."); + } catch { + console.error(`::error::the adapter does not build as a release PR for v${REHEARSED} would build it (see go's output above)`); + process.exitCode = 1; + } + if (!process.exitCode) console.log(`the adapter builds as a release PR for v${REHEARSED} would build it`); +} finally { + for (const [file, text] of originals) writeFileSync(file, text); +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51e4c14..918f6dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,6 +160,15 @@ jobs: working-directory: gateway/kube args: --build-tags e2e + # A release PR requires a core version whose tag does not exist until it merges, which main + # never does. 0.6.0's release PR failed here for that reason alone. Rehearse it on every change: + # Release Please's own regex, a version that is never tagged, then vet and build. + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + - name: rehearse a release PR (its core version is not tagged yet) + run: node .github/scripts/release-rehearsal.mjs + # Can a STRANGER actually use this? Nothing else in this repo can answer that. # # gateway/kube once carried `replace ... => ../`, and a replace applies ONLY to the main module. So diff --git a/Taskfile.yml b/Taskfile.yml index 80ecc94..5fd6194 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -83,6 +83,8 @@ tasks: - task: fixtures-check - task: lint - task: test + # After the suites, not beside them: it rewrites go.work while it runs. + - task: release-rehearsal - task: e2e-wire - task: e2e-browser # Last, because nothing else proves it: what we upload contains what the exports map advertises. @@ -112,6 +114,11 @@ tasks: cmds: - go test ./... + release-rehearsal: + desc: "Build the Go adapter as a release PR would, with a core version that is not tagged yet." + cmds: + - node .github/scripts/release-rehearsal.mjs + test-client: desc: "TypeScript: the client + its half of the conformance suite (node --test, no deps)." deps: [fixtures, _client-deps] diff --git a/docs/releasing.md b/docs/releasing.md index c4a1619..1014a2f 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -32,7 +32,10 @@ Only `@configbutler/krm-stream` is maintained on npm. Do not publish to the unsc `gateway/kube/go.mod` requires the core module at the release it ships with. Every release pull request rewrites that line to the version being released (its `x-release-please-version` marker), -so a released adapter always resolves at least its own core. Do not edit it, and do not pin a +so a released adapter always resolves at least its own core. The matching `replace` in `go.work` moves +with it: a release PR names a core version that is not tagged until it merges, and that replace is +what lets the adapter build in it. `task release-rehearsal`, run by `task verify` and CI, builds the +adapter in exactly that state. Do not edit either line, and do not pin a pseudo-version when an adapter change needs unreleased core API: `go.work` wires the two modules together in this checkout, and CI's `a stranger can go get this` job builds an unreleased adapter with the core from the same commit. Someone using an unreleased commit does the same: diff --git a/go.work b/go.work index 30cc49e..a4cb12c 100644 --- a/go.work +++ b/go.work @@ -21,3 +21,11 @@ use ( ./gateway ./gateway/kube ) + +// The adapter requires the core at the release it ships with, and a release PR names a version whose +// tag does not exist until it merges. Workspace mode still reads the required version's go.mod, so +// without this every build of the adapter in a release PR fails ("unknown revision gateway/vX.Y.Z"). +// This points that exact version at the working tree. Go refuses an unversioned replace of a +// workspace module, so the version is spelled out, and Release Please moves it together with +// gateway/kube/go.mod; CI fails if the two disagree. +replace github.com/ConfigButler/krm-stream/gateway v0.5.0 => ./gateway // x-release-please-version diff --git a/release-please-config.json b/release-please-config.json index 5ef6716..a11a655 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -27,6 +27,10 @@ { "type": "generic", "path": "go.mod" + }, + { + "type": "generic", + "path": "/go.work" } ] }