@vibe-validate/cli 0.17.6-rc.2 â 0.18.0-rc.1
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/dist/bin/vibe-validate +4 -0
- package/dist/bin/vibe-validate.js +4 -0
- package/dist/bin/vibe-validate.js.map +1 -1
- package/dist/bin/vv +4 -0
- package/dist/commands/cleanup.d.ts.map +1 -1
- package/dist/commands/cleanup.js +5 -2
- package/dist/commands/cleanup.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +3 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/create-extractor.d.ts.map +1 -1
- package/dist/commands/create-extractor.js +3 -1
- package/dist/commands/create-extractor.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +11 -5
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/generate-workflow.d.ts.map +1 -1
- package/dist/commands/generate-workflow.js +6 -2
- package/dist/commands/generate-workflow.js.map +1 -1
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +23 -13
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pre-commit.d.ts.map +1 -1
- package/dist/commands/pre-commit.js +5 -2
- package/dist/commands/pre-commit.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +12 -10
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/state.d.ts.map +1 -1
- package/dist/commands/state.js +15 -10
- package/dist/commands/state.js.map +1 -1
- package/dist/commands/sync-check.js +5 -5
- package/dist/commands/sync-check.js.map +1 -1
- package/dist/commands/watch-pr.d.ts +16 -0
- package/dist/commands/watch-pr.d.ts.map +1 -1
- package/dist/commands/watch-pr.js +572 -318
- package/dist/commands/watch-pr.js.map +1 -1
- package/dist/schemas/watch-pr-result.schema.d.ts +2010 -0
- package/dist/schemas/watch-pr-result.schema.d.ts.map +1 -0
- package/dist/schemas/watch-pr-result.schema.js +335 -0
- package/dist/schemas/watch-pr-result.schema.js.map +1 -0
- package/dist/schemas/watch-pr-schema.d.ts +6 -6
- package/dist/services/cache-manager.d.ts +113 -0
- package/dist/services/cache-manager.d.ts.map +1 -0
- package/dist/services/cache-manager.js +211 -0
- package/dist/services/cache-manager.js.map +1 -0
- package/dist/services/ci-providers/github-actions.d.ts.map +1 -1
- package/dist/services/ci-providers/github-actions.js +10 -16
- package/dist/services/ci-providers/github-actions.js.map +1 -1
- package/dist/services/external-check-extractor.d.ts +66 -0
- package/dist/services/external-check-extractor.d.ts.map +1 -0
- package/dist/services/external-check-extractor.js +114 -0
- package/dist/services/external-check-extractor.js.map +1 -0
- package/dist/services/extraction-mode-detector.d.ts +85 -0
- package/dist/services/extraction-mode-detector.d.ts.map +1 -0
- package/dist/services/extraction-mode-detector.js +200 -0
- package/dist/services/extraction-mode-detector.js.map +1 -0
- package/dist/services/github-fetcher.d.ts +210 -0
- package/dist/services/github-fetcher.d.ts.map +1 -0
- package/dist/services/github-fetcher.js +412 -0
- package/dist/services/github-fetcher.js.map +1 -0
- package/dist/services/history-summary-builder.d.ts +74 -0
- package/dist/services/history-summary-builder.d.ts.map +1 -0
- package/dist/services/history-summary-builder.js +199 -0
- package/dist/services/history-summary-builder.js.map +1 -0
- package/dist/services/watch-pr-orchestrator.d.ts +119 -0
- package/dist/services/watch-pr-orchestrator.d.ts.map +1 -0
- package/dist/services/watch-pr-orchestrator.js +420 -0
- package/dist/services/watch-pr-orchestrator.js.map +1 -0
- package/dist/utils/command-name.d.ts +21 -0
- package/dist/utils/command-name.d.ts.map +1 -0
- package/dist/utils/command-name.js +45 -0
- package/dist/utils/command-name.js.map +1 -0
- package/dist/utils/pid-lock.d.ts.map +1 -1
- package/dist/utils/pid-lock.js +3 -3
- package/dist/utils/pid-lock.js.map +1 -1
- package/dist/utils/secret-scanning.d.ts +1 -1
- package/dist/utils/secret-scanning.d.ts.map +1 -1
- package/dist/utils/secret-scanning.js +32 -4
- package/dist/utils/secret-scanning.js.map +1 -1
- package/dist/utils/temp-files.d.ts.map +1 -1
- package/dist/utils/temp-files.js +2 -2
- package/dist/utils/temp-files.js.map +1 -1
- package/dist/utils/validate-workflow.js +7 -7
- package/dist/utils/validate-workflow.js.map +1 -1
- package/package.json +7 -7
|
@@ -1,31 +1,44 @@
|
|
|
1
|
+
import { getCurrentBranch, getCurrentPR, getRemoteUrl, listPullRequests } from '@vibe-validate/git';
|
|
1
2
|
import { stringify as stringifyYaml } from 'yaml';
|
|
2
|
-
import {
|
|
3
|
+
import { WatchPROrchestrator } from '../services/watch-pr-orchestrator.js';
|
|
4
|
+
import { getCommandName } from '../utils/command-name.js';
|
|
3
5
|
/**
|
|
4
6
|
* Register the watch-pr command
|
|
5
7
|
*/
|
|
6
8
|
export function registerWatchPRCommand(program) {
|
|
7
9
|
program
|
|
8
10
|
.command('watch-pr [pr-number]')
|
|
9
|
-
.description('
|
|
10
|
-
.option('--
|
|
11
|
-
.option('--
|
|
12
|
-
.option('--
|
|
11
|
+
.description('Monitor PR checks with auto-polling, error extraction, and flaky test detection (use after creating PR, run after each push)')
|
|
12
|
+
.option('--yaml', 'Force YAML output (auto-enabled on failure)')
|
|
13
|
+
.option('--repo <owner/repo>', 'Repository (default: auto-detect from git remote)')
|
|
14
|
+
.option('--history', 'Show historical runs for the PR with pass/fail summary')
|
|
15
|
+
.option('--run-id <id>', 'Watch specific run ID instead of latest (useful for testing failed runs)')
|
|
16
|
+
.option('--timeout <seconds>', 'Maximum polling time in seconds (default: 1800 = 30 min)', '1800')
|
|
13
17
|
.option('--poll-interval <seconds>', 'Polling frequency in seconds (default: 10)', '10')
|
|
14
|
-
.option('--fail-fast', 'Exit immediately on first check failure')
|
|
18
|
+
.option('--fail-fast', 'Exit immediately on first check failure (no polling)')
|
|
15
19
|
.action(async (prNumber, options) => {
|
|
16
20
|
try {
|
|
17
21
|
const exitCode = await watchPRCommand(prNumber, options);
|
|
18
22
|
process.exit(exitCode);
|
|
19
23
|
}
|
|
20
24
|
catch (error) {
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
// Only output YAML for PR failures, not for usage/argument errors
|
|
26
|
+
// Check if this is a usage error (no PR detected, invalid args, etc.)
|
|
27
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
28
|
+
const isUsageError = errorMessage.includes('Could not auto-detect') ||
|
|
29
|
+
errorMessage.includes('Invalid PR number') ||
|
|
30
|
+
errorMessage.includes('Invalid run ID') ||
|
|
31
|
+
errorMessage.includes('Invalid --repo format') ||
|
|
32
|
+
errorMessage.includes('Could not detect repository');
|
|
33
|
+
if (isUsageError) {
|
|
34
|
+
// Output as plain text for better UX
|
|
35
|
+
process.stderr.write(`Error: ${errorMessage}\n`);
|
|
23
36
|
}
|
|
24
37
|
else {
|
|
25
|
-
//
|
|
38
|
+
// Actual PR/API errors - output as YAML for parseability
|
|
26
39
|
process.stdout.write('---\n');
|
|
27
40
|
process.stdout.write(stringifyYaml({
|
|
28
|
-
error:
|
|
41
|
+
error: errorMessage,
|
|
29
42
|
}));
|
|
30
43
|
}
|
|
31
44
|
process.exit(1);
|
|
@@ -35,295 +48,464 @@ export function registerWatchPRCommand(program) {
|
|
|
35
48
|
/**
|
|
36
49
|
* Execute watch-pr command
|
|
37
50
|
*
|
|
38
|
-
* @returns Exit code (0 = success, 1 = failure
|
|
51
|
+
* @returns Exit code (0 = success, 1 = failure)
|
|
39
52
|
*/
|
|
40
|
-
async function watchPRCommand(prNumber, options) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
export async function watchPRCommand(prNumber, options) {
|
|
54
|
+
// Detect owner/repo from git remote or --repo flag (do this early for auto-detection)
|
|
55
|
+
const { owner, repo } = options.repo
|
|
56
|
+
? parseRepoFlag(options.repo)
|
|
57
|
+
: detectOwnerRepo();
|
|
58
|
+
// Auto-detect PR number if not provided
|
|
59
|
+
let prNum;
|
|
60
|
+
if (prNumber) {
|
|
61
|
+
// Explicit PR number provided - parse and validate
|
|
62
|
+
prNum = Number.parseInt(prNumber, 10);
|
|
63
|
+
if (Number.isNaN(prNum) || prNum <= 0) {
|
|
64
|
+
throw new Error(`Invalid PR number: ${prNumber}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// No PR number - try to auto-detect from current branch
|
|
69
|
+
prNum = await autoDetectPR(owner, repo);
|
|
50
70
|
}
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
// Create orchestrator
|
|
72
|
+
const orchestrator = new WatchPROrchestrator(owner, repo);
|
|
73
|
+
// Handle --history flag (list historical runs)
|
|
74
|
+
if (options.history) {
|
|
75
|
+
await displayHistoricalRuns(orchestrator, prNum, options.yaml ?? false);
|
|
76
|
+
return 0; // Success exit code
|
|
77
|
+
}
|
|
78
|
+
// Handle --run-id mode (single fetch, no polling)
|
|
79
|
+
if (options.runId) {
|
|
80
|
+
const runId = Number.parseInt(options.runId, 10);
|
|
81
|
+
if (Number.isNaN(runId) || runId <= 0) {
|
|
82
|
+
throw new Error(`Invalid run ID: ${options.runId}. Must be a positive integer.`);
|
|
58
83
|
}
|
|
59
|
-
|
|
84
|
+
const result = await orchestrator.buildResultForRun(prNum, runId, { useCache: true });
|
|
85
|
+
return displayFinalResult(result, orchestrator, options.yaml ?? false);
|
|
60
86
|
}
|
|
61
|
-
//
|
|
62
|
-
return await
|
|
87
|
+
// Normal mode: poll until complete (or timeout/fail-fast)
|
|
88
|
+
return await pollUntilComplete(orchestrator, prNum, options);
|
|
63
89
|
}
|
|
64
90
|
/**
|
|
65
|
-
*
|
|
91
|
+
* Display final result and return exit code
|
|
92
|
+
*
|
|
93
|
+
* @param result - Result to display
|
|
94
|
+
* @param orchestrator - WatchPROrchestrator instance
|
|
95
|
+
* @param yaml - Force YAML output
|
|
96
|
+
* @returns Exit code (0 = passed, 1 = failed)
|
|
66
97
|
*/
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
pr: lastStatus.pr,
|
|
71
|
-
status: 'timeout',
|
|
72
|
-
result: 'unknown',
|
|
73
|
-
duration: formatDuration(elapsed),
|
|
74
|
-
summary: 'Timed out waiting for checks to complete',
|
|
75
|
-
checks: lastStatus.checks.map((c) => ({
|
|
76
|
-
name: c.name,
|
|
77
|
-
status: c.status,
|
|
78
|
-
conclusion: c.conclusion,
|
|
79
|
-
duration: c.duration,
|
|
80
|
-
url: c.url,
|
|
81
|
-
})),
|
|
82
|
-
};
|
|
83
|
-
// YAML mode: Output timeout result to stdout
|
|
98
|
+
function displayFinalResult(result, orchestrator, yaml) {
|
|
99
|
+
const shouldYAML = orchestrator.shouldOutputYAML(result.status, yaml);
|
|
100
|
+
if (shouldYAML) {
|
|
84
101
|
process.stdout.write('---\n');
|
|
85
102
|
process.stdout.write(stringifyYaml(result));
|
|
86
103
|
}
|
|
87
104
|
else {
|
|
88
|
-
|
|
105
|
+
displayHumanResult(result);
|
|
106
|
+
}
|
|
107
|
+
return result.status === 'passed' ? 0 : 1;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if polling has timed out
|
|
111
|
+
*
|
|
112
|
+
* @param startTime - Polling start time
|
|
113
|
+
* @param timeoutMs - Timeout in milliseconds
|
|
114
|
+
* @param previousResult - Last result (for displaying on timeout)
|
|
115
|
+
* @returns Exit code (2) if timeout, null otherwise
|
|
116
|
+
*/
|
|
117
|
+
function checkTimeout(startTime, timeoutMs, previousResult) {
|
|
118
|
+
const elapsed = Date.now() - startTime;
|
|
119
|
+
if (elapsed >= timeoutMs) {
|
|
120
|
+
if (previousResult) {
|
|
121
|
+
process.stdout.write('\nâąī¸ Timeout reached. Checks still pending.\n\n');
|
|
122
|
+
process.stdout.write('---\n');
|
|
123
|
+
process.stdout.write(stringifyYaml(previousResult));
|
|
124
|
+
}
|
|
125
|
+
return 2;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Check exit conditions (fail-fast or completion)
|
|
131
|
+
*
|
|
132
|
+
* @param result - Current result
|
|
133
|
+
* @param orchestrator - WatchPROrchestrator instance
|
|
134
|
+
* @param options - Command options
|
|
135
|
+
* @returns Exit code if should exit, null to continue polling
|
|
136
|
+
*/
|
|
137
|
+
function checkExitConditions(result, orchestrator, options) {
|
|
138
|
+
// Check if should fail fast
|
|
139
|
+
if (options.failFast && result.status === 'failed') {
|
|
140
|
+
process.stdout.write('\n⥠Failing fast (--fail-fast enabled)\n\n');
|
|
141
|
+
process.stdout.write('---\n');
|
|
142
|
+
process.stdout.write(stringifyYaml(result));
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
// Check if all checks are complete
|
|
146
|
+
if (result.status !== 'pending') {
|
|
147
|
+
process.stdout.write('\n');
|
|
148
|
+
return displayFinalResult(result, orchestrator, options.yaml ?? false);
|
|
89
149
|
}
|
|
90
|
-
return
|
|
150
|
+
return null; // Continue polling
|
|
91
151
|
}
|
|
92
152
|
/**
|
|
93
|
-
*
|
|
153
|
+
* Poll until PR checks complete (or timeout/fail-fast)
|
|
154
|
+
*
|
|
155
|
+
* @param orchestrator - WatchPROrchestrator instance
|
|
156
|
+
* @param prNumber - PR number
|
|
157
|
+
* @param options - Command options
|
|
158
|
+
* @returns Exit code
|
|
94
159
|
*/
|
|
95
|
-
async function
|
|
96
|
-
const timeoutMs = Number.parseInt(options.timeout ?? '
|
|
97
|
-
const pollIntervalMs = Number.parseInt(options.pollInterval ?? '10') * 1000;
|
|
160
|
+
async function pollUntilComplete(orchestrator, prNumber, options) {
|
|
161
|
+
const timeoutMs = Number.parseInt(options.timeout ?? '1800', 10) * 1000;
|
|
162
|
+
const pollIntervalMs = Number.parseInt(options.pollInterval ?? '10', 10) * 1000;
|
|
98
163
|
const startTime = Date.now();
|
|
99
|
-
let
|
|
164
|
+
let previousResult = null;
|
|
100
165
|
let iteration = 0;
|
|
101
166
|
while (true) {
|
|
102
|
-
const elapsed = Date.now() - startTime;
|
|
103
167
|
// Check timeout
|
|
104
|
-
|
|
105
|
-
|
|
168
|
+
const timeoutCode = checkTimeout(startTime, timeoutMs, previousResult);
|
|
169
|
+
if (timeoutCode !== null) {
|
|
170
|
+
return timeoutCode;
|
|
106
171
|
}
|
|
107
172
|
// Fetch current status
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
// Check if we should fail fast
|
|
115
|
-
const shouldFailFast = options.failFast && status.checks.some((c) => c.conclusion === 'failure');
|
|
116
|
-
if (shouldFailFast) {
|
|
117
|
-
return await handleCompletion(provider, status, options, elapsed);
|
|
173
|
+
const result = await orchestrator.buildResult(prNumber);
|
|
174
|
+
// Display changes (first iteration shows everything)
|
|
175
|
+
const changes = detectChanges(previousResult, result);
|
|
176
|
+
if (iteration === 0 || changes.length > 0) {
|
|
177
|
+
displayChanges(changes, result, iteration === 0);
|
|
118
178
|
}
|
|
119
|
-
|
|
120
|
-
if (status.status === 'completed') {
|
|
121
|
-
return await handleCompletion(provider, status, options, elapsed);
|
|
122
|
-
}
|
|
123
|
-
// Wait before next poll
|
|
179
|
+
previousResult = result;
|
|
124
180
|
iteration++;
|
|
181
|
+
// Check exit conditions (fail-fast or completion)
|
|
182
|
+
const exitCode = checkExitConditions(result, orchestrator, options);
|
|
183
|
+
if (exitCode !== null) {
|
|
184
|
+
return exitCode;
|
|
185
|
+
}
|
|
186
|
+
// Sleep before next poll
|
|
125
187
|
await sleep(pollIntervalMs);
|
|
126
188
|
}
|
|
127
189
|
}
|
|
128
190
|
/**
|
|
129
|
-
*
|
|
191
|
+
* Auto-detect PR number from current branch
|
|
192
|
+
*
|
|
193
|
+
* Tries two approaches:
|
|
194
|
+
* 1. Use `gh pr view` (fast, but can fail in some scenarios)
|
|
195
|
+
* 2. Fall back to branch name matching (more reliable)
|
|
196
|
+
*
|
|
197
|
+
* @param owner - Repository owner
|
|
198
|
+
* @param repo - Repository name
|
|
199
|
+
* @returns PR number
|
|
130
200
|
*/
|
|
131
|
-
async function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
201
|
+
async function autoDetectPR(owner, repo) {
|
|
202
|
+
try {
|
|
203
|
+
// Approach 1: Try gh pr view (fast path)
|
|
204
|
+
// Uses getCurrentPR from @vibe-validate/git (architectural compliance)
|
|
205
|
+
const prNumber = getCurrentPR(owner, repo);
|
|
206
|
+
if (prNumber !== null) {
|
|
207
|
+
return prNumber;
|
|
208
|
+
}
|
|
209
|
+
throw new Error('No PR number found');
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Approach 2: Fall back to branch name matching (Issue #5 fix)
|
|
213
|
+
// This handles cases where gh pr view fails (detached HEAD, etc.)
|
|
135
214
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
215
|
+
// Get current branch name using getCurrentBranch from @vibe-validate/git
|
|
216
|
+
const currentBranch = getCurrentBranch();
|
|
217
|
+
if (!currentBranch || currentBranch === 'HEAD') {
|
|
218
|
+
throw new Error('Not on a branch (detached HEAD?)');
|
|
219
|
+
}
|
|
220
|
+
// Get all open PRs with branch names
|
|
221
|
+
const prsData = listPullRequests(owner, repo, 20, ['number', 'headRefName']);
|
|
222
|
+
// Find PR matching current branch
|
|
223
|
+
const matchingPR = prsData.find(pr => pr.headRefName === currentBranch);
|
|
224
|
+
if (matchingPR) {
|
|
225
|
+
return matchingPR.number;
|
|
226
|
+
}
|
|
227
|
+
throw new Error(`No PR found for branch: ${currentBranch}`);
|
|
144
228
|
}
|
|
145
|
-
catch
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
229
|
+
catch {
|
|
230
|
+
// Both approaches failed - show suggestions
|
|
231
|
+
const suggestions = await suggestOpenPRs(owner, repo);
|
|
232
|
+
const cmd = getCommandName();
|
|
233
|
+
throw new Error(`Could not auto-detect PR from current branch.\n\n` +
|
|
234
|
+
`${suggestions}\n\n` +
|
|
235
|
+
`Usage: ${cmd} watch-pr <pr-number>\n` +
|
|
236
|
+
`Example: ${cmd} watch-pr 90`);
|
|
152
237
|
}
|
|
153
|
-
}));
|
|
154
|
-
// Output final result
|
|
155
|
-
if (options.yaml) {
|
|
156
|
-
const result = {
|
|
157
|
-
pr: {
|
|
158
|
-
id: status.pr.id,
|
|
159
|
-
title: status.pr.title,
|
|
160
|
-
url: status.pr.url,
|
|
161
|
-
},
|
|
162
|
-
status: status.status,
|
|
163
|
-
result: status.result,
|
|
164
|
-
duration: formatDuration(elapsedMs),
|
|
165
|
-
summary: `${status.checks.filter((c) => c.conclusion === 'success').length}/${status.checks.length} checks passed`,
|
|
166
|
-
checks: status.checks.map((c) => ({
|
|
167
|
-
name: c.name,
|
|
168
|
-
status: c.status,
|
|
169
|
-
conclusion: c.conclusion,
|
|
170
|
-
duration: c.duration,
|
|
171
|
-
url: c.url,
|
|
172
|
-
})),
|
|
173
|
-
failures: failureDetails.length > 0 ? failureDetails : undefined,
|
|
174
|
-
};
|
|
175
|
-
// YAML mode: Output completion result to stdout
|
|
176
|
-
process.stdout.write('---\n');
|
|
177
|
-
process.stdout.write(stringifyYaml(result));
|
|
178
238
|
}
|
|
179
|
-
|
|
180
|
-
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Suggest open PRs to help user choose
|
|
242
|
+
*
|
|
243
|
+
* @param owner - Repository owner
|
|
244
|
+
* @param repo - Repository name
|
|
245
|
+
* @returns Formatted suggestion text
|
|
246
|
+
*/
|
|
247
|
+
async function suggestOpenPRs(owner, repo) {
|
|
248
|
+
try {
|
|
249
|
+
const prsData = listPullRequests(owner, repo, 5, ['number', 'title', 'author', 'headRefName']);
|
|
250
|
+
if (prsData.length === 0) {
|
|
251
|
+
return 'No open PRs found in this repository.';
|
|
252
|
+
}
|
|
253
|
+
const prList = prsData
|
|
254
|
+
.map((pr) => ` #${pr.number} - ${pr.title}\n` +
|
|
255
|
+
` (${pr.headRefName} by ${pr.author.login})`)
|
|
256
|
+
.join('\n');
|
|
257
|
+
return `Open PRs in ${owner}/${repo}:\n${prList}`;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return 'Could not fetch open PRs.';
|
|
181
261
|
}
|
|
182
|
-
// Return appropriate exit code
|
|
183
|
-
return status.result === 'success' ? 0 : 1;
|
|
184
262
|
}
|
|
185
263
|
/**
|
|
186
|
-
*
|
|
264
|
+
* Detect owner/repo from git remote
|
|
265
|
+
*
|
|
266
|
+
* @returns Owner and repo from GitHub remote
|
|
187
267
|
*/
|
|
188
|
-
function
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
268
|
+
function detectOwnerRepo() {
|
|
269
|
+
try {
|
|
270
|
+
// Use getRemoteUrl from @vibe-validate/git (architectural compliance)
|
|
271
|
+
const remote = getRemoteUrl('origin');
|
|
272
|
+
// Parse GitHub URL (supports HTTPS, SSH, and SSH with custom host aliases)
|
|
273
|
+
// HTTPS: https://github.com/owner/repo.git
|
|
274
|
+
// SSH: git@github.com:owner/repo.git
|
|
275
|
+
// SSH with alias: git@github.com-personal:owner/repo.git
|
|
276
|
+
// SSH with alias: git@github.com-work:owner/repo.git
|
|
277
|
+
const regex = /github\.com[^/:]*[/:]([\w-]+)\/([\w-]+)/;
|
|
278
|
+
const match = regex.exec(remote);
|
|
279
|
+
if (!match) {
|
|
280
|
+
throw new Error('Could not parse GitHub owner/repo from remote URL');
|
|
281
|
+
}
|
|
282
|
+
const owner = match[1];
|
|
283
|
+
const repo = match[2].replace(/\.git$/, '');
|
|
284
|
+
return { owner, repo };
|
|
192
285
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
286
|
+
catch {
|
|
287
|
+
// Error occurred - could not detect repo from git remote
|
|
288
|
+
throw new Error('Could not detect repository from git remote.\n' +
|
|
289
|
+
'Ensure you are in a git repository with a GitHub remote.\n' +
|
|
290
|
+
'Or specify --repo <owner/repo> explicitly.');
|
|
197
291
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Parse --repo flag (owner/repo format)
|
|
295
|
+
*
|
|
296
|
+
* @param repoFlag - Repository in owner/repo format
|
|
297
|
+
* @returns Owner and repo
|
|
298
|
+
*/
|
|
299
|
+
function parseRepoFlag(repoFlag) {
|
|
300
|
+
const parts = repoFlag.split('/');
|
|
301
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
302
|
+
throw new Error(`Invalid --repo format: ${repoFlag}\n` +
|
|
303
|
+
'Expected format: --repo owner/repo\n' +
|
|
304
|
+
'Example: --repo jdutton/vibe-validate');
|
|
204
305
|
}
|
|
205
|
-
|
|
206
|
-
const completed = status.checks.filter((c) => c.status === 'completed').length;
|
|
207
|
-
const total = status.checks.length;
|
|
208
|
-
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
209
|
-
console.log(`\n${completed}/${total} checks complete (${percentage}%)`);
|
|
306
|
+
return { owner: parts[0], repo: parts[1] };
|
|
210
307
|
}
|
|
211
308
|
/**
|
|
212
|
-
* Display
|
|
309
|
+
* Display historical runs for a PR
|
|
310
|
+
*
|
|
311
|
+
* @param orchestrator - WatchPROrchestrator instance
|
|
312
|
+
* @param prNumber - PR number
|
|
313
|
+
* @param yaml - Output in YAML format
|
|
213
314
|
*/
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
315
|
+
async function displayHistoricalRuns(orchestrator, prNumber, yaml) {
|
|
316
|
+
const runs = await orchestrator.fetchRunsForPR(prNumber);
|
|
317
|
+
if (runs.length === 0) {
|
|
318
|
+
console.log(`No workflow runs found for PR #${prNumber}`);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (yaml) {
|
|
322
|
+
// YAML output
|
|
323
|
+
process.stdout.write('---\n');
|
|
324
|
+
process.stdout.write(stringifyYaml({ runs }));
|
|
221
325
|
}
|
|
222
326
|
else {
|
|
223
|
-
|
|
224
|
-
console.log(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
// Show extracted errors from failed step (v0.15.0+)
|
|
238
|
-
if (failedStep?.extraction?.errors && failedStep.extraction.errors.length > 0) {
|
|
239
|
-
console.log(`\n Failed tests/errors:`);
|
|
240
|
-
for (const error of failedStep.extraction.errors.slice(0, 10)) {
|
|
241
|
-
let location = 'Unknown location';
|
|
242
|
-
if (error.file) {
|
|
243
|
-
if (error.line) {
|
|
244
|
-
const columnPart = error.column ? `:${error.column}` : '';
|
|
245
|
-
location = `${error.file}:${error.line}${columnPart}`;
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
location = error.file;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
const message = error.message ?? 'Error';
|
|
252
|
-
console.log(` â ${location} - ${message}`);
|
|
253
|
-
}
|
|
254
|
-
if (failedStep.extraction.errors.length > 10) {
|
|
255
|
-
console.log(` ... and ${failedStep.extraction.errors.length - 10} more`);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
else if (failedStep?.extraction?.summary) {
|
|
259
|
-
// Show summary if no specific errors extracted
|
|
260
|
-
console.log(`\n Error summary: ${failedStep.extraction.summary}`);
|
|
261
|
-
}
|
|
327
|
+
// Human-friendly table
|
|
328
|
+
console.log(`\nđ Workflow Runs for PR #${prNumber}\n`);
|
|
329
|
+
console.log(' RUN ID CONCLUSION DURATION WORKFLOW STARTED');
|
|
330
|
+
console.log(' ' + 'â'.repeat(95));
|
|
331
|
+
for (const run of runs) {
|
|
332
|
+
const runId = run.run_id.toString().padEnd(12);
|
|
333
|
+
const conclusion = (run.conclusion ?? 'pending').padEnd(11);
|
|
334
|
+
const duration = (run.duration ?? '?').padEnd(9);
|
|
335
|
+
const workflow = run.workflow_name.slice(0, 29).padEnd(29);
|
|
336
|
+
const startedAt = new Date(run.started_at).toLocaleString();
|
|
337
|
+
// Color code by conclusion
|
|
338
|
+
let icon = 'âŗ'; // pending
|
|
339
|
+
if (run.conclusion === 'success') {
|
|
340
|
+
icon = 'â
';
|
|
262
341
|
}
|
|
263
|
-
else if (failure
|
|
264
|
-
|
|
342
|
+
else if (run.conclusion === 'failure') {
|
|
343
|
+
icon = 'â';
|
|
344
|
+
}
|
|
345
|
+
console.log(`${icon} ${runId} ${conclusion} ${duration} ${workflow} ${startedAt}`);
|
|
346
|
+
}
|
|
347
|
+
console.log('\nđĄ Tip: Use --run-id <id> to drill into a specific run for extraction testing');
|
|
348
|
+
const cmd = getCommandName();
|
|
349
|
+
console.log(` Example: ${cmd} watch-pr ${prNumber} --run-id ${runs[0].run_id}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Display human-friendly result
|
|
354
|
+
*
|
|
355
|
+
* @param result - WatchPRResult
|
|
356
|
+
*/
|
|
357
|
+
function displayHumanResult(result) {
|
|
358
|
+
console.log(`\nđ PR #${result.pr.number}: ${result.pr.title}`);
|
|
359
|
+
console.log(` ${result.pr.url}\n`);
|
|
360
|
+
// Display checks
|
|
361
|
+
const allChecks = [...result.checks.github_actions, ...result.checks.external_checks];
|
|
362
|
+
for (const check of allChecks) {
|
|
363
|
+
const icon = getCheckIcon(check.conclusion);
|
|
364
|
+
const statusStr = check.conclusion ?? check.status;
|
|
365
|
+
console.log(`${icon} ${check.name.padEnd(40)} ${statusStr}`);
|
|
366
|
+
}
|
|
367
|
+
// Display summary
|
|
368
|
+
const failedSuffix = result.checks.failed > 0 ? ` (${result.checks.failed} failed)` : '';
|
|
369
|
+
console.log(`\n${result.checks.passed}/${result.checks.total} checks passed${failedSuffix}`);
|
|
370
|
+
// Display guidance
|
|
371
|
+
if (result.guidance) {
|
|
372
|
+
console.log(`\n${result.guidance.summary}`);
|
|
373
|
+
if (result.guidance.next_steps) {
|
|
374
|
+
console.log('\nNext steps:');
|
|
375
|
+
for (const step of result.guidance.next_steps) {
|
|
376
|
+
const icon = getStepIcon(step.severity);
|
|
377
|
+
console.log(`${icon} ${step.action}`);
|
|
378
|
+
if (step.reason) {
|
|
379
|
+
console.log(` ${step.reason}`);
|
|
380
|
+
}
|
|
265
381
|
}
|
|
266
|
-
console.log(`\n Next steps:`);
|
|
267
|
-
for (const step of failure.nextSteps)
|
|
268
|
-
console.log(` - ${step}`);
|
|
269
382
|
}
|
|
270
|
-
// Suggest reporting extractor issues if extraction quality is poor
|
|
271
|
-
console.log('\nđĄ Error output unclear or missing details?');
|
|
272
|
-
console.log(' Help improve extraction: https://github.com/jdutton/vibe-validate/issues/new?template=extractor-improvement.yml');
|
|
273
383
|
}
|
|
274
|
-
console.log('='.repeat(60));
|
|
275
384
|
}
|
|
276
385
|
/**
|
|
277
|
-
* Get icon for
|
|
386
|
+
* Get icon for step severity
|
|
387
|
+
*
|
|
388
|
+
* @param severity - Step severity
|
|
389
|
+
* @returns Icon emoji
|
|
278
390
|
*/
|
|
279
|
-
function
|
|
280
|
-
if (
|
|
391
|
+
function getStepIcon(severity) {
|
|
392
|
+
if (severity === 'error')
|
|
393
|
+
return 'â';
|
|
394
|
+
if (severity === 'warning')
|
|
395
|
+
return 'â ī¸';
|
|
396
|
+
return 'âšī¸';
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Get icon for check conclusion
|
|
400
|
+
*
|
|
401
|
+
* @param conclusion - Check conclusion
|
|
402
|
+
* @returns Icon emoji
|
|
403
|
+
*/
|
|
404
|
+
function getCheckIcon(conclusion) {
|
|
405
|
+
if (conclusion === 'success')
|
|
281
406
|
return 'â
';
|
|
282
|
-
if (
|
|
407
|
+
if (conclusion === 'failure')
|
|
283
408
|
return 'â';
|
|
284
|
-
if (
|
|
409
|
+
if (conclusion === 'neutral')
|
|
410
|
+
return 'âšī¸';
|
|
411
|
+
if (conclusion === 'cancelled')
|
|
285
412
|
return 'đĢ';
|
|
286
|
-
if (
|
|
287
|
-
return 'âī¸
|
|
288
|
-
if (
|
|
289
|
-
return '
|
|
290
|
-
|
|
413
|
+
if (conclusion === 'skipped')
|
|
414
|
+
return 'âī¸';
|
|
415
|
+
if (conclusion === 'timed_out')
|
|
416
|
+
return 'âąī¸';
|
|
417
|
+
if (conclusion === 'action_required')
|
|
418
|
+
return 'â ī¸';
|
|
419
|
+
return 'â¸ī¸';
|
|
291
420
|
}
|
|
292
421
|
/**
|
|
293
|
-
*
|
|
422
|
+
* Sleep for specified milliseconds
|
|
423
|
+
*
|
|
424
|
+
* @param ms - Milliseconds to sleep
|
|
294
425
|
*/
|
|
295
|
-
function
|
|
296
|
-
|
|
297
|
-
// Find the failed step's command (v0.15.0+: rerunCommand removed, use step.command)
|
|
298
|
-
if (validationResult) {
|
|
299
|
-
const failedStep = validationResult.phases
|
|
300
|
-
?.flatMap(phase => phase.steps ?? [])
|
|
301
|
-
.find(step => step && step.name === validationResult.failedStep);
|
|
302
|
-
if (failedStep?.command) {
|
|
303
|
-
steps.push(`Run locally: ${failedStep.command}`);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
steps.push(`View logs: gh run view ${checkId} --log-failed`);
|
|
307
|
-
steps.push(`Re-run check: gh run rerun ${checkId} --failed`);
|
|
308
|
-
return steps;
|
|
426
|
+
function sleep(ms) {
|
|
427
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
309
428
|
}
|
|
310
429
|
/**
|
|
311
|
-
*
|
|
430
|
+
* Detect changes between two results
|
|
431
|
+
*
|
|
432
|
+
* @param previous - Previous result (null on first iteration)
|
|
433
|
+
* @param current - Current result
|
|
434
|
+
* @returns Array of check changes
|
|
312
435
|
*/
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
436
|
+
function detectChanges(previous, current) {
|
|
437
|
+
if (!previous) {
|
|
438
|
+
// First iteration - everything is a change
|
|
439
|
+
return [
|
|
440
|
+
...current.checks.github_actions.map((c) => ({
|
|
441
|
+
name: c.name,
|
|
442
|
+
previousStatus: null,
|
|
443
|
+
newStatus: c.status,
|
|
444
|
+
conclusion: c.conclusion,
|
|
445
|
+
})),
|
|
446
|
+
...current.checks.external_checks.map((c) => ({
|
|
447
|
+
name: c.name,
|
|
448
|
+
previousStatus: null,
|
|
449
|
+
newStatus: c.status,
|
|
450
|
+
conclusion: c.conclusion,
|
|
451
|
+
})),
|
|
452
|
+
];
|
|
453
|
+
}
|
|
454
|
+
const changes = [];
|
|
455
|
+
// Build map of previous checks
|
|
456
|
+
const previousChecks = new Map();
|
|
457
|
+
for (const check of [...previous.checks.github_actions, ...previous.checks.external_checks]) {
|
|
458
|
+
previousChecks.set(check.name, { status: check.status, conclusion: check.conclusion });
|
|
319
459
|
}
|
|
320
|
-
|
|
460
|
+
// Compare current checks to previous
|
|
461
|
+
for (const check of [...current.checks.github_actions, ...current.checks.external_checks]) {
|
|
462
|
+
const prev = previousChecks.get(check.name);
|
|
463
|
+
if (!prev || prev.status !== check.status || prev.conclusion !== check.conclusion) {
|
|
464
|
+
changes.push({
|
|
465
|
+
name: check.name,
|
|
466
|
+
previousStatus: prev?.status ?? null,
|
|
467
|
+
newStatus: check.status,
|
|
468
|
+
conclusion: check.conclusion,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return changes;
|
|
321
473
|
}
|
|
322
474
|
/**
|
|
323
|
-
*
|
|
475
|
+
* Display changes incrementally during polling
|
|
476
|
+
*
|
|
477
|
+
* @param changes - Array of check changes
|
|
478
|
+
* @param result - Current result
|
|
479
|
+
* @param isFirstIteration - Whether this is the first poll
|
|
324
480
|
*/
|
|
325
|
-
function
|
|
326
|
-
|
|
481
|
+
function displayChanges(changes, result, isFirstIteration) {
|
|
482
|
+
if (isFirstIteration) {
|
|
483
|
+
// First iteration - show PR info and all checks
|
|
484
|
+
process.stdout.write(`\nđ Monitoring PR #${result.pr.number}: ${result.pr.title}\n`);
|
|
485
|
+
process.stdout.write(` ${result.pr.url}\n\n`);
|
|
486
|
+
}
|
|
487
|
+
// Display each change
|
|
488
|
+
for (const change of changes) {
|
|
489
|
+
const icon = getCheckIcon(change.conclusion);
|
|
490
|
+
const statusStr = change.conclusion ?? change.newStatus;
|
|
491
|
+
if (change.previousStatus === null) {
|
|
492
|
+
// New check started
|
|
493
|
+
process.stdout.write(`${icon} ${change.name} - ${statusStr}\n`);
|
|
494
|
+
}
|
|
495
|
+
else if (change.newStatus === 'completed') {
|
|
496
|
+
// Check completed
|
|
497
|
+
process.stdout.write(`${icon} ${change.name} - ${statusStr}\n`);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
// Status changed
|
|
501
|
+
process.stdout.write(`${icon} ${change.name} - ${change.previousStatus} â ${statusStr}\n`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// Show progress summary
|
|
505
|
+
const { passed, failed, pending } = result.checks;
|
|
506
|
+
if (changes.length > 0) {
|
|
507
|
+
process.stdout.write(`\nâ¸ī¸ ${pending} running, ${passed} passed, ${failed} failed\n`);
|
|
508
|
+
}
|
|
327
509
|
}
|
|
328
510
|
/**
|
|
329
511
|
* Show verbose help with detailed documentation
|
|
@@ -331,142 +513,214 @@ function sleep(ms) {
|
|
|
331
513
|
export function showWatchPRVerboseHelp() {
|
|
332
514
|
console.log(`# watch-pr Command Reference
|
|
333
515
|
|
|
334
|
-
>
|
|
516
|
+
> Monitor PR checks with auto-polling, error extraction, and flaky test detection
|
|
335
517
|
|
|
336
518
|
## Overview
|
|
337
519
|
|
|
338
|
-
The \`watch-pr\` command monitors
|
|
520
|
+
The \`watch-pr\` command monitors pull request CI checks with **automatic polling** until completion. It provides:
|
|
521
|
+
- **Auto-polling**: Waits for checks to complete (no manual refresh)
|
|
522
|
+
- **Error extraction**: Extracts file:line:message from failed GitHub Actions logs
|
|
523
|
+
- **Flaky test detection**: Tracks history to identify unstable tests (e.g., "Failed last 2 runs", 60% success rate)
|
|
524
|
+
- **External check summaries**: codecov coverage %, SonarCloud quality gates
|
|
525
|
+
- **PR metadata**: branch, labels, linked issues, mergeable state
|
|
526
|
+
- **File change context**: What files changed, insertions/deletions
|
|
527
|
+
- **Intelligent guidance**: Severity-based next steps
|
|
528
|
+
|
|
529
|
+
**YAML output is auto-enabled on failure** (consistent with validate command).
|
|
530
|
+
|
|
531
|
+
## When to Use
|
|
532
|
+
|
|
533
|
+
Use \`watch-pr\` after creating a PR and after each push:
|
|
534
|
+
|
|
535
|
+
\`\`\`bash
|
|
536
|
+
# Workflow:
|
|
537
|
+
git push # Push commits to PR branch
|
|
538
|
+
vibe-validate watch-pr 90 # Monitor CI (auto-polls until complete)
|
|
539
|
+
# â Returns structured result when checks finish
|
|
540
|
+
# â YAML output if failed (with extracted errors)
|
|
541
|
+
# â Text output if passed (human-friendly)
|
|
542
|
+
\`\`\`
|
|
543
|
+
|
|
544
|
+
**Note**: PR must exist on GitHub (watch-pr fetches data from GitHub API).
|
|
339
545
|
|
|
340
546
|
## How It Works
|
|
341
547
|
|
|
342
|
-
1.
|
|
343
|
-
2.
|
|
344
|
-
3.
|
|
345
|
-
4.
|
|
346
|
-
5.
|
|
347
|
-
6.
|
|
548
|
+
1. **Fetches PR metadata** and check results from GitHub
|
|
549
|
+
2. **Auto-polls** until checks complete (no manual refresh needed)
|
|
550
|
+
3. **Classifies checks** (GitHub Actions vs external)
|
|
551
|
+
4. **Extracts errors** from failed GitHub Actions logs (matrix + non-matrix mode)
|
|
552
|
+
5. **Extracts summaries** from external checks (codecov, SonarCloud)
|
|
553
|
+
6. **Builds history** (last 10 runs, success rate, patterns like "flaky")
|
|
554
|
+
7. **Outputs YAML on failure**, text on success (unless --yaml forced)
|
|
348
555
|
|
|
349
556
|
## Options
|
|
350
557
|
|
|
351
|
-
- \`--
|
|
352
|
-
- \`--
|
|
353
|
-
- \`--
|
|
354
|
-
- \`--poll-interval <seconds>\` - Polling frequency in seconds (default: 10)
|
|
355
|
-
- \`--fail-fast\` - Exit immediately on first check failure
|
|
558
|
+
- \`--yaml\` - Force YAML output (auto-enabled on failure)
|
|
559
|
+
- \`--repo <owner/repo>\` - Repository (default: auto-detect from git remote)
|
|
560
|
+
- \`--run-id <id>\` - Watch specific run ID instead of latest (useful for testing failed runs)
|
|
356
561
|
|
|
357
562
|
## Exit Codes
|
|
358
563
|
|
|
359
564
|
- \`0\` - All checks passed
|
|
360
565
|
- \`1\` - One or more checks failed
|
|
361
|
-
- \`2\` - Timeout reached before completion
|
|
362
566
|
|
|
363
567
|
## Examples
|
|
364
568
|
|
|
365
569
|
\`\`\`bash
|
|
366
|
-
#
|
|
367
|
-
|
|
368
|
-
vibe-validate watch-pr # Auto-detect PR
|
|
369
|
-
|
|
370
|
-
# Watch specific PR
|
|
371
|
-
vibe-validate watch-pr 42
|
|
570
|
+
# Watch PR (auto-detect repo from git remote)
|
|
571
|
+
vibe-validate watch-pr 90
|
|
372
572
|
|
|
373
|
-
# YAML output
|
|
374
|
-
vibe-validate watch-pr --yaml
|
|
573
|
+
# Force YAML output (even on success)
|
|
574
|
+
vibe-validate watch-pr 90 --yaml
|
|
375
575
|
|
|
376
|
-
#
|
|
377
|
-
vibe-validate watch-pr --
|
|
576
|
+
# Watch PR in different repo
|
|
577
|
+
vibe-validate watch-pr 42 --repo jdutton/vibe-validate
|
|
378
578
|
|
|
379
|
-
#
|
|
380
|
-
vibe-validate watch-pr --
|
|
579
|
+
# Watch specific failed run (useful for testing extraction with failures)
|
|
580
|
+
vibe-validate watch-pr 104 --run-id 19754182675 --repo jdutton/mcp-typescript-simple --yaml
|
|
381
581
|
\`\`\`
|
|
382
582
|
|
|
383
|
-
##
|
|
583
|
+
## Output Format
|
|
584
|
+
|
|
585
|
+
### YAML (auto on failure, or with --yaml)
|
|
586
|
+
|
|
587
|
+
\`\`\`yaml
|
|
588
|
+
pr:
|
|
589
|
+
number: 90
|
|
590
|
+
title: "Enhancement: Add watch-pr improvements"
|
|
591
|
+
branch: "feature/watch-pr"
|
|
592
|
+
mergeable: true
|
|
593
|
+
labels: ["enhancement"]
|
|
594
|
+
linked_issues:
|
|
595
|
+
- number: 42
|
|
596
|
+
title: "Improve watch-pr"
|
|
597
|
+
|
|
598
|
+
status: failed
|
|
599
|
+
|
|
600
|
+
checks:
|
|
601
|
+
total: 3
|
|
602
|
+
passed: 2
|
|
603
|
+
failed: 1
|
|
604
|
+
history_summary:
|
|
605
|
+
total_runs: 5
|
|
606
|
+
recent_pattern: "Failed last 2 runs"
|
|
607
|
+
success_rate: "60%"
|
|
608
|
+
|
|
609
|
+
github_actions:
|
|
610
|
+
- name: "Test"
|
|
611
|
+
conclusion: failure
|
|
612
|
+
run_id: 123
|
|
613
|
+
extraction:
|
|
614
|
+
errors:
|
|
615
|
+
- file: "test.ts"
|
|
616
|
+
line: 42
|
|
617
|
+
message: "Expected success"
|
|
618
|
+
summary: "1 test failure"
|
|
619
|
+
totalErrors: 1
|
|
620
|
+
|
|
621
|
+
external_checks:
|
|
622
|
+
- name: "codecov/patch"
|
|
623
|
+
conclusion: success
|
|
624
|
+
extracted:
|
|
625
|
+
summary: "Coverage: 85%"
|
|
626
|
+
|
|
627
|
+
guidance:
|
|
628
|
+
status: failed
|
|
629
|
+
severity: error
|
|
630
|
+
summary: "1 check(s) failed"
|
|
631
|
+
next_steps:
|
|
632
|
+
- action: "Fix Test failure"
|
|
633
|
+
url: "https://github.com/.../runs/123"
|
|
634
|
+
severity: error
|
|
635
|
+
\`\`\`
|
|
384
636
|
|
|
385
|
-
###
|
|
637
|
+
### Text (on success, unless --yaml)
|
|
386
638
|
|
|
387
|
-
\`\`\`
|
|
388
|
-
#
|
|
389
|
-
|
|
639
|
+
\`\`\`
|
|
640
|
+
đ PR #90: Enhancement: Add watch-pr improvements
|
|
641
|
+
https://github.com/jdutton/vibe-validate/pull/90
|
|
390
642
|
|
|
391
|
-
|
|
392
|
-
|
|
643
|
+
â
Test success
|
|
644
|
+
â
Lint success
|
|
645
|
+
â
codecov/patch success
|
|
393
646
|
|
|
394
|
-
|
|
395
|
-
vibe-validate watch-pr
|
|
647
|
+
3/3 checks passed
|
|
396
648
|
|
|
397
|
-
|
|
398
|
-
gh pr merge
|
|
649
|
+
All checks passed
|
|
399
650
|
|
|
400
|
-
|
|
401
|
-
|
|
651
|
+
Next steps:
|
|
652
|
+
âšī¸ Ready to merge
|
|
402
653
|
\`\`\`
|
|
403
654
|
|
|
404
|
-
|
|
655
|
+
## Extraction Modes
|
|
405
656
|
|
|
406
|
-
|
|
407
|
-
# Push changes
|
|
408
|
-
git push origin my-branch
|
|
409
|
-
|
|
410
|
-
# Watch with fail-fast (exit on first failure for quick feedback)
|
|
411
|
-
vibe-validate watch-pr --fail-fast
|
|
657
|
+
### Matrix Mode (vibe-validate repos)
|
|
412
658
|
|
|
413
|
-
|
|
414
|
-
vibe-validate watch-pr 42 --yaml | yq '.failures[0].validationResult'
|
|
659
|
+
If check uses \`vv run\` or \`vv validate\`, YAML output is parsed and extraction passed through:
|
|
415
660
|
|
|
416
|
-
|
|
417
|
-
|
|
661
|
+
\`\`\`yaml
|
|
662
|
+
extraction:
|
|
663
|
+
errors:
|
|
664
|
+
- file: "test.ts"
|
|
665
|
+
line: 42
|
|
666
|
+
summary: "1 test failure"
|
|
667
|
+
totalErrors: 1
|
|
418
668
|
\`\`\`
|
|
419
669
|
|
|
420
|
-
###
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
670
|
+
### Non-Matrix Mode (other repos)
|
|
671
|
+
|
|
672
|
+
For raw test output, extractors detect tool (vitest, jest, eslint) and extract errors:
|
|
673
|
+
|
|
674
|
+
\`\`\`yaml
|
|
675
|
+
extraction:
|
|
676
|
+
errors:
|
|
677
|
+
- file: "test.integration.test.ts"
|
|
678
|
+
line: 10
|
|
679
|
+
message: "Connection refused"
|
|
680
|
+
summary: "2 test failures"
|
|
681
|
+
totalErrors: 2
|
|
682
|
+
metadata:
|
|
683
|
+
detection:
|
|
684
|
+
extractor: vitest
|
|
432
685
|
\`\`\`
|
|
433
686
|
|
|
434
|
-
##
|
|
687
|
+
## Common Workflows
|
|
435
688
|
|
|
436
|
-
###
|
|
689
|
+
### Standard PR workflow
|
|
437
690
|
|
|
438
|
-
**View validation result from YAML output:**
|
|
439
691
|
\`\`\`bash
|
|
440
|
-
#
|
|
441
|
-
vibe-validate watch-pr
|
|
692
|
+
# Check PR status
|
|
693
|
+
vibe-validate watch-pr 90
|
|
694
|
+
|
|
695
|
+
# If failed, view extraction
|
|
696
|
+
vibe-validate watch-pr 90 --yaml | yq '.checks.github_actions[0].extraction'
|
|
442
697
|
|
|
443
698
|
# Re-run failed check
|
|
444
699
|
gh run rerun <run-id> --failed
|
|
445
700
|
\`\`\`
|
|
446
701
|
|
|
447
|
-
###
|
|
702
|
+
### AI agent workflow
|
|
448
703
|
|
|
449
|
-
**Create PR first:**
|
|
450
704
|
\`\`\`bash
|
|
451
|
-
#
|
|
452
|
-
|
|
705
|
+
# AI agent checks PR (always YAML for parsing)
|
|
706
|
+
vibe-validate watch-pr 90 --yaml
|
|
453
707
|
|
|
454
|
-
#
|
|
455
|
-
|
|
708
|
+
# Parse result
|
|
709
|
+
# - If passed: proceed with merge
|
|
710
|
+
# - If failed: extract errors and fix
|
|
456
711
|
\`\`\`
|
|
457
712
|
|
|
458
|
-
##
|
|
713
|
+
## Caching
|
|
459
714
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
-
|
|
463
|
-
|
|
464
|
-
-
|
|
465
|
-
|
|
466
|
-
|
|
715
|
+
Results are cached locally (5 minute TTL):
|
|
716
|
+
\`\`\`
|
|
717
|
+
/tmp/vibe-validate/watch-pr-cache/<repo>/<pr-number>/
|
|
718
|
+
metadata.json # Complete WatchPRResult
|
|
719
|
+
logs/<run-id>.log # Raw logs from GitHub Actions
|
|
720
|
+
extractions/ # Extracted errors
|
|
721
|
+
\`\`\`
|
|
467
722
|
|
|
468
|
-
|
|
469
|
-
- **Tracking**: https://github.com/jdutton/vibe-validate/issues
|
|
723
|
+
Cache location is included in YAML output (\`cache.location\` field).
|
|
470
724
|
`);
|
|
471
725
|
}
|
|
472
726
|
//# sourceMappingURL=watch-pr.js.map
|