From 3a64a193bb38116d14876e4395a1505d0b77f1e0 Mon Sep 17 00:00:00 2001 From: "Dr. Ernie Prabhakar" Date: Mon, 15 Dec 2025 11:06:08 -0800 Subject: [PATCH 01/11] Refactor version.ts to use @std/semver and timestamp-based dev versions - Use @std/semver for proper semantic version parsing and incrementing - Switch to timestamp-based dev versions (e.g., 0.3.0-dev.1734293847) for merge safety - Add validation to prevent bumping while on dev/prerelease versions - Refactor with cleaner helper functions: isDev(), getStableBase(), parseSemVerOrThrow() - Update help text to reflect timestamp-based dev versioning This makes dev releases merge-safe and prevents version conflicts when multiple branches create dev releases simultaneously. --- scripts/version.ts | 115 +++++++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 55 deletions(-) diff --git a/scripts/version.ts b/scripts/version.ts index 361051c..9d5e602 100644 --- a/scripts/version.ts +++ b/scripts/version.ts @@ -17,6 +17,7 @@ */ import { join } from "@std/path"; +import { format, increment, parse } from "@std/semver"; interface DenoConfig { version: string; @@ -58,40 +59,42 @@ async function getCurrentVersion(): Promise { return config.version; } -function parseVersion( - version: string, -): { major: number; minor: number; patch: number } { - const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/); - if (!match) { - throw new Error(`Invalid version format: ${version}`); - } - return { - major: parseInt(match[1]), - minor: parseInt(match[2]), - patch: parseInt(match[3]), - }; +function isDev(version: string): boolean { + return /^.+-dev\.\d+$/.test(version); } -function bumpVersion(version: string, type: BumpType): string { - const { major, minor, patch } = parseVersion(version); +function getStableBase(version: string): string { + const devMatch = version.match(/^(.+)-dev\.(\d+)$/); + return devMatch ? devMatch[1] : version; +} - switch (type) { - case "major": - return `${major + 1}.0.0`; - case "minor": - return `${major}.${minor + 1}.0`; - case "patch": - return `${major}.${minor}.${patch + 1}`; +function parseSemVerOrThrow(version: string) { + try { + return parse(version); + } catch { + throw new Error(`Invalid semver in deno.json: ${version}`); } } -async function updateVersion(newVersion: string): Promise { - const { config, path } = await getConfig(); - config.version = newVersion; +function bumpStableVersion(version: string, type: BumpType): string { + // We intentionally disallow bumping while on a prerelease/dev version to avoid surprises. + if (version.includes("-")) { + throw new Error( + `Cannot bump a prerelease/dev version (${version}). Reset to a stable version first (deno task version reset).`, + ); + } - // Write back with pretty formatting - const configText = JSON.stringify(config, null, 2) + "\n"; - await Deno.writeTextFile(path, configText); + const sem = parseSemVerOrThrow(version); + const bumped = increment(sem, type); + return format(bumped); +} + +function getTimestampDevVersion(currentVersion: string): string { + // Always generate a monotonic, merge-safe dev identifier. + // Use seconds to keep it short and avoid leading zeros. + const base = getStableBase(currentVersion); + const epochSeconds = Math.floor(Date.now() / 1000); + return `${base}-dev.${epochSeconds}`; } async function checkGitStatus(): Promise { @@ -110,9 +113,7 @@ async function displayVersion(): Promise { const version = await getCurrentVersion(); console.log(version); - // Check if it's a dev version - const isDevVersion = version.match(/^(.+)-dev\.(\d+)$/); - if (isDevVersion) { + if (isDev(version)) { console.log("\nāš ļø Current version is a dev pre-release."); console.log("To reset to stable version, run: deno task version reset"); } @@ -120,7 +121,7 @@ async function displayVersion(): Promise { async function bump(type: BumpType): Promise { const currentVersion = await getCurrentVersion(); - const newVersion = bumpVersion(currentVersion, type); + const newVersion = bumpStableVersion(currentVersion, type); console.log(`šŸ“¦ Version Bump (${type})\n`); console.log(`Current version: ${currentVersion}`); @@ -153,31 +154,24 @@ async function bump(type: BumpType): Promise { console.log("5. Tag: deno task version tag"); } -function getNextDevVersion(currentVersion: string): string { - // Check if already a dev version (e.g., "0.2.1-dev.2") - const devMatch = currentVersion.match(/^(.+)-dev\.(\d+)$/); - - if (devMatch) { - // Increment existing dev number - const baseVersion = devMatch[1]; - const devNumber = parseInt(devMatch[2]); - return `${baseVersion}-dev.${devNumber + 1}`; - } else { - // Start new dev sequence from stable version - return `${currentVersion}-dev.1`; - } +async function updateVersion(newVersion: string): Promise { + const { config, path } = await getConfig(); + config.version = newVersion; + + // Write back with pretty formatting + const configText = JSON.stringify(config, null, 2) + "\n"; + await Deno.writeTextFile(path, configText); } async function resetFromDev(): Promise { const currentVersion = await getCurrentVersion(); - const devMatch = currentVersion.match(/^(.+)-dev\.(\d+)$/); - if (!devMatch) { + if (!isDev(currentVersion)) { console.log("āœ… Current version is already stable:", currentVersion); return; } - const stableVersion = devMatch[1]; + const stableVersion = getStableBase(currentVersion); console.log("šŸ”„ Resetting from Dev to Stable Version\n"); console.log(`šŸ“¦ Current Version: ${currentVersion}`); console.log(`šŸ“¦ Stable Version: ${stableVersion}`); @@ -233,9 +227,20 @@ async function resetFromDev(): Promise { async function tagDev(): Promise { const currentVersion = await getCurrentVersion(); - const devVersion = getNextDevVersion(currentVersion); + const devVersion = getTimestampDevVersion(currentVersion); const tagName = `v${devVersion}`; + // Check if tag already exists (very unlikely with timestamp-based dev versions, but still safe) + console.log(`\nšŸ” Checking if tag ${tagName} exists...`); + const tagExists = await checkTagExists(tagName); + if (tagExists) { + console.error( + `\nāŒ Tag ${tagName} already exists. Re-run to generate a new timestamped dev version.`, + ); + Deno.exit(1); + } + console.log(`āœ… Tag ${tagName} does not exist yet`); + console.log("šŸ·ļø Creating Dev Pre-release Tag\n"); console.log(`šŸ“¦ Current Version: ${currentVersion}`); console.log(`šŸ“¦ New Dev Version: ${devVersion}`); @@ -322,21 +327,21 @@ async function tagDev(): Promise { async function tag(): Promise { const version = await getCurrentVersion(); - const tagName = `v${version}`; - // Check if it's a dev version - const devMatch = version.match(/^(.+)-dev\.(\d+)$/); - if (devMatch) { + if (isDev(version)) { + const stableBase = getStableBase(version); console.error( "āŒ Cannot create stable release tag for dev version:", version, ); console.error("\nYou must reset to a stable version first."); console.error("Run: deno task version reset"); - console.error(`\nThis will reset from ${version} to ${devMatch[1]}`); + console.error(`\nThis will reset from ${version} to ${stableBase}`); Deno.exit(1); } + const tagName = `v${version}`; + console.log("šŸ·ļø Creating Git Tag for Release\n"); console.log(`šŸ“¦ Version: ${version}`); @@ -412,7 +417,7 @@ Usage: deno task version patch Bump patch version (0.2.0 -> 0.2.1) deno task version minor Bump minor version (0.2.0 -> 0.3.0) deno task version major Bump major version (0.2.0 -> 1.0.0) - deno task version reset Reset from dev version to stable (0.2.1-dev.1 -> 0.2.1) + deno task version reset Reset from dev version to stable (0.2.1-dev. -> 0.2.1) deno task version tag Create and push git tag for current version deno task version dev Create and push dev pre-release tag deno task version help Show this help message @@ -425,7 +430,7 @@ Examples: deno task version patch git commit -am "Bump version to $(deno task version)" - # Create dev pre-release for testing (use 'deno task tag-dev' to run tests first) + # Create dev pre-release for testing (timestamp-based, merge-safe) deno task version dev # Reset to stable version after dev testing From c7500f4fd5ba2392dbbb68865b413e9537dc811b Mon Sep 17 00:00:00 2001 From: "Dr. Ernie Prabhakar" Date: Mon, 15 Dec 2025 11:06:59 -0800 Subject: [PATCH 02/11] Update CLAUDE.md with timestamp-based dev versioning documentation - Document new timestamp-based dev versioning (e.g., 0.3.0-dev.1734293847) - Add dev pre-release workflow section - Explain merge-safety benefits of timestamp-based versions - Document version reset command - Note restriction on bumping while on dev/prerelease versions - Fix markdown linting warnings (use headings instead of bold emphasis) --- CLAUDE.md | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d613b1c..c45565d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -207,16 +207,44 @@ deno task version patch # 0.3.0 -> 0.3.1 deno task version minor # 0.3.0 -> 0.4.0 deno task version major # 0.3.0 -> 1.0.0 -# Create and push release tag +# Create and push dev pre-release tag (timestamp-based, merge-safe) +deno task version dev # 0.3.0 -> 0.3.0-dev.1734293847 + +# Reset from dev version to stable +deno task version reset # 0.3.0-dev.1734293847 -> 0.3.0 + +# Create and push stable release tag deno task version tag # Show help deno task version help ``` +**Dev Versioning**: Dev releases use timestamp-based identifiers (epoch seconds) +like `0.3.0-dev.1734293847`. This ensures monotonic, merge-safe versions that +won't conflict when multiple branches create dev releases simultaneously. + ### Release Workflow -#### Option 1: Automated version bump +#### Dev Pre-release (for testing) + +Use dev releases to test JSR publication before stable releases: + +1. Create dev release: `deno task version dev` + - Generates timestamp-based version (e.g., `0.3.0-dev.1734293847`) + - Commits, tags, and pushes automatically +2. Test the dev release: + `deno run -A jsr:@theswanfactory/deno-hooks@0.3.0-dev.1734293847` +3. Reset to stable: `deno task version reset` + - Removes `-dev.timestamp` suffix + - Commits and pushes automatically + +**Note**: You cannot bump versions while on a dev/prerelease version. You must +reset to stable first. + +#### Stable Release + +##### Option 1: Automated version bump 1. Update version: `deno task version major` (for v1.0.0) 2. Update `CHANGELOG.md` (move changes from `[Unreleased]` to new version) @@ -224,7 +252,7 @@ deno task version help 4. Push: `git push` 5. Create tag: `deno task version tag` -#### Option 2: Manual version in deno.json +##### Option 2: Manual version in deno.json 1. Edit `deno.json` to change version manually 2. Update `CHANGELOG.md` (move changes from `[Unreleased]` to new version) From 9553c2ef59add43051dc315b58c6e6534b84fedc Mon Sep 17 00:00:00 2001 From: "Dr. Ernie Prabhakar" Date: Mon, 15 Dec 2025 11:10:18 -0800 Subject: [PATCH 03/11] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13e9655..b9b9f20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ ### Added +- Now properly creates releases - Self-contained hook scripts that work without deno-hooks installed - Support for any shell command in hooks (not just deno commands) - Success message "āœ“ All hooks passed" after successful hook execution From 397bb0fbef2f8c8e4e60c0fda1a4129570ad0f6d Mon Sep 17 00:00:00 2001 From: "Dr. Ernie Prabhakar" Date: Mon, 15 Dec 2025 11:13:12 -0800 Subject: [PATCH 04/11] Update publish.yml --- .github/workflows/publish.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 52ff679..7c95ae0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -37,3 +37,25 @@ jobs: else deno publish fi + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref }} + name: Release ${{ steps.get_version.outputs.version }} + body: | + ## Release ${{ steps.get_version.outputs.version }} + + Published to JSR: [@theswanfactory/deno-hooks@${{ steps.get_version.outputs.version }}](https://jsr.io/@theswanfactory/deno-hooks@${{ steps.get_version.outputs.version }}) + + ### Installation + + ```bash + deno add @theswanfactory/deno-hooks@${{ steps.get_version.outputs.version }} + ``` + + See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/${{ github.ref_name }}/CHANGELOG.md) for details. + draft: false + prerelease: ${{ contains(steps.get_version.outputs.version, '-dev.') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 3d4877dfd5985688981157fe3de7fde1cae5299f Mon Sep 17 00:00:00 2001 From: "Dr. Ernie Prabhakar" Date: Mon, 15 Dec 2025 11:14:00 -0800 Subject: [PATCH 05/11] Add @std/semver to imports in deno.json --- deno.json | 1 + 1 file changed, 1 insertion(+) diff --git a/deno.json b/deno.json index 1231351..78b6658 100644 --- a/deno.json +++ b/deno.json @@ -33,6 +33,7 @@ "@std/path": "jsr:@std/path@^1.0.8", "@std/fs": "jsr:@std/fs@^1.0.8", "@std/expect": "jsr:@std/expect@^1.0.8", + "@std/semver": "jsr:@std/semver@^1.0.3", "deno-hooks": "./src/mod.ts" }, "__comments": { From a320a74356cac19a530605e9db7cc2055e9230e5 Mon Sep 17 00:00:00 2001 From: "Dr. Ernie Prabhakar" Date: Mon, 15 Dec 2025 11:14:18 -0800 Subject: [PATCH 06/11] Bump version to 0.3.0-dev.1765826058 for JSR dev release --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 78b6658..45e6c84 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@theswanfactory/deno-hooks", - "version": "0.3.0", + "version": "0.3.0-dev.1765826058", "exports": { ".": "./src/mod.ts" }, From 79ba4748661bbc4b35e295f7a573c9c7e383f566 Mon Sep 17 00:00:00 2001 From: "Dr. Ernie Prabhakar" Date: Mon, 15 Dec 2025 11:20:21 -0800 Subject: [PATCH 07/11] Fix: Add contents:write permission for GitHub releases --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7c95ae0..afcad50 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest permissions: - contents: read + contents: write # Required for creating releases id-token: write # Required for OIDC authentication with JSR steps: From b94ce125c532619101bdb4548fa3a13faf355bef Mon Sep 17 00:00:00 2001 From: "Dr. Ernie Prabhakar" Date: Mon, 15 Dec 2025 11:20:39 -0800 Subject: [PATCH 08/11] Reset version to 0.3.0 after dev testing --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 45e6c84..78b6658 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@theswanfactory/deno-hooks", - "version": "0.3.0-dev.1765826058", + "version": "0.3.0", "exports": { ".": "./src/mod.ts" }, From 5bcc93d06c321752dcbab22f91200535f4ea1cfe Mon Sep 17 00:00:00 2001 From: "Dr. Ernie Prabhakar" Date: Mon, 15 Dec 2025 11:20:50 -0800 Subject: [PATCH 09/11] Bump version to 0.3.0-dev.1765826450 for JSR dev release --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 78b6658..50704a9 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@theswanfactory/deno-hooks", - "version": "0.3.0", + "version": "0.3.0-dev.1765826450", "exports": { ".": "./src/mod.ts" }, From c96d510a302aedd5491451e2c56180623a0e745a Mon Sep 17 00:00:00 2001 From: "Dr. Ernie Prabhakar" Date: Mon, 15 Dec 2025 11:45:24 -0800 Subject: [PATCH 10/11] Bump version to 0.3.0-dev.1765827924 for JSR dev release --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 50704a9..d0381df 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@theswanfactory/deno-hooks", - "version": "0.3.0-dev.1765826450", + "version": "0.3.0-dev.1765827924", "exports": { ".": "./src/mod.ts" }, From 025aea6ac975c416c4c97009c144fa2505137656 Mon Sep 17 00:00:00 2001 From: "Dr. Ernie Prabhakar" Date: Mon, 15 Dec 2025 11:47:01 -0800 Subject: [PATCH 11/11] Reset version to 0.3.0 after dev testing --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index d0381df..78b6658 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@theswanfactory/deno-hooks", - "version": "0.3.0-dev.1765827924", + "version": "0.3.0", "exports": { ".": "./src/mod.ts" },