Consolidate Build & Test CI (#32929)

* Consolidate Build & Test CI

* Add missing workflow dependency

* Fix artifact name clash

* Fix playwright config

* Fix playwright_ew job

* Fix ed tests

* Fix playwright tags

* Iterate

* Fix file reads

* Fix sample-files paths

* Fix PW_TAG

* Fix blob report paths

* Delint

* Fix build-and-test.yaml

* Iterate

* Fix consentHomeserver.ts

* Simplify

* Iterate

* Delint

* Iterate

* Iterate

* Iterate

* Specify shell

* Simplify

* Delete apps/web/playwright/sample-files/index.ts

* Discard changes to apps/web/playwright/sample-files/index.ts

* Exclude playwright-common from coverage gate

* Attempt to speed up arm64 desktop test

* Revert "Attempt to speed up arm64 desktop test"

This reverts commit 8fa8ff0c785da6dad05bda938c8af24fa6af0451.

* Iterate

* Fix cache key

* Accept python or python3 as per node-gyp

* Accept python or python3 as per node-gypd

* Exclude apps/desktop/hak from coverage gate
This commit is contained in:
Michael Telatynski 2026-03-31 14:52:50 +02:00 committed by GitHub
parent cabac4ef0e
commit 2b3720b4a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 203 additions and 254 deletions

View File

@ -5,7 +5,7 @@ on:
# Privilege escalation necessary to publish to Netlify
# 🚨 We must not execute any checked out code here.
workflow_run: # zizmor: ignore[dangerous-triggers]
workflows: ["End to End Tests"]
workflows: ["Build & Test"]
types:
- completed

View File

@ -1,7 +1,13 @@
# Produce a build of element-web with this version of react-sdk
# and any matching branches of element-web and js-sdk, output it
# as an artifact and run end-to-end tests.
name: End to End Tests
# builds Element Web
# runs Playwright tests against the built Element Web
# builds Element Desktop using the built Element Web
#
# Tries to use a matching js-sdk branch for the build.
#
# Produces a `webapp` artifact
# Produces multiple Desktop artifacts
# Produces multiple Playwright report artifacts
name: Build & Test
on:
# CRON to run all Projects at 6am UTC
schedule:
@ -10,7 +16,8 @@ on:
merge_group:
types: [checks_requested]
push:
branches: [develop, master]
# We do not build on push to develop as the merge_group check handles that
branches: [staging, master]
repository_dispatch:
types: [element-web-notify]
@ -35,15 +42,15 @@ concurrency:
env:
# fetchdep.sh needs to know our PR number
PR_NUMBER: ${{ github.event.pull_request.number }}
# Use 6 runners in the default case, but 4 when running on a schedule where we run all 5 projects (20 runners total)
NUM_RUNNERS: ${{ github.event_name == 'schedule' && 4 || 6 }}
# Use 4 runners in the default case, but only 1 when running on a schedule where we run all 5 projects
NUM_RUNNERS: ${{ github.event_name == 'schedule' && 1 || 4 }}
NX_DEFAULT_OUTPUT_STYLE: stream-without-prefixes
permissions: {} # No permissions required
jobs:
build:
name: "Build Element-Web"
build_ew:
name: "Build Element Web"
runs-on: ubuntu-24.04
if: inputs.skip != true
outputs:
@ -94,9 +101,9 @@ jobs:
const matrix = Array.from({ length: numRunners }, (_, i) => i + 1);
core.setOutput("matrix", JSON.stringify(matrix));
playwright:
name: "Run Tests [${{ matrix.project }}] ${{ matrix.runner }}/${{ needs.build.outputs.num-runners }}"
needs: build
playwright_ew:
name: "Run Tests [${{ matrix.project }}] ${{ matrix.runner }}/${{ needs.build_ew.outputs.num-runners }}"
needs: build_ew
if: inputs.skip != true
runs-on: ubuntu-24.04
permissions:
@ -107,7 +114,7 @@ jobs:
fail-fast: false
matrix:
# Run multiple instances in parallel to speed up the tests
runner: ${{ fromJSON(needs.build.outputs.runners-matrix) }}
runner: ${{ fromJSON(needs.build_ew.outputs.runners-matrix) }}
project:
- Chrome
- Firefox
@ -179,29 +186,85 @@ jobs:
--project="${{ matrix.project }}" \
${{ (github.event_name == 'pull_request' && matrix.runAllTests == false ) && '--grep-invert @mergequeue' || '' }}
env:
SHARD: ${{ format('{0}/{1}', matrix.runner, needs.build.outputs.num-runners) }}
SHARD: ${{ format('{0}/{1}', matrix.runner, needs.build_ew.outputs.num-runners) }}
- name: Upload blob report to GitHub Actions Artifacts
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }}
name: blob-report-${{ matrix.project }}-${{ matrix.runner }}
path: apps/web/blob-report
retention-days: 1
if-no-files-found: error
downstream-modules:
name: Downstream Playwright tests [element-modules]
needs: build
needs: build_ew
if: inputs.skip != true && github.event_name == 'merge_group'
uses: element-hq/element-modules/.github/workflows/reusable-playwright-tests.yml@main # zizmor: ignore[unpinned-uses]
with:
webapp-artifact: webapp
prepare_ed:
name: "Prepare Element Desktop"
uses: ./.github/workflows/build_desktop_prepare.yaml
needs: build_ew
if: inputs.skip != true
permissions:
contents: read
with:
config: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'element.io/nightly' || 'element.io/release' }}
version: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'develop' || '' }}
webapp-artifact: webapp
build_ed_windows:
needs: prepare_ed
name: "Desktop Windows"
uses: ./.github/workflows/build_desktop_windows.yaml
if: inputs.skip != true
strategy:
matrix:
arch: [x64, ia32, arm64]
with:
arch: ${{ matrix.arch }}
blob_report: true
build_ed_linux:
needs: prepare_ed
name: "Desktop Linux"
uses: ./.github/workflows/build_desktop_linux.yaml
if: inputs.skip != true
strategy:
matrix:
sqlcipher: [system, static]
arch: [amd64, arm64]
runAllTests:
- ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'X-Run-All-Tests') }}
# We ship static sqlcipher builds, so delegate testing the system builds to the merge queue
exclude:
- runAllTests: false
sqlcipher: system
with:
sqlcipher: ${{ matrix.sqlcipher }}
arch: ${{ matrix.arch }}
blob_report: true
build_ed_macos:
needs: prepare_ed
name: "Desktop macOS"
uses: ./.github/workflows/build_desktop_macos.yaml
if: inputs.skip != true
with:
blob_report: true
complete:
name: end-to-end-tests
needs:
- playwright
- playwright_ew
- downstream-modules
- build_ed_windows
- build_ed_linux
- build_ed_macos
if: always()
runs-on: ubuntu-24.04
steps:
@ -227,18 +290,20 @@ jobs:
if: inputs.skip != true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: all-blob-reports-*
path: apps/web/all-blob-reports
pattern: blob-report-*
path: all-blob-reports
merge-multiple: true
- name: Merge into HTML Report
if: inputs.skip != true
working-directory: apps/web
run: pnpm playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts,@element-hq/element-web-playwright-common/lib/stale-screenshot-reporter.js ./all-blob-reports
run: |
pnpm playwright merge-reports \
--config=playwright-merge.config.ts \
./all-blob-reports
env:
# Only pass creds to the flaky-reporter on main branch runs
GITHUB_TOKEN: ${{ github.ref_name == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }}
PLAYWRIGHT_HTML_TITLE: ${{ case(github.event_name == 'pull_request', format('EW Playwright Report PR-{0}', env.PR_NUMBER), 'EW Playwright Report') }}
PLAYWRIGHT_HTML_TITLE: ${{ case(github.event_name == 'pull_request', format('Playwright Report PR-{0}', env.PR_NUMBER), 'Playwright Report') }}
# Upload the HTML report even if one of our reporters fails, this can happen when stale screenshots are detected
- name: Upload HTML report
@ -246,7 +311,7 @@ jobs:
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: html-report
path: apps/web/playwright-report
path: playwright-report
retention-days: 14
if-no-files-found: error

View File

@ -1,89 +0,0 @@
name: Build and Test
on:
pull_request: {}
push:
branches: [develop, staging, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {} # No permissions required
jobs:
fetch:
uses: ./.github/workflows/build_desktop_prepare.yaml
permissions:
contents: read
with:
config: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'element.io/nightly' || 'element.io/release' }}
version: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'develop' || '' }}
branch-matching: true
windows:
needs: fetch
name: Windows
uses: ./.github/workflows/build_desktop_windows.yaml
strategy:
matrix:
arch: [x64, ia32, arm64]
with:
arch: ${{ matrix.arch }}
blob_report: true
linux:
needs: fetch
name: "Linux (${{ matrix.arch }}) (sqlcipher: ${{ matrix.sqlcipher }})"
uses: ./.github/workflows/build_desktop_linux.yaml
strategy:
matrix:
sqlcipher: [system, static]
arch: [amd64, arm64]
with:
sqlcipher: ${{ matrix.sqlcipher }}
arch: ${{ matrix.arch }}
blob_report: true
macos:
needs: fetch
name: macOS
uses: ./.github/workflows/build_desktop_macos.yaml
with:
blob_report: true
tests-done:
needs: [windows, linux, macos]
runs-on: ubuntu-24.04
if: ${{ !cancelled() }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
cache: "pnpm"
node-version: "lts/*"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: blob-report-*
path: apps/desktop/all-blob-reports
merge-multiple: true
- name: Merge into HTML Report
working-directory: apps/desktop
run: pnpm playwright merge-reports -c ./playwright.config.ts --reporter=html ./all-blob-reports
- name: Upload HTML report
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: html-report
path: apps/desktop/playwright-report
retention-days: 14
- if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1

View File

@ -28,7 +28,7 @@ on:
type: string
required: false
description: |
The name of the prepare artifact to use, defaults to 'webapp'.
The name of the prepare artifact to use, defaults to 'desktop-prepare'.
The artifact must contain the following:
+ webapp.asar - the asar archive of the webapp to embed in the desktop app
+ electronVersion - the version of electron to use for cache keying
@ -38,7 +38,7 @@ on:
The artifact can also contain any additional files which will be applied as overrides to the checkout root before building,
for example icons in the `build/` directory to override the app icons.
default: "webapp"
default: "desktop-prepare"
test:
type: boolean
required: false
@ -73,20 +73,8 @@ jobs:
# https://github.com/matrix-org/seshat/issues/135
runs-on: ${{ inputs.runs-on || (inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04') }}
env:
HAK_DOCKER_IMAGE: ghcr.io/element-hq/element-web/desktop-build-env
HAK_DOCKER_IMAGE: ghcr.io/element-hq/element-web/desktop-build-env:${{ case(github.event_name == 'push', inputs.ref || github.ref_name, github.event_name == 'release', 'staging', 'develop') }}
steps:
- name: Resolve docker image tag for push
if: github.event_name == 'push'
run: echo "HAK_DOCKER_IMAGE=$HAK_DOCKER_IMAGE:$REF" >> $GITHUB_ENV
env:
REF: ${{ inputs.ref || github.ref_name }}
- name: Resolve docker image tag for release
if: github.event_name == 'release'
run: echo "HAK_DOCKER_IMAGE=$HAK_DOCKER_IMAGE:staging" >> $GITHUB_ENV
- name: Resolve docker image tag for other triggers
if: github.event_name != 'push' && github.event_name != 'release'
run: echo "HAK_DOCKER_IMAGE=$HAK_DOCKER_IMAGE:develop" >> $GITHUB_ENV
- uses: nbucic/variable-mapper@0673f6891a0619ba7c002ecfed0f9f4f39017b6f
id: config
with:
@ -95,11 +83,9 @@ jobs:
map: |
{
"amd64": {
"target": "x86_64-unknown-linux-gnu",
"arch": "x86-64"
},
"arm64": {
"target": "aarch64-unknown-linux-gnu",
"arch": "aarch64",
"build-args": "--arm64"
}
@ -120,7 +106,7 @@ jobs:
id: cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
key: ${{ runner.os }}-${{ github.ref_name }}-${{ inputs.sqlcipher }}-${{ inputs.arch }}-${{ hashFiles('hakHash', 'electronVersion', 'dockerbuild/*') }}
key: ${{ runner.os }}-${{ github.ref_name }}-${{ inputs.sqlcipher }}-${{ inputs.arch }}-${{ hashFiles('apps/desktop/hakHash', 'apps/desktop/electronVersion', 'apps/desktop/dockerbuild/*') }}
path: |
apps/desktop/.hak
@ -135,7 +121,7 @@ jobs:
- name: Install Deps
working-directory: apps/desktop
run: pnpm install --frozen-lockfile
run: "pnpm install --frozen-lockfile --filter element-desktop"
- name: "Get modified files"
id: changed_files

View File

@ -37,7 +37,7 @@ on:
type: string
required: false
description: |
The name of the prepare artifact to use, defaults to 'webapp'.
The name of the prepare artifact to use, defaults to 'desktop-prepare'.
The artifact must contain the following:
+ webapp.asar - the asar archive of the webapp to embed in the desktop app
+ electronVersion - the version of electron to use for cache keying
@ -46,7 +46,7 @@ on:
The artifact can also contain any additional files which will be applied as overrides to the checkout root before building,
for example icons in the `build/` directory to override the app icons.
default: "webapp"
default: "desktop-prepare"
test:
type: boolean
required: false
@ -92,7 +92,7 @@ jobs:
id: cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
key: ${{ runner.os }}-${{ hashFiles('hakHash', 'electronVersion') }}
key: ${{ runner.os }}-${{ hashFiles('apps/desktop/hakHash', 'apps/desktop/electronVersion') }}
path: |
apps/desktop/.hak
@ -121,7 +121,7 @@ jobs:
- name: Install Deps
working-directory: apps/desktop
run: "pnpm install --frozen-lockfile"
run: "pnpm install --frozen-lockfile --filter element-desktop"
- name: Build Natives
if: steps.cache.outputs.cache-hit != 'true'

View File

@ -20,11 +20,10 @@ on:
required: false
default: false
description: "Whether the build should be deployed to production"
branch-matching:
type: boolean
webapp-artifact:
type: string
required: false
default: false
description: "Whether the branch name should be matched to find the element-web commit"
description: "Name of the webapp artifact that should be used, will fetch a relevant build if omitted"
secrets:
# Required if `nightly` is set
CF_R2_ACCESS_KEY_ID:
@ -66,28 +65,26 @@ jobs:
- name: Install Deps
working-directory: apps/desktop
run: "pnpm install --frozen-lockfile"
run: "pnpm install --frozen-lockfile --filter element-desktop"
- name: Fetch Element Web (matching branch)
id: branch-matching
if: inputs.branch-matching
- name: Fetch Element Web (from artifact)
if: inputs.webapp-artifact != ''
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.webapp-artifact }}
path: apps/desktop/webapp
- name: Build webapp.asar (from artifact)
if: inputs.webapp-artifact != ''
working-directory: apps/desktop
continue-on-error: true
run: |
scripts/branch-match.sh
cp "$CONFIG_DIR/config.json" element-web/
pnpm --cwd element-web install --frozen-lockfile
pnpm --cwd element-web run build
mv element-web/webapp .
cp -f "$CONFIG_DIR/config.json" webapp/config.json
pnpm run asar-webapp
env:
# These must be set for branch-match.sh to get the right branch
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
CONFIG_DIR: ${{ inputs.config }}
- name: Fetch Element Web (${{ inputs.version }})
if: steps.branch-matching.outcome == 'failure' || steps.branch-matching.outcome == 'skipped'
if: inputs.webapp-artifact == ''
working-directory: apps/desktop
run: pnpm run fetch --noverify -d ${CONFIG} ${VERSION}
env:
@ -189,7 +186,7 @@ jobs:
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: webapp
name: desktop-prepare
retention-days: 1
path: |
apps/desktop/webapp.asar

View File

@ -48,8 +48,7 @@ jobs:
cache: "pnpm"
- name: Install Deps
working-directory: apps/desktop
run: "pnpm install --frozen-lockfile"
run: "pnpm install --frozen-lockfile --filter element-desktop"
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
@ -85,12 +84,19 @@ jobs:
EXECUTABLE: ${{ steps.executable.outputs.path }}
- name: Run tests
uses: coactions/setup-xvfb@6b00cf1889f4e1d5a48635647013c0508128ee1a
timeout-minutes: 20
with:
run: pnpm -C apps/desktop test --project=${{ inputs.project }} ${{ runner.os != 'Linux' && '--ignore-snapshots' || '' }} ${{ inputs.blob_report == false && '--reporter=html' || '' }} ${{ inputs.args }}
shell: bash
working-directory: apps/desktop
run: |
$PREFIX pnpm playwright test \
${{ runner.os != 'Linux' && '--ignore-snapshots' || '' }} \
${{ inputs.blob_report == false && '--reporter=html' || '' }} \
$ARGS
env:
PREFIX: ${{ runner.os == 'Linux' && 'xvfb-run' || '' }}
PW_TAG: ${{ inputs.project }}
ELEMENT_DESKTOP_EXECUTABLE: ${{ steps.executable.outputs.path }}
ARGS: ${{ inputs.args }}
- name: Upload blob report
if: always() && inputs.blob_report
@ -99,6 +105,7 @@ jobs:
name: blob-report-${{ inputs.artifact }}
path: apps/desktop/blob-report
retention-days: 1
if-no-files-found: error
- name: Upload HTML report
if: always() && inputs.blob_report == false
@ -107,3 +114,4 @@ jobs:
name: ${{ inputs.artifact }}-test
path: apps/desktop/playwright-report
retention-days: 14
if-no-files-found: error

View File

@ -42,7 +42,7 @@ on:
type: string
required: false
description: |
The name of the prepare artifact to use, defaults to 'webapp'.
The name of the prepare artifact to use, defaults to 'desktop-prepare'.
The artifact must contain the following:
+ webapp.asar - the asar archive of the webapp to embed in the desktop app
+ electronVersion - the version of electron to use for cache keying
@ -52,7 +52,7 @@ on:
The artifact can also contain any additional files which will be applied as overrides to the checkout root before building,
for example icons in the `build/` directory to override the app icons.
default: "webapp"
default: "desktop-prepare"
test:
type: boolean
required: false
@ -123,7 +123,7 @@ jobs:
id: cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
key: ${{ runner.os }}-${{ inputs.arch }}-${{ hashFiles('hakHash', 'electronVersion') }}
key: ${{ runner.os }}-${{ inputs.arch }}-${{ hashFiles('apps/desktop/hakHash', 'apps/desktop/electronVersion') }}
path: |
apps/desktop/.hak
@ -160,7 +160,7 @@ jobs:
- name: Install Deps
working-directory: apps/desktop
run: "pnpm install --frozen-lockfile"
run: "pnpm install --frozen-lockfile --filter element-desktop"
- name: Insert config snippet
if: steps.config.outputs.extra_config != ''

View File

@ -14,10 +14,7 @@ import type { Tool } from "../../scripts/hak/hakEnv.ts";
import type { DependencyInfo } from "../../scripts/hak/dep.ts";
export default async function (hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
const tools: Tool[] = [
["rustc", "--version"],
["python", "--version"], // node-gyp uses python for reasons beyond comprehension
];
const tools: Tool[] = [["rustc", "--version"]];
if (hakEnv.isWin()) {
tools.push(["perl", "--version"]); // for openssl configure
tools.push(["nasm", "-v"]); // for openssl building
@ -28,6 +25,14 @@ export default async function (hakEnv: HakEnv, moduleInfo: DependencyInfo): Prom
}
await hakEnv.checkTools(tools);
try {
// node-gyp uses python for reasons beyond comprehension
await hakEnv.checkTools([["python", "--version"]]);
} catch {
// try python3 too
await hakEnv.checkTools([["python3", "--version"]]);
}
// Ensure Rust target exists (nb. we avoid depending on rustup)
await new Promise((resolve, reject) => {
const rustc = childProcess.execFile(

View File

@ -8,25 +8,9 @@ Please see LICENSE files in the repository root for full details.
import { defineConfig } from "@playwright/test";
const projects = [
"macos",
"win-x64",
"win-ia32",
"win-arm64",
"linux-amd64-sqlcipher-system",
"linux-amd64-sqlcipher-static",
"linux-arm64-sqlcipher-system",
"linux-arm64-sqlcipher-static",
];
export default defineConfig({
// Allows the GitHub action to specify a project name (OS + arch) for the combined report to make sense
// workaround for https://github.com/microsoft/playwright/issues/33521
projects: process.env.CI
? projects.map((name) => ({
name,
}))
: undefined,
projects: [{ name: "Desktop" }],
tag: process.env.PW_TAG ? `@${process.env.PW_TAG}` : undefined,
use: {
viewport: { width: 1280, height: 720 },
video: "retain-on-failure",

View File

@ -1,6 +1,7 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "app",
"projectType": "application",
"implicitDependencies": ["element-web"],
"root": "apps/desktop",
"targets": {
"docker:build": {

View File

@ -1,48 +0,0 @@
#!/bin/bash
# Script for downloading a branch of element-web matching the branch a PR is contributed from
set -x
deforg="element-hq"
defrepo="element-web"
# The PR_NUMBER variable must be set explicitly.
default_org_repo=${GITHUB_REPOSITORY:-"$deforg/$defrepo"}
PR_ORG=${PR_ORG:-${default_org_repo%%/*}}
PR_REPO=${PR_REPO:-${default_org_repo##*/}}
# A function that clones a branch of a repo based on the org, repo and branch
clone() {
org=$1
repo=$2
branch=$3
if [ -n "$branch" ]
then
echo "Trying to use $org/$repo#$branch"
# Disable auth prompts: https://serverfault.com/a/665959
GIT_TERMINAL_PROMPT=0 git clone https://github.com/$org/$repo.git $repo --branch "$branch" --depth 1 && exit 0
fi
}
echo "Getting info about a PR with number $PR_NUMBER"
apiEndpoint="https://api.github.com/repos/$PR_ORG/$PR_REPO/pulls/$PR_NUMBER"
head=$(curl "$apiEndpoint" | jq -r '.head.label')
# for forks, $head will be in the format "fork:branch", so we split it by ":"
# into an array. On non-forks, this has the effect of splitting into a single
# element array given ":" shouldn't appear in the head - it'll just be the
# branch name. Based on the results, we clone.
BRANCH_ARRAY=(${head//:/ })
TRY_ORG=$deforg
TRY_BRANCH=${BRANCH_ARRAY[0]}
if [[ "$head" == *":"* ]]; then
# ... but only match that fork if it's a real fork
if [ "${BRANCH_ARRAY[0]}" != "$PR_ORG" ]; then
TRY_ORG=${BRANCH_ARRAY[0]}
fi
TRY_BRANCH=${BRANCH_ARRAY[1]}
fi
clone "$TRY_ORG" "$defrepo" "$TRY_BRANCH"
exit 1

View File

@ -75,5 +75,8 @@ export default {
nx: {
config: ["{nx,package,project}.json", "{apps,packages,modules}/**/{package,project}.json"],
},
playwright: {
config: ["playwright.config.ts", "playwright-merge.config.ts"],
},
tags: ["-knipignore"],
} satisfies KnipConfig;

View File

@ -27,9 +27,11 @@
"devDependencies": {
"@action-validator/cli": "^0.6.0",
"@action-validator/core": "^0.6.0",
"@element-hq/element-web-playwright-common": "catalog:",
"@nx-tools/nx-container": "^7.2.1",
"@nx/jest": "^22.5.0",
"@types/node": "22",
"@playwright/test": "catalog:",
"cronstrue": "^3.0.0",
"eslint-plugin-matrix-org": "^3.0.0",
"husky": "^9.0.0",

View File

@ -26,7 +26,7 @@ type PaginationLinks = {
// We see quite a few test flakes which are caused by the app exploding
// so we have some magic strings we check the logs for to better track the flake with its cause
const SPECIAL_CASES = {
const SPECIAL_CASES: Record<string, string> = {
"ChunkLoadError": "ChunkLoadError",
"Unreachable code should not be executed": "Rust crypto panic",
"Out of bounds memory access": "Rust crypto memory error",
@ -37,7 +37,7 @@ class FlakyReporter implements Reporter {
public onTestEnd(test: TestCase): void {
// Ignores flakes on Dendrite and Pinecone as they have their own flakes we do not track
if (["Dendrite", "Pinecone"].includes(test.parent.project()?.name)) return;
if (["Dendrite", "Pinecone"].includes(test.parent.project()!.name!)) return;
let failures = [`${test.location.file.split("playwright/e2e/")[1]}: ${test.title}`];
if (test.outcome() === "flaky") {
const timedOutRuns = test.results.filter((result) => result.status === "timedOut");
@ -46,7 +46,7 @@ class FlakyReporter implements Reporter {
);
// If a test failed due to a systemic fault then the test is not flaky, the app is, record it as such.
const specialCases = Object.keys(SPECIAL_CASES).filter((log) =>
pageLogs.some((attachment) => attachment.name.startsWith("page-") && attachment.body.includes(log)),
pageLogs.some((attachment) => attachment.name.startsWith("page-") && attachment.body?.includes(log)),
);
if (specialCases.length > 0) {
failures = specialCases.map((specialCase) => SPECIAL_CASES[specialCase]);
@ -56,7 +56,7 @@ class FlakyReporter implements Reporter {
if (!this.flakes.has(title)) {
this.flakes.set(title, []);
}
this.flakes.get(title).push(test);
this.flakes.get(title)!.push(test);
}
}
}
@ -76,8 +76,8 @@ class FlakyReporter implements Reporter {
if (!link) return map;
const matches = link.matchAll(/(<(?<link>.+?)>; rel="(?<type>.+?)")/g);
for (const match of matches) {
const { link, type } = match.groups;
map[type] = link;
const { link, type } = match.groups!;
map[type as keyof PaginationLinks] = link;
}
return map;
}
@ -102,9 +102,9 @@ class FlakyReporter implements Reporter {
issues.push(...fetchedIssues);
// Get the next link for fetching more results
const linkHeader = issuesResponse.headers.get("Link");
const linkHeader = issuesResponse.headers.get("Link")!;
const parsed = this.parseLinkHeader(linkHeader);
url = parsed.next;
url = parsed.next!;
}
return issues;
}

View File

@ -0,0 +1,20 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { defineConfig } from "@playwright/test";
export default defineConfig({
// Playwright only supports merging using a single testDir, specifying web here
// means that some parts of the report for Desktop will be incorrect, but this is minor.
// https://github.com/microsoft/playwright/issues/39855
testDir: "apps/web/playwright/e2e",
reporter: [
["html", { open: "never" }],
["./packages/playwright-common/flaky-reporter.ts"],
["@element-hq/element-web-playwright-common/lib/stale-screenshot-reporter.js"],
],
});

33
pnpm-lock.yaml generated
View File

@ -135,12 +135,18 @@ importers:
'@action-validator/core':
specifier: ^0.6.0
version: 0.6.0
'@element-hq/element-web-playwright-common':
specifier: 'catalog:'
version: 2.2.7(@element-hq/element-web-module-api@1.12.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4))(@playwright/test@1.58.2)(playwright-core@1.58.2)
'@nx-tools/nx-container':
specifier: ^7.2.1
version: 7.2.1(@nx/devkit@22.5.3(nx@22.5.4))(@nx/js@22.5.3(@babel/traverse@7.29.0)(nx@22.5.4))(dotenv@17.3.1)(nx@22.5.4)(tslib@2.8.1)
'@nx/jest':
specifier: ^22.5.0
version: 22.5.3(@babel/traverse@7.29.0)(@types/node@18.19.130)(babel-plugin-macros@3.1.0)(nx@22.5.4)(typescript@5.9.3)
'@playwright/test':
specifier: 'catalog:'
version: 1.58.2
'@types/node':
specifier: 18.19.130
version: 18.19.130
@ -179,10 +185,10 @@ importers:
version: 5.9.3
vitepress:
specifier: ^1.6.4
version: 1.6.4(@algolia/client-search@5.50.0)(@types/node@18.19.130)(@types/react@19.2.10)(axios@1.13.5)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.8)(qrcode@1.5.4)(search-insights@2.17.3)(sugarss@5.0.1(postcss@8.5.8))(terser@5.46.0)(typescript@5.9.3)
version: 1.6.4(@algolia/client-search@5.50.0)(@types/node@18.19.130)(@types/react@19.2.10)(axios@1.13.5)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.8)(qrcode@1.5.4)(react@19.2.4)(search-insights@2.17.3)(sugarss@5.0.1(postcss@8.5.8))(terser@5.46.0)(typescript@5.9.3)
vitepress-plugin-mermaid:
specifier: ^2.0.17
version: 2.0.17(mermaid@11.13.0)(vitepress@1.6.4(@algolia/client-search@5.50.0)(@types/node@18.19.130)(@types/react@19.2.10)(axios@1.13.5)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.8)(qrcode@1.5.4)(search-insights@2.17.3)(sugarss@5.0.1(postcss@8.5.8))(terser@5.46.0)(typescript@5.9.3))
version: 2.0.17(mermaid@11.13.0)(vitepress@1.6.4(@algolia/client-search@5.50.0)(@types/node@18.19.130)(@types/react@19.2.10)(axios@1.13.5)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.8)(qrcode@1.5.4)(react@19.2.4)(search-insights@2.17.3)(sugarss@5.0.1(postcss@8.5.8))(terser@5.46.0)(typescript@5.9.3))
yaml:
specifier: 2.8.3
version: 2.8.3
@ -11457,6 +11463,10 @@ packages:
sanitize-html@2.17.1:
resolution: {integrity: sha512-ehFCW+q1a4CSOWRAdX97BX/6/PDEkCqw7/0JXZAGQV57FQB3YOkTa/rrzHPeJ+Aghy4vZAFfWMYyfxIiB7F/gw==}
sax@1.4.4:
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
engines: {node: '>=11.0.0'}
sax@1.6.0:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
engines: {node: '>=11.0.0'}
@ -14541,9 +14551,9 @@ snapshots:
'@docsearch/css@3.8.2': {}
'@docsearch/js@3.8.2(@algolia/client-search@5.50.0)(@types/react@19.2.10)(search-insights@2.17.3)':
'@docsearch/js@3.8.2(@algolia/client-search@5.50.0)(@types/react@19.2.10)(react@19.2.4)(search-insights@2.17.3)':
dependencies:
'@docsearch/react': 3.8.2(@algolia/client-search@5.50.0)(@types/react@19.2.10)(search-insights@2.17.3)
'@docsearch/react': 3.8.2(@algolia/client-search@5.50.0)(@types/react@19.2.10)(react@19.2.4)(search-insights@2.17.3)
preact: 10.28.3
transitivePeerDependencies:
- '@algolia/client-search'
@ -14552,7 +14562,7 @@ snapshots:
- react-dom
- search-insights
'@docsearch/react@3.8.2(@algolia/client-search@5.50.0)(@types/react@19.2.10)(search-insights@2.17.3)':
'@docsearch/react@3.8.2(@algolia/client-search@5.50.0)(@types/react@19.2.10)(react@19.2.4)(search-insights@2.17.3)':
dependencies:
'@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.50.0)(algoliasearch@5.50.0)(search-insights@2.17.3)
'@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.50.0)(algoliasearch@5.50.0)
@ -14560,6 +14570,7 @@ snapshots:
algoliasearch: 5.50.0
optionalDependencies:
'@types/react': 19.2.10
react: 19.2.4
search-insights: 2.17.3
transitivePeerDependencies:
- '@algolia/client-search'
@ -19317,7 +19328,7 @@ snapshots:
builder-util-runtime@9.5.1:
dependencies:
debug: 4.4.3
sax: 1.6.0
sax: 1.4.4
transitivePeerDependencies:
- supports-color
@ -25261,6 +25272,8 @@ snapshots:
parse-srcset: 1.0.2
postcss: 8.5.8
sax@1.4.4: {}
sax@1.6.0: {}
saxes@6.0.0:
@ -26687,17 +26700,17 @@ snapshots:
tsx: 4.21.0
yaml: 2.8.3
vitepress-plugin-mermaid@2.0.17(mermaid@11.13.0)(vitepress@1.6.4(@algolia/client-search@5.50.0)(@types/node@18.19.130)(@types/react@19.2.10)(axios@1.13.5)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.8)(qrcode@1.5.4)(search-insights@2.17.3)(sugarss@5.0.1(postcss@8.5.8))(terser@5.46.0)(typescript@5.9.3)):
vitepress-plugin-mermaid@2.0.17(mermaid@11.13.0)(vitepress@1.6.4(@algolia/client-search@5.50.0)(@types/node@18.19.130)(@types/react@19.2.10)(axios@1.13.5)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.8)(qrcode@1.5.4)(react@19.2.4)(search-insights@2.17.3)(sugarss@5.0.1(postcss@8.5.8))(terser@5.46.0)(typescript@5.9.3)):
dependencies:
mermaid: 11.13.0
vitepress: 1.6.4(@algolia/client-search@5.50.0)(@types/node@18.19.130)(@types/react@19.2.10)(axios@1.13.5)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.8)(qrcode@1.5.4)(search-insights@2.17.3)(sugarss@5.0.1(postcss@8.5.8))(terser@5.46.0)(typescript@5.9.3)
vitepress: 1.6.4(@algolia/client-search@5.50.0)(@types/node@18.19.130)(@types/react@19.2.10)(axios@1.13.5)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.8)(qrcode@1.5.4)(react@19.2.4)(search-insights@2.17.3)(sugarss@5.0.1(postcss@8.5.8))(terser@5.46.0)(typescript@5.9.3)
optionalDependencies:
'@mermaid-js/mermaid-mindmap': 9.3.0
vitepress@1.6.4(@algolia/client-search@5.50.0)(@types/node@18.19.130)(@types/react@19.2.10)(axios@1.13.5)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.8)(qrcode@1.5.4)(search-insights@2.17.3)(sugarss@5.0.1(postcss@8.5.8))(terser@5.46.0)(typescript@5.9.3):
vitepress@1.6.4(@algolia/client-search@5.50.0)(@types/node@18.19.130)(@types/react@19.2.10)(axios@1.13.5)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.8)(qrcode@1.5.4)(react@19.2.4)(search-insights@2.17.3)(sugarss@5.0.1(postcss@8.5.8))(terser@5.46.0)(typescript@5.9.3):
dependencies:
'@docsearch/css': 3.8.2
'@docsearch/js': 3.8.2(@algolia/client-search@5.50.0)(@types/react@19.2.10)(search-insights@2.17.3)
'@docsearch/js': 3.8.2(@algolia/client-search@5.50.0)(@types/react@19.2.10)(react@19.2.4)(search-insights@2.17.3)
'@iconify-json/simple-icons': 1.2.75
'@shikijs/core': 2.5.0
'@shikijs/transformers': 2.5.0

View File

@ -18,11 +18,13 @@ sonar.coverage.exclusions=\
apps/web/scripts/**/*,\
apps/desktop/scripts/**/*,\
apps/desktop/playwright/**/*,\
apps/desktop/hak/**/*,\
scripts/**/*,\
apps/web/src/components/views/dialogs/devtools/**/*,\
apps/web/src/utils/SessionLock.ts,\
apps/web/src/**/*.d.ts,\
apps/web/src/vector/mobile_guide/**/*,\
packages/shared-components/src/test/**/*,\
packages/shared-components/src/**/*.stories.tsx
packages/shared-components/src/**/*.stories.tsx,\
packages/playwright-common/**/*
sonar.testExecutionReportPaths=apps/web/coverage/jest-sonar-report.xml