diff --git a/bindings/go/sysprims/README.md b/bindings/go/sysprims/README.md index 1c8e09b..e10729f 100644 --- a/bindings/go/sysprims/README.md +++ b/bindings/go/sysprims/README.md @@ -9,11 +9,11 @@ Go bindings for the `sysprims` Rust process primitives. ## Install ```bash -go get github.com/3leaps/sysprims/bindings/go/sysprims@v0.2.2 +go get github.com/3leaps/sysprims/bindings/go/sysprims@v0.2.3 ``` -The Go module resolves `v0.2.2` through the repository's path-prefixed -`bindings/go/sysprims/v0.2.2` tag. That tag and the canonical `v0.2.2` tag +The Go module resolves `v0.2.3` through the repository's path-prefixed +`bindings/go/sysprims/v0.2.3` tag. That tag and the canonical `v0.2.3` tag identify the same reviewed commit. ### Windows toolchain diff --git a/scripts/version-pack.mjs b/scripts/version-pack.mjs index 76a22b2..77c62fd 100644 --- a/scripts/version-pack.mjs +++ b/scripts/version-pack.mjs @@ -40,6 +40,7 @@ const baseOwnedPaths = [ "VERSION", "Cargo.toml", "Cargo.lock", + "bindings/go/sysprims/README.md", "bindings/typescript/sysprims/package.json", "bindings/typescript/sysprims/package-lock.json", ...nativeDirectories.map( @@ -47,8 +48,9 @@ const baseOwnedPaths = [ `bindings/typescript/sysprims/npm/${directory}/package.json`, ), ]; -const semverPattern = - /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/; +const semverSource = + "(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-(?:(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+(?:[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?"; +const semverPattern = new RegExp(`^${semverSource}$`); function fail(message) { throw new Error(message); @@ -320,6 +322,64 @@ function checkJson(root, expected, errors) { } } +function extractUniqueRequiredVersion(text, pattern, label, expected, errors) { + const matches = [...text.matchAll(pattern)]; + if (matches.length === 0) { + errors.push(`${label} is missing`); + return; + } + if (matches.length !== 1) { + const versions = matches.map((match) => match[1]).join(", "); + errors.push(`${label} appears ${matches.length} times: ${versions}`); + return; + } + const match = matches[0]; + if (match[1] !== expected) { + errors.push(`${label} is ${match[1]}, expected ${expected}`); + } +} + +function checkGoReadme(root, expected, errors) { + const readme = readFileSync( + join(root, "bindings/go/sysprims/README.md"), + "utf8", + ); + extractUniqueRequiredVersion( + readme, + new RegExp( + `go get github\\.com\\/3leaps\\/sysprims\\/bindings\\/go\\/sysprims@v(${semverSource})`, + "g", + ), + "Go README install version", + expected, + errors, + ); + extractUniqueRequiredVersion( + readme, + new RegExp(`The Go module resolves \`v(${semverSource})\``, "g"), + "Go README module version", + expected, + errors, + ); + extractUniqueRequiredVersion( + readme, + new RegExp( + `\`bindings\\/go\\/sysprims\\/v(${semverSource})\` tag`, + "g", + ), + "Go README path-prefixed tag version", + expected, + errors, + ); + extractUniqueRequiredVersion( + readme, + new RegExp(`canonical \`v(${semverSource})\` tag`, "g"), + "Go README canonical tag version", + expected, + errors, + ); +} + function collectErrors(root) { const errors = []; let expected; @@ -339,6 +399,11 @@ function collectErrors(root) { } catch (error) { errors.push(error.message); } + try { + checkGoReadme(root, expected, errors); + } catch (error) { + errors.push(error.message); + } return errors; } @@ -424,6 +489,47 @@ function updateCargoWorkspaceDependencyPins(root, version) { writeTextAtomic(cargoPath, cargoToml); } +function replaceExactlyOnce(text, pattern, replacement, label) { + const matches = [...text.matchAll(pattern)]; + if (matches.length !== 1) { + fail(`expected exactly one ${label} replacement, found ${matches.length}`); + } + return text.replace(pattern, replacement); +} + +function updateGoReadme(root, version) { + const readmePath = join(root, "bindings/go/sysprims/README.md"); + let readme = readFileSync(readmePath, "utf8"); + readme = replaceExactlyOnce( + readme, + new RegExp( + `(go get github\\.com\\/3leaps\\/sysprims\\/bindings\\/go\\/sysprims@)v${semverSource}`, + "g", + ), + (_match, prefix) => `${prefix}v${version}`, + "Go README install version", + ); + readme = replaceExactlyOnce( + readme, + new RegExp(`(The Go module resolves \`)v${semverSource}(\`)`, "g"), + (_match, prefix, suffix) => `${prefix}v${version}${suffix}`, + "Go README module version", + ); + readme = replaceExactlyOnce( + readme, + new RegExp(`(\`bindings\\/go\\/sysprims\\/)v${semverSource}(\` tag)`, "g"), + (_match, prefix, suffix) => `${prefix}v${version}${suffix}`, + "Go README path-prefixed tag version", + ); + readme = replaceExactlyOnce( + readme, + new RegExp(`(canonical \`)v${semverSource}(\` tag)`, "g"), + (_match, prefix, suffix) => `${prefix}v${version}${suffix}`, + "Go README canonical tag version", + ); + writeTextAtomic(readmePath, readme); +} + function withRollback(root, paths, operation) { const backupRoot = mkdtempSync(join(tmpdir(), "sysprims-version-pack-")); try { @@ -464,6 +570,7 @@ export function synchronize( updateCargoWorkspaceDependencyPins(root, version); afterCargoSetVersion?.(); updateJsonSurfaces(root, version); + updateGoReadme(root, version); check(root, true); }); @@ -473,7 +580,7 @@ export function synchronize( function bump(root, component) { const current = readCanonicalVersion(root); const match = current.match(semverPattern); - if (match[4] || match[5]) { + if (current.includes("-") || current.includes("+")) { fail(`cannot ${component}-bump prerelease/build version ${current}`); } let [major, minor, patch] = current.split(".").map(Number); diff --git a/scripts/version-pack.test.mjs b/scripts/version-pack.test.mjs index 5963038..f32ab24 100644 --- a/scripts/version-pack.test.mjs +++ b/scripts/version-pack.test.mjs @@ -96,6 +96,22 @@ path = "src/lib.rs" ); writeFileSync(join(root, "crate", "src", "lib.rs"), "pub fn fixture() {}\n"); mustRun(root, "cargo", ["generate-lockfile"]); + mkdirSync(join(root, "bindings/go/sysprims"), { recursive: true }); + writeFileSync( + join(root, "bindings/go/sysprims/README.md"), + `# sysprims Go bindings + +\`\`\`bash +go get github.com/3leaps/sysprims/bindings/go/sysprims@v0.2.1 +\`\`\` + +The Go module resolves \`v0.2.1\` through the repository's path-prefixed +\`bindings/go/sysprims/v0.2.1\` tag. That tag and the canonical \`v0.2.1\` tag +identify the same reviewed commit. + +v0.1.14 remains part of the historical API notes. +`, + ); const optionalDependencies = Object.fromEntries( platformNames.map((name) => [name, "0.2.1"]), @@ -252,9 +268,37 @@ test("sync updates owned fields and removes only stale resolution evidence", () readFileSync(join(root, "Cargo.toml"), "utf8"), /sysprims-fixture = \{ version = "0\.2\.2", path = "crate" \}/, ); + const goReadme = readFileSync( + join(root, "bindings/go/sysprims/README.md"), + "utf8", + ); + assert.match( + goReadme, + /go get github\.com\/3leaps\/sysprims\/bindings\/go\/sysprims@v0\.2\.2/, + ); + assert.match(goReadme, /The Go module resolves `v0\.2\.2`/); + assert.match(goReadme, /`bindings\/go\/sysprims\/v0\.2\.2` tag/); + assert.match(goReadme, /canonical `v0\.2\.2` tag/); + assert.match(goReadme, /v0\.1\.14 remains/); assert.equal(version(root, "check").status, 0); }); +test("check fails precisely when the Go README install version is stale", () => { + const root = createFixture(); + const readmePath = join(root, "bindings/go/sysprims/README.md"); + writeFileSync( + readmePath, + readFileSync(readmePath, "utf8").replace( + /sysprims@v0\.2\.1/, + "sysprims@v0.2.0", + ), + ); + + const result = version(root, "check"); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Go README install version is 0\.2\.0/); +}); + test("check calls out stale npm resolution evidence without rewriting it", () => { const root = createFixture(); const lockPath = join( @@ -283,6 +327,42 @@ test("set canonicalizes VERSION and synchronizes in one command", () => { assert.equal(version(root, "check").status, 0); }); +test("set synchronizes prerelease versions through Go README coordinates", () => { + const root = createFixture(); + const result = version(root, "set", "0.2.2-rc.1"); + assert.equal(result.status, 0, `${result.stderr}${result.stdout}`); + + const goReadme = readFileSync( + join(root, "bindings/go/sysprims/README.md"), + "utf8", + ); + assert.match( + goReadme, + /go get github\.com\/3leaps\/sysprims\/bindings\/go\/sysprims@v0\.2\.2-rc\.1/, + ); + assert.match(goReadme, /The Go module resolves `v0\.2\.2-rc\.1`/); + assert.match(goReadme, /`bindings\/go\/sysprims\/v0\.2\.2-rc\.1` tag/); + assert.match(goReadme, /canonical `v0\.2\.2-rc\.1` tag/); + assert.equal(version(root, "check").status, 0); +}); + +test("check rejects duplicate Go README release coordinates", () => { + const root = createFixture(); + const readmePath = join(root, "bindings/go/sysprims/README.md"); + writeFileSync( + readmePath, + `${readFileSync(readmePath, "utf8")}\n` + + "go get github.com/3leaps/sysprims/bindings/go/sysprims@v0.2.0\n", + ); + + const result = version(root, "check"); + assert.notEqual(result.status, 0); + assert.match( + result.stderr, + /Go README install version appears 2 times: 0\.2\.1, 0\.2\.0/, + ); +}); + test("patch, minor, and major bumps each synchronize the full pack", () => { const root = createFixture(); for (const [component, expected] of [