Skip to main content

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

What the agent can and can't do

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:

PieceJob
GitHub ActionsBuilds ghcr.io/<owner>/hotrod:<head sha> for every commit on a pull request labeled preview.
ApplicationSetCreates one Application, hotrod-pr-<number>, for every open pull request with the preview label, and deletes it when the pull request closes.
Preview chartA ConfigMap with the sandbox specification and three hook Jobs: Sync creates or updates the sandbox, PostSync runs the tests, PostDelete deletes the sandbox.
SignadotThe 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 NotificationsSets the argocd/preview commit status: pending when a sync starts, then success or failure.
Argo CD MCP serverLets 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, gh and kubectl CLIs, and Node.js for npx.

  • 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:

FilePurpose
.github/workflows/preview-image.yamlBuilds and pushes the image for each commit on a preview pull request.
AGENTS.md and CLAUDE.mdTell the agent how previews work and which MCP tools to call when one fails. CLAUDE.md imports AGENTS.md.
.mcp.jsonStarts the Argo CD MCP server with MCP_READ_ONLY=true.
.claude/settings.jsonDenies 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:

hotrod/.github/workflows/preview-image.yaml (excerpt)
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.yaml deploys HotROD from your fork's main branch, with the Signadot DevMesh sidecar, into the hotrod namespace. If HotROD already runs there, Argo CD adopts it.
  • previews.yaml holds the previews AppProject and the ApplicationSet.
  • config.yaml adds the agent's account and the notification settings. The separate field manager keeps the defaults the Argo CD install put in argocd-cm; with the default kubectl manager, 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:

argocd/previews.yaml (excerpt)
  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:

preview/templates/sandbox.yaml (rendered)
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). Container wait-for-image uses crane to wait until CI has pushed the image. Container signadot then runs signadot sandbox apply and waits for the sandbox to be ready.
  • PostSync (hotrod-pr-<number>-e2e). Init container routing-key looks 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. Container e2e fetches the pull request's head commit and runs npm run e2e:playwright with SIGNADOT_ROUTING_KEY set.
  • PostDelete (hotrod-pr-<number>-cleanup). Runs signadot sandbox delete after 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:

argocd/config.yaml (excerpt)
  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:

hotrod/.mcp.json
{
"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 main branch. Keep the preview chart in a repository your platform team controls, and pin targetRevision to a commit or tag.
  • Treat the label as the gate. Anyone who can add preview to a pull request gets that pull request's code and tests run in your cluster.
  • Fork more services. The chart's services value 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>/hotrod is 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 an imagePullSecrets custom patch to each fork, and give the wait-for-image container the same credentials, for example a Docker config secret mounted in the container with DOCKER_CONFIG pointing 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 .... With MANIFEST_UNKNOWN, the image isn't there yet: check that Actions is enabled in your fork and that the Preview image run succeeded. With DENIED after a successful run, the package is private: make it public for HotROD, or give wait-for-image registry credentials for a private image.
  • The pull request gets no argocd/preview check. The pull request needs the preview label, and the GitHub token needs commit status access to your fork. kubectl -n argocd logs deploy/argocd-notifications-controller shows GitHub's answer.
  • get_application_workload_logs returns []. Argo CD only reads logs from Pods it shows under the Job, and it hides kinds the AppProject doesn't allow. Keep Pod in the previews project.
  • 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: run gh repo set-default $OWNER/hotrod in 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.

See also​