Full-Stack Developer

Preview Deployments for Every Branch β€” Stakeholders Review Before Merge

Staging conflicts eliminated, every MR gets live previewEngineering & DevOps4 min read

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

MetricShared StagingPer-Branch Preview
Environment conflictsWeeklyImpossible
Time to review"Wait for staging" (hours)Instant (auto-deployed)
Review accuracyScreenshots, descriptionsLive running app
QA coverage per MRManual spot checksFull automated suite
Merge confidence"Probably fine"Verified
Feedback loopAsync (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.

VercelPreview DeploymentsGitLabQACI/CD

Want results like these?

Start free with your own AI team. No credit card required.

Preview Deployments for Every Branch β€” Stakeholders Review Before Merge β€” Mr.Chief