@@ -175,12 +175,6 @@ describe('create-release.ts', () => {
175175 expect ( isClean ) . toBe ( false )
176176 } )
177177
178- it ( 'should check if tag exists' , ( ) => {
179- const tagExists = 'abc123'
180-
181- expect ( tagExists . length ) . toBeGreaterThan ( 0 )
182- } )
183-
184178 it ( 'should handle non-existent tag error' , ( ) => {
185179 const throwError = ( ) => {
186180 throw new Error ( 'tag not found' )
@@ -222,6 +216,141 @@ describe('create-release.ts', () => {
222216 } )
223217 } )
224218
219+ describe ( 'Tag Existence Checking' , ( ) => {
220+ it ( 'should validate 40-character SHA hashes correctly' , ( ) => {
221+ const shaRegex = / ^ [ 0 - 9 a - f ] { 40 } $ / i
222+
223+ // Valid SHA hashes
224+ expect ( shaRegex . test ( 'cf5e203d0890ca2450876def639f1c7ed3f83f1c' ) ) . toBe ( true )
225+ expect ( shaRegex . test ( 'ABCDEF1234567890ABCDEF1234567890ABCDEF12' ) ) . toBe ( true )
226+
227+ // Invalid - too short
228+ expect ( shaRegex . test ( 'cf5e203' ) ) . toBe ( false )
229+
230+ // Invalid - too long
231+ expect ( shaRegex . test ( 'cf5e203d0890ca2450876def639f1c7ed3f83f1c1' ) ) . toBe ( false )
232+
233+ // Invalid - contains non-hex characters
234+ expect ( shaRegex . test ( 'v1.2.3' ) ) . toBe ( false )
235+ expect ( shaRegex . test ( 'fatal: unknown revision' ) ) . toBe ( false )
236+ expect ( shaRegex . test ( 'Needed a single revision' ) ) . toBe ( false )
237+ } )
238+
239+ it ( 'should detect tag exists when git returns valid SHA' , ( ) => {
240+ // This simulates successful git rev-parse --verify
241+ const result = 'cf5e203d0890ca2450876def639f1c7ed3f83f1c'
242+ const shaRegex = / ^ [ 0 - 9 a - f ] { 40 } $ / i
243+ const exists = result . length > 0 && shaRegex . test ( result )
244+
245+ expect ( exists ) . toBe ( true )
246+ } )
247+
248+ it ( 'should detect tag does NOT exist when git throws error' , ( ) => {
249+ // This simulates failed git rev-parse --verify (exception thrown)
250+ let exists = false
251+ try {
252+ throw new Error ( 'fatal: Needed a single revision' )
253+ } catch {
254+ exists = false
255+ }
256+
257+ expect ( exists ) . toBe ( false )
258+ } )
259+
260+ it ( 'should NOT be fooled by error messages containing tag name (THE BUG)' , ( ) => {
261+ // This is the exact bug we had: git rev-parse fails but returns text
262+ // containing the tag name, which was treated as truthy
263+ const errorOutput = "fatal: ambiguous argument 'v1.2.5': unknown revision"
264+ const shaRegex = / ^ [ 0 - 9 a - f ] { 40 } $ / i
265+
266+ // Old buggy logic would check: if (errorOutput) { tag exists }
267+ // New correct logic checks for valid SHA
268+ const exists = errorOutput . length > 0 && shaRegex . test ( errorOutput )
269+
270+ expect ( exists ) . toBe ( false ) // Should be false because it's not a valid SHA
271+ } )
272+
273+ it ( 'should handle error output that looks like it might be valid' , ( ) => {
274+ const testCases = [
275+ { output : 'v1.2.5' , shouldExist : false } ,
276+ { output : 'tag not found' , shouldExist : false } ,
277+ { output : '' , shouldExist : false } ,
278+ { output : '123' , shouldExist : false } ,
279+ { output : 'abcdef' , shouldExist : false } , // Too short
280+ { output : 'cf5e203d0890ca2450876def639f1c7ed3f83f1c' , shouldExist : true } , // Valid SHA
281+ ]
282+
283+ const shaRegex = / ^ [ 0 - 9 a - f ] { 40 } $ / i
284+
285+ testCases . forEach ( ( { output, shouldExist } ) => {
286+ const exists = output . length > 0 && shaRegex . test ( output )
287+ expect ( exists ) . toBe ( shouldExist )
288+ } )
289+ } )
290+
291+ it ( 'should detect tag on remote when ls-remote returns data' , ( ) => {
292+ // Simulate git ls-remote --tags origin refs/tags/v1.2.3
293+ const remoteOutput = 'cf5e203d0890ca2450876def639f1c7ed3f83f1c\trefs/tags/v1.2.3'
294+ const exists = remoteOutput . length > 0
295+
296+ expect ( exists ) . toBe ( true )
297+ } )
298+
299+ it ( 'should detect tag NOT on remote when ls-remote returns empty' , ( ) => {
300+ // Simulate git ls-remote --tags origin refs/tags/v1.2.3 (tag doesn't exist)
301+ const remoteOutput = ''
302+ const exists = remoteOutput . length > 0
303+
304+ expect ( exists ) . toBe ( false )
305+ } )
306+
307+ it ( 'should check tag existence before making file changes' , ( ) => {
308+ // This tests the order of operations - tag check should come FIRST
309+ const operations : string [ ] = [ ]
310+
311+ // Simulate the workflow
312+ const checkTag = ( ) => {
313+ operations . push ( 'check_tag' )
314+ return false // Tag doesn't exist
315+ }
316+
317+ const updateFiles = ( ) => operations . push ( 'update_files' )
318+ const commitChanges = ( ) => operations . push ( 'commit' )
319+ const createTag = ( ) => operations . push ( 'create_tag' )
320+
321+ // Correct order
322+ if ( ! checkTag ( ) ) {
323+ updateFiles ( )
324+ commitChanges ( )
325+ createTag ( )
326+ }
327+
328+ expect ( operations ) . toEqual ( [ 'check_tag' , 'update_files' , 'commit' , 'create_tag' ] )
329+ expect ( operations [ 0 ] ) . toBe ( 'check_tag' ) // Tag check MUST be first
330+ } )
331+
332+ it ( 'should exit early if tag already exists' , ( ) => {
333+ // This tests that we don't modify files if tag exists
334+ const operations : string [ ] = [ ]
335+
336+ const checkTag = ( ) => {
337+ operations . push ( 'check_tag' )
338+ return true // Tag EXISTS
339+ }
340+
341+ const updateFiles = ( ) => operations . push ( 'update_files' )
342+ const commitChanges = ( ) => operations . push ( 'commit' )
343+
344+ // Should exit early
345+ if ( ! checkTag ( ) ) {
346+ updateFiles ( )
347+ commitChanges ( )
348+ }
349+
350+ expect ( operations ) . toEqual ( [ 'check_tag' ] ) // Only tag check, nothing else!
351+ } )
352+ } )
353+
225354 describe ( 'Argument Parsing' , ( ) => {
226355 it ( 'should parse --version argument' , ( ) => {
227356 process . argv = [ 'node' , 'script.ts' , '--version' , '1.2.3' ]
0 commit comments