Step-by-Step Guide: Refresh Your Resume Automatically with Playwright

Ayush BaliyanAyush Baliyan
8 min read

Keeping your resume updated and accessible online is crucial — but who wants to do it manually every time there's a tiny change? As a developer, I found a better way: automating the entire process using Playwright and GitHub Actions.

In this post, I’ll walk you through how I built a CI workflow that fetches the latest version of my resume using Playwright, replaces the old file in my portfolio repo, and commits the changes — all without me lifting a finger.

🤯 The Resume Update Struggle Is Real

As a developer, I keep my resume up to date — new roles, skills, achievements, etc. However, updating it manually in my portfolio repository every time was:

  • 🧍‍♂️ Repetitive – Updating Resume on Overleaf → Download → Open Project on local machine → Update there → Commit → Push

  • 🧠 Easy to forget – Especially when focusing on actual work

  • 💥 Error-prone – Accidentally forgetting to push the latest version

  • 💻 Unnecessary context-switching – It felt like a task that should be automated

I wanted a setup where:

  • My resume gets updated on one click button.

  • It runs in a containerized environment using Playwright.

  • It integrates tightly with GitHub Actions.

  • It works both locally (via ACT) and on GitHub CI.

🛠️ Automating It: The Smarter Way to Keep Your Resume Fresh

Manually updating your resume in your portfolio every time you tweak a line is not just tedious — it’s error-prone and wastes valuable dev time. So I decided to automate it. Using GitHub Actions, Playwright, and a bit of Git wizardry, I built a workflow that:

  • Automatically downloads the latest version of my resume from a predefined source.

  • Replaces the existing resume file in my portfolio repo.

  • Commits and pushes the updated resume automatically — securely and reliably.

The best part? I can trigger this anytime with a single click via workflow_dispatch.

🔧 Step-by-Step Implementation: Automate Resume Updates with GitHub Actions & Playwright

Let me walk you through every component of the workflow that keeps my resume up-to-date, cleanly committed, and live on my portfolio — all in one go.

1️⃣ Project Structure

Your project should have a structure similar to this:

Portfolio/
├── .github/
│   └── workflows/
│       └── update-resume.yaml    # GitHub Actions workflow
├── public/
│   └── Resume.pdf  # Resume file served by your site
├── scripts/
│   └── downloadResume.js         # Script to download the latest resume
├── package.json
└── ...

2️⃣ Script to Download Resume (Using Playwright)

Inside scripts/downloadResume.js, I used Playwright to grammatically go to my resume link and download the latest copy.

// scripts/downloadResume.js
const fs = require("fs");
const path = require("path");
const { chromium } = require("playwright");

(async () => {
  const browser = await chromium.launch();
  const context = await browser.newContext({
    acceptDownloads: true,
  });
  const page = await context.newPage();

  await page.goto("https://example.com/resume"); // replace with actual URL
  const [download] = await Promise.all([
    page.waitForEvent("download"),
    page.click("text=Download Resume"), // selector for your download button
  ]);

  const resumePath = path.join(__dirname, "..", "Resume.pdf");
  await download.saveAs(resumePath);

  await browser.close();
})();

3️⃣ GitHub Actions Workflow

Create .github/workflows/update-resume.yaml. This file sets up the automation. Here’s the full setup:

name: Update Resume via Playwright

on:
  workflow_dispatch:

jobs:
  update-resume:
    runs-on: ubuntu-latest

    container:
      image: mcr.microsoft.com/playwright:v1.52.0-noble

    steps:
      - name: Checkout repo
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
          token: ${{ secrets.GH_PAT }}  # Use a GitHub PAT with repo access

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 24

      - name: Install dependencies
        run: npm ci

      - name: Download Resume using Playwright
        run: |
          npx playwright install chromium --with-deps
          npm run download-resume

      - name: Replace resume
        run: mv Resume.pdf public/Resume.pdf

      - name: Commit and push updated resume
        env:
          GH_PAT: ${{ secrets.GH_PAT }}
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git remote set-url origin https://x-access-token:${GH_PAT}@github.com/${{ github.repository }}
          git add public/Resume.pdf
          git commit -m "Auto-update resume" || echo "No changes to commit"
          git push origin HEAD:${{ github.ref_name }}
        working-directory: ${{ github.workspace }}

Notes:

  • Use a GitHub Personal Access Token (PAT) stored in secrets.GH_PAT.

  • fetch-depth: 0 ensures your git history is intact for pushing changes.

  • The container runs a prebuilt Playwright image with all dependencies.

4️⃣ Package.json script

Make sure you add this in package.json:

{
  "scripts": {
    "download-resume": "node scripts/downloadResume.js"
  }
}

🛠️ Common Pitfalls & How I Fixed Them

While setting this up, I ran into a bunch of frustrating issues. Here’s a breakdown of each one — what went wrong and how I resolved it.


❌ Error: fatal: not in a git directory

What happened?
My workflow was running inside a container, and Git was failing to detect the .git directory even though the repo was checked out.

Fix:
I ensured two things:

  1. The actions/checkout@v3 step ran with fetch-depth: 0 to pull the full history.

  2. I explicitly used token: ${{ secrets.GH_PAT }} to allow pushes from inside the container.

- uses: actions/checkout@v3
  with:
    fetch-depth: 0
    token: ${{ secrets.GH_PAT }}

❌ Error: Git push fails due to authentication

What happened?
GitHub Actions' default token wasn’t sufficient when running inside a custom Docker container (Playwright image).

Fix:
I generated a Personal Access Token with repo scope and stored it in the repo secrets as GH_PAT. Then updated the remote URL:

git remote set-url origin https://x-access-token:${GH_PAT}@github.com/${{ github.repository }}

This enabled seamless pushes from within the container environment.


❌ Playwright can’t download file

What happened?
Initially, Playwright wouldn’t trigger the download because:

  • The selector was incorrect.

  • Or the button opened the file in a new tab instead of downloading.

Fix:
I ensured that:

  • The selector pointed to a clickable element (text=Download Resume).

  • Playwright had acceptDownloads: true set in the browser context.

  • I used page.waitForEvent('download') to catch the download event.

const context = await browser.newContext({ acceptDownloads: true });

❌ Error: Playwright not detected or Chromium not launching on GitHub Actions

What happened?
Even though Playwright was working locally, I faced multiple issues on GitHub Actions:

  • Sometimes npx playwright install didn't actually install the browsers correctly.

  • Other times, Playwright couldn’t detect or launch headless Chromium, throwing cryptic errors like:

Error: Failed to launch browser. Error: spawn ... ENOENT

or

Failed to launch the browser process!

Why this happens:
GitHub Actions runners often lack the necessary system dependencies or don’t persist installations across steps unless explicitly handled.


✅ Fix: Use the Official Playwright Docker Image as the Container

To solve all environment-related inconsistencies, I switched to running the entire job inside Playwright’s own Docker image. It comes preinstalled with:

  • All supported browsers

  • Dependencies for headless mode

  • Correct Linux libraries for Chromium

Here’s how I configured it:

container:
  image: mcr.microsoft.com/playwright:v1.52.0-noble

This ensures your scripts run in the same environment that Playwright expects — no weird missing dependencies or permission issues.


✅ Bonus: Explicitly Install Dependencies in Workflow

Even with the Docker image, I still made sure to install everything cleanly in the GitHub Action:

npx playwright install chromium --with-deps

This makes doubly sure Chromium is properly installed with all dependencies — even if the image is updated or cached differently.


✅ Tip: Always commit conditionally

To avoid unnecessary workflow failures when there are no updates, I added:

git commit -m "Auto-update resume" || echo "No changes to commit"

This way, the step exits gracefully if there’s nothing to commit.

🎨 Final Touches & Power-Ups

After I got the basic GitHub Action working, I started thinking — how can I make this workflow even cleaner, smarter, and more scalable?

Here are some cool improvements and best practices you might want to add:


🕒 1. Automate It Periodically (Optional)

If you want your resume to auto-update (say, weekly) without manual triggering:

on:
  schedule:
    - cron: '0 0 * * 0' # every Sunday at midnight UTC

This means your resume always stays fresh without lifting a finger.


📦 2. Handle Playwright Cache Smarter

To speed things up, you can cache installed dependencies or browser binaries using:

- name: Cache node modules
  uses: actions/cache@v3
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

While Playwright's Docker image helps, caching can still shave seconds off your CI time.


🛡️ 3. Use a Secure Personal Access Token (GH_PAT)

GitHub doesn't allow Actions to push commits with the default GITHUB_TOKEN unless it's on the same repo and not from a forked PR.
So I created a Personal Access Token (PAT) with repo scope and saved it as GH_PAT in GitHub Secrets.

This unlocked full commit/push access safely.


🛎️ 4. Optional: Auto PR Instead of Direct Push

If you're nervous about pushing directly to main, you can instead:

  • Push to a new branch (resume-bot/update)

  • Create a pull request via GitHub CLI or the REST API

  • Optionally, auto-approve and merge it

This keeps your repo clean and review-friendly.


✅ Final Workflow YAML (Copy-Paste Ready)

name: Update Resume via Playwright

on:
  workflow_dispatch: # 🔘 Manual trigger
  # schedule:
  #   - cron: '0 0 * * 0' # ⏰ Optional: every Sunday at midnight UTC

jobs:
  update-resume:
    runs-on: ubuntu-latest

    container:
      image: mcr.microsoft.com/playwright:v1.52.0-noble # 🐳 Use Playwright’s official Docker image

    steps:
      - name: Checkout repo
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
          token: ${{ secrets.GH_PAT }} # 🔐 Personal Access Token for pushing

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 24

      - name: Install dependencies
        run: npm ci

      - name: Install and run Playwright
        run: |
          npx playwright install chromium --with-deps
          npm run download-resume

      - name: Replace resume
        run: mv Resume.pdf public/_Resume.pdf

      - name: Commit and push updated resume
        env:
          GH_PAT: ${{ secrets.GH_PAT }}
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git remote set-url origin https://x-access-token:${GH_PAT}@github.com/${{ github.repository }}
          git add public/Baliyan_Resume.pdf
          git commit -m "Auto-update resume" || echo "No changes to commit"
          git push origin HEAD:${{ github.ref_name }}
        working-directory: ${{ github.workspace }}

🧠 Quick Recap

This workflow:

  • Runs inside the Playwright Docker image

  • Pulls down the repo and installs dependencies

  • Uses npm run download-resume to scrape or generate your PDF

  • Moves the generated resume to the public/ folder

  • Commits and pushes the updated resume to your repo using your PAT

17
Subscribe to my newsletter

Read articles from Ayush Baliyan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ayush Baliyan
Ayush Baliyan