Using Playwright with Github Actions and Auth

You’ve got a webapp now and want to make sure it works. You’ve done the right thing of requiring auth for all users and TOTP so your bots also have to use this.

You’ve chosen playwright, and are wondering how to make

  • The various browsers use the same cookies/session for auth
  • avoid having to download all the various browsers every time

Configuration

TOTPs can only be used, and for a limited time period (it’s in the name)

Due to this, we want to authenticate once, and then re-use those cookies/etc for the various browsers for this bot user

// e2e-tests/external-users/auth.setup.ts

import { test as setup } from "@playwright/test";
import { promises as fs } from "fs";
import path from "path";

import { login } from "../lib/login";

const authFile = path.join(import.meta.dirname, "external-users.auth.json");

setup("authenticate", async ({ page }) => {
  // login is my custom function to do all the authentication/totp goodness based on https://playwrightsolutions.com/playwright-login-test-with-2-factor-authentication-2fa-enabled/
  // login will still need to have a backoff in case multiple github actions run at the same time, and deal with retries
  await login(page, "USERNAME", "EMAIL", "PASSWORD", "TOTP_SEED");

  await fs.mkdir(path.dirname(authFile), { recursive: true }).catch(() => {});

  // Save out the cookies/etc for use by *all* browsers
  await page.context().storageState({ path: authFile });
});

Now for the actual playwright configuration, main useful part is dependencies: ["setup"], and storageState: "e2e-tests/external-users/external-users.auth.json", which means the auth is only done once and then re-used by the other browsers

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

/**
 * See https://playwright.dev/docs/test-configuration.
 */
// eslint-disable-next-line no-restricted-exports
export default defineConfig({
  testDir: "./e2e-tests",
  /* Run tests in files in parallel */
  fullyParallel: true,
  /* Fail the build on CI if you accidentally left test.only in the source code. */
  forbidOnly: !!process.env.CI,
  /* Retry on CI only */
  retries: process.env.CI ? 2 : 0,
  // 10 minutes since LLMs can be slow
  timeout: 600000,
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
  reporter: "html",
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
  use: {
    /* Base URL to use in actions like `await page.goto('/')`. */
    // baseURL: 'http://127.0.0.1:3000',

    /* Record trace for failed tests. See https://playwright.dev/docs/trace-viewer */
    trace: "retain-on-failure",

    // Record video for failed tests
    video: "retain-on-failure",
  },

  /* Configure projects for major browsers */
  projects: [
    { name: "setup", testMatch: /.*\.setup\.ts/ },
    {
      name: "external-users-chromium",
      use: {
        ...devices["Desktop Chrome"],
        storageState: "e2e-tests/external-users/external-users.auth.json",
      },
      testMatch: /external-users\/.*\.spec\.ts/,
      dependencies: ["setup"],
    },

    {
      name: "external-users-firefox",
      use: {
        ...devices["Desktop Firefox"],
        storageState: "e2e-tests/external-users/external-users.auth.json",
      },
      testMatch: /external-users\/.*\.spec\.ts/,
      dependencies: ["setup"],
    },

    {
      name: "external-users-webkit",
      use: {
        ...devices["Desktop Safari"],
        storageState: "e2e-tests/external-users/external-users.auth.json",
      },
      testMatch: /external-users\/.*\.spec\.ts/,
      dependencies: ["setup"],
    },
  ],
});

Caching and Github Actions

Here is my customised version of the initial playwright Github Action. The main change is

  • Cache the browsers downloaded, based on playwright version and configuration
name: Scheduled Playwright Tests
on:
  push:
    schedule:
      - cron: "*/30 * * * *" # Run every 30 minutes
  workflow_dispatch: {}
concurrency:
  group: playwright
jobs:
  test:
    timeout-minutes: 15
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
          cache: "npm"
      - name: Install dependencies
        run: npm ci
      - name: Get installed Playwright version
        id: playwright-version
        run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package-lock.json').packages['node_modules/@playwright/test'].version)")" >> $GITHUB_ENV
      - name: Get hashed Playwright configuration
        run: echo "PLAYWRIGHT_CONFIG_HASH=$(node -e "console.log(require('crypto').createHash('sha256').update(require('fs').readFileSync('./playwright.config.ts', 'utf8')).digest('hex'))")" >> $GITHUB_ENV
      - name: Restore cached playwright binaries
        # From https://playwrightsolutions.com/playwright-github-action-to-cache-the-browser-binaries/
        uses: actions/cache/restore@v4
        id: playwright-read-cache
        with:
          path: |
            ~/.cache/ms-playwright
          key: $-playwright-$-$
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
      - name: Cache Playwright Browsers
        if: steps.playwright-read-cache.outputs.cache-hit != 'true'
        uses: actions/cache/save@v4
        id: playwright-write-cache
        with:
          path: |
            ~/.cache/ms-playwright
          key: $-playwright-$-$
      - name: Run Playwright tests
        run: npx playwright test
        env:
          USERNAME: $
          EMAIL: $
          PASSWORD: $
          TOTP_SEED: $
      - uses: actions/upload-artifact@v4
        if: $
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 8

Copyright © 2025 Richard Finlay Tweed. All rights reserved. All views expressed are my own