CI/CD and Pipelines: A Comprehensive Guide to Integrating Your Project with Vercel Using GitHub Actions and Custom Pipelines


Richard Pecha

10min


In today's fast-paced development environment, continuous integration and continuous deployment (CI/CD) are essential practices for maintaining agility and quality in software projects. This guide walks you through the process of integrating your project with Vercel using GitHub Actions and custom pipelines. By the end of this tutorial, you'll have a streamlined deployment process that accommodates multiple testing environments.

Setting Up Your Project

The first step in this journey begins with setting up your project repository on GitHub. After creating the repository, make an initial commit and push it to the main branch. Once this is done, connect your repository to Vercel. This connection triggers an initial "production" build on Vercel, which is then linked to a generated URL. Congratulations! You now have your Vercel account linked to your GitHub repository, and it's time to delve into setting up different environments.

Managing Branches

Within your GitHub repository, create a dev branch and set it as the default. This is a crucial step, as it lays the groundwork for the deployment flow that follows. While you can set up various rules for your dev and main branches—such as requiring approval from a colleague before merging—this isn't necessary at the moment.

Configuring URLs for Different Environments

Now, let's return to Vercel for a moment to set up the necessary URLs for your testing environments. The pipelines you'll set up later in this guide will cater to four different environments:

  • DEV: The environment where developers work and push changes from their branches.
  • STAGING: A staging area for a larger batch of changes from DEV, typically used for QA.
  • RC (Release Candidate): The environment for final checks by the client before moving to production.
  • PROD: The live environment for actual users and visitors.

The first URL generated by Vercel will be your production URL. Based on this, we'll create URLs for the other environments.

Setting Up Pipelines

This is where the fun begins. Start by creating a new branch from dev, for instance, feat/pipelines. In your IDE, create a .github folder with workflows folder in it and put there these pipelines. Let's break down the pipelines one by one:

CI Pipeline
#ci.yml
name: CI

on:
pull_request:
branches: ["main"]

env:
# add variables required for build of the application
EXAMPLE_TOKEN: ${{ secrets.EXAMPLE_TOKEN }}

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [18.x]

steps:
- uses: actions/checkout@v3

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}

- name: Install
run: |
yarn
- name: Build
run: |
yarn build

This workflow ensures that every pull request against the main branch triggers the following steps:

  • Cloning the repository.
  • Setting the correct version of Node.js.
  • Installing dependencies.
  • Running code linting (if applicable).
  • Building the project.

Take note of the env step in the pipeline. This is where you'll define all the environment variables necessary for the build process to function correctly. These variables will later be added both to Vercel and GitHub, as I'll explain further.

Enforce PR Labels Pipeline
#enforce_pr_labels.yml
name: Enforce PR Labels

on:
pull_request:
types: [labeled, unlabeled, opened, edited, synchronize]
jobs:
enforce-label:
runs-on: ubuntu-latest
steps:
- uses: yogevbd/enforce-label-action@2.2.2
with:
REQUIRED_LABELS_ANY: "major,minor,patch"
REQUIRED_LABELS_ANY_DESCRIPTION: "Please apply one label ['major','minor','patch']"

This workflow ensures that each pull request has at least one of the labels "major," "minor," or "patch." If none of these labels are applied, the workflow will provide a descriptive message requesting one. This pipeline runs on several pull request-related events, ensuring checks are performed during opening, editing, syncing, or label changes in the pull request.

Release Drafter Pipeline
#release-drafter.yml
name: Release Drafter

on:
push:
# branches to consider in the event; optional, defaults to all
branches:
- main
# pull_request event is required only for autolabeler
pull_request:
# Only following types are handled by the action, but one can default to all as well
types: [opened, reopened, synchronize]
# pull_request_target event is required for autolabeler to support PRs from forks
# pull_request_target:
# types: [opened, reopened, synchronize]

permissions:
contents: read

jobs:
update_release_draft:
permissions:
# write permission is required to create a github release
contents: write
# write permission is required for autolabeler
# otherwise, read permission is required at least
pull-requests: write
runs-on: ubuntu-latest
steps:
# (Optional) GitHub Enterprise requires GHE_HOST variable set
#- name: Set GHE_HOST
# run: |
# echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV

# Drafts your next Release notes as Pull Requests are merged into "main"
- uses: release-drafter/release-drafter@v5
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
# with:
# config-name: my-config.yml
# disable-autolabeler: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

This workflow automatically drafts release notes based on changes merged into the main branch. The workflow triggers on certain push and pull request events, using the Release Drafter action to generate the draft release notes. This is essential for keeping your release documentation up-to-date and organized.

Release-Preview Pipeline
#release-preview.yml
name: "Release preview"

on:
pull_request:
branches: ["dev"]

jobs:
release:
runs-on: ubuntu-latest
name: "Deploy"
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_SCOPE: ${{ secrets.VERCEL_SCOPE }}

steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.x

- name: "Determine Branch"
id: branches
uses: transferwise/sanitize-branch-name@v1

- name: "Deploy to Vercel"
id: 'vercel_deployment'
run: |
echo "URL=$(npx vercel --token ${VERCEL_TOKEN})" >> $GITHUB_OUTPUT

- name: "Alias deployment URL"
run: |
npx vercel alias --token=${{ secrets.VERCEL_TOKEN }} --scope=${{ secrets.VERCEL_SCOPE }} set ${{ steps.vercel_deployment.outputs.URL }} example-${{ steps.branches.outputs.sanitized-branch-name }}.vercel.app

- name: Comment PR
uses: thollander/actions-comment-pull-request@v2
with:
message: |
:rocket: PR deployed to https://example-${{ steps.branches.outputs.sanitized-branch-name }}.vercel.app
comment_tag: deploy

This GitHub Actions workflow automatically deploys the application to Vercel with every pull request to the dev branch. Here's a brief overview of the process:

  • Trigger: The workflow starts when a pull request to the dev branch is created or updated.
  • Environment: Environment variables for Vercel (such as token, project ID, org ID, scope) are defined, retrieved from GitHub Secrets.
  • Steps: The repository is cloned, Node.js 18.x is set up, and the branch name is normalized. The application is then deployed to Vercel, and a custom alias is set based on the branch name. Finally, a comment is added to the pull request with the URL of the deployed application.

This workflow streamlines the process of deploying preview versions for pull requests, making it easier to test and review changes.

Release Workflows (Development, Staging, RC, Production)
#release-development.yml
name: "Release to Development"

on:
workflow_dispatch:
push:
branches:
- dev

jobs:
release:
runs-on: ubuntu-latest
name: "Deploy to Development"
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_SCOPE: ${{ secrets.VERCEL_SCOPE }}

steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.x

- name: "Deploy to Vercel"
id: "vercel_deployment"
run: |
echo "URL=$(npx vercel --token ${VERCEL_TOKEN})" >> $GITHUB_OUTPUT

- name: "Alias deployment URL"
run: |
npx vercel alias --token=${{ secrets.VERCEL_TOKEN }} --scope=${{ secrets.VERCEL_SCOPE }} set ${{ steps.vercel_deployment.outputs.URL }} dev-example-url.vercel.app

#release-staging.yml
name: "Release to Staging"

on:
workflow_dispatch:
push:
branches:
- main

jobs:
release:
runs-on: ubuntu-latest
name: "Deploy to Staging"
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_SCOPE: ${{ secrets.VERCEL_SCOPE }}

steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.x

- name: "Deploy to Vercel"
id: 'vercel_deployment'
run: |
echo "URL=$(npx vercel --token ${VERCEL_TOKEN})" >> $GITHUB_OUTPUT

- name: "Alias deployment URL"
run: |
npx vercel alias --token=${{ secrets.VERCEL_TOKEN }} --scope=${{ secrets.VERCEL_SCOPE }} set ${{ steps.vercel_deployment.outputs.URL }} staging-example-url.vercel.app

#release-rc.yml
name: "Release to RC"

on:
workflow_dispatch:

jobs:
release:
runs-on: ubuntu-latest
name: "Deploy to Release Candidate"
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_SCOPE: ${{ secrets.VERCEL_SCOPE }}

steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.x

- name: "Deploy to Vercel"
id: 'vercel_deployment'
run: |
echo "URL=$(npx vercel --token ${VERCEL_TOKEN})" >> $GITHUB_OUTPUT

- name: "Alias deployment URL"
run: |
npx vercel alias --token=${{ secrets.VERCEL_TOKEN }} --scope=${{ secrets.VERCEL_SCOPE }} set ${{ steps.vercel_deployment.outputs.URL }} rc-example-url.vercel.app

#release-production.yml
name: "Release to Production"

on:
release:
types: [published]

env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_SCOPE: ${{ secrets.VERCEL_SCOPE }}

jobs:
release:
runs-on: ubuntu-latest
name: "Deploy to Production"
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.x

- name: "Deploy to Vercel"
id: 'vercel_deployment'
run: |
echo "URL=$(npx vercel --token ${VERCEL_TOKEN})" >> $GITHUB_OUTPUT

- name: "Alias deployment URL"
run: |
npx vercel alias --token=${{ secrets.VERCEL_TOKEN }} --scope=${{ secrets.VERCEL_SCOPE }} set ${{ steps.vercel_deployment.outputs.URL }} example-url.vercel.app

These workflows function similarly to the release-preview pipeline but are tailored to specific environments. Ensure that the URLs are correctly adjusted for each environment. Once you’ve customized the URLs, you can commit your changes—but hold off on pushing them just yet, as we need to set up environment variables first.

Configuring Environment Variables

We'll return to your GitHub repository settings to configure the environment variables that the pipelines need.

  1. Go to Repository Settings → Secrets and variables → Actions.
  2. Add the following variables:
  • VERCEL_PROJECT_ID: This can be found in your Vercel dashboard under Settings → General → Project ID.
  • VERCEL_TOKEN: If you're working within an organization on GitHub, you can add this variable at the organization level, making it available across all repositories. And you will get the token like this.
  • VERCEL_ORG_ID: Similar to the token, this can be set globally. You’ll find it in your Vercel account settings under Org ID.
  • VERCEL_SCOPE: This can also be set globally if you’re working on corporate projects. It’s located in Vercel under Account Settings → General → Team URL.

If your project relies on third-party APIs or services, you'll need to add these variables to your Vercel project as well. Go to Vercel Settings → Environment Variables and ensure that all variables match those in your .env file.

Finally, add a vercel.json file to the root of your project, which should look something like this:

//vercel.json
{
"git": {
"deploymentEnabled": false
}
}

This file ensures that not everyone in your organization needs direct access to Vercel, and without it, preview URLs would not be generated for all pull requests.

Workflow Overview

With all variables added, you can now push your changes to the remote branch containing the pipelines and create a pull request. Here's a summary of what happens at each stage:

  • Your Branch to DEV Branch: Remember to select a label so the drafter pipeline can create release notes for a later production release. This pull request triggers the release-preview, enforce PR labels, and release drafter pipelines. Once merged, the release to development pipeline triggers, updating your DEV environment with the latest version of your application.
  • DEV to MAIN: This pull request triggers the same workflows as DEV, with the addition of the CI pipeline, which ensures the build succeeds. Once merged, the release to staging pipeline runs, deploying the version to your STAGING environment. After this step, merge main back into dev to avoid future merge conflicts.
  • Release to RC: This step is performed manually. Navigate to Actions → Release to RC → Run workflow → Branch: main.
  • Release to Production: This step is also manual but slightly more complex. From your repository’s main page, click on Releases. You'll see a draft release created by the release drafter pipeline. Click "Edit," then "Publish release" to trigger the release to production. You can monitor the release process in the Actions tab.

By following these steps, you’ve established a robust CI/CD pipeline that automates the deployment process across multiple environments, making it easier to maintain, test, and release your application.

Contact us

Let us know about your project. We’re usually replying within 24 hours