Skip to content
Latchkey

Playwright E2E Tests workflow (wekan/wekan)

The Playwright E2E Tests workflow from wekan/wekan, explained and optimized by Latchkey.

A

CI health: A - excellent

Point runs-on at Latchkey and get job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.

Grade your own workflow free or run it on Latchkey →
Source: wekan/wekan.github/workflows/playwright.ymlLicense MITView source

What it does

This is the Playwright E2E Tests workflow from the wekan/wekan repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.

Below, Latchkey shows a faster, safer version produced by its optimization engine.

The workflow

workflow (.yml)
name: Playwright E2E Tests

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

concurrency:
  group: playwright-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read

jobs:
  unit-tests:
    name: Meteor unit tests
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7

      - name: Setup Node 24
        uses: actions/setup-node@v6
        with:
          node-version: '24'
          cache: 'npm'

      - name: Cache Meteor installation
        uses: actions/cache@v6
        with:
          path: ~/.meteor
          key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
          restore-keys: meteor-

      - name: Cache Meteor build (.meteor/local)
        uses: actions/cache@v6
        with:
          path: .meteor/local
          key: meteor-local-unit-${{ hashFiles('.meteor/versions') }}
          restore-keys: meteor-local-unit-

      - name: Install Meteor
        run: |
          if [ ! -x ~/.meteor/meteor ]; then
            curl https://install.meteor.com/ | sh
          fi
          echo "$HOME/.meteor" >> "$GITHUB_PATH"

      - name: Install npm dependencies
        run: npm ci

      - name: Run Meteor unit tests
        # First Meteor build is slow; allow time before the test driver starts.
        timeout-minutes: 45
        # NOTE: do not set TEST_WATCH here. meteortesting:mocha computes
        # `testWatch: TEST_WATCH || …`, and the string '0' is truthy in JS, so
        # TEST_WATCH=0 turns ON watch mode and the process never exits - that is
        # what made this job hang until the 45-minute timeout. With TEST_WATCH
        # unset, `meteor test --once` runs the server tests and exits with the
        # correct status (matching the local rebuild-wekan.sh run).
        run: meteor test --once --driver-package meteortesting:mocha

  e2e-tests:
    name: Playwright E2E (${{ matrix.project }} ${{ matrix.shard }}/2)
    runs-on: ubuntu-24.04
    needs: unit-tests
    strategy:
      fail-fast: false
      matrix:
        # Playwright bundles Chromium, Firefox and WebKit builds that all run
        # headless on the Linux runner (installed per-job below with --with-deps).
        project: [chromium, firefox, webkit]
        # Split each browser's ~245 specs across 2 parallel shards (6 jobs total)
        # to cut wall-clock time. Each shard job starts its own WeKan + Mongo, so
        # there is no shared-server contention between shards. Raise the shard
        # count (here and in the --shard flag below) once the baseline is green
        # and/or once E2E runs against a prebuilt bundle (cheaper per-job startup).
        shard: [1, 2]

    services:
      mongodb:
        image: mongo:7
        ports: ['27017:27017']
        options: >-
          --health-cmd "mongosh --eval 'db.adminCommand(\"ping\")'"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v7

      - name: Setup Node 24
        uses: actions/setup-node@v6
        with:
          node-version: '24'
          cache: 'npm'

      - name: Cache Meteor installation
        uses: actions/cache@v6
        with:
          path: ~/.meteor
          key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
          restore-keys: meteor-

      - name: Cache Meteor build (.meteor/local)
        uses: actions/cache@v6
        with:
          path: .meteor/local
          key: meteor-local-e2e-${{ hashFiles('.meteor/versions') }}
          restore-keys: meteor-local-e2e-

      - name: Install Meteor
        run: |
          if [ ! -x ~/.meteor/meteor ]; then
            curl https://install.meteor.com/ | sh
          fi
          echo "$HOME/.meteor" >> "$GITHUB_PATH"

      - name: Install app npm dependencies
        run: npm ci

      - name: Install Playwright test dependencies
        run: npm ci
        working-directory: tests/playwright

      - name: Install Playwright browsers
        run: npx playwright install --with-deps ${{ matrix.project }}
        working-directory: tests/playwright

      - name: Build and start WeKan (production bundle)
        run: |
          # Run E2E against a PRODUCTION build instead of `meteor run` (dev mode).
          # In dev the client JS is served by the rspack dev server, which did not
          # serve the bundle reliably in headless CI: the browser received HTML for
          # the bundle ("Unexpected token '<'"), Meteor never initialised, and every
          # spec timed out. A production build bakes the client JS into the bundle,
          # served statically by the Meteor server at :3000.
          meteor build ./output --directory
          ( cd output/bundle/programs/server && npm install )
          # WeKan's server/00checkStartup.js refuses to start unless WRITABLE_PATH
          # (used for attachment/file storage) is set and writable.
          mkdir -p "$PWD/wekan-writable"
          # Match the env the dev runner (rebuild-wekan.sh) and the specs expect:
          # WITH_API enables the REST API (specs 17/23/24/28), comment-editor
          # flag matches dev.
          MONGO_URL=mongodb://localhost:27017/wekan_test \
          ROOT_URL=http://localhost:3000 \
          PORT=3000 \
          WRITABLE_PATH="$PWD/wekan-writable" \
          WITH_API=true \
          RICHER_CARD_COMMENT_EDITOR=false \
          node output/bundle/main.js > wekan-run.log 2>&1 &
          # The production server serves the client JS statically, so once :3000
          # responds the bundle is being served. Wait up to 10 minutes.
          for i in $(seq 1 60); do
            if curl -sf http://localhost:3000 > /dev/null; then
              echo "WeKan is up after $((i * 10))s"
              break
            fi
            echo "Waiting for WeKan... attempt $i/60"
            sleep 10
          done
          if ! curl -sf http://localhost:3000 > /dev/null; then
            echo "WeKan failed to start. Last 250 log lines:"
            tail -n 250 wekan-run.log
            exit 1
          fi
        env:
          TOOL_NODE_FLAGS: --max-old-space-size=8192
          NODE_OPTIONS: --max-old-space-size=8192
          DISABLE_CAPTCHA: 'true' 

      - name: Run Playwright tests (${{ matrix.project }} shard ${{ matrix.shard }}/2)
        run: |
          npx playwright test \
            --project=${{ matrix.project }} \
            --shard=${{ matrix.shard }}/2 \
            --reporter=github
        working-directory: tests/playwright
        env:
          WEKAN_BASE_URL: http://localhost:3000
          WEKAN_MONGO_URL: mongodb://localhost:27017/wekan_test
          # Make the firefox and webkit projects available to --project
          # (playwright.config.js only defines them when this is set).
          WEKAN_PLAYWRIGHT_ALL: '1'

      - name: Upload Playwright report
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: playwright-report-${{ matrix.project }}-shard${{ matrix.shard }}
          path: tests/playwright/playwright-report/
          retention-days: 14

      - name: Upload test results (screenshots / videos)
        if: failure()
        uses: actions/upload-artifact@v7
        with:
          name: test-results-${{ matrix.project }}-shard${{ matrix.shard }}
          path: tests/playwright/test-results/
          retention-days: 7

  e2e-regression:
    name: Puppeteer regression suite
    runs-on: ubuntu-24.04
    needs: unit-tests

    services:
      mongodb:
        image: mongo:7
        ports: ['27017:27017']
        options: >-
          --health-cmd "mongosh --eval 'db.adminCommand(\"ping\")'"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v7

      - name: Setup Node 24
        uses: actions/setup-node@v6
        with:
          node-version: '24'
          cache: 'npm'

      - name: Cache Meteor installation
        uses: actions/cache@v6
        with:
          path: ~/.meteor
          key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
          restore-keys: meteor-

      - name: Cache Meteor build (.meteor/local)
        uses: actions/cache@v6
        with:
          path: .meteor/local
          key: meteor-local-regression-${{ hashFiles('.meteor/versions') }}
          restore-keys: meteor-local-regression-

      - name: Install Meteor
        run: |
          if [ ! -x ~/.meteor/meteor ]; then
            curl https://install.meteor.com/ | sh
          fi
          echo "$HOME/.meteor" >> "$GITHUB_PATH"

      - name: Install npm dependencies
        run: npm ci

      - name: Install Chromium
        run: sudo apt-get install -y chromium-browser

      - name: Build and start WeKan (production bundle)
        run: |
          # Run a PRODUCTION build, not `meteor run` (dev): exactly like the
          # Playwright e2e job, because the rspack dev server does not serve the
          # client JS bundle in headless CI, so Meteor never initialises and the
          # puppeteer regression test times out in waitForMeteorGlobals(). The
          # production bundle serves the client statically. WRITABLE_PATH is
          # required by server/00checkStartup.js; WITH_API mirrors the dev runner.
          meteor build ./output --directory
          ( cd output/bundle/programs/server && npm install )
          mkdir -p "$PWD/wekan-writable"
          MONGO_URL=mongodb://localhost:27017/wekan_regression \
          ROOT_URL=http://localhost:3000 \
          PORT=3000 \
          WRITABLE_PATH="$PWD/wekan-writable" \
          WITH_API=true \
          RICHER_CARD_COMMENT_EDITOR=false \
          node output/bundle/main.js > wekan-run.log 2>&1 &
          for i in $(seq 1 60); do
            if curl -sf http://localhost:3000 > /dev/null; then
              echo "WeKan is up after $((i * 10))s"
              break
            fi
            echo "Waiting for WeKan... attempt $i/60"
            sleep 10
          done
          if ! curl -sf http://localhost:3000 > /dev/null; then
            echo "WeKan failed to start. Last 250 log lines:"
            tail -n 250 wekan-run.log
            exit 1
          fi
        env:
          TOOL_NODE_FLAGS: --max-old-space-size=8192
          NODE_OPTIONS: --max-old-space-size=8192
          DISABLE_CAPTCHA: 'true'

      - name: Run list-regressions e2e
        run: node tests/e2e/list-regressions.js
        env:
          WEKAN_BASE_URL: http://localhost:3000
          WEKAN_MONGO_URL: mongodb://localhost:27017/wekan_regression
          CHROMIUM_PATH: /usr/bin/chromium-browser
          HEADLESS: 'true'

The same workflow, on Latchkey

Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.

name: Playwright E2E Tests
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
concurrency:
  group: playwright-${{ github.ref }}
  cancel-in-progress: true
 
permissions:
  contents: read
 
jobs:
  unit-tests:
    timeout-minutes: 30
    name: Meteor unit tests
    runs-on: latchkey-small
    steps:
      - uses: actions/checkout@v7
 
      - name: Setup Node 24
        uses: actions/setup-node@v6
        with:
          node-version: '24'
          cache: 'npm'
 
      - name: Cache Meteor installation
        uses: actions/cache@v6
        with:
          path: ~/.meteor
          key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
          restore-keys: meteor-
 
      - name: Cache Meteor build (.meteor/local)
        uses: actions/cache@v6
        with:
          path: .meteor/local
          key: meteor-local-unit-${{ hashFiles('.meteor/versions') }}
          restore-keys: meteor-local-unit-
 
      - name: Install Meteor
        run: |
          if [ ! -x ~/.meteor/meteor ]; then
            curl https://install.meteor.com/ | sh
          fi
          echo "$HOME/.meteor" >> "$GITHUB_PATH"
 
      - name: Install npm dependencies
        run: npm ci
 
      - name: Run Meteor unit tests
        # First Meteor build is slow; allow time before the test driver starts.
        timeout-minutes: 45
        # NOTE: do not set TEST_WATCH here. meteortesting:mocha computes
        # `testWatch: TEST_WATCH || …`, and the string '0' is truthy in JS, so
        # TEST_WATCH=0 turns ON watch mode and the process never exits - that is
        # what made this job hang until the 45-minute timeout. With TEST_WATCH
        # unset, `meteor test --once` runs the server tests and exits with the
        # correct status (matching the local rebuild-wekan.sh run).
        run: meteor test --once --driver-package meteortesting:mocha
 
  e2e-tests:
    timeout-minutes: 30
    name: Playwright E2E (${{ matrix.project }} ${{ matrix.shard }}/2)
    runs-on: latchkey-small
    needs: unit-tests
    strategy:
      fail-fast: false
      matrix:
        # Playwright bundles Chromium, Firefox and WebKit builds that all run
        # headless on the Linux runner (installed per-job below with --with-deps).
        project: [chromium, firefox, webkit]
        # Split each browser's ~245 specs across 2 parallel shards (6 jobs total)
        # to cut wall-clock time. Each shard job starts its own WeKan + Mongo, so
        # there is no shared-server contention between shards. Raise the shard
        # count (here and in the --shard flag below) once the baseline is green
        # and/or once E2E runs against a prebuilt bundle (cheaper per-job startup).
        shard: [1, 2]
 
    services:
      mongodb:
        image: mongo:7
        ports: ['27017:27017']
        options: >-
          --health-cmd "mongosh --eval 'db.adminCommand(\"ping\")'"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
 
    steps:
      - uses: actions/checkout@v7
 
      - name: Setup Node 24
        uses: actions/setup-node@v6
        with:
          node-version: '24'
          cache: 'npm'
 
      - name: Cache Meteor installation
        uses: actions/cache@v6
        with:
          path: ~/.meteor
          key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
          restore-keys: meteor-
 
      - name: Cache Meteor build (.meteor/local)
        uses: actions/cache@v6
        with:
          path: .meteor/local
          key: meteor-local-e2e-${{ hashFiles('.meteor/versions') }}
          restore-keys: meteor-local-e2e-
 
      - name: Install Meteor
        run: |
          if [ ! -x ~/.meteor/meteor ]; then
            curl https://install.meteor.com/ | sh
          fi
          echo "$HOME/.meteor" >> "$GITHUB_PATH"
 
      - name: Install app npm dependencies
        run: npm ci
 
      - name: Install Playwright test dependencies
        run: npm ci
        working-directory: tests/playwright
 
      - name: Install Playwright browsers
        run: npx playwright install --with-deps ${{ matrix.project }}
        working-directory: tests/playwright
 
      - name: Build and start WeKan (production bundle)
        run: |
          # Run E2E against a PRODUCTION build instead of `meteor run` (dev mode).
          # In dev the client JS is served by the rspack dev server, which did not
          # serve the bundle reliably in headless CI: the browser received HTML for
          # the bundle ("Unexpected token '<'"), Meteor never initialised, and every
          # spec timed out. A production build bakes the client JS into the bundle,
          # served statically by the Meteor server at :3000.
          meteor build ./output --directory
          ( cd output/bundle/programs/server && npm install )
          # WeKan's server/00checkStartup.js refuses to start unless WRITABLE_PATH
          # (used for attachment/file storage) is set and writable.
          mkdir -p "$PWD/wekan-writable"
          # Match the env the dev runner (rebuild-wekan.sh) and the specs expect:
          # WITH_API enables the REST API (specs 17/23/24/28), comment-editor
          # flag matches dev.
          MONGO_URL=mongodb://localhost:27017/wekan_test \
          ROOT_URL=http://localhost:3000 \
          PORT=3000 \
          WRITABLE_PATH="$PWD/wekan-writable" \
          WITH_API=true \
          RICHER_CARD_COMMENT_EDITOR=false \
          node output/bundle/main.js > wekan-run.log 2>&1 &
          # The production server serves the client JS statically, so once :3000
          # responds the bundle is being served. Wait up to 10 minutes.
          for i in $(seq 1 60); do
            if curl -sf http://localhost:3000 > /dev/null; then
              echo "WeKan is up after $((i * 10))s"
              break
            fi
            echo "Waiting for WeKan... attempt $i/60"
            sleep 10
          done
          if ! curl -sf http://localhost:3000 > /dev/null; then
            echo "WeKan failed to start. Last 250 log lines:"
            tail -n 250 wekan-run.log
            exit 1
          fi
        env:
          TOOL_NODE_FLAGS: --max-old-space-size=8192
          NODE_OPTIONS: --max-old-space-size=8192
          DISABLE_CAPTCHA: 'true' 
 
      - name: Run Playwright tests (${{ matrix.project }} shard ${{ matrix.shard }}/2)
        run: |
          npx playwright test \
            --project=${{ matrix.project }} \
            --shard=${{ matrix.shard }}/2 \
            --reporter=github
        working-directory: tests/playwright
        env:
          WEKAN_BASE_URL: http://localhost:3000
          WEKAN_MONGO_URL: mongodb://localhost:27017/wekan_test
          # Make the firefox and webkit projects available to --project
          # (playwright.config.js only defines them when this is set).
          WEKAN_PLAYWRIGHT_ALL: '1'
 
      - name: Upload Playwright report
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: playwright-report-${{ matrix.project }}-shard${{ matrix.shard }}
          path: tests/playwright/playwright-report/
          retention-days: 14
 
      - name: Upload test results (screenshots / videos)
        if: failure()
        uses: actions/upload-artifact@v7
        with:
          name: test-results-${{ matrix.project }}-shard${{ matrix.shard }}
          path: tests/playwright/test-results/
          retention-days: 7
 
  e2e-regression:
    timeout-minutes: 30
    name: Puppeteer regression suite
    runs-on: latchkey-small
    needs: unit-tests
 
    services:
      mongodb:
        image: mongo:7
        ports: ['27017:27017']
        options: >-
          --health-cmd "mongosh --eval 'db.adminCommand(\"ping\")'"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
 
    steps:
      - uses: actions/checkout@v7
 
      - name: Setup Node 24
        uses: actions/setup-node@v6
        with:
          node-version: '24'
          cache: 'npm'
 
      - name: Cache Meteor installation
        uses: actions/cache@v6
        with:
          path: ~/.meteor
          key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
          restore-keys: meteor-
 
      - name: Cache Meteor build (.meteor/local)
        uses: actions/cache@v6
        with:
          path: .meteor/local
          key: meteor-local-regression-${{ hashFiles('.meteor/versions') }}
          restore-keys: meteor-local-regression-
 
      - name: Install Meteor
        run: |
          if [ ! -x ~/.meteor/meteor ]; then
            curl https://install.meteor.com/ | sh
          fi
          echo "$HOME/.meteor" >> "$GITHUB_PATH"
 
      - name: Install npm dependencies
        run: npm ci
 
      - name: Install Chromium
        run: sudo apt-get install -y chromium-browser
 
      - name: Build and start WeKan (production bundle)
        run: |
          # Run a PRODUCTION build, not `meteor run` (dev): exactly like the
          # Playwright e2e job, because the rspack dev server does not serve the
          # client JS bundle in headless CI, so Meteor never initialises and the
          # puppeteer regression test times out in waitForMeteorGlobals(). The
          # production bundle serves the client statically. WRITABLE_PATH is
          # required by server/00checkStartup.js; WITH_API mirrors the dev runner.
          meteor build ./output --directory
          ( cd output/bundle/programs/server && npm install )
          mkdir -p "$PWD/wekan-writable"
          MONGO_URL=mongodb://localhost:27017/wekan_regression \
          ROOT_URL=http://localhost:3000 \
          PORT=3000 \
          WRITABLE_PATH="$PWD/wekan-writable" \
          WITH_API=true \
          RICHER_CARD_COMMENT_EDITOR=false \
          node output/bundle/main.js > wekan-run.log 2>&1 &
          for i in $(seq 1 60); do
            if curl -sf http://localhost:3000 > /dev/null; then
              echo "WeKan is up after $((i * 10))s"
              break
            fi
            echo "Waiting for WeKan... attempt $i/60"
            sleep 10
          done
          if ! curl -sf http://localhost:3000 > /dev/null; then
            echo "WeKan failed to start. Last 250 log lines:"
            tail -n 250 wekan-run.log
            exit 1
          fi
        env:
          TOOL_NODE_FLAGS: --max-old-space-size=8192
          NODE_OPTIONS: --max-old-space-size=8192
          DISABLE_CAPTCHA: 'true'
 
      - name: Run list-regressions e2e
        run: node tests/e2e/list-regressions.js
        env:
          WEKAN_BASE_URL: http://localhost:3000
          WEKAN_MONGO_URL: mongodb://localhost:27017/wekan_regression
          CHROMIUM_PATH: /usr/bin/chromium-browser
          HEADLESS: 'true'
 

What changed

What Latchkey heals here

This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:

This workflow runs 3 jobs (8 with the matrix expanded) per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow