You tell your coding agent to add a feature. It builds the thing, the demo runs on your laptop, and it announces "all tests pass" with total confidence. Then a client's page goes blank, and you find out the "passing" run never actually ran the tests.
The fix is continuous integration, or CI. You add one small file to your repo, and from then on GitHub installs your project and runs your lint and tests on every push and every pull request, on a clean machine you never touch. A green checkmark next to the code starts meaning something, because a machine produced it, not an eager model. Protect your main branch on top of that, and nothing can merge unless the check is green. On a small Node or Python repo this is one sitting, and GitHub's standard hosted runners are free for public repositories (billing docs).
Why "the tests pass" isn't proof
Your agent runs commands inside your project folder, next to everything already installed. That environment hides problems. For example: a dependency that only breaks on a fresh install, a stale cache making a test lie, a script that exits 0 without running anything. On a long session an agent can also run one test file instead of the whole suite, or report a result from an earlier run.
And you can't watch every run by hand.
CI closes the gap. The same commands run on a fresh, empty machine every push, and the result lands on the pull request where you can read it. The point isn't that CI is smarter than the agent. It's that it can't get bored, can't skip steps, and has no ego invested in the feature working.
A minimal workflow you can paste in
Save this as .github/workflows/ci.yml in your repo and commit it to main:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
- run: npm test
Line by line: it triggers on every push to main and every pull request, spins up a fresh Ubuntu machine, checks out your code, and installs Node. Then it runs three commands. npm ci does a clean install from your lockfile, which is the step that catches "works on my machine" dependencies. The lint and test lines call the scripts your package.json already defines; if your script names differ, edit the last two lines.
Python repo? Same shape. Swap in actions/setup-python@v5, install with pip install -r requirements.txt, then run pytest, plus ruff if you use it. The full syntax lives in GitHub's Actions docs.
Make main untouchable
The checkmark only helps if it can refuse entry. In your repo's Settings, under Branches, add a branch protection rule for main. Turn on "Require a pull request before merging" and "Require status checks to pass before merging," then select the check your workflow produces; it's named after the job, so test in the file above. A red CI run now blocks the merge button.
Two details worth knowing. Branch protection works on public repositories with GitHub Free and on private ones with paid plans, per GitHub's protected branches docs. And admins can bypass the rule by default, so look for "Do not allow bypassing the above settings" if you want it airtight even for yourself. If your agent pushes with your own account, it counts as an admin too, so with that box ticked a direct push to main gets rejected instead of wrecking your live site.
Put the agent on a branch
From here on, the deal with your agent is simple: no commits to main. Ask it to work on a branch and open a pull request when it's done. The PR page becomes your review queue, with the diff, the CI result, and the failure log in one place.
If you're picking a harness for this way of working, you can try the zcode agent harness free and see how it handles branch-and-PR flow. The rule stays the same either way: the agent proposes, CI checks, you decide.
Before approving anything, give the diff ten human minutes. Our ten-minute AI diff review checklist walks you through it and pairs well with the red-or-green verdict CI hands you.
When CI fails, feed the log back
A red check isn't bad news, it's the system working. Open the failed run in the Actions tab, click the failed job, and scroll to the first red step. Copy the last twenty or thirty lines of output, paste them into your agent chat with one line: "CI failed at npm run lint, here's the output." The agent fixes the real error, pushes again, and the check flips green.
Keep the paste scoped to the failing step. Dumping the whole log invites the agent to fix imaginary problems; the last lines before a failure usually name the actual one. The quiet win: you're handing the agent exact output from the same kind of clean machine that will judge the merge.
One money note: fix-it loops burn agent-plan credits, so if you're choosing a plan anyway, a referral discount on GLM coding plans takes some of the sting out.
The ways CI lies to you
Green doesn't mean correct. It means the checks you wrote passed, so write checks that can't be gamed:
- Skipped tests. A teammate (or an agent) leaving
it.skiporpytest.mark.skipbehind turns failures into green. Grep the diff for "skip" before merging. - No tests at all. A workflow with zero tests is a green decoration. Make sure the log shows a real count, not just "exit 0."
- Tests that need the internet. Anything calling a live API flakes randomly. Mock it in CI.
- An agent that edits the referee. If a PR touches
ci.ymlor deletes a failing test, that's the agent loosening the rules to pass. Read those lines first.
Screenshot this checklist and keep it by your merge button:
Before you merge a PR:
[ ] CI ran lint AND tests, not just install
[ ] main is protected with required status checks
[ ] direct pushes to main are blocked
[ ] the log shows a real test count, not just exit 0
[ ] no skipped tests snuck in (search the diff for "skip")
[ ] ci.yml untouched by the agent
Keep secrets out of the workflow
Never paste an API key, token, or password into ci.yml. Every file in a public repo is public, and secrets leak through forks, logs, and copy-paste. GitHub gives you repository secrets instead: Settings, Secrets and variables, Actions. Add the key there once, then reference it as ${{ secrets.MY_KEY }} in the workflow. The same rule covers .env files: they belong in .gitignore, never in a commit.
If a test needs a real third-party API, mock it. The clean machine should never need your production credentials to prove the code is sane.
That's the whole loop: a twenty-line workflow, two checkboxes on main, and a standing rule that agents work on branches. There's more on our blog on working with coding agents. Go protect that main branch.
If this saved you a broken deploy, a small WURK tip on the post is a nice way to say so.






Latest comments
0