1+ import { Argument , Command } from "@commander-js/extra-typings" ;
2+ import * as console from "node:console" ;
3+ import process from "node:process" ;
4+
5+ const program = new Command ( ) ;
6+
7+ function logAndError ( msg : string ) {
8+ console . error ( msg ) ;
9+ process . exit ( 1 ) ;
10+ }
11+
12+ function bumpVersion (
13+ version : string ,
14+ type : "major" | "minor" | "patch" | "prerelease" ,
15+ ) {
16+ const [ major , minor , patch ] = version . split ( "." ) . map ( ( v ) =>
17+ Number . parseInt (
18+ // Remove everything after the - if there is one
19+ v . includes ( "-" ) ? v . split ( "-" ) [ 0 ] : v ,
20+ )
21+ ) ;
22+ switch ( type ) {
23+ case "major" :
24+ return `${ major + 1 } .0.0` ;
25+ case "minor" :
26+ return `${ major } .${ minor + 1 } .0` ;
27+ case "patch" :
28+ return `${ major } .${ minor } .${ patch + 1 } ` ;
29+ case "prerelease" :
30+ return `${ major } .${ minor } .${ patch } -pre.${ Date . now ( ) } ` ;
31+ default :
32+ throw new Error ( `Invalid release type: ${ type } ` ) ;
33+ }
34+ }
35+
36+ async function getLocalVersion ( ) {
37+ const packageJson = await Deno . readTextFile ( "package.json" ) ;
38+ return JSON . parse ( packageJson ) . version ;
39+ }
40+
41+ async function saveVersion ( version : string ) {
42+ const packageJson = await Deno . readTextFile ( "package.json" ) ;
43+ const packageObj = JSON . parse ( packageJson ) ;
44+ packageObj . version = version ;
45+ // Ensure exactly one newline at the end of the file
46+ await Deno . writeTextFile (
47+ "package.json" ,
48+ `${ JSON . stringify ( packageObj , null , 2 ) } \n` ,
49+ ) ;
50+ }
51+
52+ const COMPILE_TARGETS : string [ ] = [
53+ "x86_64-unknown-linux-gnu" ,
54+ "aarch64-unknown-linux-gnu" ,
55+ "x86_64-apple-darwin" ,
56+ "aarch64-apple-darwin" ,
57+ ] ;
58+
59+ async function compileDistribution ( ) {
60+ for ( const target of COMPILE_TARGETS ) {
61+ const result = await new Deno . Command ( "deno" , {
62+ args : [
63+ "compile" ,
64+ "-A" ,
65+ "--target" ,
66+ target ,
67+ "--output" ,
68+ `dist/sf-${ target } ` ,
69+ "./src/index.ts" ,
70+ ] ,
71+ } ) . output ( ) ;
72+
73+ if ( ! result . success ) {
74+ console . error ( new TextDecoder ( ) . decode ( result . stderr ) ) ;
75+ logAndError ( `Failed to compile for ${ target } ` ) ;
76+ }
77+ console . log ( `✅ Compiled for ${ target } ` ) ;
78+
79+ const zipFileName = `dist/sf-${ target } .zip` ;
80+ const zipResult = await new Deno . Command ( "zip" , {
81+ args : [ "-j" , zipFileName , `dist/sf-${ target } ` ] ,
82+ } ) . output ( ) ;
83+
84+ if ( ! zipResult . success ) {
85+ console . error ( zipResult . stderr ) ;
86+ logAndError ( `Failed to zip the binary for ${ target } ` ) ;
87+ }
88+ console . log ( `✅ Zipped binary for ${ target } ` ) ;
89+ }
90+ }
91+
92+ async function asyncSpawn ( cmds : string [ ] ) {
93+ console . log ( "cmds" , cmds ) ;
94+ const result = await new Deno . Command ( cmds [ 0 ] , {
95+ args : cmds . slice ( 1 ) ,
96+ } ) . output ( ) ;
97+
98+ return {
99+ exitCode : result . success ? 0 : 1 ,
100+ } ;
101+ }
102+ async function createRelease ( version : string ) {
103+ // Verify zip files are valid before creating release
104+ const distFiles = Array . from ( Deno . readDirSync ( "./dist" ) ) ;
105+ const zipFiles = distFiles
106+ . filter ( ( entry ) => entry . isFile )
107+ . filter ( ( entry ) => entry . name . endsWith ( ".zip" ) )
108+ . map ( ( entry ) => `./dist/${ entry . name } ` ) ;
109+
110+ console . log ( zipFiles ) ;
111+
112+ // Verify each zip file is valid
113+ for ( const zipFile of zipFiles ) {
114+ const verifyResult = await new Deno . Command ( "unzip" , {
115+ args : [ "-t" , zipFile ] ,
116+ } ) . output ( ) ;
117+
118+ if ( ! verifyResult . success ) {
119+ logAndError ( `Invalid zip file: ${ zipFile } ` ) ;
120+ }
121+ console . log ( `✅ Verified zip file: ${ zipFile } ` ) ;
122+ }
123+
124+ const releaseFlag = version . includes ( "pre" ) ? "--prerelease" : "--latest" ;
125+ const result = await asyncSpawn ( [
126+ "gh" ,
127+ "release" ,
128+ "create" ,
129+ version ,
130+ ...zipFiles ,
131+ "--generate-notes" ,
132+ releaseFlag ,
133+ ] ) ;
134+ if ( result . exitCode !== 0 ) {
135+ console . log (
136+ "GitHub release creation failed with exit code:" ,
137+ result . exitCode ,
138+ ) ;
139+ console . log ( "Common failure reasons:" ) ;
140+ console . log ( "- GitHub CLI not installed or not authenticated" ) ;
141+ console . log ( "- Release tag already exists" ) ;
142+ console . log ( "- No write permissions to repository" ) ;
143+ console . log ( "- Network connectivity issues" ) ;
144+ logAndError ( `Failed to create GitHub release for version ${ version } ` ) ;
145+ }
146+ console . log ( `✅ Created GitHub release for version ${ version } ` ) ;
147+
148+ const gitAddResult = await asyncSpawn ( [ "git" , "add" , "package.json" ] ) ;
149+ if ( gitAddResult . exitCode !== 0 ) {
150+ logAndError ( "Failed to add package.json to git" ) ;
151+ }
152+ console . log ( "✅ Added package.json to git" ) ;
153+
154+ const gitCommitResult = await asyncSpawn ( [
155+ "git" ,
156+ "commit" ,
157+ "-m" ,
158+ `release: v${ version } ` ,
159+ ] ) ;
160+ if ( gitCommitResult . exitCode !== 0 ) {
161+ logAndError ( `Failed to commit with message "release: v${ version } "` ) ;
162+ }
163+ console . log ( `✅ Committed with message "release: v${ version } "` ) ;
164+
165+ const gitPushResult = await asyncSpawn ( [ "git" , "push" , "origin" , "main" ] ) ;
166+ if ( gitPushResult . exitCode !== 0 ) {
167+ logAndError ( "Failed to push to origin main" ) ;
168+ }
169+ console . log ( "✅ Pushed to origin main" ) ;
170+ }
171+
172+ async function cleanDist ( ) {
173+ try {
174+ await Deno . remove ( "./dist" , { recursive : true } ) ;
175+ } catch ( error ) {
176+ if ( ! ( error instanceof Deno . errors . NotFound ) ) {
177+ throw error ;
178+ }
179+ }
180+ }
181+
182+ program
183+ . name ( "release" )
184+ . description (
185+ "A github release tool for the project. Valid types are: major, minor, patch, prerelease" ,
186+ )
187+ . addArgument (
188+ new Argument ( "type" ) . choices (
189+ [
190+ "major" ,
191+ "minor" ,
192+ "patch" ,
193+ "prerelease" ,
194+ ] as const ,
195+ ) ,
196+ )
197+ . action ( async ( type ) => {
198+ try {
199+ const ghCheckResult = await new Deno . Command ( "which" , {
200+ args : [ "gh" ] ,
201+ } ) . output ( ) ;
202+
203+ if ( ! ghCheckResult . success ) {
204+ console . error (
205+ `The 'gh' command is not installed. Please install it.
206+
207+ $ brew install gh
208+
209+ ` ,
210+ ) ;
211+ process . exit ( 1 ) ;
212+ }
213+
214+ process . on ( "SIGINT" , ( ) => {
215+ console . log (
216+ "\nRelease process interrupted. Please confirm to exit (ctrl-c again to confirm)." ,
217+ ) ;
218+ process . once ( "SIGINT" , ( ) => {
219+ console . log ( "Exiting..." ) ;
220+ process . exit ( 1 ) ;
221+ } ) ;
222+ } ) ;
223+
224+ await cleanDist ( ) ;
225+ const version = await getLocalVersion ( ) ;
226+ const bumpedVersion = bumpVersion ( version , type ) ;
227+ await saveVersion ( bumpedVersion ) ;
228+ await compileDistribution ( ) ;
229+ await createRelease ( bumpedVersion ) ;
230+ } catch ( err ) {
231+ console . error ( err ) ;
232+ }
233+ } ) ;
234+
235+ program . parse ( process . argv ) ;
0 commit comments