Full-Stack Developer
Preview Deployments for Every Branch β Stakeholders Review Before Merge
Key Takeaway
Every merge request automatically gets a live Vercel preview deployment with the URL posted as a comment on the MR. Stakeholders review the real app, QA runs automated tests against it, and nothing merges without passing preview checks.
The Problem
"Can you deploy to staging so I can check this?"
That sentence has cost me more hours than any bug. Staging is shared. Staging is a queue. Developer A deploys their feature, Developer B deploys over it, Developer A's tests break, nobody knows whose code is actually running, and the PM is testing something that was already overwritten 20 minutes ago.
We had one staging environment. We had 5 concurrent features in development. The math doesn't work.
And the alternative β asking stakeholders to review from screenshots or local recordings β is a joke. "Looks good in the screenshot" has never once correlated with "works in production."
The Solution
Every merge request triggers a Vercel preview deployment. The live URL gets posted as a comment on the GitLab MR. Stakeholders (Bilal, Vivi) can click and review the actual running application. Pepe (QA agent) runs the automated test suite against the preview URL. If everything passes, the MR is auto-approved. If not, it's blocked with specific failure details.
The Process
Step 1: GitLab CI Trigger
yamlShow code
# .gitlab-ci.yml
preview-deploy:
stage: preview
image: node:20-slim
script:
# Install Vercel CLI
- npm install -g vercel
# Deploy preview with branch-specific URL
- |
DEPLOY_URL=$(vercel deploy \
--token $VERCEL_TOKEN \
--scope pyratzlabs \
--yes \
--meta gitlabMR=$CI_MERGE_REQUEST_IID \
2>&1 | grep "https://")
# Post preview URL as MR comment
- |
curl --request POST \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes" \
--data-urlencode "body=π **Preview Deployment Ready**
**URL:** $DEPLOY_URL
Preview will stay live until the MR is merged or closed.
| Check | Status |
|-------|--------|
| Build | β
Passed |
| Deploy | β
Live |
| Lighthouse | β³ Running... |
| E2E Tests | β³ Running... |"
only:
- merge_requests
environment:
name: preview/$CI_MERGE_REQUEST_IID
url: $DEPLOY_URL
on_stop: cleanup-preview
Step 2: Automated QA Against Preview
Pepe (QA agent) runs tests against the live preview URL:
javascriptShow code
// cypress.config.js β dynamically targets preview URL
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: process.env.PREVIEW_URL || 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.cy.{js,ts}',
video: true,
screenshotOnRunFailure: true,
},
});
bashShow code
# Run E2E tests against the preview
PREVIEW_URL=$DEPLOY_URL npx cypress run --reporter junit --reporter-options mochaFile=results.xml
# Run Lighthouse audit
npx lighthouse $DEPLOY_URL \
--output=json --output-path=lighthouse.json \
--chrome-flags="--headless --no-sandbox"
Step 3: Results Posted Back to MR
bashShow code
# Parse test results and update the MR comment
PASS_COUNT=$(cat results.xml | grep -c 'status="passed"')
FAIL_COUNT=$(cat results.xml | grep -c 'status="failed"')
LH_SCORE=$(cat lighthouse.json | jq '.categories.performance.score * 100')
curl --request PUT \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes/$NOTE_ID" \
--data-urlencode "body=π **Preview Deployment Ready**
**URL:** $DEPLOY_URL
| Check | Status |
|-------|--------|
| Build | β
Passed |
| Deploy | β
Live |
| E2E Tests | β
$PASS_COUNT passed, $FAIL_COUNT failed |
| Lighthouse | β
Performance: ${LH_SCORE}/100 |
| Visual Regression | β
No changes detected |"
Step 4: Auto-Approve or Block
yamlShow code
# If all checks pass, auto-approve
approve-mr:
stage: post-preview
script:
- |
if [ "$QA_STATUS" = "passed" ] && [ "$LIGHTHOUSE_SCORE" -ge 80 ]; then
curl --request POST \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/approve"
echo "β
MR auto-approved"
else
echo "β MR blocked β checks failed"
exit 1
fi
only:
- merge_requests
needs: [preview-deploy, run-qa]
The Results
| Metric | Shared Staging | Per-Branch Preview |
|---|---|---|
| Environment conflicts | Weekly | Impossible |
| Time to review | "Wait for staging" (hours) | Instant (auto-deployed) |
| Review accuracy | Screenshots, descriptions | Live running app |
| QA coverage per MR | Manual spot checks | Full automated suite |
| Merge confidence | "Probably fine" | Verified |
| Feedback loop | Async (Slack threads) | MR comments with links |
Try It Yourself
Vercel preview deployments work out of the box for any frontend project. The key additions are: posting the URL back to your MR (GitLab API), running automated tests against the preview URL (not localhost), and using the test results to gate merge approval. The infrastructure cost is negligible β Vercel preview deployments are included in the Pro plan.
Don't describe the change. Show the running app.
Related case studies
DevOps Engineer
CI/CD Pipeline Monitoring β The Agent That Never Sleeps
How an AI agent monitors CI/CD pipelines 24/7 across 5+ GitLab repos, auto-retries flaky jobs, and delivers failure alerts to Telegram β so developers never context-switch to check builds again.
Software Engineer
Creating Merge Requests From Telegram β One Message, Full CI Pipeline
Learn how PyratzLabs creates GitLab merge requests from Telegram in 15 seconds using AI agents. One message triggers MR creation, CI pipeline, and code review β replacing 10 minutes of manual work.
DevOps Engineer
Release Automation β From Tag to Changelog to GitLab Release in 60 Seconds
PyratzLabs automated the entire release process with an AI agent. One Telegram command creates a tag, generates a categorized changelog, and publishes a GitLab release in 60 seconds flat.
Want results like these?
Start free with your own AI team. No credit card required.