Automated code review is one of the few AI workflows that pays for itself on the first pull request, and you can run it entirely on a free model allowance if you design the pipeline around the model's limits rather than against them. I built a GitHub Actions workflow that calls the open source MonkeyCode project's free model access to comment on pull requests, and the result is a reviewer that catches real issues without spamming the conversation. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The open source MonkeyCode project offers a free model allowance of ten million tokens and a free server option at the time of writing, and while the server is not required for this pipeline, it becomes useful if you later want to move the same logic to a hosted webhook. The setup is deliberately simple because the value is in the workflow, not in the model. The workflow triggers on pull_request events, checks out the repository with full history, and then passes a unified diff to a Python script that asks the model for structured feedback.
The Workflow
The first step is a workflow file that captures the diff between the base branch and the head branch. Using fetch-depth: 0 ensures that git can compute the exact changes, and the diff is saved to a temporary file that the review script can read. The environment variables carry the API key and the GitHub token, so no secrets appear in the repository.
name: AI PR Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get diff
run: git diff origin/${{ github.event.pull_request.base.ref }}...HEAD > /tmp/diff.txt
- name: Run AI review
env:
MONKEYCODE_API_KEY: ${{ secrets.MONKEYCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python review.py /tmp/diff.txt
The pull_request event fires on both opened and synchronize, which means every new push to the branch will trigger a fresh review. That is usually desirable, but it also creates a duplicate-comment problem that the script must handle. The diff command compares the merge base with the head, so it only includes changes that are actually part of the pull request.
The Script
The Python script is where the design decisions matter. The diff is truncated to a reasonable size before being sent to the model, because a pull request can easily exceed the input limits of a free-tier endpoint, and the prompt asks for a JSON array of comments with a path, line, and body. The model is instructed to return an empty array when there are no issues, which makes the parsing logic trivial.
import json, os, sys, urllib.request
def review_diff(diff_path):
diff = open(diff_path).read()[:12000]
prompt = (
"You are a senior code reviewer. "
"Review this diff and return a JSON array of comments. "
"Each comment must have 'path', 'line', and 'body'. "
"Only comment on real issues. If no issues, return [].\n\n"
f"Diff:\n{diff}"
)
payload = json.dumps({
"model": "free-model",
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.2
}).encode()
req = urllib.request.Request(
"https://api.monkeycode.example/v1/chat/completions",
data=payload,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {os.environ['MONKEYCODE_API_KEY']}"
}
)
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read())
content = data["choices"][0]["message"]["content"]
try:
return json.loads(content)
except json.JSONDecodeError:
return []
The script then posts each comment through the GitHub API, but only if the comment does not already exist. This deduplication step is essential because a workflow that triggers on both opened and synchronize events will otherwise repeat the same feedback on every push. The script also limits the total number of comments to five, which keeps the noise low and forces the model to prioritize the most important findings.
def post_comments(comments):
existing = fetch_existing_comments()
posted = 0
for c in comments:
if posted >= 5:
break
key = (c["path"], c["line"], c["body"])
if key in existing:
continue
create_comment(c["path"], c["line"], c["body"])
posted += 1
Limitations
The workflow has real limitations that you should know before copying it. The free model will occasionally produce false positives, so the comments are suggestions rather than blockers, and the JSON output can be malformed, in which case the script simply skips that review cycle. The diff truncation means very large pull requests are only partially reviewed, and the model has no awareness of the surrounding codebase beyond the diff itself, so it cannot catch cross-file issues. This is not a replacement for a human reviewer; it is a triage layer that surfaces obvious problems before a human looks at the code.
The Takeaway
What surprised me most was how little code was needed to make this useful. The entire workflow is under a hundred lines, and the hardest part was not the API call but the deduplication and the output parsing, which are the same problems you would face with any external review tool. The free model allowance from MonkeyCode is generous enough for a small team's pull request volume, and the free server option becomes relevant if you want to move from GitHub Actions to a self-hosted webhook that can also handle other AI tasks.
If you want to try this pattern, check the MonkeyCode repository for the current free model access and free server details, because quotas and endpoints change over time. The workflow itself is the part worth keeping, and it transfers to any model provider you might use later. A free-tier AI reviewer is not a gimmick; it is a practical way to spend a small portion of your allowance on something that saves real attention every day.
Top comments (0)