Repository: vercel/next.js
Pull request: Fix ENOBUFS errors in pr-status.js when fetching large CI logs

## Summary

Fixes `spawnSync /bin/sh ENOBUFS` errors in `scripts/pr-status.js` when fetching large CI job logs.

The script uses `execSync` (which internally uses `spawnSync`) to fetch CI job logs via `gh api .../jobs/{id}/logs`. `execSync` has a hard `maxBuffer` limit (set to 50MB), and when CI job logs exceed that size, Node.js throws an `ENOBUFS` error. This causes the flaky test detection to silently fail, reporting 0 flaky tests.

**Before:**
```
Fetching logs for 37 failed jobs...
[stderr] Command failed: gh api "repos/vercel/next.js/actions/jobs/65135029618/logs"
spawnSync /bin/sh ENOBUFS
[stderr] Command failed: gh api "repos/vercel/next.js/actions/jobs/65135029722/logs"
spawnSync /bin/sh ENOBUFS
Found 0 flaky tests (failing on 2+ different branches)
```

**After:**
```
Fetching logs for 18 failed jobs...
Found 0 flaky tests (failing on 2+ different branches)
```

## Changes

- Added an `execAsync()` helper that uses `child_process.spawn` instead of `execSync`. `spawn` streams data through pipes with no built-in buffer limit, avoiding `ENOBUFS` entirely.
- Updated `getJobLogs()` and the log-fetching loop in `getFlakyTests()` to use `execAsync` instead of `exec`.
- All other API calls (small JSON responses) continue using the synchronous `exec()` helper since they are well within the 50MB limit.

## Test Plan

Ran `node scripts/pr-status.js 90617` against a PR with multiple failed jobs — completes successfully with no ENOBUFS errors.

Changed files:
filename: scripts/pr-status.js
patch: @@ -1,4 +1,4 @@
-const { execSync } = require('child_process')
+const { execSync, spawn } = require('child_process')
 const fs = require('fs/promises')
 const path = require('path')
 
@@ -21,6 +21,30 @@ function exec(cmd) {
   }
 }
 
+function execAsync(prog, args) {
+  return new Promise((resolve, reject) => {
+    const child = spawn(prog, args, {
+      stdio: ['ignore', 'pipe', 'pipe'],
+    })
+    const chunks = []
+    let stderr = ''
+    child.stdout.on('data', (chunk) => chunks.push(chunk))
+    child.stderr.on('data', (chunk) => {
+      stderr += chunk
+    })
+    child.on('close', (code) => {
+      if (code !== 0) {
+        const error = new Error(`Command failed: ${prog} ${args.join(' ')}`)
+        error.stderr = stderr
+        reject(error)
+      } else {
+        resolve(Buffer.concat(chunks).toString('utf8').trim())
+      }
+    })
+    child.on('error', reject)
+  })
+}
+
 function execJson(cmd) {
   const output = exec(cmd)
   return JSON.parse(output)
@@ -272,9 +296,12 @@ function getJobMetadata(jobId) {
   )
 }
 
-function getJobLogs(jobId) {
+async function getJobLogs(jobId) {
   try {
-    return exec(`gh api "repos/vercel/next.js/actions/jobs/${jobId}/logs"`)
+    return await execAsync('gh', [
+      'api',
+      `repos/vercel/next.js/actions/jobs/${jobId}/logs`,
+    ])
   } catch {
     return 'Logs not available'
   }
@@ -1021,9 +1048,10 @@ async function getFlakyTests(currentBranch, runsToCheck = 5) {
     const results = await Promise.all(
       batch.map(async ({ job, branch }) => {
         try {
-          const logs = exec(
-            `gh api "repos/vercel/next.js/actions/jobs/${job.id}/logs"`
-          )
+          const logs = await execAsync('gh', [
+            'api',
+            `repos/vercel/next.js/actions/jobs/${job.id}/logs`,
+          ])
           return { logs, branch }
         } catch {
           return { logs: null, branch }
@@ -1229,7 +1257,7 @@ async function main() {
     processedFailedJobs.push(jobMetadata)
 
     // Get job logs
-    const logs = getJobLogs(id)
+    const logs = await getJobLogs(id)
 
     // Extract test output JSON
     const testResults = extractTestOutputJson(logs)