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" } ] }