From 786ef23bf5f60b687043ffcd24e91340444614f6 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sun, 8 Mar 2026 12:26:28 +0100 Subject: [PATCH] fix: handle new repo race condition + fix AI review timeouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repository creation: - Poll for default branch existence (up to 5 retries, 2s apart) before applying branch protection — new repos don't have a branch until the first commit lands - Wrap issue creation in try/catch to prevent crash if issues API isn't ready yet AI reviews: - Add stream: false to ollama request — without it, ollama streams SSE chunks and response.json() hangs until the AbortController fires - Increase default timeout from 120s to 300s for the 3B model - Enable ai_review in config (was disabled) Co-Authored-By: Claude Opus 4.6 --- __tests__/integration/app.test.js | 4 ++ config.yml | 4 +- src/ai-review.js | 5 ++- src/app.js | 68 +++++++++++++++++++++++++++---- 4 files changed, 68 insertions(+), 13 deletions(-) diff --git a/__tests__/integration/app.test.js b/__tests__/integration/app.test.js index 38d7fc4..9ae72eb 100644 --- a/__tests__/integration/app.test.js +++ b/__tests__/integration/app.test.js @@ -144,6 +144,9 @@ function createMockOctokit() { updateComment: jest.fn().mockResolvedValue({}), deleteComment: jest.fn().mockResolvedValue({}) }, + repos: { + getBranch: jest.fn().mockResolvedValue({ data: { name: 'main' } }) + }, request: jest.fn().mockResolvedValue({ status: 200, data: {} }) }; } @@ -159,6 +162,7 @@ function createRepoCreatedContext(overrides = {}) { full_name: 'pulseengine/test-repo', name: 'test-repo', has_issues: true, + default_branch: 'main', owner: { login: 'pulseengine' }, ...(overrides.repository || {}) }, diff --git a/config.yml b/config.yml index 0192156..5cf844b 100644 --- a/config.yml +++ b/config.yml @@ -61,13 +61,13 @@ dependabot_generation: max_directories_per_ecosystem: 5 ai_review: - enabled: false + enabled: true endpoint: "http://localhost:11434/v1/chat/completions" model: "qwen2.5-coder:3b" max_diff_size: 12000 max_tokens: 2000 temperature: 0.3 - timeout: 120000 + timeout: 300000 allow_remote_endpoint: false scheduler: diff --git a/src/ai-review.js b/src/ai-review.js index fdf478f..037bf06 100644 --- a/src/ai-review.js +++ b/src/ai-review.js @@ -185,7 +185,7 @@ function buildReviewPrompt(prData, diff, files, maxDiffSize) { } async function callLocalAI(endpoint, model, systemPrompt, userPrompt, options = {}) { - const { maxTokens = 2000, temperature = 0.3, timeout = 120000 } = options; + const { maxTokens = 2000, temperature = 0.3, timeout = 300000 } = options; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); @@ -201,7 +201,8 @@ async function callLocalAI(endpoint, model, systemPrompt, userPrompt, options = { role: 'user', content: userPrompt } ], max_tokens: maxTokens, - temperature + temperature, + stream: false }), signal: controller.signal }); diff --git a/src/app.js b/src/app.js index 8b6cf41..5080c0a 100644 --- a/src/app.js +++ b/src/app.js @@ -115,20 +115,70 @@ function registerApp(app, { getRouter, addHandler } = {}) { if (repoOrg === targetOrg) { getLogger().info({ repo: repository.full_name }, 'New repository created'); + // Wait for the default branch to exist before configuring. + // When GitHub fires repository.created, the repo exists but the default + // branch is only created after the first commit. + const defaultBranch = repository.default_branch || 'main'; + const owner = repository.owner.login; + const repoName = repository.name; + let branchReady = false; + + for (let attempt = 1; attempt <= 5; attempt++) { + try { + await context.octokit.repos.getBranch({ + owner, + repo: repoName, + branch: defaultBranch + }); + branchReady = true; + break; + } catch (err) { + if (err.status === 404) { + getLogger().info( + { repo: repository.full_name, branch: defaultBranch, attempt }, + `Default branch not ready yet, retrying in 2s (attempt ${attempt}/5)` + ); + await new Promise(resolve => setTimeout(resolve, 2000)); + } else { + getLogger().warn( + { repo: repository.full_name, err: err.message }, + 'Unexpected error checking for default branch' + ); + break; + } + } + } + + if (!branchReady) { + getLogger().warn( + { repo: repository.full_name, branch: defaultBranch }, + 'Default branch never appeared — repo may still be empty. Skipping configuration.' + ); + if (deliveryId) markProcessed(deliveryId); + return; + } + const result = await configureRepository(context.octokit, repository, undefined, { enqueueTask: getEnqueueTask() }); if (repository.has_issues) { - await context.octokit.issues.create({ - owner: repository.owner.login, - repo: repository.name, - title: 'Repository Configuration', - body: result.success - ? '✅ This repository has been automatically configured with standard merge settings and branch protection.' - : `❌ Configuration failed: ${result.error}`, - labels: ['automation', 'configuration'] - }); + try { + await context.octokit.issues.create({ + owner, + repo: repoName, + title: 'Repository Configuration', + body: result.success + ? '✅ This repository has been automatically configured with standard merge settings and branch protection.' + : `❌ Configuration failed: ${result.error}`, + labels: ['automation', 'configuration'] + }); + } catch (issueErr) { + getLogger().warn( + { repo: repository.full_name, err: issueErr.message }, + 'Failed to create configuration issue — issues may not be fully initialized yet' + ); + } } }