(base: string, branch: string, title: string, body: string)
| 1273 | } |
| 1274 | |
| 1275 | async function createPR(base: string, branch: string, title: string, body: string): Promise<number | null> { |
| 1276 | console.log("Creating pull request...") |
| 1277 | |
| 1278 | // Check if an open PR already exists for this head→base combination |
| 1279 | // This handles the case where the agent created a PR via gh pr create during its run |
| 1280 | try { |
| 1281 | const existing = await withRetry(() => |
| 1282 | octoRest.rest.pulls.list({ |
| 1283 | owner, |
| 1284 | repo, |
| 1285 | head: `${owner}:${branch}`, |
| 1286 | base, |
| 1287 | state: "open", |
| 1288 | }), |
| 1289 | ) |
| 1290 | |
| 1291 | if (existing.data.length > 0) { |
| 1292 | console.log(`PR #${existing.data[0].number} already exists for branch ${branch}`) |
| 1293 | return existing.data[0].number |
| 1294 | } |
| 1295 | } catch (e) { |
| 1296 | // If the check fails, proceed to create - we'll get a clear error if a PR already exists |
| 1297 | console.log(`Failed to check for existing PR: ${e}`) |
| 1298 | } |
| 1299 | |
| 1300 | // Verify there are commits between base and head before creating the PR. |
| 1301 | // In shallow clones, the branch can appear dirty but share the same |
| 1302 | // commit as the base, causing a 422 from GitHub. |
| 1303 | if (!(await hasNewCommits(base, branch))) { |
| 1304 | console.log(`No commits between ${base} and ${branch}, skipping PR creation`) |
| 1305 | return null |
| 1306 | } |
| 1307 | |
| 1308 | try { |
| 1309 | const pr = await withRetry(() => |
| 1310 | octoRest.rest.pulls.create({ |
| 1311 | owner, |
| 1312 | repo, |
| 1313 | head: branch, |
| 1314 | base, |
| 1315 | title, |
| 1316 | body, |
| 1317 | }), |
| 1318 | ) |
| 1319 | return pr.data.number |
| 1320 | } catch (e: unknown) { |
| 1321 | // Handle "No commits between X and Y" validation error from GitHub. |
| 1322 | // This can happen when the branch was pushed but has no new commits |
| 1323 | // relative to the base (e.g. shallow clone edge cases). |
| 1324 | if (e instanceof Error && e.message.includes("No commits between")) { |
| 1325 | console.log(`GitHub rejected PR: ${e.message}`) |
| 1326 | return null |
| 1327 | } |
| 1328 | throw e |
| 1329 | } |
| 1330 | } |
| 1331 | |
| 1332 | async function withRetry<T>(fn: () => Promise<T>, retries = 1, delayMs = 5000): Promise<T> { |
no test coverage detected