--- name: E2E Tests - Playwright Template (test system io dispatch) # Delegates Playwright spec dispatch + reporting to test system io. # Authenticates via GitHub Actions OIDC; calling job MUST grant # `id-token: write`. on: workflow_call: inputs: workers: description: "Number of parallel test system io dispatch workers" type: number required: false default: 8 enabled_docker_services: description: "Space-separated list of docker services to enable" type: string required: false default: "postgres inbucket" commit_sha: type: string required: true branch: type: string required: true build_id: type: string required: true server_image_tag: description: "Server image tag (e.g., master or short SHA)" type: string required: true server: type: string required: false default: onprem server_edition: description: "Server edition: enterprise (default), fips, or team" type: string required: false default: enterprise server_image_repo: description: "Docker registry: mattermostdevelopment (default) or mattermost" type: string required: false default: mattermostdevelopment server_image_aliases: description: "Comma-separated alias tags for description" type: string required: false enable_reporting: type: boolean required: false default: false report_type: type: string required: false ref_branch: type: string required: false pr_number: type: string required: false context_name: description: "GitHub commit status context name" type: string required: true playwright_project: description: "Playwright project name (passed to dispatch-begin metadata and dispatch-run --project=)." type: string required: false default: chrome playwright_retries: description: "Playwright --retries=N (per-spec, in-process retry of flaky tests)" type: number required: false default: 1 retest_on_fail: description: "Re-dispatch failed dispatch units once (whole-spec retry, on top of Playwright --retries)" type: boolean required: false default: true secrets: MM_LICENSE: required: false REPORT_WEBHOOK_URL: required: false # Callers must grant: contents: read, statuses: write, id-token: write permissions: contents: read statuses: write id-token: write env: SERVER_IMAGE: "${{ inputs.server_image_repo }}/${{ inputs.server_edition == 'fips' && 'mattermost-enterprise-fips-edition' || inputs.server_edition == 'team' && 'mattermost-team-edition' || 'mattermost-enterprise-edition' }}:${{ inputs.server_image_tag }}" jobs: prepare-run: runs-on: ubuntu-24.04 permissions: contents: read id-token: write statuses: write outputs: composite-identity-json: ${{ steps.composite-identity.outputs.composite-identity-json }} workers-matrix: ${{ steps.matrix.outputs.workers }} start_time: ${{ steps.matrix.outputs.start_time }} steps: - name: ci/checkout-repo uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false ref: ${{ inputs.commit_sha }} fetch-depth: 1 - name: ci/composite-identity id: composite-identity env: CONTEXT_NAME: ${{ inputs.context_name }} GITHUB_REPOSITORY: ${{ github.repository }} MM_BRANCH: ${{ inputs.branch }} MM_SHA: ${{ inputs.commit_sha }} PR_NUMBER: ${{ inputs.pr_number }} run: | # Derive the test-system-io run name from the GitHub commit-status # context: drop the `e2e-test/` prefix (the framework name already # implies E2E in the dashboard) and swap remaining `/` for `-` so # the dashboard URL is path-safe. The commit-status context itself # stays unchanged elsewhere — branch protection rules depend on it. NAME="${CONTEXT_NAME#e2e-test/}" NAME="${NAME//\//-}" if [ -n "$PR_NUMBER" ]; then COMPOSITE_IDENTITY=$(jq -nc \ --arg repo "${GITHUB_REPOSITORY}" \ --arg sha "${MM_SHA}" \ --arg run_id "${GITHUB_RUN_ID}" \ --arg name "${NAME}" \ --arg attempt "${GITHUB_RUN_ATTEMPT}" \ --arg branch "${MM_BRANCH}" \ --arg pr "${PR_NUMBER}" \ '{repository:$repo, commit_sha:$sha, gh_run_id:$run_id, name:$name, gh_run_attempt:$attempt, branch:$branch, gh_pr_number:$pr}') else COMPOSITE_IDENTITY=$(jq -nc \ --arg repo "${GITHUB_REPOSITORY}" \ --arg sha "${MM_SHA}" \ --arg run_id "${GITHUB_RUN_ID}" \ --arg name "${NAME}" \ --arg attempt "${GITHUB_RUN_ATTEMPT}" \ --arg branch "${MM_BRANCH}" \ '{repository:$repo, commit_sha:$sha, gh_run_id:$run_id, name:$name, gh_run_attempt:$attempt, branch:$branch}') fi echo "composite-identity-json=${COMPOSITE_IDENTITY}" >> $GITHUB_OUTPUT - name: ci/matrix id: matrix env: INPUT_WORKERS: ${{ inputs.workers }} run: | echo "workers=$(jq -nc --argjson n "${INPUT_WORKERS}" '[range(1; $n+1)]')" >> $GITHUB_OUTPUT echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT # Install webapp node_modules once via the shared webapp-setup action, then # workers restore the same stable cache. The node_modules cache is keyed only # on webapp/package-lock.json and is shared with webapp-ci.yml jobs. prep-deps: name: prep-deps runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: read steps: - name: ci/checkout-repo uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false ref: ${{ inputs.commit_sha }} fetch-depth: 1 - name: ci/setup-webapp-node-modules uses: ./.github/actions/webapp-setup - name: ci/cache-playwright-deps # Caches node_modules + the rolled-up @mattermost/playwright-lib dist # so workers don't re-run rollup on every job. id: cache-playwright uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: | e2e-tests/playwright/node_modules e2e-tests/playwright/lib/dist e2e-tests/playwright/lib/node_modules key: e2e-playwright-deps-${{ runner.os }}-${{ hashFiles('e2e-tests/playwright/package-lock.json', 'e2e-tests/playwright/lib/src/**', 'e2e-tests/playwright/lib/package.json', 'e2e-tests/playwright/lib/rollup.config.js', 'e2e-tests/playwright/lib/tsconfig.json') }} - name: ci/install-playwright-deps # `npm ci` creates symlinks at node_modules/@mattermost/{client,types} # → webapp/platform/{client,types}; targets must already be built. # The postinstall then builds lib/dist via rollup. Skip browser # download here — chromium is cached separately below. if: steps.cache-playwright.outputs.cache-hit != 'true' working-directory: e2e-tests/playwright env: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" run: npm ci - name: ci/cache-playwright-browsers # Cache chromium binary (~150MB) keyed on the playwright lockfile so a # version bump invalidates. Restored by workers; no docker image needed. id: cache-pw-browsers uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.cache/ms-playwright key: playwright-browsers-${{ runner.os }}-${{ hashFiles('e2e-tests/playwright/package-lock.json') }} - name: ci/install-playwright-chromium if: steps.cache-pw-browsers.outputs.cache-hit != 'true' working-directory: e2e-tests/playwright run: npx playwright install chromium # Register the Test System IO run AFTER prep-deps so workers reach # dispatch-run within Test System IO's inactivity window. dispatch-begin: runs-on: ubuntu-24.04 needs: [prepare-run, prep-deps] permissions: contents: read id-token: write statuses: write steps: - name: ci/checkout-repo uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false ref: ${{ inputs.commit_sha }} fetch-depth: 1 - name: ci/dispatch-begin uses: mattermost/mattermost-test-system-io/.github/actions/test-system-io-dispatch-begin@1631d8fcea24f4545a0b3b7f77e41c2fe0be4418 # 2026-07-28 with: use-staging: ${{ vars.E2E_USE_STAGING_TEST_IO_URL != 'false' }} framework: playwright repo-dir: ${{ github.workspace }} composite-identity: ${{ needs.prepare-run.outputs.composite-identity-json }} total-reports-expected: ${{ inputs.workers }} retest-on-fail: ${{ inputs.retest_on_fail }} playwright-project: ${{ inputs.playwright_project }} github-token: ${{ secrets.GITHUB_TOKEN }} commit-status-context: ${{ inputs.context_name }} image-tag: ${{ inputs.server_image_tag }} image-aliases: ${{ inputs.server_image_aliases }} workers: name: dispatch-run-${{ matrix.worker_index }} runs-on: ubuntu-24.04 timeout-minutes: 30 needs: [prepare-run, dispatch-begin] permissions: contents: read id-token: write strategy: fail-fast: false matrix: worker_index: ${{ fromJSON(needs.prepare-run.outputs.workers-matrix) }} env: COMPOSITE_IDENTITY: ${{ needs.prepare-run.outputs.composite-identity-json }} MM_LICENSE: "${{ secrets.MM_LICENSE }}" TEST: playwright BRANCH: "${{ inputs.branch }}" BUILD_ID: "${{ inputs.build_id }}" CI_BASE_URL: "full-test-${{ matrix.worker_index }}" # testcontainers mode: global setup brings up Postgres, Inbucket, the Mattermost # server, and these services itself. Every worker enables the same fixed set because # dispatch pulls specs from a shared queue, so no worker knows in advance which # service-tagged spec (e.g. @ldap, @saml, @elasticsearch) it will draw. PW_USE_TESTCONTAINERS: "true" PW_TESTCONTAINERS_SERVICES: "openldap,keycloak,elasticsearch,opensearch,minio,azurite" # Keeps the server alive across this worker's per-spec dispatch invocations instead of # tearing down after each — otherwise ci/prepare-playwright's own global-teardown call would # stop the server that ci/dispatch-run needs next. ci/testcontainers-teardown does the final # teardown once the whole queue is done. PW_TESTCONTAINERS_REUSE: "true" steps: - name: ci/checkout-actions # Sparse-checkout just .github/actions from the triggering ref (master) # so the composite action below is available before the full checkout # overwrites the workspace with inputs.commit_sha. uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false sparse-checkout: .github/actions sparse-checkout-cone-mode: true - name: ci/runner-prep-for-openldap uses: ./.github/actions/runner-prep-openldap - name: ci/checkout-repo uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false ref: ${{ inputs.commit_sha }} fetch-depth: 0 - name: ci/setup-webapp-node-modules uses: ./.github/actions/webapp-setup with: read-only: "true" - name: ci/restore-playwright-deps uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: | e2e-tests/playwright/node_modules e2e-tests/playwright/lib/dist e2e-tests/playwright/lib/node_modules key: e2e-playwright-deps-${{ runner.os }}-${{ hashFiles('e2e-tests/playwright/package-lock.json', 'e2e-tests/playwright/lib/src/**', 'e2e-tests/playwright/lib/package.json', 'e2e-tests/playwright/lib/rollup.config.js', 'e2e-tests/playwright/lib/tsconfig.json') }} fail-on-cache-miss: true - name: ci/restore-playwright-browsers uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.cache/ms-playwright key: playwright-browsers-${{ runner.os }}-${{ hashFiles('e2e-tests/playwright/package-lock.json') }} fail-on-cache-miss: true # Brings up the stack via Testcontainers in `testcontainers` mode (global setup). Also runs the # `setup` project so per-spec dispatches can pass --no-deps and skip plugin-load + # server-deployment checks. node_modules, lib/dist, and chromium are all restored from cache. - name: ci/prepare-playwright working-directory: e2e-tests/playwright run: npx playwright test --project=setup - name: ci/dispatch-run uses: mattermost/mattermost-test-system-io/.github/actions/test-system-io-dispatch-run@1631d8fcea24f4545a0b3b7f77e41c2fe0be4418 # 2026-07-28 with: use-staging: ${{ vars.E2E_USE_STAGING_TEST_IO_URL != 'false' }} framework: playwright composite-identity: ${{ needs.prepare-run.outputs.composite-identity-json }} repo-dir: ${{ github.workspace }} artifacts-root: ${{ github.workspace }}/worker-artifacts github-token: ${{ secrets.GITHUB_TOKEN }} gh-job-name: dispatch-run-${{ matrix.worker_index }} playwright-retries: ${{ inputs.playwright_retries }} playwright-project: ${{ inputs.playwright_project }} - name: ci/upload-debug-artifacts if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: playwright-full-${{ inputs.server_edition }}-debug-${{ matrix.worker_index }} path: | e2e-tests/playwright/logs/ e2e-tests/playwright/results/ worker-artifacts/ retention-days: 5 if-no-files-found: ignore report: runs-on: ubuntu-24.04 needs: [prepare-run, dispatch-begin, workers] if: always() permissions: contents: read id-token: write statuses: write outputs: commit_status_description: ${{ steps.summary.outputs.commit_status_description }} webhook_payload: ${{ steps.summary.outputs.webhook_payload }} steps: - name: ci/checkout-repo uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: ci/run-summary id: summary continue-on-error: true uses: mattermost/mattermost-test-system-io/.github/actions/test-system-io-summary@e2d5032cfa71a3ad11b0975b135cd221564f2787 # 2026-08-06 with: use-staging: ${{ vars.E2E_USE_STAGING_TEST_IO_URL != 'false' }} composite-identity: ${{ needs.prepare-run.outputs.composite-identity-json }} framework: playwright report-type: ${{ inputs.report_type }} image-tag: ${{ inputs.server_image_tag }} image-aliases: ${{ inputs.server_image_aliases }} server-image: ${{ env.SERVER_IMAGE }} pr-number: ${{ inputs.pr_number }} ref-branch: ${{ inputs.ref_branch }} commit-status-context: ${{ inputs.context_name }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: ci/publish-webhook if: inputs.enable_reporting && env.REPORT_WEBHOOK_URL != '' env: REPORT_WEBHOOK_URL: ${{ secrets.REPORT_WEBHOOK_URL }} PAYLOAD: ${{ steps.summary.outputs.webhook_payload }} run: | curl -X POST -H "Content-Type: application/json" -d "$PAYLOAD" "$REPORT_WEBHOOK_URL" - name: ci/assert-results env: SUMMARY_OUTCOME: ${{ steps.summary.outcome }} run: | [ "$SUMMARY_OUTCOME" = "success" ]