A zero-infrastructure CI gate verifies AI-generated C++ patches by sending each pull-request diff to a free AI server, applying the returned unified diff on an isolated worktree, and running your existing C++ tests—with no extra servers. I use this pattern to reject patches that fail to compile or break tests before they consume a maintainer’s review slot.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. The free tier and free server options are operator-supplied claims. Token quotas, model lists, and server limits change over time, so verify the current terms before wiring this into your CI.
Why a CI Gate Beats Manual Review of AI C++ Patches
AI-generated patches land in my repositories faster than I can review them. A single maintainer might review about ten diffs per hour. A CI runner can compile and test far more in the same window. That gap is the bottleneck, and it grows every time a contributor pastes model output into a pull request.
Automation does not replace human review. It filters the obvious failures first. I compare the two approaches like this:
- Manual-only review — a person reads every diff, including ones that do not compile. Throughput stays near ten diffs per hour.
-
CI gate, then review — the runner applies the AI patch, builds, and runs
ctest. Only patches that apply, compile, and pass existing tests reach a person.
A patch that does not compile, or that breaks an existing test, should never wait in a review queue. Only the survivors reach a person.
The GitHub Actions documentation makes this pattern straightforward: I define a workflow once, and it runs on every relevant pull request. No new VMs, no extra infrastructure—just a YAML file in the repository. Compared with standing up a dedicated review bot or a private runner fleet, this approach reuses the GitHub-hosted minutes I already have and the CMake/ctest suite I already maintain.
The trade-off is intentionally narrow. The gate answers one question: does this AI patch apply cleanly, compile, and pass the tests I already trust? It does not answer whether the fix is semantically correct under load or against untested edge cases.
How the Four-Stage Pipeline Works
I keep the workflow to four stages so each failure is obvious in the Actions log:
-
Trigger — fire on
openedandsynchronizepull-request events that touch source or test paths. - Generate — send the diff to the MonkeyCode server with a narrow prompt that asks for a minimal unified-diff fix, not an explanation.
- Apply — apply the returned patch to a clean worktree, never the runner’s primary checkout.
- Verify — run the project’s existing test suite and post the result back to the pull request.
Isolation is the design decision I will not drop. The AI never touches the real working tree. Every candidate patch lands in a temporary directory, so a malformed diff cannot corrupt the checkout the workflow still needs for reporting.
Two cheap guards sit around that isolation:
-
Path filter — a documentation-only change skips the AI round-trip, which saves both tokens and runner minutes. If your tree uses
include/orlib/instead ofsrc/, change the globs rather than removing the filter. -
Unified diff, not a full rewrite — the payload is smaller than a whole-file replacement, and
git apply --checkcan reject a bad patch before CMake starts.
If the generate step returns prose instead of a patch, I want the job to fail at JSON extraction, not at a mysterious empty build.
Step-by-Step GitHub Actions Implementation
The workflow is about sixty lines of YAML. I copy it into .github/workflows/ai-patch-verification.yml and then swap paths and build commands. The sequence below is the same one I run.
Trigger only on C++ source and tests
name: ai-patch-verification
on:
pull_request:
types: [opened, synchronize]
paths:
- 'src/**'
- 'tests/**'
types covers a new PR and later pushes to the same branch, so an updated AI patch gets re-checked. paths is the cheapest skip: a README change does not need this job.
Request a minimal fix from the free server
Checkout with full history so git diff origin/main...HEAD is accurate. Write the PR diff to a temp file, then POST it with a prompt that forbids prose.
jobs:
verify-ai-patch:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get diff
id: diff
run: |
git fetch origin main
git diff origin/main...HEAD > /tmp/changes.patch
echo "patch_size=$(wc -c < /tmp/changes.patch)" >> "$GITHUB_OUTPUT"
- name: Request fix from MonkeyCode server
id: ai
env:
MC_API_KEY: ${{ secrets.MONKEYCODE_API_KEY }}
run: |
jq -n --arg diff "$(cat /tmp/changes.patch)" \
'{prompt: "The attached diff contains a bug. Return only a unified diff that fixes it. Do not explain.", diff: $diff}' \
| curl -s -X POST \
-H "Authorization: Bearer $MC_API_KEY" \
-H "Content-Type: application/json" \
-d @- > /tmp/ai-response.json
jq -r '.patch' /tmp/ai-response.json > /tmp/fix.patch
echo "fix_size=$(wc -c < /tmp/fix.patch)" >> "$GITHUB_OUTPUT"
Store MONKEYCODE_API_KEY as a repository secret. The jq extract fails the job early if the JSON has no patch field—better than applying an empty file and wondering why CMake did nothing. If your default branch is not main, change both the fetch and the diff bases.
Apply the patch in an isolated worktree
The git worktree documentation is why I prefer a second checkout over git apply in the primary tree: the reporting job still has a clean repo if the patch is garbage.
- name: Apply patch to clean worktree
run: |
git worktree add /tmp/clean-checkout origin/main
cd /tmp/clean-checkout
git apply --check /tmp/fix.patch
git apply /tmp/fix.patch
- name: Run test suite
working-directory: /tmp/clean-checkout
run: |
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j2
ctest --test-dir build --output-on-failure
git apply --check is the guard. A patch that fails this check is rejected without touching the build. I use -j2 on GitHub-hosted runners to stay inside typical two-core machines; bump it if you use larger runners.
If the project is not CMake, replace those three commands with your real build—ninja, make test, or a wrapper script. The isolation pattern stays the same.
Report the verdict on the pull request
The GitHub CLI documentation covers gh pr comment. I post on every outcome so the PR timeline shows a pass or a fail without opening the log first.
- name: Post result to PR
if: always()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${{ job.status }}" = "success" ]; then
gh pr comment "${{ github.event.pull_request.number }}" \
--body "AI patch verification passed. The fix compiles and all tests pass."
else
gh pr comment "${{ github.event.pull_request.number }}" \
--body "AI patch verification failed. See the workflow log for details."
fi
if: always() posts even when tests fail. A green check is not a correctness proof; it is a signal that the patch is worth a human’s time.
Limitations and When I Would Not Use This
This pipeline verifies that a patch compiles and passes existing tests. It does not prove the fix is correct. A data race can pass a single ctest run and still crash under load. Teams that need stronger guarantees should add sanitizers and stress tests to the same workflow, not treat the AI response as a review.
The free tier is not a replacement for a dedicated CI budget. The token allowance covers a limited number of patches per period. Consider a project that receives twenty AI-assisted pull requests per week. Each request sends a diff of roughly five kilobytes and receives a patch of similar size. The token cost per request is small, but the cumulative count adds up. I log token usage from the API response and compare it against the allowance at the end of each month.
Teams with proprietary code should check whether the free server keeps their data inside their network. The self-hosted option exists for that reason, but it shifts the operational burden onto the team.
I skip this gate when any of the following is true:
- The team cannot send diffs to a third-party server.
- We need a specific model version pinned for reproducibility.
- Patch volume exceeds the free allowance and a mid-sprint pause is unacceptable.
What to Do Next
Copy the workflow into .github/workflows/ai-patch-verification.yml, adjust the paths globs and the CMake/ctest commands, and add MONKEYCODE_API_KEY as a repository secret. Then run this smoke check:
- Open a small, intentionally broken pull request that touches
src/ortests/. - Confirm the isolated worktree rejects a bad patch.
- Push a clean patch to the same branch and confirm the job passes and comments on the PR.
If you hit a snag, or you want a follow-up on adding sanitizers and stress tests in the same job, leave a comment below. I read them all.
MonkeyCode provides free models that can run this workflow.
Top comments (0)