From 9bd255f9cd024e1b8296ff99336462e37c5b05ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 04:58:36 +0000 Subject: [PATCH] test(release): cover deno version sync helper Co-authored-by: okikio <17222836+okikio@users.noreply.github.com> --- RELEASING.md | 7 +- scripts/update_release_version.ts | 61 +++++++++++++----- scripts/update_release_version_test.ts | 88 ++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 18 deletions(-) create mode 100644 scripts/update_release_version_test.ts diff --git a/RELEASING.md b/RELEASING.md index d617b7a..4aea823 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -66,8 +66,8 @@ working. ### `release-please` Good option if you want a reviewable release PR. It updates changelogs and -versions reliably, but its release-PR model is still close to the Forge flow you -said you do not like. +versions reliably, but its release-PR model adds the same PR-based ceremony this +repo aims to avoid. ### `changesets` @@ -84,7 +84,8 @@ version from commit history and to keep `deno.jsonc` in sync. ### JSR and Deno-native options - `@roka/forge`: best fit for Deno workspaces, but the bump-PR plus - draft-release flow is more hands-on than this repo wants + draft-release flow is more hands-on than needed for this single-package + library - `@deno/bump-workspaces`: promising for Deno workspaces, but it is workspace-first and centered on pull-request driven bumps - `@eser/codebase/release`: Deno-native and capable of version bump + changelog diff --git a/scripts/update_release_version.ts b/scripts/update_release_version.ts index e3140bb..794f1eb 100644 --- a/scripts/update_release_version.ts +++ b/scripts/update_release_version.ts @@ -1,24 +1,55 @@ /** - * Rewrites the version field in deno.jsonc for semantic-release. + * Bridges semantic-release's calculated version back into deno.jsonc. + * + * semantic-release can calculate the next SemVer number and changelog entry for + * this repository, but it does not natively know how to update a Deno JSONC + * config file. This script keeps the release workflow small by validating the + * next version and rewriting only the `version` field, leaving the rest of + * deno.jsonc untouched. */ +const semverPattern = + /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/; +const versionFieldPattern = /("version"\s*:\s*")[^"]*(")/; -const [version] = Deno.args; - -if (typeof version !== "string" || version.length === 0) { - throw new Error("Expected the next release version as the first argument."); +/** + * Returns true when the provided string follows SemVer's `x.y.z` structure, + * optionally with pre-release or build metadata. + */ +export function isSemVerVersion(version: string): boolean { + return semverPattern.test(version); } -const config_path = new URL("../deno.jsonc", import.meta.url); -const config_text = await Deno.readTextFile(config_path); -const version_pattern = /("version"\s*:\s*")[^"]*(")/; +/** + * Rewrites the `version` field in deno.jsonc text while preserving the rest of + * the file's content and formatting. + */ +export function updateReleaseVersion( + configText: string, + version: string, +): string { + if (!isSemVerVersion(version)) { + throw new Error( + `Expected a SemVer-compatible version, received "${version}".`, + ); + } -if (!version_pattern.test(config_text)) { - throw new Error('Expected deno.jsonc to contain a "version" field.'); + if (!versionFieldPattern.test(configText)) { + throw new Error('Expected deno.jsonc to contain a "version" field.'); + } + + return configText.replace(versionFieldPattern, `$1${version}$2`); } -const next_config_text = config_text.replace( - version_pattern, - `$1${version}$2`, -); +if (import.meta.main) { + const [version] = Deno.args; + + if (typeof version !== "string" || version.length === 0) { + throw new Error("Expected the next release version as the first argument."); + } -await Deno.writeTextFile(config_path, next_config_text); + const configPath = new URL("../deno.jsonc", import.meta.url); + const configText = await Deno.readTextFile(configPath); + const nextConfigText = updateReleaseVersion(configText, version); + + await Deno.writeTextFile(configPath, nextConfigText); +} diff --git a/scripts/update_release_version_test.ts b/scripts/update_release_version_test.ts new file mode 100644 index 0000000..282c3f4 --- /dev/null +++ b/scripts/update_release_version_test.ts @@ -0,0 +1,88 @@ +import { + isSemVerVersion, + updateReleaseVersion, +} from "./update_release_version.ts"; + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(message); + } +} + +Deno.test("accepts release and prerelease semver strings", () => { + assert(isSemVerVersion("1.2.3"), "expected 1.2.3 to be valid semver"); + assert( + isSemVerVersion("1.2.3-beta.1+build.5"), + "expected prerelease with metadata to be valid semver", + ); +}); + +Deno.test("rejects non-semver version strings", () => { + assert(!isSemVerVersion("1.2"), "expected 1.2 to be rejected"); + assert( + !isSemVerVersion("release-next"), + "expected release-next to be rejected", + ); +}); + +Deno.test("rewrites only the version field", () => { + const configText = [ + "{", + ' "name": "@okikio/observables",', + ' "version": "0.0.0",', + ' "description": "Observables"', + "}", + ].join("\n"); + + const nextConfigText = updateReleaseVersion(configText, "1.4.0"); + + assert( + nextConfigText === [ + "{", + ' "name": "@okikio/observables",', + ' "version": "1.4.0",', + ' "description": "Observables"', + "}", + ].join("\n"), + "expected only the version field to change", + ); +}); + +Deno.test("throws when the new version is invalid", () => { + let thrownError: unknown; + + try { + updateReleaseVersion('{"version":"0.0.0"}', "next"); + } catch (error) { + thrownError = error; + } + + if (!(thrownError instanceof Error)) { + throw new Error("expected an Error to be thrown"); + } + + assert( + thrownError.message === + 'Expected a SemVer-compatible version, received "next".', + "expected the invalid version error message", + ); +}); + +Deno.test("throws when the version field is missing", () => { + let thrownError: unknown; + + try { + updateReleaseVersion('{"name":"@okikio/observables"}', "1.0.0"); + } catch (error) { + thrownError = error; + } + + if (!(thrownError instanceof Error)) { + throw new Error("expected an Error to be thrown"); + } + + assert( + thrownError.message === 'Expected deno.jsonc to contain a "version" field.', + "expected the missing version field error message", + ); +}); -- 2.51.2