From 6739ab3ca56874786b5670df040ae85d8892e6cc Mon Sep 17 00:00:00 2001 From: Yoshiya Hinosawa Date: Tue, 7 May 2024 12:41:53 +0900 Subject: [PATCH] feat: support manual version updates (#13) --- .github/workflows/ci.yml | 3 +- mod.ts | 18 +++++++++-- util.ts | 56 ++++++++++++++++++++++++++++++-- util_test.ts | 69 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd7783c..22890f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,8 @@ jobs: - uses: denoland/setup-deno@v1 - run: deno fmt --check - run: deno lint - - run: | + - name: Test + run: | git fetch origin # some branches are necessary for testing deno task cov - uses: codecov/codecov-action@v4 diff --git a/mod.ts b/mod.ts index 3c700d4..273a1fa 100644 --- a/mod.ts +++ b/mod.ts @@ -80,18 +80,27 @@ export async function bumpWorkspaces( }: BumpWorkspaceOptions = {}, ) { const now = new Date(); - const [configPath, modules] = await getWorkspaceModules(root); start ??= await $`git describe --tags --abbrev=0`.text(); - const newBranchName = createReleaseBranchName(now); base ??= await $`git branch --show-current`.text(); if (!base) { console.error("The current branch is not found."); Deno.exit(1); } + + await $`git checkout ${start}`; + const [_oldConfigPath, oldModules] = await getWorkspaceModules(root); + await $`git checkout -`; + await $`git checkout ${base}`; + const [configPath, modules] = await getWorkspaceModules(root); + await $`git checkout -`; + + const newBranchName = createReleaseBranchName(now); releaseNotePath = join(root, releaseNotePath); + const text = await $`git --no-pager log --pretty=format:${separator}%H%B ${start}..${base}` .text(); + const commits = text.split(separator).map((commit) => { const hash = commit.slice(0, 40); commit = commit.slice(40); @@ -103,7 +112,8 @@ export async function bumpWorkspaces( const body = commit.slice(i + 1).trim(); return { hash, subject, body }; }); - commits.shift(); + commits.shift(); // drop the first empty item + console.log( `Found ${cyan(commits.length.toString())} commits between ${ magenta(start) @@ -147,9 +157,11 @@ export async function bumpWorkspaces( let denoJson = await Deno.readTextFile(configPath); for (const summary of summaries) { const module = getModule(summary.module, modules)!; + const oldModule = getModule(summary.module, oldModules); const [denoJson_, versionUpdate] = await applyVersionBump( summary, module, + oldModule, denoJson, dryRun === true, ); diff --git a/util.ts b/util.ts index 88b28cb..3293900 100644 --- a/util.ts +++ b/util.ts @@ -8,8 +8,9 @@ import { increment, parse as parseSemVer, } from "@std/semver"; +import { red } from "@std/fmt/colors"; -export type VersionUpdate = "major" | "minor" | "patch"; +export type VersionUpdate = "major" | "minor" | "patch" | "prerelease"; export type Commit = { subject: string; @@ -234,7 +235,7 @@ export async function getWorkspaceModules( const workspaces = denoConfig.workspaces; if (!Array.isArray(workspaces)) { - console.log("deno.json doesn't have workspaces field."); + console.log(red("Error") + " deno.json doesn't have workspaces field."); Deno.exit(1); } @@ -277,13 +278,64 @@ export function checkModuleName( }; } +export function calcVersionDiff( + newVersionStr: string, + oldVersionStr: string, +): VersionUpdate { + const newVersion = parseSemVer(newVersionStr); + const oldVersion = parseSemVer(oldVersionStr); + if (newVersion.prerelease && newVersion.prerelease.length > 0) { + return "prerelease"; + } else if (newVersion.major !== oldVersion.major) { + return "major"; + } else if (newVersion.minor !== oldVersion.minor) { + return "minor"; + } else if (newVersion.patch !== oldVersion.patch) { + return "patch"; + } else { + throw new Error( + `Unexpected manual version update: ${oldVersion} -> ${newVersion}`, + ); + } +} + /** Apply the version bump to the file system. */ export async function applyVersionBump( summary: VersionBumpSummary, module: WorkspaceModule, + oldModule: WorkspaceModule | undefined, denoJson: string, dryRun = false, ): Promise<[denoJson: string, VersionUpdateResult]> { + if (!oldModule) { + // The module is newly added + console.info(`New module ${module.name} detected.`); + const diff = calcVersionDiff(module.version, "0.0.0"); + summary.version = diff; + return [denoJson, { + from: "0.0.0", + to: module.version, + diff, + summary, + path: module[pathProp], + }]; + } + if (oldModule.version !== module.version) { + // The version is manually updated + console.info( + `Manaul version update detected for ${module.name}: ${oldModule.version} -> ${module.version}`, + ); + + const diff = calcVersionDiff(module.version, oldModule.version); + summary.version = diff; + return [denoJson, { + from: oldModule.version, + to: module.version, + diff, + summary, + path: module[pathProp], + }]; + } const oldVersionStr = module.version; const oldVersion = parseSemVer(oldVersionStr); let diff = summary.version; diff --git a/util_test.ts b/util_test.ts index aef0d22..007fbf0 100644 --- a/util_test.ts +++ b/util_test.ts @@ -688,6 +688,7 @@ Deno.test("applyVersionBump() updates the version of the given module", async () commits: [], }, { name: "@scope/foo", version: "1.0.0", [pathProp]: "foo/deno.json" }, + { name: "@scope/foo", version: "1.0.0", [pathProp]: "foo/deno.json" }, `{ "imports": { "scope/foo": "jsr:@scope/foo@^1.0.0", @@ -722,6 +723,7 @@ Deno.test("applyVersionBump() consider major bump for 0.x version as minor bump" commits: [], }, { name: "@scope/foo", version: "0.0.0", [pathProp]: "foo/deno.jsonc" }, + { name: "@scope/foo", version: "0.0.0", [pathProp]: "foo/deno.jsonc" }, `{ "imports": { "scope/foo": "jsr:@scope/foo@^0.0.0", @@ -748,6 +750,72 @@ Deno.test("applyVersionBump() consider major bump for 0.x version as minor bump" ); }); +Deno.test("applyVersionBump() respect manual version upgrade if the version between start and base is different", async () => { + const [denoJson, updateResult] = await applyVersionBump( + { + module: "foo", + version: "minor", // This version is ignored, instead manually given version is used for calculating actual version diff + commits: [], + }, + { name: "@scope/foo", version: "1.0.0-rc.1", [pathProp]: "foo/deno.jsonc" }, + { name: "@scope/foo", version: "0.224.0", [pathProp]: "foo/deno.jsonc" }, + `{ + "imports": { + "scope/foo": "jsr:@scope/foo@^1.0.0-rc.1", + "scope/bar": "jsr:@scope/bar@^1.0.0" + } + }`, + true, + ); + assertEquals(updateResult.from, "0.224.0"); + assertEquals(updateResult.to, "1.0.0-rc.1"); + assertEquals(updateResult.diff, "prerelease"); + assertEquals( + denoJson, + `{ + "imports": { + "scope/foo": "jsr:@scope/foo@^1.0.0-rc.1", + "scope/bar": "jsr:@scope/bar@^1.0.0" + } + }`, + ); +}); + +Deno.test("applyVersionBump() works for new module (the case when oldModule is undefined)", async () => { + const [denoJson, updateResult] = await applyVersionBump( + { + module: "foo", + version: "patch", // <= this version is ignored, instead manually given version is used for calculating actual version diff + commits: [], + }, + { name: "@scope/foo", version: "0.1.0", [pathProp]: "foo/deno.jsonc" }, + undefined, + `{ + "imports": { + "scope/foo": "jsr:@scope/foo@^0.1.0", + "scope/foo/": "jsr:@scope/foo@^0.1.0/", + "scope/bar": "jsr:@scope/bar@^1.0.0", + "scope/bar/": "jsr:@scope/bar@^1.0.0/" + } + }`, + true, + ); + assertEquals(updateResult.from, "0.0.0"); + assertEquals(updateResult.to, "0.1.0"); + assertEquals(updateResult.diff, "minor"); + assertEquals( + denoJson, + `{ + "imports": { + "scope/foo": "jsr:@scope/foo@^0.1.0", + "scope/foo/": "jsr:@scope/foo@^0.1.0/", + "scope/bar": "jsr:@scope/bar@^1.0.0", + "scope/bar/": "jsr:@scope/bar@^1.0.0/" + } + }`, + ); +}); + async function createVersionUpdateResults( versionBumps: VersionBump[], modules: WorkspaceModule[], @@ -763,6 +831,7 @@ async function createVersionUpdateResults( const [_denoJson, versionUpdate] = await applyVersionBump( summary, getModule(summary.module, modules)!, + getModule(summary.module, modules)!, "", true, );