Skip to content

Commit 247f9a8

Browse files
committed
feat: add --timeout option with --format default for real-time streaming
- Add --timeout=N CLI option (bare number=seconds, 10s=10 seconds, 10m=10 minutes, 10h=10 hours). Default: 30 minutes. - Change opencode run commands from --format json to --format default to enable streaming-compatible output with subagent-reporter plugin. - Add SIGALRM-based timeout enforcement: proc_terminate after timeout. - Replace blocking fgets() loop with stream_select() loop in streamOpencodeOutput() for responsive timeout checking. - Fix: timeout:: was optional arg (double colon) → timeout: required. - Fix: add \$opencodeTimeout to processJob() global scope for logging. - Fix: alarm handler uses use(\$timeout) instead of \$GLOBALS reference. - Fix: add if (\$timeout > 0) guard before pcntl_alarm(). - Fix: remove dead global \$opencodeTimeout from streamOpencodeOutput().
1 parent 15e602d commit 247f9a8

1 file changed

Lines changed: 76 additions & 9 deletions

File tree

scripts/github-code-review.php

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@
4040
$running = true;
4141
$logFile = null;
4242
$optind = 0;
43-
$args = getopt('vhwb:a:', ['verbose', 'help', 'wait-if-empty', 'review-bots', 'show-agent', 'show-all-issues', 'log::'], $optind);
43+
$args = getopt('vhwb:a:', ['verbose', 'help', 'wait-if-empty', 'review-bots', 'show-agent', 'show-all-issues', 'timeout:', 'log::'], $optind);
4444
if (isset($args['h']) || isset($args['help'])) {
45-
fwrite(STDOUT, "Usage: php scripts/github-code-review.php [-v|--verbose]... [-w|--wait-if-empty] [-b|--review-bots] [-a|--show-agent] [--show-all-issues] [--log=FILE]\n");
45+
fwrite(STDOUT, "Usage: php scripts/github-code-review.php [-v|--verbose]... [-w|--wait-if-empty] [-b|--review-bots] [-a|--show-agent] [--show-all-issues] [--timeout=30m] [--log=FILE]\n");
4646
fwrite(STDOUT, " -v, --verbose Increase verbosity (can stack: -vv, -vvv, -vvvv)\n");
4747
fwrite(STDOUT, " -v [INFO] Essential progress (job start/complete/errors)\n");
4848
fwrite(STDOUT, " -vv [DEBUG] Detailed progress (checkpoints, function entry)\n");
@@ -52,6 +52,7 @@
5252
fwrite(STDOUT, " -b, --review-bots Also review PRs from bots like dependabot (default: skip)\n");
5353
fwrite(STDOUT, " -a, --show-agent Stream opencode agent messages in real-time (colorized at -vvvv)\n");
5454
fwrite(STDOUT, " --show-all-issues Include issues without file/line in report (default: skip summary sections)\n");
55+
fwrite(STDOUT, " --timeout=N Timeout for opencode analysis (default: 30m). Examples: 30, 30s, 30m\n");
5556
fwrite(STDOUT, " --log=FILE Append all log output to FILE (in addition to stderr)\n");
5657
fwrite(STDOUT, " -h, --help Show this help message\n");
5758
fwrite(STDOUT, "\n Press Ctrl-C to exit gracefully at any time.\n");
@@ -67,6 +68,28 @@
6768
$reviewBots = isset($args['b']) || isset($args['review-bots']);
6869
$showAgent = isset($args['a']) || isset($args['show-agent']);
6970
$showAllIssues = isset($args['show-all-issues']);
71+
72+
// Parse timeout: bare number = seconds, number + s = seconds, number + m = minutes
73+
$opencodeTimeout = 30 * 60; // default: 30 minutes in seconds
74+
if (isset($args['timeout'])) {
75+
$raw = is_array($args['timeout']) ? $args['timeout'][0] : $args['timeout'];
76+
if ($raw !== false && $raw !== '') {
77+
$raw = trim($raw);
78+
if (preg_match('/^(\d+)([smh])?$/i', $raw, $m)) {
79+
$val = (int)$m[1];
80+
$unit = strtolower($m[2] ?? '');
81+
if ($unit === 'm') {
82+
$opencodeTimeout = $val * 60;
83+
} elseif ($unit === 'h') {
84+
$opencodeTimeout = $val * 3600;
85+
} else {
86+
$opencodeTimeout = $val; // bare number = seconds
87+
}
88+
} else {
89+
fwrite(STDERR, "github-code-review: invalid --timeout value '{$raw}', using default 30m\n");
90+
}
91+
}
92+
}
7093
if (isset($args['log'])) {
7194
$logFile = $args['log'] !== false ? $args['log'] : 'github-code-review.log';
7295
}
@@ -250,10 +273,11 @@ function parseOpencodeEventLine(string $line, int $verbose = 4): ?string
250273
* @param string|null $logFile Optional log file path
251274
* @return string Accumulated raw output
252275
*/
253-
function streamOpencodeOutput($stdout, int $verbose, ?string $logFile = null): string
276+
function streamOpencodeOutput($stdout, int $verbose, ?string $logFile = null, int $timeout = 0): string
254277
{
255278
$accumulated = '';
256279
$renderer = null;
280+
$startTime = microtime(true);
257281

258282
if ($verbose >= 4) {
259283
try {
@@ -263,7 +287,30 @@ function streamOpencodeOutput($stdout, int $verbose, ?string $logFile = null): s
263287
}
264288
}
265289

290+
$timeoutSecs = $timeout > 0 ? $timeout : 86400; // default 24h if no timeout
291+
$deadline = $startTime + $timeoutSecs;
292+
266293
while (!feof($stdout)) {
294+
// Calculate remaining time for this select call
295+
$remaining = max(1, (int)($deadline - microtime(true)));
296+
$read = [$stdout];
297+
$write = null;
298+
$except = null;
299+
$changed = @stream_select($read, $write, $except, 1, 0); // 1 second timeout + microseconds
300+
301+
if ($changed === false) {
302+
break; // error on stream
303+
}
304+
305+
if ($changed === 0) {
306+
// Timeout on select - check if overall deadline exceeded
307+
if (microtime(true) >= $deadline) {
308+
verbose_log("streamOpencodeOutput: overall timeout reached ({$timeoutSecs}s), stopping", 1);
309+
break;
310+
}
311+
continue; // No data ready yet, loop and try again
312+
}
313+
267314
$line = fgets($stdout);
268315
if ($line === false) {
269316
break;
@@ -629,7 +676,7 @@ function main(): void
629676
*/
630677
function processJob(array $job): string
631678
{
632-
global $githubToken, $checkoutRoot, $opencodeAnalyzeCmd, $opencodeImproveCmd, $verbose, $reviewBots, $running, $showAgent, $showAllIssues;
679+
global $githubToken, $checkoutRoot, $opencodeAnalyzeCmd, $opencodeImproveCmd, $verbose, $reviewBots, $running, $showAgent, $showAllIssues, $opencodeTimeout;
633680

634681
// Check if shutdown was requested before starting work
635682
if (!$running) {
@@ -719,8 +766,8 @@ function processJob(array $job): string
719766
// Step 3: Initialize git repo and run opencode analysis on the modified working dir
720767
initGitRepo($checkoutPath);
721768

722-
verbose_log("starting opencode analysis for {$repo}#{$prNumber} in {$checkoutPath}", 2);
723-
$analysisOutput = runOpencodeAnalysis($checkoutPath, $jobId, $opencodeAnalyzeCmd, $showAgent);
769+
verbose_log("starting opencode analysis for {$repo}#{$prNumber} in {$checkoutPath}" . ($opencodeTimeout > 0 ? " (timeout: " . ($opencodeTimeout >= 3600 ? round($opencodeTimeout/3600,1)."h" : round($opencodeTimeout/60,1)."m") . ")" : ""), 2);
770+
$analysisOutput = runOpencodeAnalysis($checkoutPath, $jobId, $opencodeAnalyzeCmd, $showAgent, $opencodeTimeout);
724771
verbose_log("analysis command completed", 3);
725772

726773
$issues = parseAnalysisOutput($analysisOutput, $showAllIssues);
@@ -1363,8 +1410,9 @@ function checkoutBranch(string $repo, string $branch, string $checkoutPath): str
13631410
* @param string $cmdTemplate Unused, kept for signature compatibility
13641411
* @return string Raw opencode output
13651412
*/
1366-
function runOpencodeAnalysis(string $dir, string $jobId, string $cmdTemplate, bool $showAgent = false): string
1413+
function runOpencodeAnalysis(string $dir, string $jobId, string $cmdTemplate, bool $showAgent = false, int $timeout = 1800): string
13671414
{
1415+
global $running;
13681416
// Load review prompt from file (allows easy tweaking without code changes)
13691417
static $reviewPrompt = null;
13701418
if ($reviewPrompt === null) {
@@ -1390,8 +1438,9 @@ function runOpencodeAnalysis(string $dir, string $jobId, string $cmdTemplate, bo
13901438
file_put_contents($promptFile, $reviewPrompt);
13911439

13921440
// Use bash -c with $(cat ...) to read prompt from file - avoids all escaping issues
1441+
// --format default enables streaming-compatible output with the subagent-reporter plugin
13931442
$opencodeCmd = sprintf(
1394-
'cd %s && git diff --name-only && git diff && opencode run "$(cat %s)" --format json',
1443+
'cd %s && git diff --name-only && git diff && opencode run "$(cat %s)" --format default',
13951444
escapeshellarg($dir),
13961445
escapeshellarg($promptFile)
13971446
);
@@ -1418,7 +1467,25 @@ function runOpencodeAnalysis(string $dir, string $jobId, string $cmdTemplate, bo
14181467
// Set stdout to non-blocking for streaming reads
14191468
stream_set_blocking($pipes[1], false);
14201469

1421-
$result = streamOpencodeOutput($pipes[1], $verbose, $logFile);
1470+
// --- Timeout enforcement via SIGALRM ---
1471+
$timedOut = false;
1472+
$alarmHandler = function (int $signo) use ($proc, &$timedOut, $timeout): void {
1473+
if ($timedOut) { return; }
1474+
$timedOut = true;
1475+
verbose_log("runOpencodeAnalysis: timeout reached ({$timeout}s), terminating opencode process", 1);
1476+
proc_terminate($proc, SIGTERM);
1477+
};
1478+
pcntl_async_signals(true);
1479+
pcntl_signal(SIGALRM, $alarmHandler);
1480+
if ($timeout > 0) {
1481+
pcntl_alarm($timeout);
1482+
}
1483+
1484+
$result = streamOpencodeOutput($pipes[1], $verbose, $logFile, $timeout);
1485+
1486+
// Cancel the alarm (process finished before timeout)
1487+
pcntl_alarm(0);
1488+
pcntl_signal(SIGALRM, SIG_DFL);
14221489

14231490
// Read any remaining stderr
14241491
$stderr = '';

0 commit comments

Comments
 (0)