Skip to content

E2E_API Tests

E2E_API Tests #4859

Workflow file for this run

name: E2E_API Tests
on:
# Runs once a day only, to keep Actions cost down. Pull requests and pushes
# to develop no longer trigger this suite -- use the "Run workflow" button
# (workflow_dispatch) below to test a branch on demand.
# Target start time is 9:00 AM BDT. GitHub queues scheduled runs and
# releases them once capacity frees up: on this repository that delay has
# been roughly 70 minutes every day, so 02:00 UTC (nominally 8:00 AM BDT)
# is what actually starts the run near 9:00 AM BDT. Check the real start
# times of recent scheduled runs before "correcting" this to 03:00 UTC.
schedule:
# 02:00 UTC -- the gating run, on the pinned WordPress/WooCommerce versions.
- cron: 0 2 * * *
# 14:00 UTC -- the canary run, on whatever WordPress/WooCommerce is current.
# It warns and never gates: see WP_STACK below and the continue-on-error on
# the test jobs. Change this cron and you must change the WP_STACK expression.
- cron: 0 14 * * *
# workflow can be manually triggered
workflow_dispatch:
inputs:
testsuite:
description: Choose which test suite to run
default: All
type: choice
options:
- E2E
- API
- All
wp_stack:
description: 'pinned = the versions committed in .wp-env*.json; latest = resolve WordPress and WooCommerce to current (failures do not gate)'
default: pinned
type: choice
options:
- pinned
- latest
# Cancels all previous workflow runs for pull requests that have not completed.
concurrency:
# The concurrency group contains the workflow name and the branch name for pull requests or the commit hash for any other events.
# The stack is part of the group so the pinned run and the latest-canary never cancel
# each other on the same commit.
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}-${{ inputs.wp_stack || github.event.schedule || 'pinned' }}
cancel-in-progress: true
# workflow permissions
permissions:
checks: write
contents: write
issues: write
pull-requests: write
# workflow environment variables
env:
# Which WordPress/WooCommerce versions this run installs.
# pinned -- the versions committed in .wp-env*.json (the gating lane)
# latest -- whatever WordPress and WooCommerce are current (the canary lane)
# Sourced from the workflow_dispatch input, else from which cron fired, else pinned.
# `inputs.wp_stack` is empty (falsy) on scheduled runs, so the fallbacks apply.
WP_STACK: ${{ inputs.wp_stack || (github.event.schedule == '0 14 * * *' && 'latest') || 'pinned' }}
ADMIN: admin
ADMIN_PASSWORD: password
VENDOR: vendor1
VENDOR2: vendor2
CUSTOMER: customer1
CUSTOMER2: customer2
USER_PASSWORD: 01dokan01
GMAP: ${{secrets.GMAP}}
MAPBOX: ${{secrets.MAPBOX}}
LICENSE_KEY: ${{secrets.LICENSE_KEY}}
# Stripe Express test keys + the real EXPRESS connected accounts (all secrets) so the money/checkout
# specs RUN in CI. The connected accounts MUST belong to the Express test platform — the Connect-suite
# STRIPE_VENDOR*_ACCT secrets are a different platform and cannot receive Express transfers, so these
# use distinct STRIPE_EXPRESS_VENDOR*_ACCT secret names.
TEST_PUBLISH_KEY_STRIPE_EXPRESS: ${{secrets.TEST_PUBLISH_KEY_STRIPE_EXPRESS}}
TEST_SECRET_KEY_STRIPE_EXPRESS: ${{secrets.TEST_SECRET_KEY_STRIPE_EXPRESS}}
STRIPE_EXPRESS_VENDOR1_ACCT: ${{secrets.STRIPE_EXPRESS_VENDOR1_ACCT}}
STRIPE_EXPRESS_VENDOR2_ACCT: ${{secrets.STRIPE_EXPRESS_VENDOR2_ACCT}}
STRIPE_EXPRESS_REQUIRED: true
BASE_URL: http://localhost:9999
CI: true
HEADLESS: true
# TODO: Remove this after testing
DOKAN_PRO: true
NO_SETUP: false
FORCE_COLOR: 1
DB_HOST_NAME: localhost
DB_USER_NAME: root
DB_USER_PASSWORD: password
DB_PORT: 9998
DATABASE: tests-wordpress
DB_PREFIX: wp
PR_NUMBER: ${{ github.event.number }}
SHA: ${{ github.event.pull_request.head.sha }}
SYSTEM_INFO: ./tests/pw/playwright/systemInfo.json
API_TEST_RESULT: ./tests/pw/playwright-report/api/summary-report/results.json
E2E_TEST_RESULT: ./tests/pw/playwright-report/e2e/summary-report/results.json
API_COVERAGE: ./tests/pw/playwright-report/api/coverage-report/coverage.json
E2E_COVERAGE: ./tests/pw/playwright-report/e2e/coverage-report/coverage.json
jobs:
# Build dokan-lite and dokan-pro once. Two parallel jobs (build_lite, build_pro)
# so the slower of the two (~6 min) gates downstream jobs instead of ~12 min serial.
# Each consumer job downloads the tar artifacts and skips its own build steps.
build_lite:
name: build dokan-lite
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout testing repo
uses: actions/checkout@v4
- name: Use desired version of NodeJS
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Composer install and build (Dokan-lite)
run: |
composer i || composer update
composer i --no-dev -o || composer update --no-dev -o
# node_modules cache: skips ~2-3 min of `npm ci` on cache hit.
# Keyed on the lockfile so any dependency change invalidates the
# cache automatically. Same key is restored in build_pro to skip
# the redundant lite install there too.
- name: Cache lite node_modules
id: lite-nm
uses: actions/cache@v4
with:
path: node_modules
key: lite-nm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Npm install (Dokan-lite)
if: steps.lite-nm.outputs.cache-hit != 'true'
run: |
npm ci --prefer-offline --no-audit --no-fund || npm i
- name: Npm build (Dokan-lite)
run: |
npm run build
# Tar the runtime tree (excludes node_modules, .git, dev-only output).
# tar preserves perms + symlinks unlike upload-artifact's zip path.
- name: Tar dokan-lite build output
run: |
tar --exclude=./.git \
--exclude=./node_modules \
--exclude=./tests/pw/node_modules \
--exclude=./tests/pw/playwright \
--exclude=./tests/pw/playwright-report \
--exclude=./tests/pw/wp-data \
--exclude=./tests/pw/test-results \
-czf /tmp/dokan-lite-build.tar.gz .
- name: Upload dokan-lite build artifact
uses: actions/upload-artifact@v4
with:
name: dokan-lite-build
path: /tmp/dokan-lite-build.tar.gz
retention-days: 1
compression-level: 0
build_pro:
name: build dokan-pro
runs-on: ubuntu-latest
timeout-minutes: 15
# Only run when secrets are available (i.e. not a fork PR). Same gate as today.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
steps:
# Pro's webpack.config.js calls getDokanLitePath() which resolves
# ../dokan-lite from inside the pro folder, and also requires
# lite-root files like webpack-dependency-mapping.js that pull in
# lodash from lite's node_modules. So lite needs both source AND
# node_modules in this job — but NOT a full build (we never use
# lite's assets/). Replicate the layout: lite at runner root with
# a self-symlink, pro as sibling.
- name: Checkout dokan-lite source (for pro webpack path resolution)
uses: actions/checkout@v4
- name: Prepare dokan-lite folder
run: |
ln -s . dokan-lite
- name: Checkout Dokan Pro
uses: actions/checkout@v4
with:
repository: getdokan/dokan-pro
ref: develop
path: dokan-pro
token: ${{ secrets.DOKAN_PRO_TOKEN }}
- name: Checkout Dokan Invoice
uses: actions/checkout@v4
with:
repository: getdokan/dokan-invoice
ref: develop
path: dokan-invoice
- name: Use desired version of NodeJS
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: |
package-lock.json
dokan-pro/package-lock.json
# Share the same node_modules cache that build_lite produced.
# Same key, so a cache hit here means pro skips the ~3 min of
# redundant lite install entirely. On cache miss we still fall
# back to npm ci (e.g., after a lockfile change).
- name: Cache lite node_modules
id: lite-nm
uses: actions/cache@v4
with:
path: node_modules
key: lite-nm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
# Install lite's node_modules so dokan-pro/webpack.config.js can
# require lite-root JS helpers (webpack-dependency-mapping.js etc).
# Skip `npm run build` — we don't ship lite's assets from this job.
- name: Install dokan-lite npm deps (no build, skip on cache hit)
if: steps.lite-nm.outputs.cache-hit != 'true'
run: |
npm ci --prefer-offline --no-audit --no-fund || npm i
# Cache pro's own node_modules separately (its lockfile is independent).
- name: Cache pro node_modules
id: pro-nm
uses: actions/cache@v4
with:
path: dokan-pro/node_modules
key: pro-nm-${{ runner.os }}-${{ hashFiles('dokan-pro/package-lock.json') }}
- name: Build Dokan Pro
working-directory: dokan-pro
run: |
composer i --no-dev -o
if [ "${{ steps.pro-nm.outputs.cache-hit }}" != "true" ]; then
npm ci --prefer-offline --no-audit --no-fund || npm i
fi
npm run build
# Fetch only tests/plugins from test_utils branch (premium plugins not in default branch)
- name: Fetch WooCommerce premium plugins from test_utils
working-directory: dokan-pro
run: |
git fetch origin test_utils
git checkout origin/test_utils -- tests/plugins
# Extract any zipped premium plugins collected from test_utils
- name: Collect WooCommerce premium plugins
run: |
PLUGINS_DIR="dokan-pro/tests/plugins"
for zip in "$PLUGINS_DIR"/*.zip; do
[ -f "$zip" ] || continue
plugin_dir="${zip%.zip}"
if [ ! -d "$plugin_dir" ]; then
echo "Extracting: $zip"
unzip -q "$zip" -d "$PLUGINS_DIR"
fi
done
echo "Available premium plugins:"
ls -la "$PLUGINS_DIR"
- name: Tar dokan-pro + dokan-invoice
run: |
tar --exclude='dokan-pro/.git' \
--exclude='dokan-pro/node_modules' \
--exclude='dokan-invoice/.git' \
-czf /tmp/dokan-pro-build.tar.gz dokan-pro dokan-invoice
- name: Upload dokan-pro build artifact
uses: actions/upload-artifact@v4
with:
name: dokan-pro-build
path: /tmp/dokan-pro-build.tar.gz
retention-days: 1
compression-level: 0
e2e_tests:
name: e2e tests
runs-on: ubuntu-latest
timeout-minutes: 75
# The canary lane warns, it never gates. `env` is not readable from a job-level
# continue-on-error, so the WP_STACK expression is repeated here.
continue-on-error: ${{ inputs.wp_stack == 'latest' || github.event.schedule == '0 14 * * *' }}
# Wait for both build jobs. build_pro is conditional (fork PRs) and may not run;
# `always() && build_lite succeeded` covers the fork-PR path too.
needs: [build_lite, build_pro]
if: always() && needs.build_lite.result == 'success' && (needs.build_pro.result == 'success' || needs.build_pro.result == 'skipped')
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
shardTotal: [12]
steps:
- name: Download dokan-lite build artifact
uses: actions/download-artifact@v4
with:
name: dokan-lite-build
path: /tmp/
- name: Extract dokan-lite build
id: clone-dokan-lite
run: |
tar -xzf /tmp/dokan-lite-build.tar.gz
rm /tmp/dokan-lite-build.tar.gz
ln -s . dokan-lite
- name: Download dokan-pro build artifact
if: needs.build_pro.result == 'success'
uses: actions/download-artifact@v4
with:
name: dokan-pro-build
path: /tmp/
- name: Extract dokan-pro build
if: needs.build_pro.result == 'success'
run: |
tar -xzf /tmp/dokan-pro-build.tar.gz
rm /tmp/dokan-pro-build.tar.gz
- name: Use desired version of NodeJS
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: tests/pw/package-lock.json
- name: Cache test deps (tests/pw/node_modules)
id: pw-nm
uses: actions/cache@v4
with:
path: tests/pw/node_modules
key: pw-nm-${{ runner.os }}-${{ hashFiles('tests/pw/package-lock.json') }}
- name: Install test dependencies
if: steps.pw-nm.outputs.cache-hit != 'true'
working-directory: tests/pw
run: |
npm ci --prefer-offline --no-audit --no-fund || npm i
- name: Create wp debuglog file
id: debug-log
working-directory: tests/pw
run: |
mkdir -p wp-data && touch wp-data/debug.log
# Use CI config (../../dokan-pro) when dokan-pro present; lite-only when not (fork PR)
- name: Prepare wp-env config
working-directory: tests/pw
run: |
if [ -d "../../dokan-pro" ]; then
echo "Dokan Pro present, using .wp-env.ci.json"
cp .wp-env.ci.json .wp-env.override.json
else
echo "Dokan Pro not present, using lite-only wp-env config"
cp .wp-env.json .wp-env.override.json
fi
# Canary lane: drop the version pins so wp-env resolves whatever
# WordPress and WooCommerce are current. Gating lane is untouched.
if [ "$WP_STACK" = "latest" ]; then
jq '.env.tests.core = null
| .env.tests.plugins |= map(sub("/woocommerce\\.[0-9][0-9.]*\\.zip$"; "/woocommerce.zip"))' \
.wp-env.override.json > .wp-env.override.json.tmp
mv .wp-env.override.json.tmp .wp-env.override.json
echo "WP_STACK=latest — core and WooCommerce unpinned"
fi
# Route Docker Hub pulls through Google's public pull-through mirror. The wp-env images
# (mariadb, phpmyadmin, wordpress, composer — all official library/* images) then come
# from the mirror instead of registry-1.docker.io, dodging Docker Hub's anonymous
# pull-rate-limit / connection timeouts that randomly fail one shard's `wp-env start`.
# No credentials needed; Docker falls back to Docker Hub if the mirror misses an image.
- name: Route Docker Hub pulls through a mirror (avoids anonymous rate-limit/timeouts)
run: |
# Merge into any existing daemon.json (jq is preinstalled on the runner) so we
# never clobber the runner's own Docker config; create it only if absent.
sudo mkdir -p /etc/docker
if [ -s /etc/docker/daemon.json ]; then
sudo jq '. + {"registry-mirrors": ["https://mirror.gcr.io"]}' /etc/docker/daemon.json | sudo tee /etc/docker/daemon.json.tmp >/dev/null
sudo mv /etc/docker/daemon.json.tmp /etc/docker/daemon.json
else
echo '{ "registry-mirrors": ["https://mirror.gcr.io"] }' | sudo tee /etc/docker/daemon.json >/dev/null
fi
sudo systemctl restart docker
# Wait for the daemon socket instead of a fixed sleep.
for i in $(seq 1 15); do docker info >/dev/null 2>&1 && break; sleep 1; done
# wp-env pulls four Docker Hub library images on every run: mariadb:lts and
# phpmyadmin via compose, plus the wordpress / wordpress:cli build bases.
# Anonymous Docker Hub pulls from shared runner IPs randomly time out even
# with the gcr mirror configured (the daemon silently falls back to
# registry-1.docker.io when the mirror misses), so pre-seed the images:
# restore them from the actions cache, else pull from AWS's public mirror
# of Docker official images and retag. Once the images exist locally,
# compose/build skip the Docker Hub pull entirely. The ISO week in the
# cache key stops the floating tags (lts, latest) from pinning forever.
- name: Compute wp-env image list and cache key
id: wp-env-images
working-directory: tests/pw
run: |
PHP=$(jq -r '.env.tests.phpVersion // empty' .wp-env.override.json)
if [ -z "$PHP" ]; then PHP=$(jq -r '.env.tests.phpVersion // empty' .wp-env.json); fi
IMAGES="mariadb:lts phpmyadmin:latest wordpress${PHP:+:php$PHP} wordpress:cli${PHP:+-php$PHP}"
echo "list=$IMAGES" >> "$GITHUB_OUTPUT"
echo "key=wp-env-images-v1-php${PHP:-default}-$(date +%G-%V)" >> "$GITHUB_OUTPUT"
echo "images: $IMAGES"
- name: Restore wp-env docker images from cache
uses: actions/cache@v4
with:
path: ~/.cache/wp-env-images
key: ${{ steps.wp-env-images.outputs.key }}
- name: Pre-seed wp-env docker images
run: |
IMAGES="${{ steps.wp-env-images.outputs.list }}"
TAR="$HOME/.cache/wp-env-images/images.tar"
if [ -f "$TAR" ]; then
docker load -i "$TAR"
else
mkdir -p "$(dirname "$TAR")"
complete=true
for img in $IMAGES; do
pulled=false
for attempt in 1 2 3; do
if docker pull "public.ecr.aws/docker/library/$img"; then pulled=true; break; fi
sleep 15
done
if [ "$pulled" = true ]; then
docker tag "public.ecr.aws/docker/library/$img" "$img"
docker rmi "public.ecr.aws/docker/library/$img" >/dev/null
else
echo "::warning::could not pre-seed $img from the ECR mirror; wp-env will pull it from Docker Hub"
complete=false
fi
done
# Only cache a complete set so a partial failure is retried next run.
if [ "$complete" = true ]; then docker save -o "$TAR" $IMAGES; fi
fi
docker image ls
- name: Start WordPress Env
id: wp-env
uses: nick-fields/retry@v3
with:
timeout_minutes: 6
max_attempts: 3
# Space the attempts out: Docker Hub's anonymous throttle window
# outlives back-to-back retries, so waiting beats hammering.
retry_wait_seconds: 90
retry_on: error
command: |
cd tests/pw
npm run start:env
# What actually got installed, not what we asked for. On the canary lane this is
# the whole point of the run; on the gating lane it is the proof the pins held.
- name: Record resolved WordPress / WooCommerce versions
working-directory: tests/pw
run: |
# `--` is required: without it npm swallows wp-cli's own flags, so
# `--field=version` never reaches wp-cli, `wp plugin get` prints its
# full field/value table, no line starts with a digit, and the grep
# below yields "unknown" for WooCommerce on every single run.
ver() { npm run --silent wp-env -- run tests-cli "$@" 2>/dev/null | tr -d '\r' | grep -Eo '^[0-9]+\.[0-9.]+' | head -n1; }
WP=$(ver wp core version)
WC=$(ver wp plugin get woocommerce --field=version)
echo "stack=$WP_STACK wordpress=${WP:-unknown} woocommerce=${WC:-unknown}"
echo "- \`$WP_STACK\` lane — WordPress **${WP:-unknown}**, WooCommerce **${WC:-unknown}**" >> "$GITHUB_STEP_SUMMARY"
# Persist for merge-reports, which runs on a clean runner with no
# wp-env stack and so cannot resolve these itself. Rides along in the
# existing tests/pw/playwright artifact — no new upload needed.
mkdir -p playwright
printf '{"stack":"%s","wordpress":"%s","woocommerce":"%s"}\n' \
"$WP_STACK" "${WP:-unknown}" "${WC:-unknown}" > playwright/resolved-versions.json
- name: Activate WooCommerce premium plugins
if: needs.build_pro.result == 'success'
working-directory: tests/pw
run: |
npm run wp-env run tests-cli wp plugin activate \
woocommerce-bookings \
woocommerce-subscriptions \
woocommerce-product-addons \
woocommerce-simple-auctions \
woocommerce-pdf-invoices-packing-slips \
dokan-pro \
dokan-invoice
- name: Enable Dokan New React UI (vendor dashboard + product editor)
working-directory: tests/pw
run: |
npm run wp-env run tests-cli wp eval '
$appearance = get_option("dokan_appearance", []);
$appearance["vendor_layout_style"] = "latest";
$appearance["vendor_product_editor"] = "latest";
update_option("dokan_appearance", $appearance);
echo "Dokan appearance set to latest UI" . PHP_EOL;
'
- name: Get installed Playwright version
id: playwright-version
working-directory: tests/pw
run: |
echo "PLAYWRIGHT_VERSION=$(npm ls @playwright/test --json | jq --raw-output '.dependencies["@playwright/test"].version')" >> "$GITHUB_ENV"
- name: Cache playwright binaries
id: playwright-cache
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install Playwright browser binaries & OS dependencies
id: pw-install
if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: tests/pw
run: |
npx playwright install --with-deps chromium
- name: Run docker setup
working-directory: tests/pw
run: npm run docker:setup
# Resolve the spec list for this shard from the committed duration baseline.
# Falls back to alphabetical sharding if the baseline is missing.
- name: Resolve balanced shard spec list
id: shard-specs
working-directory: tests/pw
run: |
node ./utils/getShardSpecs.js ${{ matrix.shardIndex }} ${{ matrix.shardTotal }} > /tmp/shard-specs.txt
echo "spec count: $(wc -l < /tmp/shard-specs.txt)"
# Emit a per-shard manifest (no GITHUB_STEP_SUMMARY write here — that
# would clutter the run page with 12 collapsed cards). merge-reports
# collects these manifests from artifacts and renders one consolidated
# shard feature map below the quality report.
- name: Write shard feature manifest
if: always()
working-directory: tests/pw
run: |
if [ -s /tmp/shard-specs.txt ]; then
mkdir -p playwright
node -e '
const fs = require("fs");
const specs = fs.readFileSync("/tmp/shard-specs.txt", "utf8").split("\n").filter(Boolean);
const features = [...new Set(specs.map(s => s.replace(/^tests\/e2e\//, "").split("/")[0]))].sort();
fs.writeFileSync("playwright/shard-features.json", JSON.stringify({
shardIndex: ${{ matrix.shardIndex }},
shardTotal: ${{ matrix.shardTotal }},
specCount: specs.length,
features
}, null, 2));
'
fi
# continue-on-error: the isolation re-run below decides this shard's result.
# `outcome` still reports the true first-pass result, so every later step that
# keys off steps.e2e-test.outcome keeps working unchanged.
- name: 🧪 Run e2e tests-${{ matrix.shardIndex }}
id: e2e-test
continue-on-error: true
if: success() && (github.event_name != 'workflow_dispatch' || ( github.event_name == 'workflow_dispatch' && (github.event.inputs.testsuite == 'E2E' || github.event.inputs.testsuite == 'All')))
timeout-minutes: 60
working-directory: tests/pw
run: |
# Pass the resolved spec list as positional args (xargs -d to preserve newlines)
if [ -s /tmp/shard-specs.txt ]; then
xargs -a /tmp/shard-specs.txt -d '\n' npm run test:e2e --
else
# No baseline — fall back to Playwright's built-in alphabetical sharding
npm run test:e2e -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
fi
# Rescue the first-pass traces/screenshots BEFORE anything else can delete them.
#
# Playwright CLEARS `outputDir` (playwright/e2e/test-artifacts, playwright.config.ts:15) at
# the START of every run, and two more runs follow this one: the isolation re-run below and
# the coverage run further down. By the time "Archive test artifacts" uploads
# tests/pw/playwright, the trace for the failure that actually mattered is long gone —
# verified on run 31362069120, whose shard-8 artifact contains only shard-features.json and
# systemInfo.json. Every CI-only failure therefore had to be diagnosed by guesswork from the
# error message alone.
#
# This is the same clobbering the summary-report steps below already work around; traces were
# simply never covered. Copy, don't move: the isolation re-run's own artifacts are still
# wanted, and a copy cannot break the run if the directory is absent.
- name: 🧾 Preserve first-pass traces before the re-run wipes them
if: always() && steps.e2e-test.outcome == 'failure'
working-directory: tests/pw
run: |
if [ -d playwright/e2e/test-artifacts ]; then
mkdir -p playwright/e2e/first-pass-artifacts
cp -R playwright/e2e/test-artifacts/. playwright/e2e/first-pass-artifacts/ || true
echo "preserved $(find playwright/e2e/first-pass-artifacts -type f | wc -l) file(s)"
else
echo "no test-artifacts directory — nothing to preserve"
fi
# Re-run ONLY the failed tests, after the whole shard has finished, without
# their shard-mates. This separates a transient failure from a reproducible one.
#
# Guard on a non-empty failedTests list: if the first pass died from a crash or a
# timeout rather than test failures, `--last-failed` would select zero tests and
# "pass", turning an infrastructure failure green. No recorded failures => no re-run,
# and the gate step below keeps the shard red.
#
# --reporter=list overrides the config reporters on purpose: the blob reporter would
# otherwise emit a second set of results for these same tests and merge-reports would
# double-count them. The merged HTML report therefore keeps showing the FIRST-pass
# failures, which is what actually happened in shard context.
- name: 🔁 Re-run shard failures in isolation
id: e2e-recheck
if: always() && steps.e2e-test.outcome == 'failure'
timeout-minutes: 30
working-directory: tests/pw
run: |
LAST_RUN=playwright/e2e/test-artifacts/.last-run.json
if [ ! -f "$LAST_RUN" ] || [ "$(node -e "try{process.stdout.write(String((require('./'+process.argv[1]).failedTests||[]).length))}catch(e){process.stdout.write('0')}" "$LAST_RUN")" = "0" ]; then
echo "::warning::No recorded test failures — the first pass failed for another reason (crash/timeout). Not re-running."
exit 1
fi
set -o pipefail
NO_SETUP=true npx playwright test --project=e2e_tests --last-failed \
--reporter=list 2>&1 | tee /tmp/recheck.log
# Always say out loud which tests were rescued by the isolation re-run. A test that
# fails with its shard-mates but passes alone is the signature of shard-state
# pollution (an earlier spec leaving a global option/module/product dirty), NOT a
# timing flake — Playwright's in-place `retries` cannot green that class, this can.
- name: 📋 Report isolation re-run outcome
if: always() && steps.e2e-test.outcome == 'failure'
working-directory: tests/pw
run: |
{
echo "### Shard ${{ matrix.shardIndex }} — failures re-run in isolation"
echo
if [ "${{ steps.e2e-recheck.outcome }}" = "success" ]; then
echo "⚠️ **These tests failed alongside their shard-mates but passed alone.**"
echo "The shard is reported green. Treat this as suspected shard-state pollution"
echo "and investigate the polluter — verify by running the earlier spec and reading"
echo "the global state back, not by re-running the test that failed."
else
echo "❌ **The re-run failed too — reproducible failures, not flakes.**"
fi
echo
echo '```'
if [ -f /tmp/recheck.log ]; then
grep -E '^[[:space:]]+(✓|✘|×)' /tmp/recheck.log | head -50
else
echo "(no re-run log — first pass failed without recorded test failures)"
fi
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
# The actual gate. The shard is red unless the isolation re-run cleared every failure.
- name: Fail the shard when the isolation re-run did not clear
if: always() && steps.e2e-test.outcome == 'failure' && steps.e2e-recheck.outcome != 'success'
run: |
echo "::error::Shard ${{ matrix.shardIndex }} failed and the isolation re-run did not clear it."
exit 1
# Snapshot the e2e summary before coverage runs — both projects use
# the same playwright.config.ts and write summaryReporter output to
# the same results.json, so the coverage teardown otherwise
# overwrites the actual e2e shard summary with its own (1-test)
# report and the merge step under-counts e2e by ~1000 tests.
- name: Preserve e2e summary before coverage
if: always() && steps.e2e-test.outcome != 'cancelled'
working-directory: tests/pw
run: |
if [ -f playwright-report/e2e/summary-report/results.json ]; then
cp playwright-report/e2e/summary-report/results.json \
playwright-report/e2e/summary-report/results.e2e.bak.json
fi
# Coverage walks the suite a second time. Skip on PRs to halve the
# per-shard test runtime; develop pushes still compute it.
- name: 🧪 Run e2e coverage
if: |
always()
&& (steps.e2e-test.outcome == 'success' || steps.e2e-test.outcome == 'failure')
&& github.event_name != 'pull_request'
working-directory: tests/pw
run: |
npm run test:e2e:coverage
- name: Restore e2e summary after coverage
if: always() && steps.e2e-test.outcome != 'cancelled'
working-directory: tests/pw
run: |
if [ -f playwright-report/e2e/summary-report/results.e2e.bak.json ]; then
mv playwright-report/e2e/summary-report/results.e2e.bak.json \
playwright-report/e2e/summary-report/results.json
fi
- name: Backup Database
if: always() && steps.wp-env.outcome == 'success'
working-directory: tests/pw
run: |
npm run wp-env run tests-cli wp db export wp-data/db.sql
- name: Archive test artifacts (screenshots, HTML snapshots, Reports)
if: always() && steps.debug-log.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: test-artifact-e2e-${{ matrix.shardIndex }}
path: |
tests/pw/wp-data
tests/pw/playwright
tests/pw/playwright-report
if-no-files-found: ignore
retention-days: 1
api_tests:
name: api tests
runs-on: ubuntu-latest
timeout-minutes: 25
# See e2e_tests: the canary lane warns, it never gates.
continue-on-error: ${{ inputs.wp_stack == 'latest' || github.event.schedule == '0 14 * * *' }}
needs: [build_lite, build_pro]
if: always() && needs.build_lite.result == 'success' && (needs.build_pro.result == 'success' || needs.build_pro.result == 'skipped')
strategy:
fail-fast: false
matrix:
shardIndex: [1]
shardTotal: [1]
steps:
- name: Download dokan-lite build artifact
uses: actions/download-artifact@v4
with:
name: dokan-lite-build
path: /tmp/
- name: Extract dokan-lite build
id: clone-dokan-lite
run: |
tar -xzf /tmp/dokan-lite-build.tar.gz
rm /tmp/dokan-lite-build.tar.gz
ln -s . dokan-lite
- name: Download dokan-pro build artifact
if: needs.build_pro.result == 'success'
uses: actions/download-artifact@v4
with:
name: dokan-pro-build
path: /tmp/
- name: Extract dokan-pro build
if: needs.build_pro.result == 'success'
run: |
tar -xzf /tmp/dokan-pro-build.tar.gz
rm /tmp/dokan-pro-build.tar.gz
- name: Use desired version of NodeJS
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: tests/pw/package-lock.json
- name: Cache test deps (tests/pw/node_modules)
id: pw-nm
uses: actions/cache@v4
with:
path: tests/pw/node_modules
key: pw-nm-${{ runner.os }}-${{ hashFiles('tests/pw/package-lock.json') }}
- name: Install test dependencies
if: steps.pw-nm.outputs.cache-hit != 'true'
working-directory: tests/pw
run: |
npm ci --prefer-offline --no-audit --no-fund || npm i
- name: Create wp debuglog file
id: debug-log
working-directory: tests/pw
run: |
mkdir -p wp-data && touch wp-data/debug.log
- name: Prepare wp-env config
working-directory: tests/pw
run: |
if [ -d "../../dokan-pro" ]; then
echo "Dokan Pro present, using .wp-env.ci.json"
cp .wp-env.ci.json .wp-env.override.json
else
echo "Dokan Pro not present, using lite-only wp-env config"
cp .wp-env.json .wp-env.override.json
fi
# Canary lane: drop the version pins so wp-env resolves whatever
# WordPress and WooCommerce are current. Gating lane is untouched.
if [ "$WP_STACK" = "latest" ]; then
jq '.env.tests.core = null
| .env.tests.plugins |= map(sub("/woocommerce\\.[0-9][0-9.]*\\.zip$"; "/woocommerce.zip"))' \
.wp-env.override.json > .wp-env.override.json.tmp
mv .wp-env.override.json.tmp .wp-env.override.json
echo "WP_STACK=latest — core and WooCommerce unpinned"
fi
# Route Docker Hub pulls through Google's public pull-through mirror. The wp-env images
# (mariadb, phpmyadmin, wordpress, composer — all official library/* images) then come
# from the mirror instead of registry-1.docker.io, dodging Docker Hub's anonymous
# pull-rate-limit / connection timeouts that randomly fail one shard's `wp-env start`.
# No credentials needed; Docker falls back to Docker Hub if the mirror misses an image.
- name: Route Docker Hub pulls through a mirror (avoids anonymous rate-limit/timeouts)
run: |
# Merge into any existing daemon.json (jq is preinstalled on the runner) so we
# never clobber the runner's own Docker config; create it only if absent.
sudo mkdir -p /etc/docker
if [ -s /etc/docker/daemon.json ]; then
sudo jq '. + {"registry-mirrors": ["https://mirror.gcr.io"]}' /etc/docker/daemon.json | sudo tee /etc/docker/daemon.json.tmp >/dev/null
sudo mv /etc/docker/daemon.json.tmp /etc/docker/daemon.json
else
echo '{ "registry-mirrors": ["https://mirror.gcr.io"] }' | sudo tee /etc/docker/daemon.json >/dev/null
fi
sudo systemctl restart docker
# Wait for the daemon socket instead of a fixed sleep.
for i in $(seq 1 15); do docker info >/dev/null 2>&1 && break; sleep 1; done
# wp-env pulls four Docker Hub library images on every run: mariadb:lts and
# phpmyadmin via compose, plus the wordpress / wordpress:cli build bases.
# Anonymous Docker Hub pulls from shared runner IPs randomly time out even
# with the gcr mirror configured (the daemon silently falls back to
# registry-1.docker.io when the mirror misses), so pre-seed the images:
# restore them from the actions cache, else pull from AWS's public mirror
# of Docker official images and retag. Once the images exist locally,
# compose/build skip the Docker Hub pull entirely. The ISO week in the
# cache key stops the floating tags (lts, latest) from pinning forever.
- name: Compute wp-env image list and cache key
id: wp-env-images
working-directory: tests/pw
run: |
PHP=$(jq -r '.env.tests.phpVersion // empty' .wp-env.override.json)
if [ -z "$PHP" ]; then PHP=$(jq -r '.env.tests.phpVersion // empty' .wp-env.json); fi
IMAGES="mariadb:lts phpmyadmin:latest wordpress${PHP:+:php$PHP} wordpress:cli${PHP:+-php$PHP}"
echo "list=$IMAGES" >> "$GITHUB_OUTPUT"
echo "key=wp-env-images-v1-php${PHP:-default}-$(date +%G-%V)" >> "$GITHUB_OUTPUT"
echo "images: $IMAGES"
- name: Restore wp-env docker images from cache
uses: actions/cache@v4
with:
path: ~/.cache/wp-env-images
key: ${{ steps.wp-env-images.outputs.key }}
- name: Pre-seed wp-env docker images
run: |
IMAGES="${{ steps.wp-env-images.outputs.list }}"
TAR="$HOME/.cache/wp-env-images/images.tar"
if [ -f "$TAR" ]; then
docker load -i "$TAR"
else
mkdir -p "$(dirname "$TAR")"
complete=true
for img in $IMAGES; do
pulled=false
for attempt in 1 2 3; do
if docker pull "public.ecr.aws/docker/library/$img"; then pulled=true; break; fi
sleep 15
done
if [ "$pulled" = true ]; then
docker tag "public.ecr.aws/docker/library/$img" "$img"
docker rmi "public.ecr.aws/docker/library/$img" >/dev/null
else
echo "::warning::could not pre-seed $img from the ECR mirror; wp-env will pull it from Docker Hub"
complete=false
fi
done
# Only cache a complete set so a partial failure is retried next run.
if [ "$complete" = true ]; then docker save -o "$TAR" $IMAGES; fi
fi
docker image ls
- name: Start WordPress Env
id: wp-env
uses: nick-fields/retry@v3
with:
timeout_minutes: 6
max_attempts: 3
# Space the attempts out: Docker Hub's anonymous throttle window
# outlives back-to-back retries, so waiting beats hammering.
retry_wait_seconds: 90
retry_on: error
command: |
cd tests/pw
npm run start:env
# What actually got installed, not what we asked for. On the canary lane this is
# the whole point of the run; on the gating lane it is the proof the pins held.
- name: Record resolved WordPress / WooCommerce versions
working-directory: tests/pw
run: |
# `--` is required: without it npm swallows wp-cli's own flags, so
# `--field=version` never reaches wp-cli, `wp plugin get` prints its
# full field/value table, no line starts with a digit, and the grep
# below yields "unknown" for WooCommerce on every single run.
ver() { npm run --silent wp-env -- run tests-cli "$@" 2>/dev/null | tr -d '\r' | grep -Eo '^[0-9]+\.[0-9.]+' | head -n1; }
WP=$(ver wp core version)
WC=$(ver wp plugin get woocommerce --field=version)
echo "stack=$WP_STACK wordpress=${WP:-unknown} woocommerce=${WC:-unknown}"
echo "- \`$WP_STACK\` lane — WordPress **${WP:-unknown}**, WooCommerce **${WC:-unknown}**" >> "$GITHUB_STEP_SUMMARY"
# Persist for merge-reports, which runs on a clean runner with no
# wp-env stack and so cannot resolve these itself. Rides along in the
# existing tests/pw/playwright artifact — no new upload needed.
mkdir -p playwright
printf '{"stack":"%s","wordpress":"%s","woocommerce":"%s"}\n' \
"$WP_STACK" "${WP:-unknown}" "${WC:-unknown}" > playwright/resolved-versions.json
- name: Activate WooCommerce premium plugins
if: needs.build_pro.result == 'success'
working-directory: tests/pw
run: |
npm run wp-env run tests-cli wp plugin activate \
woocommerce-bookings \
woocommerce-subscriptions \
woocommerce-product-addons \
woocommerce-simple-auctions \
woocommerce-pdf-invoices-packing-slips \
dokan-pro \
dokan-invoice
- name: Enable Dokan New React UI (vendor dashboard + product editor)
working-directory: tests/pw
run: |
npm run wp-env run tests-cli wp eval '
$appearance = get_option("dokan_appearance", []);
$appearance["vendor_layout_style"] = "latest";
$appearance["vendor_product_editor"] = "latest";
update_option("dokan_appearance", $appearance);
echo "Dokan appearance set to latest UI" . PHP_EOL;
'
- name: Get installed Playwright version
id: playwright-version
working-directory: tests/pw
run: |
echo "PLAYWRIGHT_VERSION=$(npm ls @playwright/test --json | jq --raw-output '.dependencies["@playwright/test"].version')" >> "$GITHUB_ENV"
- name: Cache playwright binaries
id: playwright-cache
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install Playwright browser binaries & OS dependencies
id: pw-install
if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: tests/pw
run: |
npx playwright install --with-deps chromium
- name: Run docker setup
working-directory: tests/pw
run: npm run docker:setup
- name: 🧪 Run api tests-${{ matrix.shardIndex }}
id: api-test
if: always() && steps.wp-env.outcome == 'success' && ( github.event_name != 'workflow_dispatch' || ( github.event_name == 'workflow_dispatch' && (github.event.inputs.testsuite == 'API' || github.event.inputs.testsuite == 'All')))
timeout-minutes: 15
working-directory: tests/pw
run: |
npm run test:api -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Backup Database
if: always() && steps.wp-env.outcome == 'success'
working-directory: tests/pw
run: |
npm run wp-env run tests-cli wp db export wp-data/db.sql
# playwright-report/e2e is excluded: the docker:setup step runs the
# site/auth/env setup projects through playwright.config.ts, which
# writes setup-run blob/summary/spec-duration files under
# playwright-report/e2e/. Shipping those in this artifact made the
# merge-reports job count the api job's setup run as an extra e2e
# shard (inflating e2e totals) and fed its blob into the html report.
- name: Archive test artifacts (screenshots, HTML snapshots, Reports)
if: always() && steps.debug-log.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: test-artifact-api
path: |
tests/pw/wp-data
tests/pw/playwright
tests/pw/playwright-report
!tests/pw/playwright-report/e2e/**
if-no-files-found: ignore
retention-days: 1
merge-reports:
# Merge reports after playwright-tests, even if some shards have failed
if: ${{ !cancelled() }}
needs: [e2e_tests, api_tests]
runs-on: ubuntu-24.04
timeout-minutes: 5
# Override reports path specifically for this job
env:
SYSTEM_INFO: ./tests/pw/all-reports/test-artifact-api/playwright/systemInfo.json
API_TEST_RESULT: ./tests/pw/all-reports/test-artifact-api/playwright-report/api/summary-report/results.json
API_COVERAGE: ./tests/pw/all-reports/test-artifact-api/playwright-report/api/coverage-report/coverage.json
E2E_TEST_RESULT: ./tests/pw/all-reports/merged-summary.json
E2E_COVERAGE: ./tests/pw/all-reports/merged-coverage.json
steps:
- name: Checkout testing repo
id: clone-dokan-lite
uses: actions/checkout@v4
- name: Prepare dokan-lite folder
run: |
ln -s . dokan-lite
- name: Checkout Dokan Pro
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: actions/checkout@v4
with:
repository: getdokan/dokan-pro
ref: develop
path: dokan-pro
token: ${{ secrets.DOKAN_PRO_TOKEN }}
- name: Use desired version of NodeJS
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: tests/pw/package-lock.json
- name: Cache test deps (tests/pw/node_modules)
id: pw-nm
uses: actions/cache@v4
with:
path: tests/pw/node_modules
key: pw-nm-${{ runner.os }}-${{ hashFiles('tests/pw/package-lock.json') }}
- name: Install test dependencies
if: steps.pw-nm.outputs.cache-hit != 'true'
working-directory: tests/pw
run: |
npm ci --prefer-offline --no-audit --no-fund || npm i
- name: Download all test artifacts from GitHub Actions
id: download-test-artifacts
uses: actions/download-artifact@v4
with:
path: tests/pw/all-reports
pattern: test-artifact-*
# Runs before every other summary write in this job so the stack the suite
# actually ran on is the first thing on the run page. The shards each record
# their own line too; this is the consolidated one. Disagreement between
# shards is worth surfacing rather than hiding — on the canary lane it means
# WordPress or WooCommerce shipped a release mid-run.
- name: Report resolved stack at the top of the summary
if: always() && steps.download-test-artifacts.outcome == 'success'
run: |
FILES=$(find tests/pw/all-reports -name resolved-versions.json 2>/dev/null | sort)
COUNT=$(printf '%s\n' "$FILES" | grep -c . || true)
if [ "${COUNT:-0}" -eq 0 ]; then
printf '> Resolved stack: **unavailable** — no job reported one.\n\n' >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
COMBOS=$(printf '%s\n' "$FILES" | xargs jq -r '"\(.stack)|\(.wordpress)|\(.woocommerce)"' | sort -u)
printf '%s\n' "$COMBOS" | while IFS='|' read -r stack wp wc; do
printf '> **`%s` lane — WordPress %s, WooCommerce %s** _(%s jobs reporting)_\n' \
"$stack" "$wp" "$wc" "$COUNT" >> "$GITHUB_STEP_SUMMARY"
done
if [ "$(printf '%s\n' "$COMBOS" | wc -l)" -gt 1 ]; then
printf '> \n> ⚠️ Jobs did **not** all run the same stack — results are not comparable across shards.\n' >> "$GITHUB_STEP_SUMMARY"
fi
printf '\n' >> "$GITHUB_STEP_SUMMARY"
# download-artifact silently does nothing when no artifacts match
# (e.g. all upstream test jobs were skipped after a build failure),
# so the all-reports dir may not exist yet. Pre-create it so the
# subsequent move/merge steps don't crash on a missing cwd.
- name: Move all blob report folder contents into a single folder
if: always() && steps.download-test-artifacts.outcome == 'success'
run: |
mkdir -p tests/pw/all-reports/all-blob-reports
cd tests/pw/all-reports
find . -type f -path "*/blob-report/*" -exec mv {} all-blob-reports/ \;
- name: Generate HTML Report
id: generate-html-report
working-directory: tests/pw
run: |
npx playwright merge-reports --reporter html ./all-reports/all-blob-reports
mv playwright-report ./all-reports/html-report
- name: Clean all extracted blob reports (except ZIPs)
working-directory: tests/pw
run: |
find all-reports/all-blob-reports -mindepth 1 ! -name "*.zip" -exec rm -rf {} +
- name: Generate merged summary report
id: merge-summary-report
if: steps.download-test-artifacts.outcome == 'success'
working-directory: tests/pw
run: |
REPORT_TYPE=e2e npx ts-node ./utils/mergeSummaryReport.ts
# No coverage on PR runs (skipped per-shard above), so the merge
# would exit 1 on missing inputs. Quality report handles missing
# E2E_COVERAGE via an existsSync guard.
- name: Generate merged coverage report
id: merge-coverage-report
if: |
steps.download-test-artifacts.outcome == 'success'
&& github.event_name != 'pull_request'
working-directory: tests/pw
run: |
REPORT_TYPE=e2e npx ts-node ./utils/mergeCoverageSummary.ts
# Aggregate per-shard spec-durations.json files into a single
# baseline. Uploaded as artifact so maintainers can periodically
# commit it to tests/pw/utils/shard-durations.json to keep
# balanced sharding accurate as the suite evolves.
- name: Aggregate spec-duration baseline
id: aggregate-baseline
if: always() && steps.download-test-artifacts.outcome == 'success'
working-directory: tests/pw
run: |
node ./utils/aggregateSpecDurations.js \
./all-reports \
./all-reports/shard-durations.json || true
- name: Upload spec-duration baseline
if: always() && steps.aggregate-baseline.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: shard-durations-baseline
path: tests/pw/all-reports/shard-durations.json
if-no-files-found: ignore
retention-days: 14
- name: Prepare test summary
id: prepare-test-summary
if: always() && steps.merge-summary-report.outcome == 'success'
uses: actions/github-script@v7
with:
result-encoding: string
script: |
const script = require("./tests/pw/utils/gitTestSummary.ts")
return await script({github, context, core})
# Render the branded HTML quality report (full design, downloadable
# as artifact) plus a markdown variant for the GitHub job summary.
# GitHub strips <style>/CSS from step summaries, so the HTML can't
# render inline — the markdown uses shields.io badges, themed
# Mermaid charts, and HTML tables that GH does render natively.
#
# NOTE: no `working-directory` here — the job-level env block sets
# API_TEST_RESULT etc. as repo-root-relative paths, so the script
# must run from the repo root for those to resolve.
- name: Generate Quality Report
id: quality-report
if: always() && steps.download-test-artifacts.outcome == 'success'
env:
ARTIFACTS_DIR: ./tests/pw/all-reports
OUTPUT_FILE: ./tests/pw/all-reports/quality-report.html
SUMMARY_FILE: ./tests/pw/all-reports/quality-report.md
run: |
node ./tests/pw/utils/generateQualityReport.js
- name: Upload Quality Report
if: always() && steps.quality-report.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: quality-report
path: tests/pw/all-reports/quality-report.html
if-no-files-found: ignore
retention-days: 30
- name: Append Quality Report to job summary
if: always() && steps.quality-report.outcome == 'success'
run: |
if [ -f tests/pw/all-reports/quality-report.md ]; then
cat tests/pw/all-reports/quality-report.md >> "$GITHUB_STEP_SUMMARY"
fi
# Consolidate per-shard feature manifests into one table so reviewers
# see "which features ran in which shard" in a single place at the
# top of the run page — without polluting each shard's job page.
- name: Append shard feature map to job summary
if: always() && steps.download-test-artifacts.outcome == 'success'
working-directory: tests/pw
run: |
node -e '
const fs = require("fs");
const path = require("path");
const baseDir = "./all-reports";
if (!fs.existsSync(baseDir)) process.exit(0);
const shards = [];
for (const entry of fs.readdirSync(baseDir)) {
const featuresPath = path.join(baseDir, entry, "playwright", "shard-features.json");
if (fs.existsSync(featuresPath)) {
try {
shards.push(JSON.parse(fs.readFileSync(featuresPath, "utf8")));
} catch (e) { /* skip malformed */ }
}
}
if (shards.length === 0) process.exit(0);
shards.sort((a, b) => a.shardIndex - b.shardIndex);
const lines = [
"",
"## Shard Feature Map",
"",
"| Shard | Specs | Features tested |",
"|-------|-------|------------------|",
];
for (const s of shards) {
lines.push(`| ${s.shardIndex}/${s.shardTotal} | ${s.specCount} | ${s.features.join(", ")} |`);
}
lines.push("");
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, lines.join("\n"));
'
- name: Upload final test artifact
if: always() && steps.download-test-artifacts.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: final-test-artifact
path: tests/pw/all-reports
if-no-files-found: ignore
retention-days: 7