A preview environment per pull request
The payoff of branch environments is that reviewers click a URL and see the change running instead of reading a diff. This guide wires that into CI so it happens on every pull request automatically: push a branch, a preview provisions, its URL lands on the PR, and the environment is torn down when the PR closes.
The provisioning itself is just git push managed — that part is shipped today.
The CI glue that posts URLs and gates merges is built on the
public API and webhooks.
The lifecycle
Section titled “The lifecycle”- A contributor pushes a branch and opens a PR.
- CI pushes the same commit to managed.dev, which provisions an isolated preview environment seeded from production.
- CI posts the preview URL back to the PR as a comment and a status check.
- Reviewers and QA work against the live URL; you can gate the merge on observability before promoting.
- When the PR closes or merges, CI tears the environment down.
Provision on push
Section titled “Provision on push”A preview is created whenever you push a non-default branch that isn’t routed to
staging or ignore — see branch routes. In CI,
add managed.dev as a remote and push the PR’s head commit:
git remote add managed "$MANAGED_GIT_URL"git push managed "HEAD:$BRANCH_NAME"The push returns once the deploy job is queued. To get the resolved preview URL (rather than constructing it), read the environment back through the API.
Post the URL back to the PR
Section titled “Post the URL back to the PR”Use a service token (mfs_live_…) minted with a narrow
scope set — the Deploy bot preset is the right shape:
sites:read deployments:write environments:write domains:read jobs:read observability:read.
Look up the environment created for the branch and grab its URL:
curl -s https://api.managed.dev/v1/sites/site_01J7ABCXYZ/environments \ -H "Authorization: Bearer $MANAGED_TOKEN" \ -G --data-urlencode "branch=$BRANCH_NAME"const res = await fetch( `https://api.managed.dev/v1/sites/${siteId}/environments?branch=${encodeURIComponent(branch)}`, { headers: { Authorization: `Bearer ${token}` } },);const { data } = await res.json();const env = data[0];console.log(env.preview_url); // https://acme-store-feature-checkout-a1b2c3.preview.managed.devThen post that URL as a PR comment and a commit status. A GitHub Action that does the whole loop is forthcoming as a published, versioned action; until then, the illustrative workflow below shows the shape:
name: managed.dev previewon: pull_request: types: [opened, synchronize, reopened, closed]
jobs: preview: if: github.event.action != 'closed' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: { fetch-depth: 0 } - name: Push to managed.dev run: | git remote add managed "${{ secrets.MANAGED_GIT_URL }}" git push managed "HEAD:${{ github.head_ref }}" - name: Comment preview URL env: MANAGED_TOKEN: ${{ secrets.MANAGED_TOKEN }} run: ./scripts/post-preview-url.shGate the merge on observability
Section titled “Gate the merge on observability”Because previews carry the full observability pipeline, you can make “no new errors” a merge requirement instead of a vibe. Query the preview’s traces for error-status rows and fail the check if any appear:
errors=$(curl -s "https://api.managed.dev/v1/environments/$ENV_ID/observability/traces?status=error&limit=1" \ -H "Authorization: Bearer $MANAGED_TOKEN" | jq '.data | length')test "$errors" -eq 0 || { echo "preview has error traces — blocking merge"; exit 1; }This is the deploy-gated-on-observability pattern: you only promote a branch that’s demonstrably clean on its own preview.
Tear down on close
Section titled “Tear down on close”When the PR closes, delete the environment so it doesn’t linger. Deleting the branch also tears the preview down automatically, but an explicit call is cleaner in CI:
curl -X DELETE "https://api.managed.dev/v1/sites/$SITE_ID/environments/$ENV_ID" \ -H "Authorization: Bearer $MANAGED_TOKEN" \ -H "Idempotency-Key: pr-${PR_NUMBER}-teardown"The Idempotency-Key makes a retried teardown safe — a replay returns the
original result instead of erroring. Production and staging are never removed this
way; only previews are ephemeral.