Skip to content

Technology

GitHub Actions AI Code Review: Working YAML Setup

A GitHub Actions workflow that sends your pull request diff to an AI model and posts the review as a comment. Full YAML and troubleshooting.

GitHub Actions AI Code Review: Working YAML Setup

This GitHub Actions workflow runs on every pull request, sends the code changes (the diff) to an AI model, and posts the model's review as a comment on the pull request. Setup takes about ten minutes: one secret, one YAML file, and one test pull request.

Here is what happens on each run:

  1. The workflow checks out your repository with its full history.
  2. It builds the diff between the pull request and its base branch, leaving out lockfiles and capping the size at 60 KB.
  3. It sends the diff to the AI API with a short reviewer prompt.
  4. It posts the answer as a comment on the pull request, so reviewers see it next to the code.

The AI only comments. It never edits code or blocks a merge, which keeps the setup low-risk.

Last checked: October 29, 2026, against GitHub's Actions documentation and the Anthropic API documentation. This guide uses Anthropic's API for the examples; see "Using a different AI provider" below for others.

What you need

  • A GitHub repository where you can add workflow files, with GitHub Actions turned on.
  • An API key from an AI provider. The examples use Anthropic's API.
  • Pull requests that come from branches in the same repository. Pull requests from forks do not receive your secrets, so this workflow skips them (more on that below).

The workflow uses git, curl, jq and the GitHub CLI (gh), which are available on GitHub's standard ubuntu-latest runners.

Step 1: Add your API key as a secret

Never put an API key in a workflow file. Store it as an encrypted repository secret instead.

  1. In your repository, open Settings, then Secrets and variables, then Actions.
  2. Click New repository secret.
  3. Name it ANTHROPIC_API_KEY and paste your key as the value.
  4. Click Add secret.

The name must match the one used in the workflow exactly. If you use a different provider, pick a matching name, such as OPENAI_API_KEY, and change it in the YAML too.

Step 2: Create the workflow file

Create this file in your repository:

.github/workflows/ai-review.yml

Paste in the following workflow:

name: AI Code Review

on:
  pull_request:
    branches: [main, develop]

permissions:
  contents: read
  pull-requests: write

jobs:
  ai_review:
    if: github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          fetch-depth: 0

      - name: Collect the pull request diff
        id: diff
        env:
          BASE_SHA: ${{ github.event.pull_request.base.sha }}
        run: |
          git diff "$BASE_SHA"...HEAD -- . ':(exclude)*package-lock.json' ':(exclude)*pnpm-lock.yaml' ':(exclude)*.lock' > full.diff
          head -c 60000 full.diff > diff.txt
          if [ -s diff.txt ]; then
            echo "has_diff=true" >> "$GITHUB_OUTPUT"
          fi
          echo "Diff size: $(wc -c < full.diff) bytes, sent: $(wc -c < diff.txt) bytes"

      - name: Ask the AI for a review
        if: steps.diff.outputs.has_diff == 'true'
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          jq -n --rawfile diff diff.txt '{
            model: "claude-sonnet-5-5",
            max_tokens: 2000,
            system: "You are a senior code reviewer. Review the git diff you are given. List bugs, security problems and unclear code, most serious first. For each point name the file and say how to fix it. If the diff looks fine, say so in one sentence. Be concise.",
            messages: [{role: "user", content: $diff}]
          }' > request.json
          curl -sS --fail-with-body https://api.anthropic.com/v1/messages \
            -H "content-type: application/json" \
            -H "x-api-key: $ANTHROPIC_API_KEY" \
            -H "anthropic-version: 2026-10-21" \
            -d @request.json > response.json
          jq -r '[.content[] | select(.type == "text") | .text] | join("\n")' response.json > review.md
          test -s review.md
          if [ "$(wc -c < full.diff)" -gt 60000 ]; then
            printf '\n\n_Only the first 60 KB of this diff was reviewed._\n' >> review.md
          fi

      - name: Post the review as a comment
        if: steps.diff.outputs.has_diff == 'true'
        env:
          GH_TOKEN: ${{ github.token }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: gh pr comment "$PR_NUMBER" --body-file review.md

Commit it to your default branch or to a branch you open a pull request from. The workflow runs on pull requests that target main or develop; change those names to match your repository.

What each part does

PartWhat it does and why
on: pull_requestRuns when a pull request is opened or updated against the listed branches.
if: ...head.repo.full_name == github.repositorySkips pull requests from forks, which do not get your secrets.
fetch-depth: 0Fetches the full history. The default fetches only one commit, which is not enough to compare against the base.
BASE_SHA in envPasses the base commit through an environment variable instead of pasting GitHub expressions into the script, which is the safer habit.
git diff "$BASE_SHA"...HEADShows only what the pull request changes. The :(exclude) patterns drop lockfiles, which are large and not worth reviewing.
head -c 60000Caps what is sent at 60 KB to control cost and stay within model limits. It runs on a saved file, not a pipe, so it cannot fail a step that uses pipefail.
has_diff outputLets the later steps skip when there is nothing to review, for example when only a lockfile changed.
jq -n --rawfileBuilds the JSON request safely, so quotes, backslashes and newlines in your code cannot break it.
--fail-with-bodyMakes the step fail on an API error and prints the error message in the log.
select(.type == "text")Takes only the text from the response, in case the model returns other block types.
test -s review.mdFails the step if the review is empty, so you never post a blank comment.
gh pr commentPosts review.md as a comment, using the built-in GITHUB_TOKEN.

Step 3: Understand the permissions

Every workflow run gets a temporary token called GITHUB_TOKEN. The permissions block at the top of the file decides what that token may do. GitHub sets any permission you do not list to none, and recommends giving the token the least access it needs.

This workflow asks for two permissions:

  • contents: read lets it check out your code.
  • pull-requests: write lets it post the review comment.

If the last step fails with an error such as "Resource not accessible by integration", first check Settings, then Actions, then General, then Workflow permissions in your repository, since an organization or repository default can limit the token. If that does not fix it, add issues: write as well. Pull request conversation comments use the same comment mechanism as issues, and GitHub's docs say issues: write allows commenting.

Pull requests from forks. For a pull request from a fork, GitHub does not pass your secrets to the workflow, and it gives GITHUB_TOKEN read-only permissions. This workflow would fail at the AI step, so the if line on the job skips fork pull requests. Do not switch to the pull_request_target trigger to get around this. GitHub warns that running untrusted pull request code with that trigger can expose secrets and give unintended write access. For open source projects, a common safe approach is to have a maintainer re-run the review from a branch inside the repository.

Step 4: Write a review prompt that helps

The system line inside the jq command is the instruction the model follows. The default is deliberately general. You will get better reviews by telling the model about your project and what to ignore.

Here are changes that make a difference:

  • Say what the project is. For example: "This is a Laravel 13 app using Pest for tests."
  • Say what to skip. Formatting and style are better left to your linter, so add "Ignore formatting and naming style."
  • Ask for a fixed format. For example: "Label each point High, Medium or Low, and start with the file name."
  • Allow a clean result. The default already says to reply in one sentence if the diff looks fine, so reviewers are not buried in filler.
  • Narrow the focus. A security-only reviewer is easy to build: "Look only for security problems such as injection, broken access control and exposed secrets."

A stronger version of the line, ready to paste in place of the existing system: text:

You are a senior code reviewer for a Laravel 13 app that uses Pest for tests. Review the git diff you are given. Ignore formatting and naming style. List bugs, security problems and missing tests, most serious first. Label each point High, Medium or Low, name the file, and say how to fix it. If the diff looks fine, say so in one sentence.

Keep double quotes out of the prompt text, because it sits inside a JSON string in the script. For more ready-made wording, see the Prompt Library for Code Review.

Step 5: Test it on a pull request

  1. Create a branch and add a change with an obvious problem. This PHP line builds a database query from user input:$user = DB::select("SELECT * FROM users WHERE email = '" . $request->email . "'");
  2. Push the branch and open a pull request against main.
  3. Open the Actions tab and watch the AI Code Review run. The three steps should turn green.
  4. Go back to the pull request. A comment from github-actions should appear within a minute or so. For the example above, expect it to point out the SQL injection risk and suggest a parameterized query.
  5. Push another commit. The workflow runs again and posts a new comment, so earlier reviews stay in the conversation.

If something fails, open the failed step in the Actions tab and check the table in the troubleshooting section below.

Cost, limits and your code

Cost. You pay your AI provider for the text you send and the text it returns. As a rough rule, one token is about four characters of English or code, so the 60 KB cap means up to about 15,000 input tokens per run, plus the reply. Multiply that by your provider's current price and your number of pull requests per month. Most providers let you set a monthly spending limit in their console; set one before you turn this on.

Large pull requests. Only the first 60 KB of the diff is sent, and the comment says so. To review more of a big change, raise the number in head -c 60000 (and in the -gt 60000 check) or review files in separate runs. To skip folders you do not want reviewed, add path filters to the trigger:

on:
  pull_request:
    branches: [main, develop]
    paths: ['app/**', 'src/**', 'tests/**']

Failures. If the API is down or your key is wrong, the step fails and the log shows the error. If you would rather not have a red check on the pull request, add continue-on-error: true to the AI step.

Your code goes to a third party. The diff is sent to your AI provider's servers. Read their data-retention and training terms before using this on private or client code. A secret that was committed by mistake would be sent too, so keep keys out of your repository and consider excluding files such as .env* from the diff.

Treat the review as advice. Text inside a diff, such as a code comment, can try to steer the model. Here the model can only write a comment and has no tools, so the worst case is a misleading review. Do not merge automatically based on what it says.

Using a different AI provider

The workflow is the same for any provider. Only four things change: the URL, the authentication header, the shape of the JSON body, and where the answer sits in the response. Here is the comparison for Anthropic (used above) and OpenAI's Chat Completions API:

Anthropic Messages API (used above)OpenAI Chat Completions API
URLhttps://api.anthropic.com/v1/messageshttps://api.openai.com/v1/chat/completions
Auth headerx-api-key: $ANTHROPIC_API_KEYAuthorization: Bearer $OPENAI_API_KEY
Extra headeranthropic-version (copy the current value from the docs)none
Bodymodel, max_tokens, system, messagesmodel, messages (put the system prompt as the first message with role system)
Answer in responsethe blocks in content with type textchoices[0].message.content

For OpenAI, the request step would look like this (replace YOUR_MODEL_ID with a model from OpenAI's models page):

jq -n --rawfile diff diff.txt '{
  model: "YOUR_MODEL_ID",
  messages: [
    {role: "system", content: "You are a senior code reviewer. Review the git diff you are given. List bugs, security problems and unclear code, most serious first. Be concise."},
    {role: "user", content: $diff}
  ]
}' > request.json
curl -sS --fail-with-body https://api.openai.com/v1/chat/completions \
  -H "content-type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d @request.json > response.json
jq -r '.choices[0].message.content' response.json > review.md

Remember to store the key as OPENAI_API_KEY and pass it in the step's env. OpenAI now recommends its newer Responses API for new projects, and its models page says the listed models are available through the Responses API. Check that your chosen model supports Chat Completions, or follow OpenAI's Responses documentation instead.

Troubleshooting

Open the failed step in the Actions tab first. The log shows which command stopped and the error message.

SymptomLikely causeFix
The AI step fails with a 401 or authentication errorWrong key, or the secret name does not matchCheck the secret is called ANTHROPIC_API_KEY exactly, with no extra spaces, and that the key is active
The AI step fails on a pull request from a forkForks do not receive secretsThe if on the job skips forks. Re-run the review from a branch in the repository
The comment step says "Resource not accessible by integration"The token cannot write to pull requestsKeep pull-requests: write, check Settings, Actions, General, Workflow permissions, and try adding issues: write
The diff step fails with "unknown revision" or "bad object"A shallow checkout is missing the base commitMake sure fetch-depth: 0 is set on the checkout step
The diff step reports 0 bytes and the later steps are skippedOnly lockfiles changed, or nothing changedThis is expected. The review is skipped on purpose
The AI step fails with a 400 error about the requestThe diff is too large or the model name is wrongLower the head -c limit, and check the model name in the provider's docs
The AI step fails with test -s review.mdThe response had no textOpen response.json in the log output, or add cat response.json to see what came back
The workflow never startsWrong folder, or the branch filter does not matchThe file must be in .github/workflows/, and the pull request must target a branch listed under branches
The run is green but there is no commentThe diff was empty, so the AI and comment steps were skippedCheck the diff step's output line for sent: 0 bytes

FAQ

Does this work on pull requests from forks? Not as written. GitHub does not pass secrets to workflows triggered by fork pull requests, and gives the token read-only access, so the workflow skips them. A maintainer can re-run the review from a branch inside the repository.

How much does it cost? It depends on your provider's prices and how many pull requests you open. A run sends up to 60 KB of diff, which is roughly 15,000 tokens, plus the reply. Check your provider's pricing page and set a monthly spending limit.

Can I review only certain folders? Yes. Add a paths filter under pull_request in the on: section, as shown in the cost section above.

Can I use OpenAI, Gemini or another provider? Yes. Change the URL, the authentication header, the request body and the place you read the answer from. The OpenAI comparison above shows the pattern.

Is it safe to send my code to an AI API? The diff leaves your repository and goes to the provider's servers. Read their data-retention and training terms first, avoid committing secrets, and consider whether client or private code is allowed to be shared this way.

Will it block merging? No. It only posts a comment. If you want a failing check to block merges, you would need to add that logic and mark the check as required, which is a larger change than this guide covers.

Join the conversation

What do you think?

Write a comment

Comments are reviewed before they appear.

No comments yet. Be the first to share a thought.

More stories

All stories