AI code review in GitHub Actions: a workflow with curl and gh CLI

One YAML file, and every pull request gets a comment with the model's findings. The workflow step by step, token permissions, fork limitations, and the cost of a single review.

5 min · · автоматизация · development

Human reviewers get tired, miss the obvious in long diffs, and aren't always around on a Friday evening. AI code review in GitHub Actions doesn't replace them, but it handles the first layer: forgotten None checks, secrets leaking into logs, typos in conditions. Below is a ready workflow built on curl and gh that comments on every pull request, plus a calculation of what one review costs.

How the workflow works

Three steps on a GitHub runner:

  1. Get the pull request diff with gh pr diff.
  2. Send the diff to a model via the OpenAI-compatible endpoint https://api.zukko.pro/v1/chat/completions, using a key from repository secrets.
  3. Post the answer as a comment with gh pr comment.

GitHub CLI is preinstalled on all GitHub-hosted runners; according to GitHub's docs, every step that uses gh needs a GH_TOKEN environment variable. curl and jq are also available on ubuntu-latest, so there's nothing to install.

Step 0. The key and the secret

  1. Create a dedicated key on the API keys page of the dashboard. Give it a daily dollar limit and allow only the model the workflow will call — that way a YAML mistake won't turn into a surprise bill. More in the key limits section.
  2. In the repository, open Settings → Secrets and variables → Actions and add a secret ZUKKO_API_KEY with the fetch_... value.

AI code review in GitHub Actions: the workflow

Save this as .github/workflows/ai-review.yml:

bash
name: AI code review

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  pull-requests: write

jobs:
  review:
    # secrets aren't passed to runs from forks — skip those PRs
    if: github.event.pull_request.head.repo.full_name == github.repository

What matters here

  • Trigger. According to GitHub's docs, the pull_request event runs on opened, synchronize and reopened by default; they're listed explicitly here to make it clear the review also runs on every new push to the branch.
  • Permissions. pull-requests: write is needed to post a comment; contents: read is minimal code access. With an explicit permissions block, every other token permission is disabled.
  • No checkout. gh pr diff fetches the diff through the GitHub API, so there's no need to clone the repository; --repo tells it which repository to look in.
  • Trimming the diff. head -c 150000 caps the request size and cost. Models read huge PRs worse anyway — better to split them. Recent gh versions add an --exclude flag to gh pr diff for leaving out files by pattern, for example --exclude '*.lock'.
  • JSON via jq. --rawfile reads the diff as a string and escapes quotes and newlines — building JSON by gluing strings together in bash is fragile.
  • Errors. curl --fail-with-body fails the step on HTTP 400 and above, and the response body shows up in the log: 401 — invalid key, 402 — insufficient balance, 403 model_not_allowed_for_key — the model isn't allowed for this key.

Pull requests from forks

According to GitHub's docs, runs triggered from forks don't receive secrets (other than GITHUB_TOKEN), and GITHUB_TOKEN itself is read-only. That's why the if: condition in the example skips such PRs. The pull_request_target event does expose secrets, but running it against code or data from someone else's fork is risky — for public repositories it's safer to trigger the review manually after looking at the PR.

A Python variant

If your prompt is complex or you need to split a large diff into parts, the "Ask the model" step is easier to write in Python:

bash
import os
from pathlib import Path

from openai import OpenAI

client = OpenAI(
    base_url="https://api.zukko.pro/v1",
    api_key=os.environ["ZUKKO_API_KEY"],
)

diff = Path("pr_trimmed.diff").read_text(encoding="utf-8", errors="replace")
response = client.chat.completions.create(
    model=os.environ.get("MODEL", "claude-sonnet-5"),
    max_tokens=2000,
    messages=[
        {"role": "system", "content": "You are an experienced code reviewer. Look for bugs "
                                      "and security issues, don't comment on style."},
        {"role": "user", "content": f"Pull request diff:\n\n{diff}"},
    ],
)
Path("review.md").write_text(response.choices[0].message.content, encoding="utf-8")
print("usage:", response.usage)

In the workflow, replace the step with pip install openai and python review.py. Request parameters are covered in the chat/completions reference.

What one review costs

The formula:

cost = input_tokens × input_price / 1,000,000 + output_tokens × output_price / 1,000,000

Input tokens are the diff plus the system prompt; the workflow prints the exact number in its log via jq '.usage'. Check it on a few real PRs. For the example, take 20,000 input and 1,500 output tokens (prices as of October 6, 2026, $ per 1M tokens):

ModelInputOutputTotal per review
deepseek-v4.1-flash20,000 × 0.013 / 1M = $0.000261,500 × 0.038 / 1M = $0.000057$0.000317
claude-sonnet-520,000 × 0.2 / 1M = $0.0041,500 × 1.013 / 1M = $0.0015195$0.0055195
gpt-6.1-sol20,000 × 0.27 / 1M = $0.00541,500 × 1.32 / 1M = $0.00198$0.00738
claude-opus-5-520,000 × 0.4 / 1M = $0.0081,500 × 2 / 1M = $0.003$0.011

Keep in mind that synchronize runs a review on every push. At 300 runs a month on claude-sonnet-5, that's 300 × $0.0055195 ≈ $1.66. If the model reasons, vendors usually count reasoning tokens as output, which increases the second part of the sum — check the actual completion_tokens in the log. More in the article on the cost of reasoning.

Which model finds more real problems in your code is something to test yourself: run 10–15 closed PRs with known bugs through 2–3 models and compare. For the current list and prices, see GET https://api.zukko.pro/v1/models or the Models page in your dashboard. A similar setup in Anthropic format is covered in the article on automated code review via the API.

FAQ

Why doesn't the workflow run for PRs from forks?

By design: GitHub doesn't pass secrets to runs from forks, so ZUKKO_API_KEY won't be there. The if: condition skips such PRs so the step doesn't fail with a 401.

How do I avoid a new comment on every push?

gh pr comment has --edit-last and --create-if-none flags: the first edits the current user's last comment, the second creates a comment if there's nothing to edit. Check that the gh version on the runner supports them.

Can I leave comments on specific lines?

Yes, through the pull request reviews REST API (for example with gh api), but then you need to map diff lines to file positions. Start with a general comment — it's easier to maintain.

Does the code leave GitHub?

Yes: the diff is sent to the model through the gateway. If your company policy restricts that, get approval first or exclude sensitive files from the diff.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

AI code review in GitHub Actions: step by step · Zukko