diff --git a/.github/MAINTAINERS b/.github/MAINTAINERS index 2087bbf5..c74a2309 100644 --- a/.github/MAINTAINERS +++ b/.github/MAINTAINERS @@ -1,4 +1,5 @@ # GitHub usernames with Herdr maintainer authority, one per line. # Maintainers must also use the canonical repository and have write access. ogulcancelik +Pimpmuckl kangal-bot diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index 92b4c09b..4866dc88 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -2,7 +2,7 @@ name: PR Gate on: pull_request_target: - types: [opened, closed, reopened] + types: [opened, closed, reopened, ready_for_review] concurrency: group: pr-gate-${{ github.event.pull_request.number }} @@ -27,7 +27,7 @@ jobs: 49699333, // dependabot[bot] 41898282, // github-actions[bot] ]); - const REVIEW_LABEL = 'ai-review'; + const REVIEW_TRIGGER_MARKER = ''; const COMMENT_MARKER = ''; const pullNumber = context.payload.pull_request.number; @@ -85,16 +85,6 @@ jobs: return ['admin', 'maintain', 'write'].includes(await getPermission(username)); } - async function currentLabels() { - const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullNumber, - per_page: 100, - }); - return new Set(labels.map(label => label.name)); - } - async function hasVerifiedRecovery() { const events = await github.paginate(github.rest.issues.listEventsForTimeline, { owner: context.repo.owner, @@ -108,28 +98,31 @@ jobs: await isVerifiedMaintainer(latestStateEvent.actor?.login); } - async function addReviewLabel() { - if ((await currentLabels()).has(REVIEW_LABEL)) return; - await github.rest.issues.addLabels({ + async function requestAiReviews() { + if (pr.draft) { + core.info(`PR #${pullNumber} is a draft; deferring AI reviews until ready`); + return; + } + + const marker = REVIEW_TRIGGER_MARKER; + const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: pullNumber, - labels: [REVIEW_LABEL], + per_page: 100, }); - } - - async function removeReviewLabel() { - if (!(await currentLabels()).has(REVIEW_LABEL)) return; - try { - await github.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{name}', { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullNumber, - name: REVIEW_LABEL, - }); - } catch (error) { - if (error.status !== 404) throw error; + if (comments.some(comment => + comment.user?.id === KANGAL_USER_ID && comment.body?.includes(marker))) { + core.info(`AI reviews already requested for PR #${pullNumber}`); + return; } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pullNumber, + body: [marker, '@coderabbitai review', '@greptileai'].join('\n'), + }); } async function upsertGateComment(message) { @@ -163,10 +156,9 @@ jobs: async function closePullRequest(reason) { if (await hasVerifiedRecovery()) { core.info(`PR #${pullNumber} was recovered by a verified maintainer; leaving it open`); - await addReviewLabel(); + await requestAiReviews(); return; } - await removeReviewLabel(); const message = [ `Hi @${prAuthor}, thanks for your interest in contributing.`, '', @@ -185,7 +177,7 @@ jobs: await upsertGateComment(message); if (await hasVerifiedRecovery()) { core.info(`PR #${pullNumber} was recovered while the gate was running; leaving it open`); - await addReviewLabel(); + await requestAiReviews(); return; } await github.rest.pulls.update({ @@ -196,26 +188,22 @@ jobs: }); } - if (pr.state === 'closed') { - await removeReviewLabel(); - return; - } + if (pr.state === 'closed') return; if (CI_ONLY_PR_AUTHOR_IDS.has(pr.user.id)) { core.info(`Leaving CI-only bot PR open without automated AI review: ${prAuthor}`); - await removeReviewLabel(); return; } if (await isVerifiedMaintainer(prAuthor)) { core.info(`${prAuthor} is a verified maintainer`); - await addReviewLabel(); + await requestAiReviews(); return; } if (approvedContributors.has(prAuthor.toLowerCase())) { core.info(`${prAuthor} is in the approved contributors list`); - await addReviewLabel(); + await requestAiReviews(); return; }