@@ -2,7 +2,6 @@ import fs from "node:fs";
22import path from "node:path" ;
33import { createInterface } from "node:readline/promises" ;
44import { execa } from "execa" ;
5- import pkg from "../package.json" with { type : "json" } ;
65
76// --- Configuration ---
87const CHANGELOG_PATH = path . resolve ( process . cwd ( ) , "CHANGELOG.md" ) ;
@@ -52,35 +51,77 @@ async function askForConfirmation(question) {
5251 return answer . toLowerCase ( ) . trim ( ) ;
5352}
5453
54+ async function checkPrerequisites ( ) {
55+ log . info ( "Checking prerequisites..." ) ;
56+
57+ // Check for gh CLI installation
58+ try {
59+ await execa ( "gh" , [ "--version" ] , { stdio : "ignore" } ) ;
60+ } catch ( error ) {
61+ if ( error . code === "ENOENT" ) {
62+ log . error (
63+ "GitHub CLI (gh) is not installed. Please install it and try again)." ,
64+ ) ;
65+ process . exit ( 1 ) ;
66+ }
67+ log . error ( "An unexpected error occurred while checking for gh CLI:" ) ;
68+ console . error ( error ) ;
69+ process . exit ( 1 ) ;
70+ }
71+
72+ // Check for gh auth status
73+ const authStatus = await execa ( "gh" , [ "auth" , "status" ] , { reject : false } ) ;
74+ if ( authStatus . failed ) {
75+ log . error (
76+ 'You are not logged into the GitHub CLI. Please run "gh auth login" and try again.' ,
77+ ) ;
78+ process . exit ( 1 ) ;
79+ }
80+
81+ log . success ( "Prerequisites met." ) ;
82+ }
83+
5584// --- Main Script ---
5685async function main ( ) {
5786 log . info ( "Starting release process..." ) ;
5887 if ( DRY_RUN ) {
5988 log . info ( "Running in dry-run mode. No commands will be executed." ) ;
6089 }
6190
62- // 1. Check for staged changes
63- const { stdout : stagedFiles } = await execa ( "git" , [
64- "diff" ,
65- "--staged" ,
66- "--name-only" ,
67- ] ) ;
68- if ( ! stagedFiles ) {
69- log . error (
70- "No staged changes to commit. Stage your changes for the release first (e.g., package.json, CHANGELOG.md)." ,
91+ await checkPrerequisites ( ) ;
92+
93+ // 1. Run changelogen to bump version and update changelog
94+ log . info ( "Bumping version and generating changelog with changelogen..." ) ;
95+ if ( DRY_RUN ) {
96+ log . info ( "Previewing changelog generation..." ) ;
97+ // We use execa directly here to bypass the dry-run logic of our `run` helper
98+ // and show a preview of what changelogen will do.
99+ await execa ( "pnpx" , [ "changelogen@latest" , "--no-output" ] , {
100+ stdio : "inherit" ,
101+ } ) ;
102+ const continueAnswer = await askForConfirmation (
103+ "Apply file changes and continue with dry-run?" ,
71104 ) ;
72- process . exit ( 1 ) ;
105+ if ( continueAnswer !== "y" && continueAnswer !== "yes" ) {
106+ log . info ( "Dry-run aborted by user." ) ;
107+ process . exit ( 0 ) ;
108+ }
109+ log . info ( "Applying changelog changes to proceed with dry-run..." ) ;
110+ await execa ( "pnpx" , [ "changelogen@latest" , "--bump" ] , { stdio : "inherit" } ) ;
111+ } else {
112+ await run ( "pnpx" , [ "changelogen@latest" , "--bump" ] ) ;
73113 }
74- log . info ( `Staged files for release: \n${ stagedFiles } ` ) ;
75114
76- // 2. Get version and construct tag
115+ // 2. Get version from the updated package.json
116+ const packageJsonPath = path . resolve ( process . cwd ( ) , "package.json" ) ;
117+ const pkg = JSON . parse ( fs . readFileSync ( packageJsonPath , "utf-8" ) ) ;
77118 const { version } = pkg ;
78119 if ( ! version ) {
79120 log . error ( "Could not read version from package.json" ) ;
80121 process . exit ( 1 ) ;
81122 }
82123 const tag = `v${ version } ` ;
83- log . info ( `Found version : ${ version } ` ) ;
124+ log . info ( `Version to be released : ${ version } ` ) ;
84125
85126 // 3. Extract release notes from CHANGELOG.md
86127 log . info ( "Extracting release notes from CHANGELOG.md..." ) ;
@@ -126,28 +167,45 @@ async function main() {
126167 }
127168 log . success ( "Successfully extracted release notes." ) ;
128169
129- if ( DRY_RUN ) {
130- log . info ( "--- Extracted Release Notes (for review) ---" ) ;
131- const indentedNotes = releaseNotes
132- . split ( "\n" )
133- . map ( ( line ) => ` ${ line } ` )
134- . join ( "\n" ) ;
135- console . log ( indentedNotes ) ;
136- log . info ( "--------------------------------------------" ) ;
170+ // 4. Show extracted notes for review
171+ log . info ( "--- Release Notes (for review) ---" ) ;
172+ const indentedNotes = releaseNotes
173+ . split ( "\n" )
174+ . map ( ( line ) => ` ${ line } ` )
175+ . join ( "\n" ) ;
176+ console . log ( indentedNotes ) ;
177+ log . info ( "----------------------------------" ) ;
178+
179+ // 5. Ask to continue with commit and tag
180+ if ( ! DRY_RUN ) {
181+ const answer = await askForConfirmation ( "Proceed with commit and tag?" ) ;
182+ if ( answer !== "y" && answer !== "yes" ) {
183+ log . info ( "Release aborted by user." ) ;
184+ const revertAnswer = await askForConfirmation (
185+ "Revert changes to package.json and CHANGELOG.md?" ,
186+ ) ;
187+ if ( revertAnswer === "y" || revertAnswer === "yes" ) {
188+ await run ( "git" , [ "checkout" , "package.json" , "CHANGELOG.md" ] ) ;
189+ log . success ( "Changes have been reverted." ) ;
190+ }
191+ process . exit ( 0 ) ;
192+ }
137193 }
138194
139- // 4. Commit the staged changes
195+ // 6. Stage and commit the changes
196+ log . info ( "Staging package.json and CHANGELOG.md..." ) ;
197+ await run ( "git" , [ "add" , "package.json" , "CHANGELOG.md" ] ) ;
140198 const commitMessage = `chore(release): ${ tag } ` ;
141199 log . info ( `Committing with message: "${ commitMessage } "` ) ;
142200 await run ( "git" , [ "commit" , "-m" , commitMessage ] ) ;
143201
144- // 5 . Create an annotated tag
202+ // 7 . Create an annotated tag
145203 log . info ( `Creating annotated tag: ${ tag } ` ) ;
146204 await run ( "git" , [ "tag" , "-a" , tag , "-m" , tag ] ) ;
147205
148206 log . success ( "Local commit and tag created successfully!" ) ;
149207
150- // 6 . Ask to push
208+ // 8 . Ask to push
151209 if ( ! DRY_RUN ) {
152210 const answer = await askForConfirmation ( "Push commit and tags to remote?" ) ;
153211 if ( answer !== "y" && answer !== "yes" ) {
@@ -170,13 +228,13 @@ async function main() {
170228 }
171229 }
172230
173- // 7 . Push to remote
231+ // 9 . Push to remote
174232 log . info ( "Pushing commit and tags to remote..." ) ;
175233 await run ( "git" , [ "push" ] ) ;
176234 await run ( "git" , [ "push" , "--tags" ] ) ;
177235 log . success ( "Commit and tags pushed to remote." ) ;
178236
179- // 8 . Create a GitHub release
237+ // 10 . Create a GitHub release
180238 log . info ( "Creating GitHub release..." ) ;
181239 await run ( "gh" , [
182240 "release" ,
@@ -188,7 +246,14 @@ async function main() {
188246 releaseNotes ,
189247 ] ) ;
190248
191- log . success ( "Release process completed and published successfully!" ) ;
249+ if ( DRY_RUN ) {
250+ await execa ( "git" , [ "checkout" , "package.json" , "CHANGELOG.md" ] , {
251+ stdio : "inherit" ,
252+ } ) ;
253+ log . success ( "Dry-run completed and temporary changes have been reverted." ) ;
254+ } else {
255+ log . success ( "Release process completed and published successfully!" ) ;
256+ }
192257}
193258
194259main ( ) . catch ( ( err ) => {
0 commit comments