@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
|
@@ -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 -
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
96
|
-
expect(capturedErrors.length).
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
).toBe(
|
|
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
|
-
//
|
|
308
|
-
expect(capturedErrors.
|
|
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
|
})
|