Test Coding Agent Pull Requests with Argo CD and Signadot
Coding agents open pull requests faster than anyone can test them by hand. If you already run Argo CD, it can give each of those pull requests a preview of its own, and the Argo CD preview environments article explains why a Signadot sandbox beats a full copy of the app for that.
This tutorial builds the whole loop. A coding agent changes HotROD,
a demo ride-sharing app, and opens a pull request. An Argo CD ApplicationSet turns the pull request
into an Application whose hooks create a sandbox for the changed route service, run the pull
request's Playwright suite against it, and delete the sandbox when the pull request closes. Argo CD
Notifications reports the result as an argocd/preview check. When the check fails, the agent reads
why through the read-only Argo CD MCP server,
pushes a fix and waits for the next result.
Time: about 40 minutes.
Example files: signadot/examples/argocd-tutorial
The agent pushes commits and reads Argo CD. It needs no kubeconfig and no Signadot API key, and
the repository's Claude Code settings block kubectl, argocd and signadot. It can't sync or
change anything in Argo CD, and its Argo CD account sees only preview Applications. The preview is
its only way to test end to end.
How it works
Each piece does one job:
| Piece | Job |
|---|---|
| GitHub Actions | Builds ghcr.io/<owner>/hotrod:<head sha> for every commit on a pull request labeled preview. |
| ApplicationSet | Creates one Application, hotrod-pr-<number>, for every open pull request with the preview label, and deletes it when the pull request closes. |
| Preview chart | A ConfigMap with the sandbox specification and three hook Jobs: Sync creates or updates the sandbox, PostSync runs the tests, PostDelete deletes the sandbox. |
| Signadot | The sandbox runs the pull request's image in place of route for requests that carry its routing key. Everything else comes from the baseline. |
| Argo CD Notifications | Sets the argocd/preview commit status: pending when a sync starts, then success or failure. |
| Argo CD MCP server | Lets the agent read the Application's status, events and hook logs, and nothing else. |
Argo CD doesn't build anything here. It does what it already does for the baseline, reconciling what Git says should exist, plus one post-deploy test. The Argo CD integration guide compares this with letting CI run the tests.
The preview chart lives in the examples repository, not in HotROD, so an agent's pull request can't change how its own preview is created or tested. It controls only its code and its Playwright tests.
Prerequisites
-
A Kubernetes cluster with the Signadot Operator installed. Docker Desktop, kind or minikube works.
-
The Signadot CLI, logged in, with a local connection for the cluster in
~/.signadot/config.yaml:local:
connections:
- cluster: <your-signadot-cluster>
kubeContext: <your-kube-context>
type: PortForward -
A Signadot API key for the preview hooks, from a service account under Admin → Service Accounts in the dashboard. See API Keys.
-
The
argocd,ghandkubectlCLIs, and Node.js fornpx. -
Claude Code, or another coding agent that supports MCP.
This tutorial was tested with Argo CD v3.5.6, argocd-mcp 0.9.0, Signadot CLI v1.9.0, Operator 1.4.0 and Claude Code 2.1 on Docker Desktop (October 2026).
Step 1: Fork HotROD and add the preview files
The agent works in your fork of HotROD, so its pull requests and images are yours. Clone the examples repository next to it and copy the preview files into the fork:
export OWNER=<your-github-user> # lowercase
git clone https://github.com/signadot/examples.git
gh repo fork signadot/hotrod --clone
cd hotrod
gh repo set-default $OWNER/hotrod
cp -R ../examples/argocd-tutorial/hotrod/. .
git rm -q .github/workflows/build.yml
git add -A && git commit -m "Add Argo CD previews" && git push origin main
gh label create preview
gh repo fork --clone makes the upstream repository the default for gh. Without
gh repo set-default, the agent's gh pr create would open its pull request on
signadot/hotrod, where your ApplicationSet never sees it. HotROD's own build.yml needs
Signadot's secrets, so the fork drops it.
The copied files:
| File | Purpose |
|---|---|
.github/workflows/preview-image.yaml | Builds and pushes the image for each commit on a preview pull request. |
AGENTS.md and CLAUDE.md | Tell the agent how previews work and which MCP tools to call when one fails. CLAUDE.md imports AGENTS.md. |
.mcp.json | Starts the Argo CD MCP server with MCP_READ_ONLY=true. |
.claude/settings.json | Denies Claude Code kubectl, argocd and signadot. |
The workflow triggers on labeled, synchronize and reopened. GitHub sends a labeled event
for labels set when a pull request is opened, so each commit is built once:
on:
pull_request:
types: [labeled, synchronize, reopened]
jobs:
build:
if: >-
github.event.action == 'labeled' && github.event.label.name == 'preview' ||
github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'preview')
Open your fork's Actions tab and enable workflows. GitHub disables them on new forks.
Step 2: Install Argo CD and add two secrets
kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts \
-f https://raw.githubusercontent.com/argoproj/argo-cd/v3.5.6/manifests/install.yaml
kubectl create namespace hotrod-previews
kubectl -n hotrod-previews create secret generic signadot \
--from-literal=SIGNADOT_ORG=<your-org> --from-literal=SIGNADOT_API_KEY=<your-api-key>
kubectl -n argocd patch secret argocd-notifications-secret \
-p '{"stringData": {"github-token": "<your-github-token>"}}'
The preview's hook Jobs run in hotrod-previews and read the Signadot key from the signadot
secret. The GitHub token lets the ApplicationSet list your pull requests and Argo CD Notifications
set commit statuses. Create a fine-grained token for your fork with Commit statuses: Read and
write and Pull requests: Read.
Step 3: Deploy the baseline and the ApplicationSet
export CLUSTER=<your-signadot-cluster>
cd ../examples/argocd-tutorial
sed -i.orig -e "s/<owner>/$OWNER/g" -e "s/<cluster>/$CLUSTER/g" argocd/*.yaml
kubectl apply --server-side --field-manager=argocd-tutorial -f argocd/
kubectl -n argocd get applications
NAME SYNC STATUS HEALTH STATUS
hotrod Synced Healthy
This applies three files:
hotrod.yamldeploys HotROD from your fork'smainbranch, with the Signadot DevMesh sidecar, into thehotrodnamespace. If HotROD already runs there, Argo CD adopts it.previews.yamlholds thepreviewsAppProject and the ApplicationSet.config.yamladds the agent's account and the notification settings. The separate field manager keeps the defaults the Argo CD install put inargocd-cm; with the defaultkubectlmanager, this apply would remove them.
The ApplicationSet polls GitHub every minute, because no webhook can reach a local cluster. Each Application renders the preview chart with the pull request's number and head commit:
generators:
- pullRequest:
github:
owner: <owner>
repo: hotrod
labels:
- preview
tokenRef:
secretName: argocd-notifications-secret
key: github-token
requeueAfterSeconds: 60
template:
metadata:
name: hotrod-pr-{{ .number }}
spec:
project: previews
source:
repoURL: https://github.com/signadot/examples.git
targetRevision: main
path: argocd-tutorial/preview
helm:
valuesObject:
pr: "{{ .number }}"
headSha: "{{ .head_sha }}"
image: ghcr.io/<owner>/hotrod:{{ .head_sha }}
# ...
syncPolicy:
automated:
prune: true
retry:
limit: 0
Two settings matter for agent feedback:
retry.limit: 0. Argo CD 3.5 retries a failed automated sync five times by default, which delays a failing result by several minutes. Here a failed preview reports at once, and the agent's next push is the retry.- The sandbox specification is a ConfigMap. Argo CD doesn't count hooks when it compares an Application with Git. The ConfigMap changes with every commit, so each push makes the Application OutOfSync, and the automated sync reruns the hooks.
The sandbox specification forks only route and runs the pull request's image:
name: hotrod-pr-323
spec:
cluster: <your-signadot-cluster>
description: HotROD pull request 323
ttl:
duration: 3d
offsetFrom: updatedAt
forks:
- forkOf:
kind: Deployment
namespace: hotrod
name: route
customizations:
images:
- image: ghcr.io/<owner>/hotrod:<head sha>
defaultRouteGroup:
endpoints:
- name: frontend
target: http://frontend.hotrod.svc:8080
Keep the description the same for every commit: Signadot rejects updates that change it. The TTL is a backstop in case the PostDelete hook never runs.
The hooks:
- Sync (
hotrod-pr-<number>-sandbox). Containerwait-for-imageusescraneto wait until CI has pushed the image. Containersignadotthen runssignadot sandbox applyand waits for the sandbox to be ready. - PostSync (
hotrod-pr-<number>-e2e). Init containerrouting-keylooks up the sandbox's routing key. It is the only container in this Job that holds the Signadot API key, and it fails the Job if no key comes back, so the tests never run against the baseline by mistake. Containere2efetches the pull request's head commit and runsnpm run e2e:playwrightwithSIGNADOT_ROUTING_KEYset. - PostDelete (
hotrod-pr-<number>-cleanup). Runssignadot sandbox deleteafter the ApplicationSet deletes the Application.
The previews AppProject allows only ConfigMaps, Jobs and Pods, and only in hotrod-previews.
Pods are listed so that Argo CD shows the hook Jobs' Pods, which is where it reads their logs.
Step 4: Give the agent read-only access
signadot local connect
argocd login argocd-server.argocd.svc --insecure --username admin \
--password "$(argocd admin initial-password -n argocd | head -1)"
export ARGOCD_API_TOKEN=$(argocd account generate-token --account agent --expires-in 24h)
argocd logout argocd-server.argocd.svc --insecure
signadot local connect makes in-cluster addresses such as argocd-server.argocd.svc resolve on
your machine. The Argo CD UI is at https://argocd-server.argocd.svc. Logging out keeps your admin
session out of the shell the agent runs in.
The agent account comes from config.yaml. It has an API key and no login, and its RBAC allows
reading preview Applications and their logs:
accounts.agent: apiKey
---
policy.csv: |
p, agent, applications, get, previews/*, allow
p, agent, logs, get, previews/*, allow
.mcp.json passes the token to the MCP server and turns on read-only mode, which removes the
create, update, delete, sync and resource-action tools:
{
"mcpServers": {
"argocd": {
"command": "npx",
"args": ["-y", "argocd-mcp@0.9.0", "stdio"],
"env": {
"ARGOCD_BASE_URL": "${ARGOCD_BASE_URL:-https://argocd-server.argocd.svc}",
"ARGOCD_API_TOKEN": "${ARGOCD_API_TOKEN}",
"MCP_READ_ONLY": "true",
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
}
}
}
}
This Argo CD uses a self-signed certificate, so --insecure and NODE_TLS_REJECT_UNAUTHORIZED=0
skip verification. Drop both once Argo CD has a trusted certificate.
Step 5: Hand the agent the task
cd ../../hotrod
claude
Approve the argocd MCP server from .mcp.json, then give it the task from
prompt.md:
In the route service, make the "Resolving routes" notification show the ETA that route computed,
for example "Resolving routes (ETA 12m30s)". Open a pull request for it and don't stop until its
preview passes. Then tell me the PR URL, the preview URL and what you checked.
In our run, Claude Code found the notification in services/route/server.go and the Playwright
test that asserts its text, and changed both:
- Body: "Resolving routes",
+ Body: fmt.Sprintf("Resolving routes (ETA %s)", eta),
- ).toHaveText("Resolving routes")
+ ).toHaveText(/^Resolving routes \(ETA (\d+h)?(\d+m)?\d+s\)$/);
It opened the pull request with gh pr create --label preview and waited with
gh pr checks --watch. Within a minute, the ApplicationSet created hotrod-pr-323 and the check
turned pending. The Sync hook waited for the image:
waiting for CI to push ghcr.io/<owner>/hotrod:<head sha>: Error: GET https://ghcr.io/v2/<owner>/hotrod/manifests/<head sha>: MANIFEST_UNKNOWN: manifest unknown
The image build took under two minutes. Then the Sync hook created the sandbox and the PostSync hook ran the pull request's tests against it:
Testing 6651b6c22905f7543e15ef683033570a444ff956 in sandbox hotrod-pr-323 (routing key ttdg1bpt6cgcj)
Running 2 tests using 1 worker
✓ 1 [chromium] › basic.spec.ts:4:6 › Request a ride › check route resolve routes (2.7s)
✓ 2 [chromium] › basic.spec.ts:17:6 › Request a ride › check driver series (2.5s)
2 passed (6.7s)
About two and a half minutes after the push, argocd/preview passed. The agent then called
get_application_workload_logs on the e2e Job to confirm the run tested its own commit, and put
the preview URL in the pull request description.
The first image build creates the package ghcr.io/<owner>/hotrod. If GitHub makes it private,
the Sync hook keeps logging DENIED after the build succeeds. HotROD is open source, so make the
package public in its Package settings, and the preview continues on its own. Keep images of
your own services private; see Use it with your own services.
Step 6: Watch the agent fix a failing preview
A passing first try doesn't show the loop's real value, so break the preview the way a teammate
might. driver gives each route call a one-second deadline (services/route/client.go). Once
the agent is done, in a second terminal in hotrod, with the agent's branch checked out, push a
commit that makes route slower than that:
perl -pi -e 's/50 \* time\.Millisecond/1500 * time.Millisecond/' pkg/config/route.go
git commit -am "route: simulate a slower routing engine" && git push
The change builds and passes unit tests. In the preview, driver gives up on every route lookup, no
ride gets a driver, and both Playwright tests fail:
Error: Timed out 5000ms waiting for expect(locator).toHaveText(expected)
Locator: locator('//div[p[2][contains(text(), 'driver')]]/p[4]').last()
Expected pattern: /.*T7\d{5}C.*/
Received string: "Finding an available driver"
Tell the agent:
The argocd/preview check on <PR URL> has been failing since a teammate pushed a commit to your
branch. Find out why, fix it, and don't stop until the preview passes.
In our run, the agent checked the pull request with gh pr checks, then called get_application
for hotrod-pr-323 and get_application_workload_logs for its e2e Job. It read the teammate's
commit and the route client code, found the one-second deadline, and pushed a revert:
Revert "route: simulate a realistic routing engine (1.5s per calculation)"
This reverts commit 31cdf11. The driver service calls FindRoute with a
1s deadline (services/route/client.go), so a 1.5s calculation makes every
route lookup fail: dispatch never completes and no route notification is
stored. A slower routing engine needs the client deadline raised first,
in a change that also redeploys driver.
The check passed less than two minutes after its push. The agent also explained why raising the
timeout in the pull request wouldn't help: the timeout runs in driver, and the preview forks only
route.
Step 7: Review and merge
The argocd/preview check links to the preview URL,
https://frontend--hotrod-pr-<number>.preview.signadot.com. Open it while signed in to Signadot
and request a ride. The route notification shows the ETA, because the preview URL adds the
sandbox's routing key to your requests. Dispatching one ride with the routing key and one without shows the difference in the route
notification:
hotrod-pr-323 | Resolving routes (ETA 21m27s)
baseline | Resolving routes
Merge or close the pull request. On its next poll, the ApplicationSet deletes hotrod-pr-<number>,
the PostDelete hook deletes the sandbox, and signadot sandbox list no longer shows it. In our runs
this took about a minute.
Clean up
Close any open preview pull requests first, so their hooks can delete their sandboxes. Then,
from examples/argocd-tutorial:
kubectl delete -f argocd/
kubectl delete namespace hotrod-previews argocd
HotROD keeps running in hotrod.
Use it with your own services
- Keep the chart where the agent can't change it. The agent pushes with your GitHub
credentials, so it can push to your app repository's
mainbranch. Keep the preview chart in a repository your platform team controls, and pintargetRevisionto a commit or tag. - Treat the label as the gate. Anyone who can add
previewto a pull request gets that pull request's code and tests run in your cluster. - Fork more services. The chart's
servicesvalue lists the Deployments the sandbox forks. All of HotROD's services share one image; for your services, give each fork its own image. - Keep your images private.
ghcr.io/<owner>/hotrodis public only because HotROD is open source. Push your own services' images to a private registry the cluster can pull from. If pulling needs credentials, add animagePullSecretscustom patch to each fork, and give thewait-for-imagecontainer the same credentials, for example a Docker config secret mounted in the container withDOCKER_CONFIGpointing at it. - Use webhooks instead of polling. Point a GitHub webhook at the ApplicationSet controller instead of polling every minute. See Webhook Configuration in the Argo CD docs.
- Rotate the agent's token. The token in step 4 expires after 24 hours. Generate a new one when it does.
Troubleshooting
- The Sync hook keeps logging
waiting for CI to push .... WithMANIFEST_UNKNOWN, the image isn't there yet: check that Actions is enabled in your fork and that thePreview imagerun succeeded. WithDENIEDafter a successful run, the package is private: make it public for HotROD, or givewait-for-imageregistry credentials for a private image. - The pull request gets no
argocd/previewcheck. The pull request needs thepreviewlabel, and the GitHub token needs commit status access to your fork.kubectl -n argocd logs deploy/argocd-notifications-controllershows GitHub's answer. get_application_workload_logsreturns[]. Argo CD only reads logs from Pods it shows under the Job, and it hides kinds the AppProject doesn't allow. KeepPodin thepreviewsproject.- The sandbox Job fails with
cannot update description. The sandbox's description changed between commits. Signadot doesn't allow that; keep it constant. - The agent's pull request doesn't get a preview. Check that it opened the pull request on your
fork, not on
signadot/hotrod: rungh repo set-default $OWNER/hotrodin the clone. - The agent's MCP calls fail with 401. The token from step 4 has expired. Generate a new one and restart the agent.
Frequently asked questions
Does the coding agent need cluster or Signadot credentials?
No. The agent pushes commits to Git and reads Argo CD through the read-only Argo CD MCP server, as a local account that can only get Applications in the previews project and their logs. The Signadot API key stays in a Kubernetes secret that only the preview's hook Jobs use, and the containers that run the pull request's own code never see it.
Is Argo CD acting as the CI system?
Only partly. GitHub Actions still builds the image for each commit. Argo CD manages the preview's lifecycle and runs the end-to-end tests as a PostSync hook, next to the sandbox in the cluster, so CI needs no network path to the cluster and the agent reads the result where it already looks. If your tests already live in CI, Argo CD can manage only the sandbox and CI can run the tests; the Argo CD integration guide compares the options.
Why fork one service instead of deploying the whole app for each pull request?
A Signadot sandbox runs only the services a pull request changes, here route, and routes requests that carry its routing key to them. Everything else, including Kafka, MySQL and Redis, comes from the baseline Argo CD already manages. Each preview adds one pod instead of a copy of the app, which keeps previews cheap enough to give to every agent pull request.
Will Argo CD prune the sandbox forks or mark the baseline OutOfSync?
No. Forked workloads carry no Argo CD tracking annotation, and they are owned by Signadot objects rather than being top-level resources, so Argo CD neither tracks, prunes nor reports them as orphaned. In this tutorial the baseline hotrod Application stayed Synced and Healthy while previews ran.
Does this work with coding agents other than Claude Code?
Yes. Any agent that can push to Git and call an MCP server can follow the same loop. AGENTS.md in the repository holds the instructions, and the Argo CD MCP server runs over stdio with npx. The .claude/settings.json deny rules are specific to Claude Code; give other agents an equivalent restriction.
Does this work on Akuity Platform?
The ApplicationSet, chart and notifications are standard Argo CD, but this tutorial was tested on open source Argo CD. On Akuity Platform, TokenRef strict mode is on by default, so point the generator's tokenRef at the platform-managed application-set-secret, and agents can use the Akuity MCP Server, which is read-only by default. See the Argo CD integration guide.