@aaronshaf/ger 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaronshaf/ger",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Gerrit CLI and SDK - A modern CLI tool and TypeScript SDK for Gerrit Code Review, built with Effect-TS",
5
5
  "keywords": [
6
6
  "gerrit",
@@ -37,7 +37,8 @@ const VERIFIED_PLUS_PATTERN = /Verified\s*[+]\s*1/
37
37
  const VERIFIED_MINUS_PATTERN = /Verified\s*[-]\s*1/
38
38
 
39
39
  /**
40
- * Parse messages to determine build status based on "Build Started" and verification messages
40
+ * Parse messages to determine build status based on "Build Started" and verification messages.
41
+ * Only considers verification messages for the same patchset as the latest build.
41
42
  */
42
43
  const parseBuildStatus = (messages: readonly MessageInfo[]): BuildStatus => {
43
44
  // Empty messages means change exists but has no activity yet - return pending
@@ -45,11 +46,13 @@ const parseBuildStatus = (messages: readonly MessageInfo[]): BuildStatus => {
45
46
  return { state: 'pending' }
46
47
  }
47
48
 
48
- // Find the most recent "Build Started" message
49
+ // Find the most recent "Build Started" message and its revision number
49
50
  let lastBuildDate: string | null = null
51
+ let lastBuildRevision: number | undefined = undefined
50
52
  for (const msg of messages) {
51
53
  if (BUILD_STARTED_PATTERN.test(msg.message)) {
52
54
  lastBuildDate = msg.date
55
+ lastBuildRevision = msg._revision_number
53
56
  }
54
57
  }
55
58
 
@@ -58,12 +61,18 @@ const parseBuildStatus = (messages: readonly MessageInfo[]): BuildStatus => {
58
61
  return { state: 'pending' }
59
62
  }
60
63
 
61
- // Check for verification messages after the build started
64
+ // Check for verification messages after the build started AND for the same revision
62
65
  for (const msg of messages) {
63
66
  const date = msg.date
64
67
  // Gerrit timestamps are ISO 8601 strings (lexicographically sortable)
65
68
  if (date <= lastBuildDate) continue
66
69
 
70
+ // Only consider verification messages for the same patchset
71
+ // If revision numbers are available, they must match
72
+ if (lastBuildRevision !== undefined && msg._revision_number !== undefined) {
73
+ if (msg._revision_number !== lastBuildRevision) continue
74
+ }
75
+
67
76
  if (VERIFIED_PLUS_PATTERN.test(msg.message)) {
68
77
  return { state: 'success' }
69
78
  } else if (VERIFIED_MINUS_PATTERN.test(msg.message)) {
@@ -99,13 +108,6 @@ const pollBuildStatus = (
99
108
  const startTime = Date.now()
100
109
  const timeoutMs = options.timeout * 1000
101
110
 
102
- // Initial message to stderr
103
- yield* Effect.sync(() => {
104
- console.error(
105
- `Watching build status (polling every ${options.interval}s, timeout: ${options.timeout}s)...`,
106
- )
107
- })
108
-
109
111
  while (true) {
110
112
  // Check timeout
111
113
  const elapsed = Date.now() - startTime
@@ -138,25 +140,18 @@ const pollBuildStatus = (
138
140
  process.stdout.write(JSON.stringify(status) + '\n')
139
141
  })
140
142
 
141
- // Terminal states - return immediately
142
- if (
143
- status.state === 'success' ||
144
- status.state === 'failure' ||
145
- status.state === 'not_found'
146
- ) {
147
- yield* Effect.sync(() => {
148
- console.error(`Build completed with status: ${status.state}`)
149
- })
143
+ // Terminal states - wait for interval before returning to allow logs to be written
144
+ if (status.state === 'success' || status.state === 'not_found') {
150
145
  return status
151
146
  }
152
147
 
153
- // Non-terminal states - log progress and wait
154
- const elapsedSeconds = Math.floor(elapsed / 1000)
155
- yield* Effect.sync(() => {
156
- console.error(`[${elapsedSeconds}s elapsed] Build status: ${status.state}`)
157
- })
148
+ if (status.state === 'failure') {
149
+ // Wait for interval seconds to allow build failure logs to be fully written
150
+ yield* Effect.sleep(options.interval * 1000)
151
+ return status
152
+ }
158
153
 
159
- // Sleep for interval duration
154
+ // Non-terminal states - sleep for interval duration
160
155
  yield* Effect.sleep(options.interval * 1000)
161
156
  }
162
157
  })
@@ -92,12 +92,8 @@ describe('build-status command - watch mode', () => {
92
92
  expect(JSON.parse(capturedStdout[1])).toEqual({ state: 'running' })
93
93
  expect(JSON.parse(capturedStdout[2])).toEqual({ state: 'success' })
94
94
 
95
- // Should have logged progress to stderr
96
- expect(capturedErrors.length).toBeGreaterThan(0)
97
- expect(capturedErrors.some((e: string) => e.includes('Watching build status'))).toBe(true)
98
- expect(
99
- capturedErrors.some((e: string) => e.includes('Build completed with status: success')),
100
- ).toBe(true)
95
+ // Minimalistic output: no stderr messages except on timeout/error
96
+ expect(capturedErrors.length).toBe(0)
101
97
  })
102
98
 
103
99
  test('polls until failure state is reached', async () => {
@@ -155,9 +151,9 @@ describe('build-status command - watch mode', () => {
155
151
 
156
152
  expect(capturedStdout.length).toBeGreaterThanOrEqual(2)
157
153
  expect(JSON.parse(capturedStdout[capturedStdout.length - 1])).toEqual({ state: 'failure' })
158
- expect(
159
- capturedErrors.some((e: string) => e.includes('Build completed with status: failure')),
160
- ).toBe(true)
154
+
155
+ // Minimalistic output: no stderr messages except on timeout/error
156
+ expect(capturedErrors.length).toBe(0)
161
157
  })
162
158
 
163
159
  test('times out after specified duration', async () => {
@@ -303,9 +299,10 @@ describe('build-status command - watch mode', () => {
303
299
 
304
300
  expect(capturedStdout.length).toBe(1)
305
301
  expect(JSON.parse(capturedStdout[0])).toEqual({ state: 'not_found' })
302
+
306
303
  // 404 errors bypass pollBuildStatus and are handled in error handler
307
- // So we get "Watching build status" but not "Build completed" message
308
- expect(capturedErrors.some((e: string) => e.includes('Watching build status'))).toBe(true)
304
+ // Minimalistic output: no stderr messages for not_found state
305
+ expect(capturedErrors.length).toBe(0)
309
306
  })
310
307
 
311
308
  test('without watch flag, behaves as single check', async () => {
@@ -637,4 +637,153 @@ describe('build-status command', () => {
637
637
  // Regex should handle extra whitespace
638
638
  expect(output).toEqual({ state: 'running' })
639
639
  })
640
+
641
+ test('ignores verification from older patchset when newer patchset build is running', async () => {
642
+ // This test replicates the bug scenario:
643
+ // - PS 3 build started, then PS 4 build started
644
+ // - PS 3 verification (-1) comes AFTER PS 4 build started
645
+ // - Should return "running" because PS 4 has no verification yet
646
+ const messages: MessageInfo[] = [
647
+ {
648
+ id: 'msg1',
649
+ message: 'Build Started https://jenkins.example.com/job/123/',
650
+ date: '2024-01-15 11:12:00.000000000',
651
+ _revision_number: 2,
652
+ author: {
653
+ _account_id: 9999,
654
+ name: 'Service Cloud Jenkins',
655
+ },
656
+ },
657
+ {
658
+ id: 'msg2',
659
+ message: 'Patch Set 2: Verified -1\n\nBuild Failed',
660
+ date: '2024-01-15 11:23:00.000000000',
661
+ _revision_number: 2,
662
+ author: {
663
+ _account_id: 9999,
664
+ name: 'Service Cloud Jenkins',
665
+ },
666
+ },
667
+ {
668
+ id: 'msg3',
669
+ message: 'Build Started https://jenkins.example.com/job/456/',
670
+ date: '2024-01-15 13:57:00.000000000',
671
+ _revision_number: 3,
672
+ author: {
673
+ _account_id: 9999,
674
+ name: 'Service Cloud Jenkins',
675
+ },
676
+ },
677
+ {
678
+ id: 'msg4',
679
+ message: 'Build Started https://jenkins.example.com/job/789/',
680
+ date: '2024-01-15 14:02:00.000000000',
681
+ _revision_number: 4,
682
+ author: {
683
+ _account_id: 9999,
684
+ name: 'Service Cloud Jenkins',
685
+ },
686
+ },
687
+ {
688
+ id: 'msg5',
689
+ message: 'Patch Set 3: Verified -1\n\nBuild Failed : ABORTED',
690
+ date: '2024-01-15 14:03:00.000000000',
691
+ _revision_number: 3,
692
+ author: {
693
+ _account_id: 9999,
694
+ name: 'Service Cloud Jenkins',
695
+ },
696
+ },
697
+ ]
698
+
699
+ server.use(
700
+ http.get('*/a/changes/12345', ({ request }) => {
701
+ const url = new URL(request.url)
702
+ if (url.searchParams.get('o') === 'MESSAGES') {
703
+ return HttpResponse.json(
704
+ { messages },
705
+ {
706
+ headers: { 'Content-Type': 'application/json' },
707
+ },
708
+ )
709
+ }
710
+ return HttpResponse.text('Not Found', { status: 404 })
711
+ }),
712
+ )
713
+
714
+ const effect = buildStatusCommand('12345').pipe(
715
+ Effect.provide(GerritApiServiceLive),
716
+ Effect.provide(createMockConfigLayer()),
717
+ )
718
+
719
+ await Effect.runPromise(effect)
720
+
721
+ expect(capturedStdout.length).toBe(1)
722
+ const output = JSON.parse(capturedStdout[0])
723
+ // PS 4 build started at 14:02, PS 3 verification at 14:03 should be IGNORED
724
+ // because it's for a different revision. PS 4 build is still running.
725
+ expect(output).toEqual({ state: 'running' })
726
+ })
727
+
728
+ test('returns success when verification matches the latest patchset', async () => {
729
+ const messages: MessageInfo[] = [
730
+ {
731
+ id: 'msg1',
732
+ message: 'Build Started',
733
+ date: '2024-01-15 10:00:00.000000000',
734
+ _revision_number: 1,
735
+ author: {
736
+ _account_id: 9999,
737
+ name: 'CI Bot',
738
+ },
739
+ },
740
+ {
741
+ id: 'msg2',
742
+ message: 'Build Started',
743
+ date: '2024-01-15 11:00:00.000000000',
744
+ _revision_number: 2,
745
+ author: {
746
+ _account_id: 9999,
747
+ name: 'CI Bot',
748
+ },
749
+ },
750
+ {
751
+ id: 'msg3',
752
+ message: 'Patch Set 2: Verified+1',
753
+ date: '2024-01-15 11:15:00.000000000',
754
+ _revision_number: 2,
755
+ author: {
756
+ _account_id: 9999,
757
+ name: 'CI Bot',
758
+ },
759
+ },
760
+ ]
761
+
762
+ server.use(
763
+ http.get('*/a/changes/12345', ({ request }) => {
764
+ const url = new URL(request.url)
765
+ if (url.searchParams.get('o') === 'MESSAGES') {
766
+ return HttpResponse.json(
767
+ { messages },
768
+ {
769
+ headers: { 'Content-Type': 'application/json' },
770
+ },
771
+ )
772
+ }
773
+ return HttpResponse.text('Not Found', { status: 404 })
774
+ }),
775
+ )
776
+
777
+ const effect = buildStatusCommand('12345').pipe(
778
+ Effect.provide(GerritApiServiceLive),
779
+ Effect.provide(createMockConfigLayer()),
780
+ )
781
+
782
+ await Effect.runPromise(effect)
783
+
784
+ expect(capturedStdout.length).toBe(1)
785
+ const output = JSON.parse(capturedStdout[0])
786
+ // PS 2 build started at 11:00, PS 2 verification at 11:15 - same revision, success
787
+ expect(output).toEqual({ state: 'success' })
788
+ })
640
789
  })