Building an agentic SDLC pipeline
The interesting question about AI in the SDLC is not which model writes the code. It is what happens to that code between the moment it is written and the moment it is live. An agent that opens a pull request is a demo. An agent wired into a pipeline that can reject its own output is infrastructure. This post is the second thing: the stages we run, the config behind each one, and the rule that holds it together.
The rule
Every stage an agent touches is followed by a check the agent cannot argue with.
That is the whole design. Agents are good at producing plausible output and bad at knowing when they have produced plausible wrong output. So each stage hands the next one an artifact a machine can verify — a diff, an exit code, an HTTP status — and never a judgement call. Where we could not make a check deterministic, we kept a human in the loop instead of pretending an agent’s confidence was a signal.
Five stages: spec, implement, review, gate, verify.
Stage 1 — the spec lives in the repo
Not in a wiki, not in a prompt someone pastes each time. A CLAUDE.md at the repo root, committed, reviewed like code, and read by the agent on every run.
The value is in rules that are specific enough to be violated. From the file that governs this site:
**Critical consequence:** `page` and `post` already print the title as an `<h1>`.
So the body of a page or post must **start at `##`**, never `#`.
That rule exists because a leading # produces two H1s on a page and nobody notices until the SEO report does. A line like “write clean, maintainable content” would have prevented nothing.
Two practical constraints. The file is read on every run, so its length is a recurring cost — keep it tight, and delete rules that no longer apply. And it is a spec, not a fence: it tells the agent what good looks like, but stage 4 is what actually stops bad output.
Stage 2 — implementation runs on a trigger
We do not have agents opening speculative pull requests. Work starts when a human asks for it, in the issue or PR where the context already is. In anthropics/claude-code-action terms, that is interactive mode — no prompt input, so the action waits for a mention:
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
claude:
if: contains(github.event.comment.body, '@claude')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
The line that earns its place is actions: read. It lets the agent read CI results on the pull request, which means it can read the failure it caused rather than guessing at it from the diff. The if: condition is not redundant with the action’s own trigger check — it stops a runner from starting at all on the hundreds of comments that are not for the agent.
Worth knowing before you rely on this: the action rejects a triggering user without write access, and rejects bot actors unless you list them in allowed_bots. Both defaults are correct. Leave them.
Stage 3 — review is a different agent
The agent that wrote the code is the worst candidate to review it; it has already convinced itself. So review is a separate workflow, a separate run, and a separate context, with prompt set — which puts the action in automation mode, no mention required:
name: Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
jobs:
review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
plugin_marketplaces: "https://github.com/anthropics/claude-code.git"
plugins: "code-review@claude-code-plugins"
prompt: "/code-review:code-review --comment ${{ github.repository }}/pull/${{ github.event.pull_request.number }}"
claude_args: '--allowedTools "mcp__github_inline_comment__create_inline_comment"'
Note the permissions: everything is read except the OIDC token. The reviewer can comment and nothing else. It cannot push the fix it suggests, which is the point — a reviewer that can silently repair its own complaints stops being a reviewer.
One gotcha if your repository is public: GitHub withholds secrets from workflow runs triggered by fork pull requests, so this job runs only on branches in the same repository. Outside contributions still need a human first pass.
Stage 4 — the gate is a script, not an opinion
This is the stage teams skip, and it is the only one that actually protects the main branch. Review produces advice. A gate produces an exit code.
For this site the gate is tools/check_site.py: dependency-free, python3 stdlib only, runs in about a second, and checks the things that have actually broken us. Filename date against front-matter date. Future dates, which silently hide a post rather than erroring. A # at the top of a body. Unclosed or untagged code fences. Root-relative links pointing at pages that do not exist. Committed secrets.
python3 tools/check_site.py # exit 0 = safe to push
It exists because we cannot build locally — rubygems.org is blocked in both of our sandboxes, so there is no bundle exec jekyll build to fall back on. The validator and the Actions build are the entire safety net, which is exactly why running it is not optional.
The general principle transfers: a useful gate encodes the failures you have already had, one rule at a time, and it is cheap enough that nobody is tempted to skip it. A gate that takes four minutes gets bypassed at 6pm on a Friday.
Stage 5 — verify the deploy
A push you did not verify is not done. The last stage polls the workflow run, then checks that the page actually renders:
API=https://api.github.com/repos/x2code-co/x2code-co.github.io
curl -s -H "Authorization: Bearer $TOKEN" "$API/actions/runs?branch=main&per_page=1" \
| jq -r '.workflow_runs[0] | "\(.status)\t\(.conclusion)\t\(.html_url)"'
curl -s -o /dev/null -w "%{http_code}\n" https://x2code.dev/blog/
Green run, HTTP 200, done. Red run: revert the commit and push the revert first, diagnose second. The live site does not sit broken while anyone — human or agent — works out what went wrong. Recovery is a one-line reflex; debugging is not.
What we cap
Agents fail differently from scripts. A script fails fast; an agent burns twenty minutes and a lot of tokens converging on nothing. So every agent job has bounds:
--max-turnsinclaude_args, so a run cannot loop indefinitely- a job-level
timeout-minutes, because turns and wall-clock are not the same limit - a
concurrencygroup, so two rapid pushes queue instead of racing - least-privilege
permissionsper workflow — the review job above is the model - credentials only ever as secrets referenced from the workflow, never in the file
None of this is exotic CI practice. It is the ordinary kind, applied to a step that happens to be non-deterministic.
Takeaway
Adding an agent to your SDLC is a plumbing problem, not a model problem. Put the spec in the repo where it is versioned. Trigger implementation from a human ask. Make review a separate run with read-only permissions. Then build the deterministic gate — that is the piece that turns a helpful assistant into something you can leave running. Start with the gate, even if it checks three things. Everything upstream is only as trustworthy as the check downstream of it.
The pipeline in this post is the one behind this blog, validator and all.
Reference: the action’s repository and its documentation.
“Building the future, one commit at a time.”