3 minute read

Problem

In typical software projects a CI/CD pipeline manages not only a “Production” deployment but also multiple other deployments for testing new features or running nightly regression tests etc. Using a “Staging” deployment to review proposed changes before you commit to production will prevent users from seeing bugs.

I use Github Pages to host sites for many of my personal projects (including this blog). I also use Github Actions for CI/CD, to automatically build and deploy a site whenever I commit changes. However, Github Pages only supports a single deployment of any given repository. Therefore, testing changes in a hypothetical deployment is NOT easily possible.

For professional projects I would use a more dedicated and feature rich CI/CD pipeline, but for side projects using Github Pages I wanted a simple solution:

  • free
  • low maintenance
  • close to code… all in Github if possible
  • basic staging deployment –> allow me to view a deployed version of the app BEFORE I commit to prod

Idea

To accomplish this, I used Github Actions. The lifecycle of a Pull Request (PR) controls the lifecycle of a staging deployment - ie: 1 deployment for each PR, created when the PR is opened and removed when the PR is closed.

  • Developer opens PR in the app repository
    • Github Action creates a repository for a staging deployment
    • Github Action builds the app and deploys it to the staging repository
    • Github Action writes a comment to the PR with a link to the live staging deployment
  • Developer inspects the staging deployment
  • Developer merges or closes the open PR
    • Github Action deletes the staging repository (and staging deployment)
  • If the PR was merged, Github Action builds and deploys the app to production

The final result looks something like this: pr

Creating Github Actions

This process requires 2 new Github Action actions:

  • create-git-repo: creates a new github repository
  • delete-git-repo: deletes an existing github repository

I used simple JS scripts to hit the Github API, and made dedicated actions for each. I’d recommend tightly controlling any actions that leverage secrets or personal access tokens, rather than using third party actions. See the action repositories for more details: ga-create-git-repo, ga-delete-git-repo.

Creating Workflows

Next, the overall staging process requires 2 new Github Action workflows:

  • pr_staging_deploy: creates and manages the staging deployment for open PRs
    • When a PR is opened
      • Creates the a new temporary repository to host the staging deployment
    • When a PR is opened or when a commit is added to an opened PR
      • Builds the app
      • Deploys the app to the temporary staging repository
      • Posts or updates a link in the PR comments, providing a URL to the staging deployment
  • pr_staging_teardown: removes staging deployments on PR merge/close
    • When a PR is closed (or merged), delete the staging repository & deployment

Again, the workflow centers around the lifecycle of a PR by using the pull request node id to create temporary repo names w/ deterministic behavior. See the code for more details.

name: Staging PR Deploy
# Run when pull requests are opened
on:
  pull_request:
    branches: [dev]
env:
  PR_REPO_NAME: staging-pr-$
jobs:
  create-page-host:
    runs-on: ubuntu-latest
    steps:
      - name: Create new repository for temporary deployment
        uses: dcyoung/ga-create-git-repo@v1.0.0
        with:
          name: $
          # org: dcyoung
          access-token: $
  pr-build-deploy:
    needs: create-page-host
    runs-on: ubuntu-latest
    environment:
      name: pr-staging
      url: https://dcyoung.github.io/$/
    steps:
      - name: Setup Node.js
        uses: actions/setup-node@v2
      - name: Set GitHub Actions as Commit Author
        run: |
          git config --global user.name github-actions
          git config --global user.email github-actions@github.com
      - name: Checkout Repo
        uses: actions/checkout@v2
        with:
          path: "pr-build"
      - name: Install and Build
        run: |
          cd pr-build
          npm install
          npm run build -- --base=/$/
        env:
          CI: ""
      - name: Checkout temporary deployment target repo
        uses: actions/checkout@v2
        with:
          repository: dcyoung/$
          fetch-depth: 0
          path: "pr-deploy"
          token: $
      - name: Push files to target
        run: |
          cp -r pr-build/dist/* pr-deploy
          cd pr-deploy
          git add .
          git commit -m $GITHUB_SHA
          git branch -M gh-pages
          git push -f -u origin gh-pages
      - name: Create link in PR
        uses: mshick/add-pr-comment@v2
        with:
          message: |
            **Staging Preview:**
            https://dcyoung.github.io/$/
name: Staging PR Teardown
# Run when pull requests are closed (includes merged)
on:
  pull_request:
    branches: [dev]
    types: [closed]
env:
  # The name of the staging repo created for this
  PR_REPO_NAME: staging-pr-$
jobs:
  delete-page-host:
    runs-on: ubuntu-latest
    steps:
      - name: Delete repository for temporary deployment
        uses: dcyoung/ga-delete-git-repo@v1.0.0
        with:
          name: dcyoung/$
          access-token: $

Comments