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


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:
The
actions/checkout@v3
step ran withfetch-depth: 0
to pull the full history.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 PDFMoves the generated resume to the
public/
folderCommits and pushes the updated resume to your repo using your PAT
Subscribe to my newsletter
Read articles from Ayush Baliyan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
