Electron Playwright Tests #3389
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| 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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| 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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| 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@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.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 |