Playwright E2E Tests workflow (wekan/wekan)
The Playwright E2E Tests workflow from wekan/wekan, explained and optimized by Latchkey.
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.
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
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
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Add a job timeout so a hung step cannot burn hours of runner time.
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:
- Dependency installs
- End-to-end and browser tests
- Network fetches
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.