diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 52ff679..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: @@ -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 }} 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 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) 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": { 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