Reads the diff of each open pull request, has AI note the obvious problems, and posts them as a comment.
Uses
GitHub
OpenAI
AI first-pass review on open pull requests
Every hour, the open pull requests in one GitHub repository are checked and each one not yet reviewed gets a first pass: its changed files are fetched, the AI is asked to note what is plainly visible in the diff, such as missing error handling, unclear names or new behaviour without a test, and those points are posted as a comment.
The reviewer starts from something rather than nothing. The comment says it is a first pass, and nothing is ever approved, merged or closed. Each pull request is reviewed once: after commenting, a label is added and anything carrying it is skipped, so a re-run never posts the same review twice; remove the label to have it looked at again. Drafts are left alone, and on a large pull request only the first batch of changed files is read, with the comment stating how many it covers.
You need a GitHub token that can read the repository, since the changed files are read from GitHub's API directly, and the label must already exist in the repository.
repositoryOwner to blank.Set repositoryOwner to blank.repositoryName to blank.Set repositoryName to blank.repositoryReadToken to blank.Set repositoryReadToken to blank.reviewedLabel to first-pass-reviewed.Set reviewedLabel to first-pass-reviewed.maxPullRequestsPerRun to 5.Set maxPullRequestsPerRun to 5.maxFilesPerReview to 20.Set maxFilesPerReview to 20.maxDiffCharactersPerFile to 3000.Set maxDiffCharactersPerFile to 3000.githubHeaders to a record of Authorization, Accept, and X-GitHub-Api-Version.Set githubHeaders to a record of Authorization, Accept, and X-GitHub-Api-Version.https://api.github.com/repos/, repositoryOwner, /, repositoryName] with nothing and save it as repositoryApiUrl.Join a list of [https://api.github.com/repos/, repositoryOwner, /, repositoryName] with nothing and save it as repositoryApiUrl.repositoryApiUrl, /pulls] with nothing and save it as openPullRequestsUrl.Join a list of [repositoryApiUrl, /pulls] with nothing and save it as openPullRequestsUrl.openPullRequests.Start an empty list and save it as openPullRequests.pullRequestListErrorIf a step below fails, save the error as pullRequestListErroropenPullRequests to <expr>.body.Change openPullRequests to <expr>.body.Could not read the open pull requests from GitHub: , pullRequestListError.message with openPullRequestsUrl replaced by the GitHub API address, Check the repository details and the token in the 'repositoryReadToken' step.] joined by nothing.Stop and report a problem: a list of [Could not read the open pull requests from GitHub: , pullRequestListError.message with openPullRequestsUrl replaced by the GitHub API address, Check the repository details and the token in the 'repositoryReadToken' step.] joined by nothing.reviewedThisRun to 0.Set reviewedThisRun to 0.pullRequest in openPullRequests:For each pullRequest in openPullRequests:reviewedThisRun is greater than or equal to maxPullRequestsPerRunIf reviewedThisRun is greater than or equal to maxPullRequestsPerRunReviewed , maxPullRequestsPerRun, pull requests, which is this run's limit. The rest keep their place in the queue and are picked up on the next run.] joined by nothing.Show in the output: a list of [Reviewed , maxPullRequestsPerRun, pull requests, which is this run's limit. The rest keep their place in the queue and are picked up on the next run.] joined by nothing.pullNumber to pullRequest.number.Set pullNumber to pullRequest.number.pullNumber is noIf pullNumber is noSkipped a pull request that GitHub returned without a number, so there was nothing to comment on..Show in the output: Skipped a pull request that GitHub returned without a number, so there was nothing to comment on..pullRequest.draft is yesIf pullRequest.draft is yesPull request #, pullNumber, is still a draft, so it was left alone.] joined by nothing.Show in the output: a list of [Pull request #, pullNumber, is still a draft, so it was left alone.] joined by nothing.alreadyReviewed to false.Set alreadyReviewed to false.pullRequestLabel in pullRequest.labels:For each pullRequestLabel in pullRequest.labels:pullRequestLabel.name is reviewedLabelIf pullRequestLabel.name is reviewedLabelalreadyReviewed to true.Change alreadyReviewed to true.alreadyReviewed is yesIf alreadyReviewed is yesrepositoryApiUrl, /pulls/, pullNumber, /files] with nothing and save it as changedFilesUrl.Join a list of [repositoryApiUrl, /pulls/, pullNumber, /files] with nothing and save it as changedFilesUrl.changedFiles.Start an empty list and save it as changedFiles.changedFilesErrorIf a step below fails, save the error as changedFilesErrorchangedFiles to <expr>.body.Change changedFiles to <expr>.body.Could not read the changed files for pull request #, pullNumber, : , changedFilesError.message with changedFilesUrl replaced by the GitHub API address] joined by nothing.Show in the output: a list of [Could not read the changed files for pull request #, pullNumber, : , changedFilesError.message with changedFilesUrl replaced by the GitHub API address] joined by nothing.changedFiles is noIf changedFiles is noPull request #, pullNumber, has no changed files this workflow can read, so it was left for a human.] joined by nothing.Show in the output: a list of [Pull request #, pullNumber, has no changed files this workflow can read, so it was left for a human.] joined by nothing.changedFiles and save it as changedFileCount.Count the items in changedFiles and save it as changedFileCount.maxFilesPerReview items of changedFiles and save it as filesToReview.Take the first maxFilesPerReview items of changedFiles and save it as filesToReview.filesToReview and save it as reviewedFileCount.Count the items in filesToReview and save it as reviewedFileCount.diffBlocks.Start an empty list and save it as diffBlocks.changedFile in filesToReview:For each changedFile in filesToReview:changedFileName to blank.Set changedFileName to blank.fileNameErrorIf a step below fails, save the error as fileNameErrorchangedFile.filename and save it as changedFileName.Trim changedFile.filename and save it as changedFileName.changedFileName to blank.Change changedFileName to blank.changedFileName is emptyIf changedFileName is emptySkipped a changed file in pull request #, pullNumber, because GitHub returned it without a filename.] joined by nothing.Show in the output: a list of [Skipped a changed file in pull request #, pullNumber, because GitHub returned it without a filename.] joined by nothing.changedFilePatch to blank.Set changedFilePatch to blank.filePatchErrorIf a step below fails, save the error as filePatchErrorchangedFile.patch and save it as changedFilePatch.Trim changedFile.patch and save it as changedFilePatch.changedFilePatch to blank.Change changedFilePatch to blank.diffBlockBody to (GitHub sent no text diff for this file - it may be binary, a pure rename, or too large to display.).Set diffBlockBody to (GitHub sent no text diff for this file - it may be binary, a pure rename, or too large to display.).changedFilePatch isn't emptyIf changedFilePatch isn't emptychangedFilePatch to maxDiffCharactersPerFile characters and save it as diffBlockBody.Shorten changedFilePatch to maxDiffCharactersPerFile characters and save it as diffBlockBody.File: , changedFileName, "Add a list of [File: , changedFileName,
, diffBlockBody] joined by nothing to the end of diffBlocks and save it as diffBlocks.diffBlocks is noIf diffBlocks is noNone of the changed files in pull request #, pullNumber, could be read, so it was left for a human.] joined by nothing.Show in the output: a list of [None of the changed files in pull request #, pullNumber, could be read, so it was left for a human.] joined by nothing.coverageNote to blank.Set coverageNote to blank.changedFileCount is greater than reviewedFileCountIf changedFileCount is greater than reviewedFileCountThis pull request changes , changedFileCount, files. This first pass read only the first , reviewedFileCount, of them, in the order GitHub lists them - the rest have not been looked at.] with nothing and save it as coverageNote.Join a list of [This pull request changes , changedFileCount, files. This first pass read only the first , reviewedFileCount, of them, in the order GitHub lists them - the rest have not been looked at.] with nothing and save it as coverageNote.This first pass read all , changedFileCount, changed files.] with nothing and save it as coverageNote.Join a list of [This first pass read all , changedFileCount, changed files.] with nothing and save it as coverageNote.You are giving a pull request a first-pass read for a human reviewer who has not opened it yet.
, Repository: , repositoryOwner, /, repositoryName,
Pull request #, pullNumber, : , pullRequest.title,
Opened by: , pullRequest.user.login,
, coverageNote,
The changed files and their diffs follow. Lines starting with + or - are the change itself.
, diffBlocks joined by a blank line,
Write short markdown bullet points about what is plainly visible in the diff above:
- error handling that looks missing, such as an unchecked failure or a swallowed exception
- names that do not say what the thing is
- new behaviour that arrives without a test
- anything else obviously risky, such as a hardcoded secret or an unbounded loop
Group the points by file and quote the identifier or the line you mean. Make at most eight points, most important first. If nothing stands out, reply with the single sentence: Nothing obvious stood out in this first pass. Do not approve the pull request, do not say LGTM, and do not suggest merging - a human decides that. Do not comment on anything that is not in the diff above.] with nothing and save it as reviewPrompt.reviewText to blank.Set reviewText to blank.reviewErrorIf a step below fails, save the error as reviewErrorreviewText.Trim the response text from the OpenAI “Ask OpenAI” step and save it as reviewText.Could not get a first-pass review for pull request #, pullNumber, : , reviewError.message] joined by nothing.Show in the output: a list of [Could not get a first-pass review for pull request #, pullNumber, : , reviewError.message] joined by nothing.reviewText is emptyIf reviewText is emptyThe model returned nothing for pull request #, pullNumber, , so no comment was posted and it stays unlabelled for the next run.] joined by nothing.Show in the output: a list of [The model returned nothing for pull request #, pullNumber, , so no comment was posted and it stays unlabelled for the next run.] joined by nothing.### Automated first-pass review
, coverageNote,
, reviewText,
This is a first pass, meant to give a human reviewer somewhere to start. It is not an approval, and this workflow never approves, merges or closes anything. Remove the ', reviewedLabel, ' label from this pull request to have it looked at again.] with nothing and save it as commentBody.pullNumber in repositoryOwner/repositoryName.Comment on issue pullNumber in repositoryOwner/repositoryName.commentBodylabelErrorIf a step below fails, save the error as labelErrorreviewedLabel to issue pullNumber in repositoryOwner/repositoryName.Add label reviewedLabel to issue pullNumber in repositoryOwner/repositoryName.Commented on pull request #, pullNumber, but could not add the ', reviewedLabel, ' label, so the next run will comment on it again. Add the label by hand to stop that: , labelError.message] joined by nothing.Show in the output: a list of [Commented on pull request #, pullNumber, but could not add the ', reviewedLabel, ' label, so the next run will comment on it again. Add the label by hand to stop that: , labelError.message] joined by nothing.reviewedThisRun.Add 1 to reviewedThisRun.First-pass review finished. Commented on , reviewedThisRun, pull request(s).] joined by nothing.Show in the output: a list of [First-pass review finished. Commented on , reviewedThisRun, pull request(s).] joined by nothing.