Use GitHub Actions to Deploy a Next.js SSG Site

Let’s use GitHub Actions to create a basic deployment pipeline for a Next.js SSG site hosted on Github Pages.
Source: Reddit

Let's use GitHub Actions to create a basic deployment pipeline for a Next.js SSG site hosted on Github Pages.

For reference, here’s the Github repo for the code used in this article.


Optional Readings

This article will be laser-focused on setting up GitHub Actions, so I may gloss over technical details I consider ancillary. Here are some background readings if you're interested:

  • The docs on how to build an SSG site with Next.js. For this article, AYNTK is to use the next build and next export commands to generate the files for a static site.

  • The docs on how to publish a static site on GitHub Pages. For this article, AYNTK is to commit and push the build files within the docs folder in the project root directory to deploy the site.

  • The definitions of the different components of a GitHub Actions pipeline.


Big Picture

Let's create a GitHub Actions pipeline to automate the following steps:

  1. Check out the GitHub repo
  2. Setup Node.js
  3. Install dependencies (or use cache if no changes)
  4. Build the files for the static site (or use cache if no changes)
  5. Cache dependencies and source files
  6. Commit and push the build files to the repo

Create a Workflow

GitHub Actions uses YAML syntax to define the events, jobs, and steps. These YAML files are stored in the code repository, in a directory called .github/workflows. So let's create a YAML file to define the deployment workflow.

# ./.github/workflows/deploy.workflow.yml

name: deploy-workflow
on: [push] # [A]
jobs:
  deploy-job:
    runs-on: ubuntu-latest # [B]
    steps:
      - uses: actions/checkout@v2 # [C]
      - uses: actions/setup-node@v2 # [D]
        with:
          node-version: '12'

Annotations of the code comments above:

[A] Pushing a commit to the GitHub repo is the event that triggers the workflow.
[B] Configures the job to run on a fresh Ubuntu Linux virtual machine hosted by GitHub (aka the Runner).
[C] The action to check out the repo and download the code into the Runner.
[D] The action to install Node.js in the Runner, and allow us to run npm commands.


Next, add the step to install the project dependencies in the Runner, which include the next package.

# ./.github/workflows/deploy.workflow.yml

name: deploy-workflow
on: [push] # [A]
jobs:
  deploy-job:
    runs-on: ubuntu-latest # [B]
    steps:
      - uses: actions/checkout@v2 # [C]
      - uses: actions/setup-node@v2 # [D]
        with:
          node-version: '12'
+     - run: npm install # [E]

[E] Ah, our old friend.


Now that the dependencies are installed, let’s add the step to build the static files.

# ./.github/workflows/deploy.workflow.yml

name: deploy-workflow
on: [push] # [A]
jobs:
  deploy-job:
    runs-on: ubuntu-latest # [B]
    steps:
      - uses: actions/checkout@v2 # [C]
      - uses: actions/setup-node@v2 # [D]
        with:
          node-version: '12'
      - run: npm install # [E]
+     - run: npm run build # [F]

[F] What the heck is build you ask? It’s a script defined in package.json that runs:

next build && next export -o docs

See the docs. TL;DR these are the Next.js commands to build the files for the SSG site, and export them to the docs folder.


Lastly, add the step to commit and push the updates to the docs folder to the repo.

# ./.github/workflows/deploy.workflow.yml

name: deploy-workflow
on: [push] # [A]
jobs:
  deploy-job:
    runs-on: ubuntu-latest # [B]
    steps:
      - uses: actions/checkout@v2 # [C]
      - uses: actions/setup-node@v2 # [D]
        with:
          node-version: '12'
      - run: npm install # [E]
      - run: npm run build # [F]
+     - uses: stefanzweifel/git-auto-commit-action@v4 # [G]
+       with:
+         commit_message: Automated publish

[G] This action will commit changes made in the Runner environment, and push the commit to the GitHub repo. The default commit message will be “Automated publish”.

Now, pushing a change to the repo will automatically deploy my SSG site to GitHub Pages. 🎉

I can now stare at the pipeline in the Actions tab of my GitHub repo.


Caching

I realized that the job runs a fresh npm install every time I push a commit. So let's introduce caching so that a fresh install occurs only when package-lock.json changes.

# ./.github/workflows/deploy.workflow.yml

name: deploy-workflow
on: [push] # [A]
jobs:
  deploy-job:
    runs-on: ubuntu-latest # [B]
    steps:
      - uses: actions/checkout@v2 # [C]
      - uses: actions/setup-node@v2 # [D]
        with:
          node-version: '12'
+     - uses: actions/cache@v2 # [H]
+       with:
+         path: ${{ github.workspace }}/node_modules
+         key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}
      - run: npm install # [E]
      - run: npm run build # [F]
      - uses: stefanzweifel/git-auto-commit-action@v4 # [G]
        with:
          commit_message: Automated publish

[H] This action caches the node_modules folder across builds, and makes the Runner use the cache as long as package-lock.json doesn’t change.

Additionally, noticed that next build issues the following warning about lack of caching.

warn  - No build cache found. Please configure build caching for faster rebuilds. 
Read more: https://err.sh/next.js/no-cache

The warning links you to the answer to fix it. Love it.

# ./.github/workflows/deploy.workflow.yml

name: deploy-workflow
on: [push] # [A]
jobs:
  deploy-job:
    runs-on: ubuntu-latest # [B]
    steps:
      - uses: actions/checkout@v2 # [C]
      - uses: actions/setup-node@v2 # [D]
        with:
          node-version: '12'
      - uses: actions/cache@v2 # [H]
        with:
-         path: ${{ github.workspace }}/node_modules
+         path: |
+           ${{ github.workspace }}/node_modules
+           ${{ github.workspace }}/.next/cache # [I]
-         key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}
+         # Generate a new cache whenever packages or source files change.
+         key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js') }}
+         # If source files changed but packages didn't, rebuild from a prior cache. 
+         restore-keys: |
+           ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
      - run: npm install # [E]
      - run: npm run build # [F]
      - uses: stefanzweifel/git-auto-commit-action@v4 # [G]
        with:
          commit_message: Automated publish

[I] Next.js stores its cache in the .next/cache directory. This will persist the cache across builds for faster application rebuilds. E.g., if I only updated my codebase but not the dependencies, this avoids re-bundling the dependencies.

Improved deployment time by about ~30%! 🥰


Read More

Liked what you've read?
Follow me on LinkedIn!