Caching Playwright Browsers in CI/CD When to Use This Skill Use this skill when: Setting up CI/CD for a project with Playwright E2E tests Build times are slow due to browser downloads (~400MB, 1-2 minutes) You want automatic cache invalidation when Playwright version changes Using GitHub Actions or Azure DevOps pipelines The Problem Playwright browsers (~400MB) must be downloaded on every CI run by default. This: Adds 1-2 minutes to every build Wastes bandwidth Can fail on transient network issues Slows down PR feedback loops Core Pattern Extract Playwright version from Directory.Packages.props (CPM) to use as cache key Cache browser binaries using platform-appropriate paths Conditional install - only download on cache miss Automatic cache bust - key includes version, so package upgrades invalidate cache Cache Paths by OS OS Path Linux ~/.cache/ms-playwright macOS ~/Library/Caches/ms-playwright Windows %USERPROFILE%\AppData\Local\ms-playwright GitHub Actions - name : Get Playwright Version shell : pwsh run : | $propsPath = "Directory.Packages.props" [xml]$props = Get-Content $propsPath $version = $props.Project.ItemGroup.PackageVersion | Where-Object { $.Include -eq "Microsoft.Playwright" } | Select-Object -ExpandProperty Version echo "PlaywrightVersion=$version" >> $env:GITHUB_ENV - name : Cache Playwright Browsers id : playwright - cache uses : actions/cache@v4 with : path : ~/.cache/ms - playwright key : $ { { runner.os } } - playwright - $ { { env.PlaywrightVersion } } - name : Install Playwright Browsers if : steps.playwright - cache.outputs.cache - hit != 'true' shell : pwsh run : ./build/playwright.ps1 install - - with - deps Multi-OS GitHub Actions For workflows that run on multiple operating systems: - name : Cache Playwright Browsers id : playwright - cache uses : actions/cache@v4 with : path : | ~/.cache/ms-playwright ~/Library/Caches/ms-playwright ~/AppData/Local/ms-playwright key : $ { { runner.os } } - playwright - $ { { env.PlaywrightVersion } } Azure DevOps - task : PowerShell@2 displayName : 'Get Playwright Version' inputs : targetType : 'inline' script : | [xml]$props = Get-Content "Directory.Packages.props" $version = $props.Project.ItemGroup.PackageVersion | Where-Object { $.Include -eq "Microsoft.Playwright" } | Select-Object -ExpandProperty Version Write-Host "##vso[task.setvariable variable=PlaywrightVersion]$version" - task : Cache@2 displayName : 'Cache Playwright Browsers' inputs : key : 'playwright | "$(Agent.OS)" | $(PlaywrightVersion)' path : '$(HOME)/.cache/ms-playwright' cacheHitVar : 'PlaywrightCacheHit' - task : PowerShell@2 displayName : 'Install Playwright Browsers' condition : ne(variables [ 'PlaywrightCacheHit' ] , 'true') inputs : filePath : 'build/playwright.ps1' arguments : 'install --with-deps' Helper Script: playwright.ps1 Create a build/playwright.ps1 script that discovers and runs the Playwright CLI. This abstracts away the Playwright CLI location which varies by project structure.
build/playwright.ps1
Discovers Microsoft.Playwright.dll and runs the bundled Playwright CLI
param ( [Parameter(ValueFromRemainingArguments = $true)] [string[]] $Arguments )
Find the Playwright DLL (after dotnet build/restore)
$playwrightDll
Get-ChildItem
Path . - Recurse - Filter "Microsoft.Playwright.dll" - ErrorAction SilentlyContinue | Select-Object - First 1 if ( -not $playwrightDll ) { Write-Error "Microsoft.Playwright.dll not found. Run 'dotnet build' first." exit 1 } $playwrightDir = $playwrightDll . DirectoryName
Find the playwright CLI (path varies by OS and node version)
$playwrightCmd
Get-ChildItem
Path " $playwrightDir /.playwright/node" - Recurse - Filter "playwright.cmd" - ErrorAction SilentlyContinue | Select-Object - First 1 if ( -not $playwrightCmd ) {
Try Unix executable
$playwrightCmd
Get-ChildItem
Path " $playwrightDir /.playwright/node" - Recurse - Filter "playwright" - ErrorAction SilentlyContinue | Where-Object { $_ . Name -eq "playwright" } | Select-Object - First 1 } if ( -not $playwrightCmd ) { Write-Error "Playwright CLI not found in $playwrightDir /.playwright/node" exit 1 } Write-Host "Using Playwright CLI: $ ( $playwrightCmd . FullName ) " & $playwrightCmd . FullName @Arguments Usage:
Install browsers
./build/playwright.ps1 install --with-deps
Install specific browser
./build/playwright.ps1 install chromium
Show installed browsers
./build/playwright.ps1 install --dry-run Prerequisites This pattern assumes: Central Package Management (CPM) with Directory.Packages.props : < Project
< ItemGroup
< PackageVersion Include = " Microsoft.Playwright " Version = " 1.40.0 " /> </ ItemGroup
</ Project
Project has been built before running playwright.ps1 (so DLLs exist) PowerShell available on CI agents (pre-installed on GitHub Actions and Azure DevOps) Why Version-Based Cache Keys Matter Using the Playwright version in the cache key ensures: Automatic invalidation when you upgrade Playwright No stale browser binaries that don't match the SDK version No manual cache clearing needed after version bumps If you hardcode the cache key (e.g., playwright-browsers-v1 ), you'll need to manually bump it every time you upgrade Playwright, or you'll get cryptic version mismatch errors. Troubleshooting Cache not being used Verify the version extraction step outputs the correct version Check that the cache path matches your OS Ensure Directory.Packages.props exists and has the Playwright package "Browser not found" after cache hit The cached browsers don't match the Playwright SDK version. This happens when: The cache key doesn't include the version The version extraction failed silently Fix: Ensure the Playwright version is in the cache key. playwright.ps1 can't find the DLL Run dotnet build or dotnet restore before running the script. The Playwright DLL only exists after NuGet restore. References This pattern is battle-tested in production projects: petabridge/geekedin petabridge/DrawTogether.NET