@aaronshaf/ger 0.2.2 → 0.2.5
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/CLAUDE.md +3 -3
- package/README.md +49 -0
- package/package.json +1 -1
- package/src/cli/commands/build-status.ts +116 -0
- package/src/cli/commands/show.ts +138 -64
- package/src/cli/index.ts +58 -0
- package/tests/abandon.test.ts +178 -111
- package/tests/build-status.test.ts +691 -0
- package/tests/mine.test.ts +130 -163
- package/tests/show-auto-detect.test.ts +20 -2
- package/tests/show.test.ts +226 -8
- package/tests/mocks/fetch-mock.ts +0 -142
- package/tests/setup.ts +0 -13
package/CLAUDE.md
CHANGED
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
### Testing & Coverage
|
|
26
26
|
- **ENFORCE** minimum 80% code coverage
|
|
27
27
|
- **RUN** all tests in pre-commit and pre-push hooks
|
|
28
|
-
- **USE**
|
|
28
|
+
- **USE** MSW (Mock Service Worker) for all HTTP request mocking
|
|
29
29
|
- **REQUIRE** meaningful integration tests for all commands that simulate full workflows
|
|
30
30
|
- **IMPLEMENT** both unit tests and integration tests for every command modification/addition
|
|
31
|
-
- **ENSURE** integration tests use realistic
|
|
31
|
+
- **ENSURE** integration tests use realistic MSW handlers that match Gerrit API responses
|
|
32
32
|
- **EXCLUDE** generated code and tmp/ from coverage
|
|
33
33
|
|
|
34
34
|
### Security
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
### Testing Requirements for Commands
|
|
57
57
|
- **UNIT TESTS**: Test individual functions, schemas, and utilities
|
|
58
58
|
- **INTEGRATION TESTS**: Test complete command flows with mocked HTTP requests
|
|
59
|
-
- **HTTP MOCKING**: Use
|
|
59
|
+
- **HTTP MOCKING**: Use MSW handlers with http.get/http.post patterns for mocking
|
|
60
60
|
- **SCHEMA VALIDATION**: Ensure mocks return data that validates against Effect Schemas
|
|
61
61
|
- **COMMAND COVERAGE**: Every command must have integration tests covering:
|
|
62
62
|
- Happy path execution
|
package/README.md
CHANGED
|
@@ -55,6 +55,10 @@ ger diff 12345
|
|
|
55
55
|
# Extract URLs from messages (e.g., Jenkins build links)
|
|
56
56
|
ger extract-url "build-summary-report" | tail -1
|
|
57
57
|
|
|
58
|
+
# Check CI build status (parses build messages)
|
|
59
|
+
ger build-status 12345 # Returns: pending, running, success, failure, or not_found
|
|
60
|
+
ger build-status # Auto-detects from HEAD commit
|
|
61
|
+
|
|
58
62
|
# AI-powered code review (requires claude, llm, or opencode CLI)
|
|
59
63
|
ger review 12345
|
|
60
64
|
ger review 12345 --dry-run # Preview without posting
|
|
@@ -249,6 +253,51 @@ ger extract-url "github.com" --include-comments
|
|
|
249
253
|
ger extract-url "job/[^/]+/job/[^/]+/\d+/$" --regex
|
|
250
254
|
```
|
|
251
255
|
|
|
256
|
+
### Build Status
|
|
257
|
+
|
|
258
|
+
Check the CI build status of a change by parsing Gerrit messages for build events and verification results:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
# Check build status for a specific change
|
|
262
|
+
ger build-status 12345
|
|
263
|
+
|
|
264
|
+
# Auto-detect change from HEAD commit
|
|
265
|
+
ger build-status
|
|
266
|
+
|
|
267
|
+
# Use in scripts with jq
|
|
268
|
+
ger build-status | jq -r '.state'
|
|
269
|
+
|
|
270
|
+
# Wait for build completion in CI scripts
|
|
271
|
+
while [ "$(ger build-status | jq -r '.state')" = "running" ]; do
|
|
272
|
+
echo "Waiting for build..."
|
|
273
|
+
sleep 30
|
|
274
|
+
done
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### Output format (JSON):
|
|
278
|
+
```json
|
|
279
|
+
{"state": "success"}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### Build states:
|
|
283
|
+
- **`pending`**: No "Build Started" message found yet
|
|
284
|
+
- **`running`**: "Build Started" found, but no verification result yet
|
|
285
|
+
- **`success`**: Verified +1 after most recent "Build Started"
|
|
286
|
+
- **`failure`**: Verified -1 after most recent "Build Started"
|
|
287
|
+
- **`not_found`**: Change does not exist
|
|
288
|
+
|
|
289
|
+
#### How it works:
|
|
290
|
+
1. Fetches all messages for the change
|
|
291
|
+
2. Finds the most recent "Build Started" message
|
|
292
|
+
3. Checks for "Verified +1" or "Verified -1" messages after the build started
|
|
293
|
+
4. Returns the appropriate state
|
|
294
|
+
|
|
295
|
+
#### Use cases:
|
|
296
|
+
- **CI/CD integration**: Wait for builds before proceeding with deployment
|
|
297
|
+
- **Automation**: Trigger actions based on build results
|
|
298
|
+
- **Scripting**: Check build status in shell scripts
|
|
299
|
+
- **Monitoring**: Poll build status for long-running builds
|
|
300
|
+
|
|
252
301
|
### Diff
|
|
253
302
|
```bash
|
|
254
303
|
# Full diff
|
package/package.json
CHANGED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Effect, Schema } from 'effect'
|
|
2
|
+
import { type ApiError, GerritApiService } from '@/api/gerrit'
|
|
3
|
+
import type { MessageInfo } from '@/schemas/gerrit'
|
|
4
|
+
import { getChangeIdFromHead, GitError, NoChangeIdError } from '@/utils/git-commit'
|
|
5
|
+
|
|
6
|
+
// Export types for external use
|
|
7
|
+
export type BuildState = 'pending' | 'running' | 'success' | 'failure' | 'not_found'
|
|
8
|
+
|
|
9
|
+
// Effect Schema for BuildStatus (follows project patterns)
|
|
10
|
+
export const BuildStatus: Schema.Schema<{
|
|
11
|
+
readonly state: 'pending' | 'running' | 'success' | 'failure' | 'not_found'
|
|
12
|
+
}> = Schema.Struct({
|
|
13
|
+
state: Schema.Literal('pending', 'running', 'success', 'failure', 'not_found'),
|
|
14
|
+
})
|
|
15
|
+
export type BuildStatus = Schema.Schema.Type<typeof BuildStatus>
|
|
16
|
+
|
|
17
|
+
// Message patterns for precise matching
|
|
18
|
+
const BUILD_STARTED_PATTERN = /Build\s+Started/i
|
|
19
|
+
const VERIFIED_PLUS_PATTERN = /Verified\s*[+]\s*1/
|
|
20
|
+
const VERIFIED_MINUS_PATTERN = /Verified\s*[-]\s*1/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse messages to determine build status based on "Build Started" and verification messages
|
|
24
|
+
*/
|
|
25
|
+
const parseBuildStatus = (messages: readonly MessageInfo[]): BuildStatus => {
|
|
26
|
+
// Empty messages means change exists but has no activity yet - return pending
|
|
27
|
+
if (messages.length === 0) {
|
|
28
|
+
return { state: 'pending' }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Find the most recent "Build Started" message
|
|
32
|
+
let lastBuildDate: string | null = null
|
|
33
|
+
for (const msg of messages) {
|
|
34
|
+
if (BUILD_STARTED_PATTERN.test(msg.message)) {
|
|
35
|
+
lastBuildDate = msg.date
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// If no build has started, state is "pending"
|
|
40
|
+
if (!lastBuildDate) {
|
|
41
|
+
return { state: 'pending' }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check for verification messages after the build started
|
|
45
|
+
for (const msg of messages) {
|
|
46
|
+
const date = msg.date
|
|
47
|
+
// Gerrit timestamps are ISO 8601 strings (lexicographically sortable)
|
|
48
|
+
if (date <= lastBuildDate) continue
|
|
49
|
+
|
|
50
|
+
if (VERIFIED_PLUS_PATTERN.test(msg.message)) {
|
|
51
|
+
return { state: 'success' }
|
|
52
|
+
} else if (VERIFIED_MINUS_PATTERN.test(msg.message)) {
|
|
53
|
+
return { state: 'failure' }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Build started but no verification yet, state is "running"
|
|
58
|
+
return { state: 'running' }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get messages for a change
|
|
63
|
+
*/
|
|
64
|
+
const getMessagesForChange = (
|
|
65
|
+
changeId: string,
|
|
66
|
+
): Effect.Effect<readonly MessageInfo[], ApiError, GerritApiService> =>
|
|
67
|
+
Effect.gen(function* () {
|
|
68
|
+
const gerritApi = yield* GerritApiService
|
|
69
|
+
const messages = yield* gerritApi.getMessages(changeId)
|
|
70
|
+
return messages
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build status command - determines build status from Gerrit messages
|
|
75
|
+
*/
|
|
76
|
+
export const buildStatusCommand = (
|
|
77
|
+
changeId: string | undefined,
|
|
78
|
+
): Effect.Effect<void, ApiError | Error | GitError | NoChangeIdError, GerritApiService> =>
|
|
79
|
+
Effect.gen(function* () {
|
|
80
|
+
// Auto-detect Change-ID from HEAD commit if not provided
|
|
81
|
+
const resolvedChangeId = changeId || (yield* getChangeIdFromHead())
|
|
82
|
+
|
|
83
|
+
// Fetch messages
|
|
84
|
+
const messages = yield* getMessagesForChange(resolvedChangeId)
|
|
85
|
+
|
|
86
|
+
// Parse build status
|
|
87
|
+
const status = parseBuildStatus(messages)
|
|
88
|
+
|
|
89
|
+
// Output JSON to stdout
|
|
90
|
+
const jsonOutput = JSON.stringify(status) + '\n'
|
|
91
|
+
yield* Effect.sync(() => {
|
|
92
|
+
process.stdout.write(jsonOutput)
|
|
93
|
+
})
|
|
94
|
+
}).pipe(
|
|
95
|
+
// Error handling - return not_found for API errors (e.g., change not found)
|
|
96
|
+
Effect.catchAll((error) => {
|
|
97
|
+
if (error && typeof error === 'object' && 'status' in error && error.status === 404) {
|
|
98
|
+
// Change not found - output not_found state and exit successfully
|
|
99
|
+
const status: BuildStatus = { state: 'not_found' }
|
|
100
|
+
return Effect.sync(() => {
|
|
101
|
+
process.stdout.write(JSON.stringify(status) + '\n')
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// For other errors, write to stderr and exit with error
|
|
106
|
+
const errorMessage =
|
|
107
|
+
error instanceof GitError || error instanceof NoChangeIdError || error instanceof Error
|
|
108
|
+
? error.message
|
|
109
|
+
: String(error)
|
|
110
|
+
|
|
111
|
+
return Effect.sync(() => {
|
|
112
|
+
console.error(`Error: ${errorMessage}`)
|
|
113
|
+
process.exit(1)
|
|
114
|
+
})
|
|
115
|
+
}),
|
|
116
|
+
)
|
package/src/cli/commands/show.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { formatDiffPretty } from '@/utils/diff-formatters'
|
|
|
7
7
|
import { sanitizeCDATA, escapeXML } from '@/utils/shell-safety'
|
|
8
8
|
import { formatDate } from '@/utils/formatters'
|
|
9
9
|
import { getChangeIdFromHead, GitError, NoChangeIdError } from '@/utils/git-commit'
|
|
10
|
+
import { writeFileSync } from 'node:fs'
|
|
10
11
|
|
|
11
12
|
interface ShowOptions {
|
|
12
13
|
xml?: boolean
|
|
@@ -184,12 +185,12 @@ const removeUndefined = <T extends Record<string, any>>(obj: T): Partial<T> => {
|
|
|
184
185
|
) as Partial<T>
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
const formatShowJson = (
|
|
188
|
+
const formatShowJson = async (
|
|
188
189
|
changeDetails: ChangeDetails,
|
|
189
190
|
diff: string,
|
|
190
191
|
commentsWithContext: Array<{ comment: CommentInfo; context?: any }>,
|
|
191
192
|
messages: MessageInfo[],
|
|
192
|
-
): void => {
|
|
193
|
+
): Promise<void> => {
|
|
193
194
|
const output = {
|
|
194
195
|
status: 'success',
|
|
195
196
|
change: removeUndefined({
|
|
@@ -242,82 +243,118 @@ const formatShowJson = (
|
|
|
242
243
|
),
|
|
243
244
|
}
|
|
244
245
|
|
|
245
|
-
|
|
246
|
+
const jsonOutput = JSON.stringify(output, null, 2) + '\n'
|
|
247
|
+
// Write to stdout and ensure all data is flushed before process exits
|
|
248
|
+
// Using process.stdout.write with drain handling for large payloads
|
|
249
|
+
return new Promise<void>((resolve, reject) => {
|
|
250
|
+
const written = process.stdout.write(jsonOutput, (err) => {
|
|
251
|
+
if (err) {
|
|
252
|
+
reject(err)
|
|
253
|
+
} else {
|
|
254
|
+
resolve()
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
if (!written) {
|
|
259
|
+
// If write returned false, buffer is full, wait for drain
|
|
260
|
+
process.stdout.once('drain', resolve)
|
|
261
|
+
process.stdout.once('error', reject)
|
|
262
|
+
}
|
|
263
|
+
})
|
|
246
264
|
}
|
|
247
265
|
|
|
248
|
-
const formatShowXml = (
|
|
266
|
+
const formatShowXml = async (
|
|
249
267
|
changeDetails: ChangeDetails,
|
|
250
268
|
diff: string,
|
|
251
269
|
commentsWithContext: Array<{ comment: CommentInfo; context?: any }>,
|
|
252
270
|
messages: MessageInfo[],
|
|
253
|
-
): void => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
271
|
+
): Promise<void> => {
|
|
272
|
+
// Build complete XML output as a single string to avoid multiple writes
|
|
273
|
+
const xmlParts: string[] = []
|
|
274
|
+
xmlParts.push(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
275
|
+
xmlParts.push(`<show_result>`)
|
|
276
|
+
xmlParts.push(` <status>success</status>`)
|
|
277
|
+
xmlParts.push(` <change>`)
|
|
278
|
+
xmlParts.push(` <id>${escapeXML(changeDetails.id)}</id>`)
|
|
279
|
+
xmlParts.push(` <number>${changeDetails.number}</number>`)
|
|
280
|
+
xmlParts.push(` <subject><![CDATA[${sanitizeCDATA(changeDetails.subject)}]]></subject>`)
|
|
281
|
+
xmlParts.push(` <status>${escapeXML(changeDetails.status)}</status>`)
|
|
282
|
+
xmlParts.push(` <project>${escapeXML(changeDetails.project)}</project>`)
|
|
283
|
+
xmlParts.push(` <branch>${escapeXML(changeDetails.branch)}</branch>`)
|
|
284
|
+
xmlParts.push(` <owner>`)
|
|
265
285
|
if (changeDetails.owner.name) {
|
|
266
|
-
|
|
286
|
+
xmlParts.push(` <name><![CDATA[${sanitizeCDATA(changeDetails.owner.name)}]]></name>`)
|
|
267
287
|
}
|
|
268
288
|
if (changeDetails.owner.email) {
|
|
269
|
-
|
|
289
|
+
xmlParts.push(` <email>${escapeXML(changeDetails.owner.email)}</email>`)
|
|
270
290
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
291
|
+
xmlParts.push(` </owner>`)
|
|
292
|
+
xmlParts.push(` <created>${escapeXML(changeDetails.created || '')}</created>`)
|
|
293
|
+
xmlParts.push(` <updated>${escapeXML(changeDetails.updated || '')}</updated>`)
|
|
294
|
+
xmlParts.push(` </change>`)
|
|
295
|
+
xmlParts.push(` <diff><![CDATA[${sanitizeCDATA(diff)}]]></diff>`)
|
|
276
296
|
|
|
277
297
|
// Comments section
|
|
278
|
-
|
|
279
|
-
|
|
298
|
+
xmlParts.push(` <comments>`)
|
|
299
|
+
xmlParts.push(` <count>${commentsWithContext.length}</count>`)
|
|
280
300
|
for (const { comment } of commentsWithContext) {
|
|
281
|
-
|
|
282
|
-
if (comment.id)
|
|
283
|
-
if (comment.path)
|
|
284
|
-
if (comment.line)
|
|
301
|
+
xmlParts.push(` <comment>`)
|
|
302
|
+
if (comment.id) xmlParts.push(` <id>${escapeXML(comment.id)}</id>`)
|
|
303
|
+
if (comment.path) xmlParts.push(` <path><![CDATA[${sanitizeCDATA(comment.path)}]]></path>`)
|
|
304
|
+
if (comment.line) xmlParts.push(` <line>${comment.line}</line>`)
|
|
285
305
|
if (comment.author?.name) {
|
|
286
|
-
|
|
306
|
+
xmlParts.push(` <author><![CDATA[${sanitizeCDATA(comment.author.name)}]]></author>`)
|
|
287
307
|
}
|
|
288
|
-
if (comment.updated)
|
|
308
|
+
if (comment.updated) xmlParts.push(` <updated>${escapeXML(comment.updated)}</updated>`)
|
|
289
309
|
if (comment.message) {
|
|
290
|
-
|
|
310
|
+
xmlParts.push(` <message><![CDATA[${sanitizeCDATA(comment.message)}]]></message>`)
|
|
291
311
|
}
|
|
292
|
-
if (comment.unresolved)
|
|
293
|
-
|
|
312
|
+
if (comment.unresolved) xmlParts.push(` <unresolved>true</unresolved>`)
|
|
313
|
+
xmlParts.push(` </comment>`)
|
|
294
314
|
}
|
|
295
|
-
|
|
315
|
+
xmlParts.push(` </comments>`)
|
|
296
316
|
|
|
297
317
|
// Messages section
|
|
298
|
-
|
|
299
|
-
|
|
318
|
+
xmlParts.push(` <messages>`)
|
|
319
|
+
xmlParts.push(` <count>${messages.length}</count>`)
|
|
300
320
|
for (const message of messages) {
|
|
301
|
-
|
|
302
|
-
|
|
321
|
+
xmlParts.push(` <message>`)
|
|
322
|
+
xmlParts.push(` <id>${escapeXML(message.id)}</id>`)
|
|
303
323
|
if (message.author?.name) {
|
|
304
|
-
|
|
324
|
+
xmlParts.push(` <author><![CDATA[${sanitizeCDATA(message.author.name)}]]></author>`)
|
|
305
325
|
}
|
|
306
326
|
if (message.author?._account_id) {
|
|
307
|
-
|
|
327
|
+
xmlParts.push(` <author_id>${message.author._account_id}</author_id>`)
|
|
308
328
|
}
|
|
309
|
-
|
|
329
|
+
xmlParts.push(` <date>${escapeXML(message.date)}</date>`)
|
|
310
330
|
if (message._revision_number) {
|
|
311
|
-
|
|
331
|
+
xmlParts.push(` <revision>${message._revision_number}</revision>`)
|
|
312
332
|
}
|
|
313
333
|
if (message.tag) {
|
|
314
|
-
|
|
334
|
+
xmlParts.push(` <tag>${escapeXML(message.tag)}</tag>`)
|
|
315
335
|
}
|
|
316
|
-
|
|
317
|
-
|
|
336
|
+
xmlParts.push(` <message><![CDATA[${sanitizeCDATA(message.message)}]]></message>`)
|
|
337
|
+
xmlParts.push(` </message>`)
|
|
318
338
|
}
|
|
319
|
-
|
|
320
|
-
|
|
339
|
+
xmlParts.push(` </messages>`)
|
|
340
|
+
xmlParts.push(`</show_result>`)
|
|
341
|
+
|
|
342
|
+
const xmlOutput = xmlParts.join('\n') + '\n'
|
|
343
|
+
// Write to stdout with proper drain handling for large payloads
|
|
344
|
+
return new Promise<void>((resolve, reject) => {
|
|
345
|
+
const written = process.stdout.write(xmlOutput, (err) => {
|
|
346
|
+
if (err) {
|
|
347
|
+
reject(err)
|
|
348
|
+
} else {
|
|
349
|
+
resolve()
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
if (!written) {
|
|
354
|
+
process.stdout.once('drain', resolve)
|
|
355
|
+
process.stdout.once('error', reject)
|
|
356
|
+
}
|
|
357
|
+
})
|
|
321
358
|
}
|
|
322
359
|
|
|
323
360
|
export const showCommand = (
|
|
@@ -358,9 +395,11 @@ export const showCommand = (
|
|
|
358
395
|
|
|
359
396
|
// Format output
|
|
360
397
|
if (options.json) {
|
|
361
|
-
|
|
398
|
+
yield* Effect.promise(() =>
|
|
399
|
+
formatShowJson(changeDetails, diff, commentsWithContext, messages),
|
|
400
|
+
)
|
|
362
401
|
} else if (options.xml) {
|
|
363
|
-
formatShowXml(changeDetails, diff, commentsWithContext, messages)
|
|
402
|
+
yield* Effect.promise(() => formatShowXml(changeDetails, diff, commentsWithContext, messages))
|
|
364
403
|
} else {
|
|
365
404
|
formatShowPretty(changeDetails, diff, commentsWithContext, messages)
|
|
366
405
|
}
|
|
@@ -373,22 +412,57 @@ export const showCommand = (
|
|
|
373
412
|
: String(error)
|
|
374
413
|
|
|
375
414
|
if (options.json) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
{
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
415
|
+
return Effect.promise(
|
|
416
|
+
() =>
|
|
417
|
+
new Promise<void>((resolve, reject) => {
|
|
418
|
+
const errorOutput =
|
|
419
|
+
JSON.stringify(
|
|
420
|
+
{
|
|
421
|
+
status: 'error',
|
|
422
|
+
error: errorMessage,
|
|
423
|
+
},
|
|
424
|
+
null,
|
|
425
|
+
2,
|
|
426
|
+
) + '\n'
|
|
427
|
+
const written = process.stdout.write(errorOutput, (err) => {
|
|
428
|
+
if (err) {
|
|
429
|
+
reject(err)
|
|
430
|
+
} else {
|
|
431
|
+
resolve()
|
|
432
|
+
}
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
if (!written) {
|
|
436
|
+
// Wait for drain if buffer is full
|
|
437
|
+
process.stdout.once('drain', resolve)
|
|
438
|
+
process.stdout.once('error', reject)
|
|
439
|
+
}
|
|
440
|
+
}),
|
|
385
441
|
)
|
|
386
442
|
} else if (options.xml) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
443
|
+
return Effect.promise(
|
|
444
|
+
() =>
|
|
445
|
+
new Promise<void>((resolve, reject) => {
|
|
446
|
+
const xmlError =
|
|
447
|
+
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
448
|
+
`<show_result>\n` +
|
|
449
|
+
` <status>error</status>\n` +
|
|
450
|
+
` <error><![CDATA[${sanitizeCDATA(errorMessage)}]]></error>\n` +
|
|
451
|
+
`</show_result>\n`
|
|
452
|
+
const written = process.stdout.write(xmlError, (err) => {
|
|
453
|
+
if (err) {
|
|
454
|
+
reject(err)
|
|
455
|
+
} else {
|
|
456
|
+
resolve()
|
|
457
|
+
}
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
if (!written) {
|
|
461
|
+
process.stdout.once('drain', resolve)
|
|
462
|
+
process.stdout.once('error', reject)
|
|
463
|
+
}
|
|
464
|
+
}),
|
|
465
|
+
)
|
|
392
466
|
} else {
|
|
393
467
|
console.error(`✗ Error: ${errorMessage}`)
|
|
394
468
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { ConfigServiceLive } from '@/services/config'
|
|
|
31
31
|
import { ReviewStrategyServiceLive } from '@/services/review-strategy'
|
|
32
32
|
import { GitWorktreeServiceLive } from '@/services/git-worktree'
|
|
33
33
|
import { abandonCommand } from './commands/abandon'
|
|
34
|
+
import { buildStatusCommand } from './commands/build-status'
|
|
34
35
|
import { commentCommand } from './commands/comment'
|
|
35
36
|
import { commentsCommand } from './commands/comments'
|
|
36
37
|
import { diffCommand } from './commands/diff'
|
|
@@ -419,6 +420,63 @@ Note: When no change-id is provided, it will be automatically extracted from the
|
|
|
419
420
|
}
|
|
420
421
|
})
|
|
421
422
|
|
|
423
|
+
// build-status command
|
|
424
|
+
program
|
|
425
|
+
.command('build-status [change-id]')
|
|
426
|
+
.description(
|
|
427
|
+
'Check build status from Gerrit messages (auto-detects from HEAD commit if not specified)',
|
|
428
|
+
)
|
|
429
|
+
.addHelpText(
|
|
430
|
+
'after',
|
|
431
|
+
`
|
|
432
|
+
This command parses Gerrit change messages to determine build status.
|
|
433
|
+
It looks for "Build Started" messages and subsequent verification labels.
|
|
434
|
+
|
|
435
|
+
Output is JSON with a "state" field that can be:
|
|
436
|
+
- pending: No build has started yet
|
|
437
|
+
- running: Build started but no verification yet
|
|
438
|
+
- success: Build completed with Verified+1
|
|
439
|
+
- failure: Build completed with Verified-1
|
|
440
|
+
- not_found: Change does not exist
|
|
441
|
+
|
|
442
|
+
Examples:
|
|
443
|
+
# Check build status for specific change (using change number)
|
|
444
|
+
$ ger build-status 392385
|
|
445
|
+
{"state":"success"}
|
|
446
|
+
|
|
447
|
+
# Check build status for specific change (using Change-ID)
|
|
448
|
+
$ ger build-status If5a3ae8cb5a107e187447802358417f311d0c4b1
|
|
449
|
+
{"state":"running"}
|
|
450
|
+
|
|
451
|
+
# Auto-detect from HEAD commit
|
|
452
|
+
$ ger build-status
|
|
453
|
+
{"state":"pending"}
|
|
454
|
+
|
|
455
|
+
# Use in scripts (exit code 0 on success, 1 on error)
|
|
456
|
+
$ if ger build-status | jq -e '.state == "success"' > /dev/null; then
|
|
457
|
+
echo "Build passed!"
|
|
458
|
+
fi
|
|
459
|
+
|
|
460
|
+
Note: When no change-id is provided, it will be automatically extracted from the
|
|
461
|
+
Change-ID footer in your HEAD commit.`,
|
|
462
|
+
)
|
|
463
|
+
.action(async (changeId) => {
|
|
464
|
+
try {
|
|
465
|
+
const effect = buildStatusCommand(changeId).pipe(
|
|
466
|
+
Effect.provide(GerritApiServiceLive),
|
|
467
|
+
Effect.provide(ConfigServiceLive),
|
|
468
|
+
)
|
|
469
|
+
await Effect.runPromise(effect)
|
|
470
|
+
} catch (error) {
|
|
471
|
+
// Errors are handled within the command itself
|
|
472
|
+
// This catch is just for any unexpected errors
|
|
473
|
+
if (error instanceof Error && error.message !== 'Process exited') {
|
|
474
|
+
console.error('✗ Unexpected error:', error.message)
|
|
475
|
+
process.exit(1)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
|
|
422
480
|
// extract-url command
|
|
423
481
|
program
|
|
424
482
|
.command('extract-url <pattern> [change-id]')
|