Skip to content

Electron Playwright Tests #3402

Electron Playwright Tests

Electron Playwright Tests #3402

Workflow file for this run

name: Electron Playwright Tests
on:
workflow_dispatch:
inputs:
version_name:
type: string
description: "Desktop Version. It can be a branch name or a tag or a commit SHA"
required: true
instance_details:
type: string
description: "JSON array of platform details"
required: false
MM_TEST_USER_NAME:
description: "The admin username of the test instance"
required: false
type: string
MM_TEST_PASSWORD:
description: "The admin password of the test instance"
required: false
type: string
MM_SERVER_VERSION:
type: string
description: "The server version to test against"
required: true
run_type:
type: string
description: "Run type for test reporting (PR / MASTER / RELEASE). Matterwick sends MASTER for master pushes; PR-label runs leave this blank (default PR). Settable on manual dispatch for ad-hoc RELEASE runs."
required: false
default: ""
pr_number:
type: string
description: "PR number, used to remove the E2E label from the PR after tests complete. Dispatchers should set this for PR runs."
required: false
# Least-privilege default. Jobs that need more (status checks, PR writes) grant it explicitly
# at the job level so that jobs running untrusted PR code do not inherit write scopes.
permissions:
contents: read
jobs:
prepare-matrix:
if: ${{ github.event_name == 'workflow_dispatch' && inputs.instance_details != '' }}
runs-on: ubuntu-latest
outputs:
platforms: ${{ steps.generate.outputs.platforms }}
steps:
- id: generate
env:
INSTANCE_DETAILS: ${{ inputs.instance_details }}
run: |
# Matterwick still dispatches macos-latest; pin explicitly so the job
# does not drift when GitHub retargets that label to macOS 26.
platforms=$(echo "${INSTANCE_DETAILS}" | jq -c 'map(if .runner == "macos-latest" then .runner = "macos-26" else . end)')
echo "platforms=${platforms}" >> "$GITHUB_OUTPUT"
update-initial-status:
name: Update initial status
needs: prepare-matrix
runs-on: ubuntu-24.04
permissions:
contents: read
statuses: write
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Update initial status for all platforms
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
PLATFORMS: ${{ needs.prepare-matrix.outputs.platforms }}
with:
github-token: ${{ github.token }}
script: |
const { updateInitialStatus } = require('./e2e/utils/github-actions.js');
const platforms = JSON.parse(process.env.PLATFORMS);
await updateInitialStatus({ github, context, platforms });
e2e-tests:
needs:
- prepare-matrix
- update-initial-status
name: ${{ matrix.platform }}
strategy:
matrix:
include: ${{ fromJson(needs.prepare-matrix.outputs.platforms) }}
fail-fast: false
# The reusable template runs only untrusted PR code (npm ci / Playwright) with a read-only
# token; it inherits the workflow-level contents:read default.
uses: ./.github/workflows/e2e-functional-template.yml
with:
runs-on: ${{ matrix.runner }}
DESKTOP_VERSION: ${{ inputs.version_name }}
MM_TEST_SERVER_URL: ${{ matrix.url }}
MM_SERVER_VERSION: ${{ inputs.MM_SERVER_VERSION }}
MM_TEST_USER_NAME: ${{ inputs.MM_TEST_USER_NAME }}
MM_TEST_PASSWORD: ${{ inputs.MM_TEST_PASSWORD }}
# When matterwick fires this workflow it sets run_type explicitly (MASTER for master
# pushes; PR-label runs are dispatched without run_type, defaulting to PR via the `|| 'PR'`
# below). The previous `startsWith(version_name, 'release-') && 'RELEASE'` fallback was
# for matterwick's release-branch push, which is no longer wired — removed.
TYPE: ${{ inputs.run_type || 'PR' }}
secrets: inherit
update-final-status:
name: Update final status
runs-on: ubuntu-24.04
needs:
- prepare-matrix
- e2e-tests
if: always()
permissions:
contents: read
statuses: write
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Update final status for all platforms
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
PR_NUMBER: ${{ inputs.pr_number }}
PLATFORMS: ${{ needs.prepare-matrix.outputs.platforms }}
OUTPUTS: ${{ toJSON(needs.e2e-tests.outputs) }}
E2E_TESTS_RESULT: ${{ needs.e2e-tests.result }}
with:
github-token: ${{ github.token }}
script: |
const { updateFinalStatus } = require('./e2e/utils/github-actions.js');
const platforms = JSON.parse(process.env.PLATFORMS);
const outputs = JSON.parse(process.env.OUTPUTS);
const prNumber = parseInt(process.env.PR_NUMBER, 10) || null;
await updateFinalStatus({
github,
context,
platforms,
outputs,
e2eTestsResult: process.env.E2E_TESTS_RESULT,
prNumber,
});
remove-e2e-label:
name: Remove E2E label from PR
runs-on: ubuntu-22.04
permissions:
issues: write
pull-requests: write
needs:
- e2e-tests
- e2e-policy-tests
- update-final-status
if: always()
steps:
- name: e2e/remove-label-from-pr
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
continue-on-error: true # Label might have been removed manually
with:
github-token: ${{ github.token }}
script: |
// Use pr_number if provided directly by the dispatcher (e.g. Matterwick)
let prNumber = parseInt(process.env.PR_NUMBER) || null;
// Fall back to looking up the PR by head branch when pr_number was not passed
if (!prNumber) {
const run = await github.rest.actions.getWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId,
});
const branchName = run.data.head_branch;
if (branchName) {
const headOwner = run.data.head_repository?.owner?.login || context.repo.owner;
const prs = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${headOwner}:${branchName}`,
});
if (prs.data && prs.data.length > 0) {
const matchingPr = prs.data.find((pr) => pr.head && pr.head.sha === run.data.head_sha);
prNumber = (matchingPr || prs.data[0]).number;
}
}
}
if (prNumber) {
try {
await github.rest.issues.removeLabel({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'E2E/Run',
});
} catch (error) {
if (error.status !== 404) {
throw error;
}
console.log('E2E/Run label already removed');
}
} else {
console.log('Label removal skipped - could not find associated PR');
}
env:
PR_NUMBER: ${{ inputs.pr_number }}
e2e-policy-tests:
name: policy-tests-${{ matrix.platform }}
# Runs untrusted PR code (npm ci / Playwright); grant only the commit-status write its
# check-for-failures step needs, never pull-requests: write.
permissions:
contents: read
statuses: write
strategy:
matrix:
include:
- platform: macos
runner: macos-26
- platform: windows
# Pinned to windows-2022 (not -2025) because the 2025 image ships
# Visual Studio 18 / "2026", which current node-gyp doesn't recognise —
# `npm ci` then fails to build the registry-js native module.
# The main e2e-tests matrix also runs on windows-2022; keep them aligned.
runner: windows-2022
fail-fast: false
runs-on: ${{ matrix.runner }}
defaults:
run:
shell: bash
env:
AWS_S3_BUCKET: "mattermost-cypress-report"
BRANCH: ${{ github.ref_name }}
BUILD_TAG: ${{ github.sha }}
MM_TEST_USER_NAME: ${{ inputs.MM_TEST_USER_NAME || secrets.MM_DESKTOP_E2E_USER_NAME }}
MM_TEST_PASSWORD: ${{ inputs.MM_TEST_PASSWORD || secrets.MM_DESKTOP_E2E_USER_CREDENTIALS }}
AWS_ACCESS_KEY_ID: ${{ secrets.MM_DESKTOP_E2E_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_DESKTOP_E2E_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: "us-east-1"
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
NODE_ENV: "test"
DEBUG_E2E: "true"
steps:
- name: e2e/set-server-url
env:
INSTANCE_DETAILS: ${{ inputs.instance_details }}
run: |
if [ -z "${INSTANCE_DETAILS}" ]; then
echo "Error: instance_details input is required but was not provided" >&2
exit 1
fi
RUNNER_OS=$(echo "${{ runner.os }}" | tr '[:upper:]' '[:lower:]')
URL=$(echo "${INSTANCE_DETAILS}" | jq -r --arg os "$RUNNER_OS" '.[] | select(.runner | ascii_downcase | contains($os)) | .url // empty' | head -1)
if [ -z "${URL}" ]; then
echo "Error: No server URL found in instance_details for runner OS: ${RUNNER_OS}" >&2
exit 1
fi
echo "MM_TEST_SERVER_URL=${URL}" >> $GITHUB_ENV
- name: e2e/checkout-repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.version_name }}
- name: e2e/setup-node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '22.x'
package-manager-cache: false
# node_modules is cached in e2e/cache-node-modules; disable setup-node's
# npm cache so actions/cache v5 does not hit legacy entries.
- name: e2e/use-gnu-tar-macos
if: runner.os == 'macOS'
run: |
# BSD tar on macOS can corrupt actions/cache v5 archives; prefer GNU tar when present.
# All current macOS runners are arm64 (Homebrew under /opt/homebrew); keep the
# -x guard so a future non-arm64 matrix entry falls back to BSD tar safely.
if [ -x /opt/homebrew/opt/gnu-tar/libexec/gnubin/gtar ]; then
echo "/opt/homebrew/opt/gnu-tar/libexec/gnubin" >> $GITHUB_PATH
fi
- name: e2e/cache-node-modules
id: cache-node-modules
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
node_modules
e2e/node_modules
enableCrossOsArchive: false
# v7: exact restore only — avoids probing legacy v6 blobs that warn on macOS
key: ${{ runner.os }}-build-node-modules-v7-${{ hashFiles('**/package-lock.json') }}
- name: e2e/cache-electron-gyp-windows
if: runner.os == 'Windows'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: C:\Users\runneradmin\.electron-gyp
enableCrossOsArchive: false
key: ${{ runner.os }}-electron-gyp-v7-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-electron-gyp-v7-${{ hashFiles('**/package-lock.json') }}
- name: e2e/setup-python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
env:
# Bypass pip's on-disk HTTP cache entirely so a stale/corrupt entry on the
# runner image cannot trigger deserialization warnings (actions/setup-python#1317).
PIP_NO_CACHE_DIR: "1"
with:
# Use a version pre-installed on macOS-26 (3.13 is in its toolcache), so
# setup-python doesn't install a mismatched interpreter whose pip can't
# deserialize the image's pre-baked cache. PIP_NO_CACHE_DIR above is the
# remaining guard for non-macOS runners.
python-version: "3.13"
# Omit `cache` — setup-python only accepts pip/pipenv/poetry, not `false`.
- name: e2e/install-os-dependencies
uses: ./.github/actions/install-os-dependencies
with:
os: ${{ runner.os }}
- name: e2e/install-node-dependencies
run: |
npm ci
cd e2e && npm ci
- name: e2e/suppress-macos-dialogs
if: runner.os == 'macOS'
run: |
# Suppress the "Reopen windows" dialog that blocks Electron startup
# when a previous test instance was killed by SIGTERM/SIGKILL.
defaults write com.github.Electron NSQuitAlwaysKeepsWindows -bool false || true
defaults write com.github.Electron ApplePersistenceIgnoreState -bool YES || true
defaults write com.apple.LaunchServices LSQuarantine -bool false || true
defaults write com.apple.CrashReporter DialogType none || true
- name: e2e/build-test-bundle
run: npm run build-test
- name: e2e/run-policy-tests-macos
if: runner.os == 'macOS'
run: |
cd e2e
set +e
npm run run:policy
echo "PLAYWRIGHT_EXIT_CODE=$?" >> $GITHUB_ENV
npm run send-report || true
env:
SERVER_VERSION: ${{ inputs.MM_SERVER_VERSION }}
DESKTOP_VERSION: ${{ inputs.version_name }}
- name: e2e/run-policy-tests-windows
if: runner.os == 'Windows'
run: |
cd e2e
set +e
npm run run:policy
echo "PLAYWRIGHT_EXIT_CODE=$?" >> $GITHUB_ENV
npm run send-report || true
env:
SERVER_VERSION: ${{ inputs.MM_SERVER_VERSION }}
DESKTOP_VERSION: ${{ inputs.version_name }}
- name: e2e/check-for-failures
id: check-failures
if: always()
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
process.chdir('./e2e');
const { analyzeFlakyTests } = require('./utils/analyze-flaky-test.js');
const { formatStatusDescription } = require('./utils/github-actions.js');
const { newFailedTests, failureCount, passCount, skipCount, totalCount, collectionFailed } = analyzeFlakyTests();
const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
const platform = process.platform === 'darwin' ? 'macos' : 'windows';
const hasFailed = newFailedTests && newFailedTests.length > 0;
const description = formatStatusDescription({
passed: passCount,
failed: failureCount,
collectionFailed,
});
try {
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: hasFailed ? 'failure' : 'success',
context: `policy-test/${platform}`,
description,
target_url: runUrl,
});
} catch (e) {
console.log(`Could not update commit status: ${e.message}`);
}
if (hasFailed) {
core.setFailed(`${newFailedTests.length} policy test(s) failed:\n${newFailedTests.join('\n')}`);
}
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: policy-test-results-${{ runner.os }}
path: e2e/playwright-report
if-no-files-found: ignore
retention-days: 7