11#!/usr/bin/env bun
22
3- import { execSync } from 'node:child_process'
3+ import { execFileSync } from 'node:child_process'
44import { Octokit } from '@octokit/rest'
5+ import { createLogger } from '@sim/logger'
56import { sleep } from '@sim/utils/helpers'
67
8+ const logger = createLogger ( 'CreateRelease' )
79const GITHUB_TOKEN = process . env . GH_PAT
810const REPO_OWNER = 'simstudioai'
911const REPO_NAME = 'sim'
1012
11- if ( ! GITHUB_TOKEN ) {
12- console . error ( '❌ GH_PAT environment variable is required' )
13- process . exit ( 1 )
14- }
15-
1613const targetVersion = process . argv [ 2 ]
17- if ( ! targetVersion ) {
18- console . error ( '❌ Version argument is required' )
19- console . error ( 'Usage: bun run scripts/create-single-release.ts v0.3.XX' )
20- process . exit ( 1 )
21- }
2214
2315const octokit = new Octokit ( {
2416 auth : GITHUB_TOKEN ,
@@ -40,70 +32,79 @@ interface CommitDetail {
4032 prNumber ?: string
4133}
4234
43- function execCommand ( command : string ) : string {
35+ function execGit ( args : string [ ] ) : string {
4436 try {
45- return execSync ( command , { encoding : 'utf8' } ) . trim ( )
37+ return execFileSync ( 'git' , args , { encoding : 'utf8' } ) . trim ( )
4638 } catch ( error ) {
47- console . error ( `❌ Command failed: ${ command } ` )
39+ logger . error ( 'Git command failed' , { args } )
4840 throw error
4941 }
5042}
5143
52- function findVersionCommit ( version : string ) : VersionCommit | null {
53- console . log ( `🔍 Finding commit for version ${ version } ...` )
44+ const VERSION_COMMIT_FORMAT = '--format=%H%x00%s%x00%aI%x00%an'
5445
55- const gitLog = execCommand ( 'git log --oneline --format="%H|%s|%ai|%an" main' )
56- const lines = gitLog . split ( '\n' ) . filter ( ( line ) => line . trim ( ) )
46+ function parseVersionCommit ( line : string ) : VersionCommit | null {
47+ if ( ! line ) return null
5748
58- for ( const line of lines ) {
59- const [ hash , message , date , author ] = line . split ( '|' )
49+ const [ hash , message , date , author ] = line . split ( '\0' )
50+ const versionMatch = message . match ( / ^ \s * ( v \d + \. \d + \. ? \d * ) : \s * ( .+ ) $ / )
51+ if ( ! versionMatch ) return null
6052
61- const versionMatch = message . match ( / ^ \s * ( v \d + \. \d + \. ? \d * ) : \s * ( .+ ) $ / )
62- if ( versionMatch && versionMatch [ 1 ] === version ) {
63- return {
64- hash,
65- version,
66- title : versionMatch [ 2 ] ,
67- date : new Date ( date ) . toISOString ( ) ,
68- author,
69- }
70- }
53+ return {
54+ hash,
55+ version : versionMatch [ 1 ] ,
56+ title : versionMatch [ 2 ] ,
57+ date : new Date ( date ) . toISOString ( ) ,
58+ author,
7159 }
72-
73- return null
7460}
7561
76- function findPreviousVersionCommit ( currentVersion : string ) : VersionCommit | null {
77- console . log ( `🔍 Finding previous version before ${ currentVersion } ...` )
78-
79- const gitLog = execCommand ( 'git log --oneline --format="%H|%s|%ai|%an" main' )
80- const lines = gitLog . split ( '\n' ) . filter ( ( line ) => line . trim ( ) )
81-
82- let foundCurrent = false
83-
84- for ( const line of lines ) {
85- const [ hash , message , date , author ] = line . split ( '|' )
62+ /** Reads one release candidate at a time, stopping at the first matching subject. */
63+ function findReleaseCommit ( ref : string , version ?: string , skip = 0 ) : VersionCommit | null {
64+ const versionPattern = version ? version . replaceAll ( '.' , '[.]' ) : 'v[0-9]+[.][0-9]+[.]?[0-9]*'
65+
66+ while ( true ) {
67+ const line = execGit ( [
68+ 'log' ,
69+ '--first-parent' ,
70+ '--max-count=1' ,
71+ `--skip=${ skip } ` ,
72+ '--extended-regexp' ,
73+ `--grep=^[[:space:]]*${ versionPattern } :[[:space:]]*.+` ,
74+ VERSION_COMMIT_FORMAT ,
75+ ref ,
76+ '--' ,
77+ ] )
78+ if ( ! line ) return null
79+
80+ const commit = parseVersionCommit ( line )
81+ if ( commit && ( ! version || commit . version === version ) ) return commit
82+
83+ /** Git's grep also matches commit bodies; only release subjects are boundaries. */
84+ skip ++
85+ }
86+ }
8687
87- const versionMatch = message . match ( / ^ \s * ( v \d + \. \d + \. ? \d * ) : \s * ( . + ) $ / )
88- if ( versionMatch ) {
89- if ( versionMatch [ 1 ] === currentVersion ) {
90- foundCurrent = true
91- continue
92- }
88+ export function findVersionCommit (
89+ version : string ,
90+ commitSha = process . env . GITHUB_SHA
91+ ) : VersionCommit | null {
92+ logger . info ( `Finding commit for version ${ version } ` )
93+ if ( ! / ^ v \d + \. \d + \. ? \d * $ / . test ( version ) ) return null
9394
94- if ( foundCurrent ) {
95- return {
96- hash,
97- version : versionMatch [ 1 ] ,
98- title : versionMatch [ 2 ] ,
99- date : new Date ( date ) . toISOString ( ) ,
100- author,
101- }
102- }
103- }
95+ if ( commitSha ) {
96+ const commit = parseVersionCommit (
97+ execGit ( [ 'log' , '-1' , VERSION_COMMIT_FORMAT , commitSha , '--' ] )
98+ )
99+ return commit ?. version === version ? commit : null
104100 }
105101
106- return null
102+ return findReleaseCommit ( 'main' , version )
103+ }
104+
105+ export function findPreviousVersionCommit ( currentCommit : VersionCommit ) : VersionCommit | null {
106+ logger . info ( `Finding previous version before ${ currentCommit . version } ` )
107+ return findReleaseCommit ( currentCommit . hash , undefined , 1 )
107108}
108109
109110async function fetchGitHubCommitDetails (
@@ -145,7 +146,7 @@ async function fetchGitHubCommitDetails(
145146 console . warn ( `⚠️ Could not fetch commit ${ hash . substring ( 0 , 7 ) } : ${ error ?. message || error } ` )
146147
147148 try {
148- const gitData = execCommand ( `git log --format=" %s|%an" -1 ${ hash } ` ) . split ( '|' )
149+ const gitData = execGit ( [ ' log' , ' --format=%s|%an' , '-1' , hash , '--' ] ) . split ( '|' )
149150 let message = gitData [ 0 ] || 'Unknown commit'
150151
151152 const prMatch = message . match ( / \( # ( \d + ) \) / )
@@ -188,7 +189,7 @@ async function getCommitsBetweenVersions(
188189 console . log ( `🔍 Getting commits before first version ${ currentCommit . version } ` )
189190 }
190191
191- const gitLog = execCommand ( `git log --oneline -- format=" %H|%s" ${ range } ` )
192+ const gitLog = execGit ( [ ' log' , '-- format=%H|%s' , range , '--' ] )
192193
193194 if ( ! gitLog . trim ( ) ) {
194195 console . log ( `⚠️ No commits found in range ${ range } ` )
@@ -350,6 +351,17 @@ async function generateReleaseBody(
350351}
351352
352353async function main ( ) {
354+ if ( ! GITHUB_TOKEN ) {
355+ logger . error ( 'GH_PAT environment variable is required' )
356+ process . exit ( 1 )
357+ }
358+ if ( ! targetVersion ) {
359+ logger . error (
360+ 'Version argument is required. Usage: bun run scripts/create-single-release.ts vX.Y.Z'
361+ )
362+ process . exit ( 1 )
363+ }
364+
353365 try {
354366 console . log ( `🚀 Creating single release for ${ targetVersion } ...` )
355367
@@ -363,7 +375,7 @@ async function main() {
363375 `✅ Found version commit: ${ versionCommit . hash . substring ( 0 , 7 ) } - ${ versionCommit . title } `
364376 )
365377
366- const previousCommit = findPreviousVersionCommit ( targetVersion )
378+ const previousCommit = findPreviousVersionCommit ( versionCommit )
367379 if ( previousCommit ) {
368380 console . log ( `✅ Found previous version: ${ previousCommit . version } ` )
369381 } else {
@@ -414,4 +426,6 @@ async function main() {
414426 }
415427}
416428
417- main ( )
429+ if ( import . meta. main ) {
430+ main ( )
431+ }
0 commit comments