@aaronshaf/ger 0.3.1 → 0.3.3
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/EXAMPLES.md +48 -0
- package/README.md +87 -0
- package/package.json +1 -1
- package/src/api/gerrit.ts +28 -0
- package/src/cli/commands/add-reviewer.ts +135 -0
- package/src/cli/commands/build-status.ts +69 -25
- package/src/cli/commands/push.ts +430 -0
- package/src/cli/commands/search.ts +162 -0
- package/src/cli/commands/show.ts +20 -0
- package/src/cli/index.ts +115 -74
- package/src/schemas/gerrit.ts +43 -0
- package/src/services/commit-hook.ts +314 -0
- package/tests/add-reviewer.test.ts +393 -0
- package/tests/build-status-watch.test.ts +8 -11
- package/tests/build-status.test.ts +149 -0
- package/tests/integration/commit-hook.test.ts +246 -0
- package/tests/search.test.ts +712 -0
- package/tests/unit/commands/push.test.ts +194 -0
- package/tests/unit/patterns/push-patterns.test.ts +148 -0
- package/tests/unit/services/commit-hook.test.ts +132 -0
package/EXAMPLES.md
CHANGED
|
@@ -117,6 +117,54 @@ const program = pipe(
|
|
|
117
117
|
Effect.runPromise(program).catch(console.error)
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
### 2b. Search Changes with Query Syntax
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { Effect, pipe } from 'effect'
|
|
124
|
+
import {
|
|
125
|
+
GerritApiService,
|
|
126
|
+
GerritApiServiceLive,
|
|
127
|
+
ConfigServiceLive,
|
|
128
|
+
} from '@aaronshaf/ger'
|
|
129
|
+
|
|
130
|
+
const searchChanges = (query: string, limit = 25) =>
|
|
131
|
+
Effect.gen(function* () {
|
|
132
|
+
const api = yield* GerritApiService
|
|
133
|
+
|
|
134
|
+
// Use Gerrit query syntax to search changes
|
|
135
|
+
const fullQuery = query.includes('limit:') ? query : `${query} limit:${limit}`
|
|
136
|
+
const changes = yield* api.listChanges(fullQuery)
|
|
137
|
+
|
|
138
|
+
// Group by project for organized output
|
|
139
|
+
const byProject = new Map<string, typeof changes>()
|
|
140
|
+
for (const change of changes) {
|
|
141
|
+
const existing = byProject.get(change.project) ?? []
|
|
142
|
+
existing.push(change)
|
|
143
|
+
byProject.set(change.project, existing)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(`Found ${changes.length} changes:`)
|
|
147
|
+
for (const [project, projectChanges] of byProject) {
|
|
148
|
+
console.log(`\n${project}:`)
|
|
149
|
+
for (const change of projectChanges) {
|
|
150
|
+
console.log(` #${change._number} - ${change.subject} (${change.status})`)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return changes
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Example queries
|
|
158
|
+
const program = pipe(
|
|
159
|
+
// Search for merged changes in the last week
|
|
160
|
+
searchChanges('status:merged age:7d', 10),
|
|
161
|
+
Effect.provide(GerritApiServiceLive),
|
|
162
|
+
Effect.provide(ConfigServiceLive)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
Effect.runPromise(program).catch(console.error)
|
|
166
|
+
```
|
|
167
|
+
|
|
120
168
|
### 3. Post a Comment
|
|
121
169
|
|
|
122
170
|
```typescript
|
package/README.md
CHANGED
|
@@ -49,9 +49,16 @@ ger show 12345
|
|
|
49
49
|
# Add a comment
|
|
50
50
|
ger comment 12345 -m "LGTM"
|
|
51
51
|
|
|
52
|
+
# Add reviewers to a change
|
|
53
|
+
ger add-reviewer user@example.com -c 12345
|
|
54
|
+
|
|
52
55
|
# Get diff for review
|
|
53
56
|
ger diff 12345
|
|
54
57
|
|
|
58
|
+
# Search for changes using Gerrit query syntax
|
|
59
|
+
ger search "owner:self status:open"
|
|
60
|
+
ger search "project:my-project" -n 10
|
|
61
|
+
|
|
55
62
|
# Extract URLs from messages (e.g., Jenkins build links)
|
|
56
63
|
ger extract-url "build-summary-report" | tail -1
|
|
57
64
|
|
|
@@ -98,6 +105,54 @@ ger workspace
|
|
|
98
105
|
ger workspace --pretty
|
|
99
106
|
```
|
|
100
107
|
|
|
108
|
+
### Search Changes
|
|
109
|
+
|
|
110
|
+
Search for changes across your Gerrit instance using native query syntax:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Search for all open changes (default)
|
|
114
|
+
ger search
|
|
115
|
+
|
|
116
|
+
# Search for your open changes
|
|
117
|
+
ger search "owner:self status:open"
|
|
118
|
+
|
|
119
|
+
# Search for changes by a specific user
|
|
120
|
+
ger search "owner:john@example.com"
|
|
121
|
+
|
|
122
|
+
# Search by project
|
|
123
|
+
ger search "project:my-project status:open"
|
|
124
|
+
|
|
125
|
+
# Search with date filters
|
|
126
|
+
ger search "owner:self after:2025-01-01"
|
|
127
|
+
ger search "status:merged age:7d"
|
|
128
|
+
|
|
129
|
+
# Combine filters
|
|
130
|
+
ger search "owner:self status:merged before:2025-06-01"
|
|
131
|
+
|
|
132
|
+
# Limit results (default: 25)
|
|
133
|
+
ger search "project:my-project" -n 10
|
|
134
|
+
|
|
135
|
+
# XML output for automation
|
|
136
|
+
ger search "owner:self" --xml
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### Common query operators:
|
|
140
|
+
| Operator | Description |
|
|
141
|
+
|----------|-------------|
|
|
142
|
+
| `owner:USER` | Changes owned by USER (use 'self' for yourself) |
|
|
143
|
+
| `status:STATE` | open, merged, abandoned, closed |
|
|
144
|
+
| `project:NAME` | Changes in a specific project |
|
|
145
|
+
| `branch:NAME` | Changes targeting a branch |
|
|
146
|
+
| `age:TIME` | Time since last update (e.g., 1d, 2w, 1mon) |
|
|
147
|
+
| `before:DATE` | Changes modified before date (YYYY-MM-DD) |
|
|
148
|
+
| `after:DATE` | Changes modified after date (YYYY-MM-DD) |
|
|
149
|
+
| `is:wip` | Work-in-progress changes |
|
|
150
|
+
| `is:submittable` | Changes ready to submit |
|
|
151
|
+
| `reviewer:USER` | Changes where USER is a reviewer |
|
|
152
|
+
| `label:NAME=VALUE` | Filter by label (e.g., label:Code-Review+2) |
|
|
153
|
+
|
|
154
|
+
See the [full query syntax documentation](https://gerrit-review.googlesource.com/Documentation/user-search.html).
|
|
155
|
+
|
|
101
156
|
### Comments
|
|
102
157
|
|
|
103
158
|
#### Overall Comments
|
|
@@ -359,6 +414,38 @@ ger abandon 12345
|
|
|
359
414
|
ger abandon 12345 -m "Reason"
|
|
360
415
|
```
|
|
361
416
|
|
|
417
|
+
### Add Reviewers
|
|
418
|
+
|
|
419
|
+
Add reviewers or CCs to a change:
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
# Add a single reviewer
|
|
423
|
+
ger add-reviewer user@example.com -c 12345
|
|
424
|
+
|
|
425
|
+
# Add multiple reviewers
|
|
426
|
+
ger add-reviewer user1@example.com user2@example.com -c 12345
|
|
427
|
+
|
|
428
|
+
# Add as CC instead of reviewer
|
|
429
|
+
ger add-reviewer --cc user@example.com -c 12345
|
|
430
|
+
|
|
431
|
+
# Suppress email notifications
|
|
432
|
+
ger add-reviewer --notify none user@example.com -c 12345
|
|
433
|
+
|
|
434
|
+
# XML output for automation
|
|
435
|
+
ger add-reviewer user@example.com -c 12345 --xml
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
#### Options:
|
|
439
|
+
- `-c, --change <id>` - Change ID (required)
|
|
440
|
+
- `--cc` - Add as CC instead of reviewer
|
|
441
|
+
- `--notify <level>` - Notification level: `none`, `owner`, `owner_reviewers`, `all` (default: `all`)
|
|
442
|
+
- `--xml` - XML output for LLM/automation consumption
|
|
443
|
+
|
|
444
|
+
#### Notes:
|
|
445
|
+
- Both email addresses and usernames are accepted
|
|
446
|
+
- Multiple reviewers can be added in a single command
|
|
447
|
+
- Use `--cc` for carbon copy (notified but not required to review)
|
|
448
|
+
|
|
362
449
|
### AI-Powered Review
|
|
363
450
|
|
|
364
451
|
The `ger review` command provides automated code review using AI tools (claude, llm, or opencode CLI).
|
package/package.json
CHANGED
package/src/api/gerrit.ts
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
FileInfo,
|
|
10
10
|
type GerritCredentials,
|
|
11
11
|
type ReviewInput,
|
|
12
|
+
type ReviewerInput,
|
|
13
|
+
ReviewerResult,
|
|
12
14
|
RevisionInfo,
|
|
13
15
|
} from '@/schemas/gerrit'
|
|
14
16
|
import { filterMeaningfulMessages } from '@/utils/message-filters'
|
|
@@ -50,6 +52,11 @@ export interface GerritApiServiceImpl {
|
|
|
50
52
|
revisionId?: string,
|
|
51
53
|
) => Effect.Effect<Record<string, readonly CommentInfo[]>, ApiError>
|
|
52
54
|
readonly getMessages: (changeId: string) => Effect.Effect<readonly MessageInfo[], ApiError>
|
|
55
|
+
readonly addReviewer: (
|
|
56
|
+
changeId: string,
|
|
57
|
+
reviewer: string,
|
|
58
|
+
options?: { state?: 'REVIEWER' | 'CC'; notify?: 'NONE' | 'OWNER' | 'OWNER_REVIEWERS' | 'ALL' },
|
|
59
|
+
) => Effect.Effect<ReviewerResult, ApiError>
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
// Export both the tag value and the type for use in Effect requirements
|
|
@@ -485,6 +492,26 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
|
|
|
485
492
|
return changeResponse.messages || []
|
|
486
493
|
}).pipe(Effect.map(filterMeaningfulMessages))
|
|
487
494
|
|
|
495
|
+
const addReviewer = (
|
|
496
|
+
changeId: string,
|
|
497
|
+
reviewer: string,
|
|
498
|
+
options?: {
|
|
499
|
+
state?: 'REVIEWER' | 'CC'
|
|
500
|
+
notify?: 'NONE' | 'OWNER' | 'OWNER_REVIEWERS' | 'ALL'
|
|
501
|
+
},
|
|
502
|
+
) =>
|
|
503
|
+
Effect.gen(function* () {
|
|
504
|
+
const { credentials, authHeader } = yield* getCredentialsAndAuth
|
|
505
|
+
const normalized = yield* normalizeAndValidate(changeId)
|
|
506
|
+
const url = `${credentials.host}/a/changes/${encodeURIComponent(normalized)}/reviewers`
|
|
507
|
+
const body: ReviewerInput = {
|
|
508
|
+
reviewer,
|
|
509
|
+
...(options?.state && { state: options.state }),
|
|
510
|
+
...(options?.notify && { notify: options.notify }),
|
|
511
|
+
}
|
|
512
|
+
return yield* makeRequest(url, authHeader, 'POST', body, ReviewerResult)
|
|
513
|
+
})
|
|
514
|
+
|
|
488
515
|
return {
|
|
489
516
|
getChange,
|
|
490
517
|
listChanges,
|
|
@@ -499,6 +526,7 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
|
|
|
499
526
|
getDiff,
|
|
500
527
|
getComments,
|
|
501
528
|
getMessages,
|
|
529
|
+
addReviewer,
|
|
502
530
|
}
|
|
503
531
|
}),
|
|
504
532
|
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import { type ApiError, GerritApiService } from '@/api/gerrit'
|
|
3
|
+
|
|
4
|
+
interface AddReviewerOptions {
|
|
5
|
+
change?: string
|
|
6
|
+
cc?: boolean
|
|
7
|
+
notify?: string
|
|
8
|
+
xml?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type NotifyLevel = 'NONE' | 'OWNER' | 'OWNER_REVIEWERS' | 'ALL'
|
|
12
|
+
|
|
13
|
+
const VALID_NOTIFY_LEVELS: ReadonlyArray<NotifyLevel> = ['NONE', 'OWNER', 'OWNER_REVIEWERS', 'ALL']
|
|
14
|
+
|
|
15
|
+
const escapeXml = (str: string): string =>
|
|
16
|
+
str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
17
|
+
|
|
18
|
+
const outputXmlError = (message: string): void => {
|
|
19
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
20
|
+
console.log(`<add_reviewer_result>`)
|
|
21
|
+
console.log(` <status>error</status>`)
|
|
22
|
+
console.log(` <error><![CDATA[${message}]]></error>`)
|
|
23
|
+
console.log(`</add_reviewer_result>`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class ValidationError extends Error {
|
|
27
|
+
readonly _tag = 'ValidationError'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const addReviewerCommand = (
|
|
31
|
+
reviewers: string[],
|
|
32
|
+
options: AddReviewerOptions = {},
|
|
33
|
+
): Effect.Effect<void, ApiError | ValidationError, GerritApiService> =>
|
|
34
|
+
Effect.gen(function* () {
|
|
35
|
+
const gerritApi = yield* GerritApiService
|
|
36
|
+
|
|
37
|
+
const changeId = options.change
|
|
38
|
+
|
|
39
|
+
if (!changeId) {
|
|
40
|
+
const message =
|
|
41
|
+
'Change ID is required. Use -c <change-id> or run from a branch with an active change.'
|
|
42
|
+
if (options.xml) {
|
|
43
|
+
outputXmlError(message)
|
|
44
|
+
} else {
|
|
45
|
+
console.error(`✗ ${message}`)
|
|
46
|
+
}
|
|
47
|
+
return yield* Effect.fail(new ValidationError(message))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (reviewers.length === 0) {
|
|
51
|
+
const message = 'At least one reviewer is required.'
|
|
52
|
+
if (options.xml) {
|
|
53
|
+
outputXmlError(message)
|
|
54
|
+
} else {
|
|
55
|
+
console.error(`✗ ${message}`)
|
|
56
|
+
}
|
|
57
|
+
return yield* Effect.fail(new ValidationError(message))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const state: 'REVIEWER' | 'CC' = options.cc ? 'CC' : 'REVIEWER'
|
|
61
|
+
const stateLabel = options.cc ? 'cc' : 'reviewer'
|
|
62
|
+
|
|
63
|
+
let notify: NotifyLevel | undefined
|
|
64
|
+
if (options.notify) {
|
|
65
|
+
const upperNotify = options.notify.toUpperCase()
|
|
66
|
+
if (!VALID_NOTIFY_LEVELS.includes(upperNotify as NotifyLevel)) {
|
|
67
|
+
const message = `Invalid notify level: ${options.notify}. Valid values: none, owner, owner_reviewers, all`
|
|
68
|
+
if (options.xml) {
|
|
69
|
+
outputXmlError(message)
|
|
70
|
+
} else {
|
|
71
|
+
console.error(`✗ ${message}`)
|
|
72
|
+
}
|
|
73
|
+
return yield* Effect.fail(new ValidationError(message))
|
|
74
|
+
}
|
|
75
|
+
notify = upperNotify as NotifyLevel
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const results: Array<{ reviewer: string; success: boolean; name?: string; error?: string }> = []
|
|
79
|
+
|
|
80
|
+
for (const reviewer of reviewers) {
|
|
81
|
+
const result = yield* Effect.either(
|
|
82
|
+
gerritApi.addReviewer(changeId, reviewer, { state, notify }),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if (result._tag === 'Left') {
|
|
86
|
+
const error = result.left
|
|
87
|
+
const message = 'message' in error ? String(error.message) : String(error)
|
|
88
|
+
results.push({ reviewer, success: false, error: message })
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const apiResult = result.right
|
|
93
|
+
|
|
94
|
+
if (apiResult.error) {
|
|
95
|
+
results.push({ reviewer, success: false, error: apiResult.error })
|
|
96
|
+
} else {
|
|
97
|
+
const added = apiResult.reviewers?.[0] || apiResult.ccs?.[0]
|
|
98
|
+
const name = added?.name || added?.email || reviewer
|
|
99
|
+
results.push({ reviewer, success: true, name })
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (options.xml) {
|
|
104
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
105
|
+
console.log(`<add_reviewer_result>`)
|
|
106
|
+
console.log(` <change_id>${escapeXml(changeId)}</change_id>`)
|
|
107
|
+
console.log(` <state>${escapeXml(stateLabel)}</state>`)
|
|
108
|
+
console.log(` <reviewers>`)
|
|
109
|
+
for (const r of results) {
|
|
110
|
+
if (r.success) {
|
|
111
|
+
console.log(` <reviewer status="added">`)
|
|
112
|
+
console.log(` <input>${escapeXml(r.reviewer)}</input>`)
|
|
113
|
+
console.log(` <name><![CDATA[${r.name}]]></name>`)
|
|
114
|
+
console.log(` </reviewer>`)
|
|
115
|
+
} else {
|
|
116
|
+
console.log(` <reviewer status="failed">`)
|
|
117
|
+
console.log(` <input>${escapeXml(r.reviewer)}</input>`)
|
|
118
|
+
console.log(` <error><![CDATA[${r.error}]]></error>`)
|
|
119
|
+
console.log(` </reviewer>`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
console.log(` </reviewers>`)
|
|
123
|
+
const allSuccess = results.every((r) => r.success)
|
|
124
|
+
console.log(` <status>${allSuccess ? 'success' : 'partial_failure'}</status>`)
|
|
125
|
+
console.log(`</add_reviewer_result>`)
|
|
126
|
+
} else {
|
|
127
|
+
for (const r of results) {
|
|
128
|
+
if (r.success) {
|
|
129
|
+
console.log(`✓ Added ${r.name} as ${stateLabel}`)
|
|
130
|
+
} else {
|
|
131
|
+
console.error(`✗ Failed to add ${r.reviewer}: ${r.error}`)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
})
|
|
@@ -3,6 +3,55 @@ import { type ApiError, GerritApiService } from '@/api/gerrit'
|
|
|
3
3
|
import type { MessageInfo } from '@/schemas/gerrit'
|
|
4
4
|
import { getChangeIdFromHead, GitError, NoChangeIdError } from '@/utils/git-commit'
|
|
5
5
|
|
|
6
|
+
/** Help text for build-status command - exported to keep index.ts under line limit */
|
|
7
|
+
export const BUILD_STATUS_HELP_TEXT = `
|
|
8
|
+
This command parses Gerrit change messages to determine build status.
|
|
9
|
+
It looks for "Build Started" messages and subsequent verification labels.
|
|
10
|
+
|
|
11
|
+
Output is JSON with a "state" field that can be:
|
|
12
|
+
- pending: No build has started yet
|
|
13
|
+
- running: Build started but no verification yet
|
|
14
|
+
- success: Build completed with Verified+1
|
|
15
|
+
- failure: Build completed with Verified-1
|
|
16
|
+
- not_found: Change does not exist
|
|
17
|
+
|
|
18
|
+
Exit codes:
|
|
19
|
+
- 0: Default for all states (like gh run watch)
|
|
20
|
+
- 1: Only when --exit-status is used AND build fails
|
|
21
|
+
- 2: Timeout reached in watch mode
|
|
22
|
+
- 3: API/network errors
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
# Single check (current behavior)
|
|
26
|
+
$ ger build-status 392385
|
|
27
|
+
{"state":"success"}
|
|
28
|
+
|
|
29
|
+
# Watch until completion (outputs JSON on each poll)
|
|
30
|
+
$ ger build-status 392385 --watch
|
|
31
|
+
{"state":"pending"}
|
|
32
|
+
{"state":"running"}
|
|
33
|
+
{"state":"running"}
|
|
34
|
+
{"state":"success"}
|
|
35
|
+
|
|
36
|
+
# Watch with custom interval (check every 5 seconds)
|
|
37
|
+
$ ger build-status --watch --interval 5
|
|
38
|
+
|
|
39
|
+
# Watch with custom timeout (60 minutes)
|
|
40
|
+
$ ger build-status --watch --timeout 3600
|
|
41
|
+
|
|
42
|
+
# Exit with code 1 on failure (for CI/CD pipelines)
|
|
43
|
+
$ ger build-status --watch --exit-status && deploy.sh
|
|
44
|
+
|
|
45
|
+
# Trigger notification when done (like gh run watch pattern)
|
|
46
|
+
$ ger build-status --watch && notify-send 'Build is done!'
|
|
47
|
+
|
|
48
|
+
# Parse final state in scripts
|
|
49
|
+
$ ger build-status --watch | tail -1 | jq -r '.state'
|
|
50
|
+
success
|
|
51
|
+
|
|
52
|
+
Note: When no change-id is provided, it will be automatically extracted from the
|
|
53
|
+
Change-ID footer in your HEAD commit.`
|
|
54
|
+
|
|
6
55
|
// Export types for external use
|
|
7
56
|
export type BuildState = 'pending' | 'running' | 'success' | 'failure' | 'not_found'
|
|
8
57
|
|
|
@@ -37,7 +86,8 @@ const VERIFIED_PLUS_PATTERN = /Verified\s*[+]\s*1/
|
|
|
37
86
|
const VERIFIED_MINUS_PATTERN = /Verified\s*[-]\s*1/
|
|
38
87
|
|
|
39
88
|
/**
|
|
40
|
-
* Parse messages to determine build status based on "Build Started" and verification messages
|
|
89
|
+
* Parse messages to determine build status based on "Build Started" and verification messages.
|
|
90
|
+
* Only considers verification messages for the same patchset as the latest build.
|
|
41
91
|
*/
|
|
42
92
|
const parseBuildStatus = (messages: readonly MessageInfo[]): BuildStatus => {
|
|
43
93
|
// Empty messages means change exists but has no activity yet - return pending
|
|
@@ -45,11 +95,13 @@ const parseBuildStatus = (messages: readonly MessageInfo[]): BuildStatus => {
|
|
|
45
95
|
return { state: 'pending' }
|
|
46
96
|
}
|
|
47
97
|
|
|
48
|
-
// Find the most recent "Build Started" message
|
|
98
|
+
// Find the most recent "Build Started" message and its revision number
|
|
49
99
|
let lastBuildDate: string | null = null
|
|
100
|
+
let lastBuildRevision: number | undefined = undefined
|
|
50
101
|
for (const msg of messages) {
|
|
51
102
|
if (BUILD_STARTED_PATTERN.test(msg.message)) {
|
|
52
103
|
lastBuildDate = msg.date
|
|
104
|
+
lastBuildRevision = msg._revision_number
|
|
53
105
|
}
|
|
54
106
|
}
|
|
55
107
|
|
|
@@ -58,12 +110,18 @@ const parseBuildStatus = (messages: readonly MessageInfo[]): BuildStatus => {
|
|
|
58
110
|
return { state: 'pending' }
|
|
59
111
|
}
|
|
60
112
|
|
|
61
|
-
// Check for verification messages after the build started
|
|
113
|
+
// Check for verification messages after the build started AND for the same revision
|
|
62
114
|
for (const msg of messages) {
|
|
63
115
|
const date = msg.date
|
|
64
116
|
// Gerrit timestamps are ISO 8601 strings (lexicographically sortable)
|
|
65
117
|
if (date <= lastBuildDate) continue
|
|
66
118
|
|
|
119
|
+
// Only consider verification messages for the same patchset
|
|
120
|
+
// If revision numbers are available, they must match
|
|
121
|
+
if (lastBuildRevision !== undefined && msg._revision_number !== undefined) {
|
|
122
|
+
if (msg._revision_number !== lastBuildRevision) continue
|
|
123
|
+
}
|
|
124
|
+
|
|
67
125
|
if (VERIFIED_PLUS_PATTERN.test(msg.message)) {
|
|
68
126
|
return { state: 'success' }
|
|
69
127
|
} else if (VERIFIED_MINUS_PATTERN.test(msg.message)) {
|
|
@@ -99,13 +157,6 @@ const pollBuildStatus = (
|
|
|
99
157
|
const startTime = Date.now()
|
|
100
158
|
const timeoutMs = options.timeout * 1000
|
|
101
159
|
|
|
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
160
|
while (true) {
|
|
110
161
|
// Check timeout
|
|
111
162
|
const elapsed = Date.now() - startTime
|
|
@@ -138,25 +189,18 @@ const pollBuildStatus = (
|
|
|
138
189
|
process.stdout.write(JSON.stringify(status) + '\n')
|
|
139
190
|
})
|
|
140
191
|
|
|
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
|
-
})
|
|
192
|
+
// Terminal states - wait for interval before returning to allow logs to be written
|
|
193
|
+
if (status.state === 'success' || status.state === 'not_found') {
|
|
150
194
|
return status
|
|
151
195
|
}
|
|
152
196
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
197
|
+
if (status.state === 'failure') {
|
|
198
|
+
// Wait for interval seconds to allow build failure logs to be fully written
|
|
199
|
+
yield* Effect.sleep(options.interval * 1000)
|
|
200
|
+
return status
|
|
201
|
+
}
|
|
158
202
|
|
|
159
|
-
//
|
|
203
|
+
// Non-terminal states - sleep for interval duration
|
|
160
204
|
yield* Effect.sleep(options.interval * 1000)
|
|
161
205
|
}
|
|
162
206
|
})
|