Building a personal portfolio or blog is a rite of passage for many developers. I recently rebuilt my site using Hugo, a fantastic static site generator known for its speed and flexibility. But hosting the code is only half the battle. I wanted a modern CI/CD pipeline that would:

  1. Automatically deploy my site to GitHub Pages whenever I push to main.
  2. Automatically test my site to ensure I haven’t broken anything before merging changes.

In this post, I’ll walk you through how I set up this automated workflow using GitHub Actions and Playwright.

The Infrastructure

My setup relies on a few key components:

  • Source Code: Hosted in a dedicated GitHub repository (e.g., github-pages-source).
  • Hosting: GitHub Pages, served from a gh-pages branch in a separate repository (or the same one, depending on your preference).
  • Testing: Playwright for end-to-end testing.

Step 1: Automated Deployment with GitHub Actions

First, let’s tackle deployment. I use a GitHub Action workflow defined in .github/workflows/deploy.yml. This workflow triggers on every push to the main branch.

Here is the breakdown of my deployment workflow:

name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    concurrency:
      group: ${{ github.workflow }}-${{ github.ref }}
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: true  # Key for Hugo themes included as submodules
          fetch-depth: 0    # accurate git history for Hugo's .GitInfo

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v3
        with:
          hugo-version: 'latest'
          extended: true

      - name: Build
        run: hugo --minify

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v4
        if: ${{ github.ref == 'refs/heads/main' }}
        with:
          external_repository: StaticVish/staticvish.github.io
          publish_branch: gh-pages
          publish_dir: ./public
          personal_token: ${{ secrets.DEPLOY_TO_PAGES_TOKEN }}
          commit_message: ${{ github.event.head_commit.message }}

Key Takeaways:

  • submodules: true: Essential if you use a Hugo theme as a git submodule.
  • peaceiris/actions-hugo: Sets up the Hugo environment.
  • peaceiris/actions-gh-pages: The magic action that takes the built site (in ./public) and pushes it to the gh-pages branch of my hosting repository (StaticVish/staticvish.github.io).

Step 2: Integrating Playwright for E2E Testing

Deploying is great, but deploying broken code is effectively automated embarrassment. That’s where Playwright comes in. I use it to run end-to-end tests against my site.

First, I installed Playwright and its dependencies:

npm init -y
npm install --save-dev @playwright/test
npx playwright install

This created a playwright.config.ts file, where I configured the test environment. A crucial part of this config is the webServer block, which tells Playwright how to spin up my local Hugo server before running tests:

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  // ... other config ...
  webServer: {
    command: 'hugo server -D -p 1313',
    url: 'http://localhost:1313',
    reuseExistingServer: !process.env.CI,
    stdout: 'ignore',
    stderr: 'pipe',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
    { name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
    { name: 'Mobile Safari', use: { ...devices['iPhone 12'] } },
  ],
});

This ensures that when Playwright runs, it tests against a live, locally served version of my blog.

Step 3: Automating Tests on Every PR

Finally, I created a second workflow, .github/workflows/playwright.yml, to run these tests automatically whenever I open a Pull Request or push to main.

name: Playwright Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with:
        submodules: true
        fetch-depth: 0
    
    - name: Setup Node
      uses: actions/setup-node@v4
      with:
        node-version: 'lts/*'
        
    - name: Setup Hugo
      uses: peaceiris/actions-hugo@v3
      with:
        hugo-version: 'latest'
        extended: true

    - name: Build Site
      run: hugo --minify

    - name: Install dependencies
      run: npm ci

    - name: Install Playwright Browsers
      run: npx playwright install --with-deps

    - name: Run Playwright tests
      run: npx playwright test

    - uses: actions/upload-artifact@v4
      if: always()
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

Now, if I make a change that breaks the layout or causes a regression, the CI pipeline will catch it before it gets deployed to production.

Conclusion

By combining Hugo’s speed, GitHub Pages’ free hosting, and Playwright’s robust testing, I’ve created a resilient simplified DevOps pipeline for my personal brand. It allows me to focus on writing content and coding, knowing that the “boring stuff” like deployment and verification is handled automatically.