DEV Community

Finley Li
Finley Li

Posted on

Zero-Infra CI Gate: Auto-Verify AI-Generated C++ Patches

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:

  1. Trigger — fire on opened and synchronize pull-request events that touch source or test paths.
  2. Generate — send the diff to the MonkeyCode server with a narrow prompt that asks for a minimal unified-diff fix, not an explanation.
  3. Apply — apply the returned patch to a clean worktree, never the runner’s primary checkout.
  4. 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/ or lib/ instead of src/, 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 --check can 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/**'
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Open a small, intentionally broken pull request that touches src/ or tests/.
  2. Confirm the isolated worktree rejects a bad patch.
  3. 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)