Home / Courses / CI/CD for Beginners / II — Hands-On Practice
II — Hands-On Practice
Article 5 of 6

Hands-On: Continuous Deployment to Vercel

Wiring the test-and-build pipeline into automatic deployment on Vercel via GitHub Actions, complete with a manual approval gate using GitHub Environments.

July 10, 2026

Two Ways to Deploy to Vercel

Vercel has a very easy built-in Git integration: connect the repo, and every push deploys automatically, end of story. That's great, but for this series we're deliberately using a more explicit approach — deploying through our own GitHub Actions.

Why? Because we want deployment to genuinely be the final stage of the pipeline we've been building: test first, then build, then deploy. With the native integration, Vercel deploys on its own regardless of what our tests in GitHub Actions actually say. This explicit approach is also what's typically used if you later move to a different platform — the concept is portable.

Preparing the App for Vercel

Vercel is a serverless platform — each request is ideally handled by a short-lived function, rather than an always-on server like the app.listen() pattern we're used to. Fortunately, our Express app is easy to adapt.

Step 1 — create api/index.js:

module.exports = require('../src/app');

This file just re-exports the exact same Express app we've already tested in previous articles. We're not rewriting any logic — just one line.

Step 2 — create vercel.json at the root:

{
  "rewrites": [
    { "source": "/(.*)", "destination": "/api/index" }
  ]
}

This line tells Vercel: route every request, no matter the path — /health, /api/tasks, anything — to the api/index.js function. Express itself then handles internal routing, exactly like when it runs locally.

Before moving on, we can test this Vercel behavior locally with npx vercel dev, opening localhost:3000/health, and confirming the response matches what we had before.

Setting Up a Vercel Project & Getting Credentials

npm install --global vercel
vercel login
vercel link

vercel link connects our local folder to a project in our Vercel account — if one doesn't exist yet, it creates a new one. After this, a .vercel/ folder appears, containing project.json with orgId and projectId. This folder should NOT be committed — which is why it's already in .gitignore from the start.

Grab a token:

  • Go to vercel.com → Account Settings → Tokens → Create Token.
  • Copy the token that appears (shown only once).
  • Now we have three pieces of information: VERCEL_TOKEN, VERCEL_ORG_ID, and VERCEL_PROJECT_ID. All of these are credentials that must be kept secret — remember the GitHub Secrets concept from the previous article? That's exactly what we use here.

    Add these 3 repository secrets under Settings → Secrets and variables → Actions:

  • VERCEL_TOKEN
  • VERCEL_ORG_ID
  • VERCEL_PROJECT_ID
  • The deploy Job in the Workflow

    deploy:
      needs: build
      runs-on: ubuntu-latest
      if: github.ref == 'refs/heads/main' && github.event_name == 'push'
      environment:
        name: production
        url: ${{ steps.deploy.outputs.deployment-url }}
      env:
        VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
        VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
    
      steps:
        - uses: actions/checkout@v4
    
        - name: Install Vercel CLI
          run: npm install --global vercel@latest
    
        - name: Pull Vercel environment info
          run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
    
        - name: Build project artifacts for Vercel
          run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
    
        - name: Deploy to Vercel (production)
          id: deploy
          run: |
            url=$(vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }})
            echo "deployment-url=$url" >> "$GITHUB_OUTPUT"
    

    A few important points:

  • needs: build — deploy only runs after the build job (and, transitively, test) succeeds. This is the real implementation of the pipeline diagram from the pipeline anatomy article: test → build → deploy.
  • if: github.ref == 'refs/heads/main' && github.event_name == 'push' — this job is SKIPPED when the run is triggered by a pull request. We only want to deploy to production once the code has actually landed on main.
  • environment: production — this is the GitHub Environments feature, covered in the next section for the manual gate.
  • vercel pullvercel buildvercel deploy --prebuilt — the official Vercel CLI flow for CI/CD: pull the project configuration, build on the GitHub runner (not on Vercel's servers), then deploy the already-built result. This gives us full control and full transparency over what's actually being deployed.
  • Once the run finishes, open the URL that appears in the output — that's our app, now live on Vercel. Try hitting the /health and /api/tasks endpoints directly from a browser or curl.

    Manual Approval Gate — GitHub Environments

    Remember in the first article we covered the difference between Continuous Delivery and Continuous Deployment? The only difference is whether there's manual approval before production or not. Now let's actually put that into practice.

    The steps:

  • Settings → Environments → New environment, name it production (must match exactly what's in ci.yml).
  • Enable "Required reviewers", add yourself or team members as reviewers.
  • Save.
  • Now, as soon as the deploy job is about to run, the pipeline will PAUSE and wait for manual approval — a notification goes out to the reviewers we set. Only after it's approved does the deployment continue.

    If you push to main again, the deploy job will show as "Waiting." Click "Review deployments," click "Approve and deploy," and the job continues. With this single toggle, our pipeline automatically flips from Continuous Deployment into Continuous Delivery. Once your team is fully confident in its test coverage, just turn off the required reviewer, and it goes back to full Continuous Deployment.

    Summary

    We just finished an end-to-end pipeline: push code → test across multiple Node versions → build & package into an artifact → automatically deploy to Vercel, complete with a manual approval gate via GitHub Environments. This pipeline has the exact same structure used by real production teams, just scaled down for learning.

    Topics

    CI/CDGitHub ActionsDeployment