{"slug": "better-agentic-review-with-ephemeral-deploys", "title": "Better Agentic Review with Ephemeral Deploys", "summary": "Miren released ephemeral deployments, also called pull request environments, that let CI agents such as Claude Code review pull requests against a full application deployment instead of a constrained CI environment. The workflow, demonstrated in the sample repo github.com/mirendev/agentic-eph, deploys a per-PR version of an app via the mirendev/actions/deploy GitHub Action with a 24h TTL, shares configuration with the base application, and passes the preview URL to a subsequent agent-review job; Miren recommends running ephemeral deployments against staging rather than production to avoid exposing production data.", "body_md": "# Better Agentic Review with Ephemeral Deploys\n\nUse Miren ephemeral deploys to let agents review PRs\n\nIt will come as no surprise to teams doing agentic development that the number of pull requests (PRs) teams submit has gone up precipitously in the last year. Many teams are now solving an agent problem by allowing agents to in turn review those PRs.\n\nWhen the agent in CI is reviewing a PR, it’s doing so in a constrained environment and likely with a time deadline. If the code being tested is a large application, it’s not uncommon that the app can’t be booted within the CI environment at all. This could be because of its raw CPU and memory footprint, or its service dependencies, or even data regulations. Regardless of the reason, setting up a whole application in CI comes with its own set of issues.\n\nFor all those reasons, Miren ephemeral deploys are an ideal way to give the CI agent a full deployment to test against. [Miren ephemeral deployments](https://miren.md/pr-environments), also called pull request environments, provide the ability to deploy a different version of an application that runs separately from the main application deployment. This different version shares the configuration with the main version, providing access to the same services, tokens, etc.\n\nCombined with [CI deployment](https://miren.md/ci-deploy), we can very easily create an ephemeral deployment from CI such as GitHub Actions to enable the agent to test against.\n\nLet’s go through a whole demo before we dive into the nitty gritty:\n\n### GitHub Actions\n\nThe whole sample repo with all of this is at [github.com/mirendev/agentic-eph](https://github.com/mirendev/agentic-eph).\n\nLet’s break down what this looks like in GitHub Actions.\n\nWe begin the workflow with a concurrency group that lets only one run per PR go at a time:\n\n```\nconcurrency:\n  group: pr-preview-${{ github.event.number }}\n  cancel-in-progress: true\n```\n\nThis prevents runs generated by new code being pushed to the PR from overlapping. That overlapping would cause the ephemeral deployments to change and become out of sync with the code the agent is testing.\n\nNext, a `preview` job deploys the code:\n\n```\npreview:\n  # Fork PRs don't get secrets, so they can't deploy. Same-repo PRs only.\n  if: github.event.pull_request.head.repo.full_name == github.repository\n  runs-on: ubuntu-latest\n  permissions:\n    contents: read\n    id-token: write\n  outputs:\n    url: ${{ steps.deploy.outputs.url }}\n  steps:\n    - uses: actions/checkout@v4\n    - id: deploy\n      uses: mirendev/actions/deploy@main\n      with:\n        cluster: ${{ secrets.MIREN_CLUSTER }}\n        app: brewbar\n        ephemeral: pr-${{ github.event.number }}\n        ttl: 24h\n```\n\nThe `MIREN_CLUSTER` secret is created from running `miren cluster export-address`. The job passes the preview’s URL on as an output, so later jobs can read it as `needs.preview.outputs.url`.\n\nEphemeral deployments share their configuration with the base application, including database and other add-ons. For this reason, we almost always suggest that people use ephemeral deployments against a staging version of their application. That prevents the ephemeral deployments from seeing and potentially changing production data. Our [PR previews post](https://miren.dev/blog/pr-previews#run-previews-against-staging-not-production) covers how to set that up.\n\nWith the deployment running, you can direct an agent towards it. In our demo, a second job runs Claude Code directly. It waits for `preview`, needs permission to comment on the PR, and checks out the full git history so the agent can diff against `main`:\n\n```\nagent-review:\n  needs: preview\n  runs-on: ubuntu-latest\n  permissions:\n    contents: read\n    pull-requests: write\n    id-token: write\n  steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0 # the agent diffs against origin/main\n```\n\nNow we’re ready for the core of review, invoking the agent:\n\n```\n- name: Agent tests the preview\n  id: agent\n  uses: anthropics/claude-code-action@v1\n  with:\n    anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}\n    github_token: ${{ github.token }}\n    # --json-schema makes the agent's final answer come back as the\n    # structured_output step output, so it never needs to write a file.\n    claude_args: |\n      --model claude-sonnet-5-5\n      --max-turns 40\n      --allowedTools \"Bash(curl:*),Bash(jq:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Read,Glob,Grep\"\n      --json-schema '{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"decision\",\"summary\",\"checks\"],\"properties\":{\"decision\":{\"type\":\"string\",\"enum\":[\"merge\",\"reject\"]},\"summary\":{\"type\":\"string\"},\"checks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"name\",\"result\",\"detail\"],\"properties\":{\"name\":{\"type\":\"string\"},\"result\":{\"type\":\"string\",\"enum\":[\"pass\",\"fail\"]},\"detail\":{\"type\":\"string\"}}}}}}'\n    prompt: |\n      You are the release gate for Brewbar, a small coffee-ordering web app.\n\n      Pull request #${{ github.event.number }}\n      has been deployed to a temporary preview environment at:\n\n          ${{ needs.preview.outputs.url }}\n\n      Decide whether this pull request is safe to merge into main. Base the\n      decision on what the running preview actually does, not only on\n      reading the code.\n\n      1. Understand the change.\n         - `git log --oneline origin/main..HEAD`\n         - `git diff origin/main...HEAD`\n      2. Read `docs/api.md`. It is the public API contract. The web page\n         (`static/index.html`) and a separately released mobile app depend\n         on it. Adding endpoints or optional fields is fine. Renaming,\n         removing, or changing the meaning of an existing field or endpoint\n         is a breaking change, even if the docs were edited to match.\n      3. Test the live preview with curl. At minimum:\n         - `GET /healthz` returns 200.\n         - `GET /api/menu` returns the documented shape.\n         - `POST /api/orders` happy path: work out the expected\n           subtotal, discount, tax, and total yourself from the pricing\n           rules in the contract, and compare them to the response.\n         - A discount code, and each documented error case with its\n           documented status code.\n         - `GET /` serves the page, and every JSON field the page's\n           JavaScript reads is present in the live responses.\n         - Whatever new behavior this pull request adds, exercised for\n           real against the preview.\n      4. Decide.\n         - \"merge\" only if the change works on the preview as described\n           and nothing in the existing contract broke.\n         - \"reject\" if anything is broken, or if you could not verify\n           the change.\n\n      Your shell access is limited to curl, jq, and read-only git\n      commands. Use the Read tool to read files. Run one command at a\n      time. Piping curl into jq is fine. Loops, scripts, and `&&`\n      chains will be blocked. Do the arithmetic yourself.\n\n      Return your decision as the structured output:\n      - decision: \"merge\" or \"reject\".\n      - summary: two or three plain sentences a teammate can read in\n        ten seconds.\n      - checks: one entry per thing you tested, with what you sent and\n        what came back in `detail`.\n\n      Do not edit files, and do not merge, comment on, or approve the\n      pull request yourself. The workflow does that from your decision.\n\n- name: Read the verdict\n  id: verdict\n  env:\n    VERDICT: ${{ steps.agent.outputs.structured_output }}\n  run: |\n    mkdir -p .agent\n    echo \"$VERDICT\" > .agent/verdict.json\n    jq -e '(.decision == \"merge\" or .decision == \"reject\") and (.checks | type == \"array\")' .agent/verdict.json\n    echo \"decision=$(jq -r .decision .agent/verdict.json)\" >> \"$GITHUB_OUTPUT\"\n    jq -r -f .github/agent/report.jq .agent/verdict.json > .agent/report.md\n    cat .agent/report.md >> \"$GITHUB_STEP_SUMMARY\"\n```\n\nThat’s a bit verbose, but it’s designed to keep the agent on rails in this constrained environment. The key though is that you can vary the prompt as much as you need to conform to your own setup. The `report.jq` script that turns the verdict into a readable report is in the [sample repo](https://github.com/mirendev/agentic-eph).\n\n### Reporting\n\nOur example also posts the verdict as a comment on the PR. This is great to easily be able to understand its reasoning.\n\n```\n- name: Post the report on the PR\n  env:\n    GH_TOKEN: ${{ github.token }}\n    PR_NUMBER: ${{ github.event.number }}\n  run: gh pr comment \"$PR_NUMBER\" --body-file .agent/report.md\n```\n\n### Auto-Merging\n\nIf you’ve become comfortable with the fidelity of the system, you can even go to the next level and merge the PR based solely on this agent’s decision. I’d only recommend doing this once you’ve used this for a bit and feel the agent’s decisions consistently match your own AND you’re ready to accept a level of risk with automatic, non-human merges.\n\nPart of that risk is the PR itself. The agent reads the PR’s diff and commit messages, which the PR’s author wrote, so a hostile PR could try to talk the agent into saying “merge”. Only auto-merge PRs from contributors you already trust. In our example, the `preview` job skips PRs from forks, so the agent job and this merge never run for them.\n\nThe merge uses a separate `MERGE_TOKEN` rather than the built-in `github.token`. GitHub doesn’t start new workflow runs for merges made with the built-in token, so your production deploy on `main` would never fire.\n\n```\n- name: Merge\n  if: steps.verdict.outputs.decision == 'merge'\n  env:\n    GH_TOKEN: ${{ secrets.MERGE_TOKEN }}\n    PR_NUMBER: ${{ github.event.number }}\n    HEAD_SHA: ${{ github.event.pull_request.head.sha }}\n    HEAD_REF: ${{ github.event.pull_request.head.ref }}\n  run: |\n    gh api -X PUT \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/merge\" \\\n      -f merge_method=squash -f sha=\"$HEAD_SHA\"\n    gh api -X DELETE \"repos/$GITHUB_REPOSITORY/git/refs/heads/$HEAD_REF\"\n```\n\n### Conclusion\n\nAnd there you have it! You can tell everyone on social media that you’ve implemented a robust, fully agentic testing process!", "url": "https://wpnews.pro/news/better-agentic-review-with-ephemeral-deploys", "canonical_source": "https://miren.dev/blog/agentic-pr-eph/", "published_at": "2026-10-07 12:00:00+00:00", "updated_at": "2026-10-09 18:24:22.337405+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools", "mlops"], "entities": ["Miren", "Claude Code", "GitHub Actions", "Anthropic", "github.com/mirendev/agentic-eph"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/better-agentic-review-with-ephemeral-deploys", "markdown": "https://wpnews.pro/news/better-agentic-review-with-ephemeral-deploys.md", "text": "https://wpnews.pro/news/better-agentic-review-with-ephemeral-deploys.txt", "jsonld": "https://wpnews.pro/news/better-agentic-review-with-ephemeral-deploys.jsonld"}}