# 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:
        - cron: "0 6 * * *"
    pull_request: {}
    merge_group:
        types: [checks_requested]
    push:
        # We do not build on push to develop as the merge_group check handles that
        branches: [staging, master]

    # support triggering from other workflows
    workflow_call:
        inputs:
            skip:
                type: boolean
                required: false
                default: false
                description: "A boolean to skip the playwright check itself while still creating the passing check. Useful when only running in Merge Queues."

            matrix-js-sdk-sha:
                type: string
                required: false
                description: "The Git SHA of matrix-js-sdk to build against. By default, will use a matching branch name if it exists, or develop."

concurrency:
    group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
    cancel-in-progress: true

env:
    # fetchdep.sh needs to know our PR number
    PR_NUMBER: ${{ github.event.pull_request.number }}
    # 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_ew:
        name: "Build Element Web"
        runs-on: ubuntu-24.04
        if: inputs.skip != true
        outputs:
            num-runners: ${{ env.NUM_RUNNERS }}
            runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
            # Skip pull_request runs on renovate PRs to speed up CI time, delegating to the full run in merge queue
            skip: ${{ inputs.skip || (github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }}
        steps:
            - name: Checkout code
              uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
              with:
                  repository: element-hq/element-web
                  persist-credentials: false

            - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
            - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
              with:
                  cache: "pnpm"
                  node-version: "lts/*"

            - name: Fetch layered build
              env:
                  # tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one
                  JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
              run: scripts/layered.sh

            - name: Copy config
              working-directory: apps/web
              run: cp element.io/develop/config.json config.json

            - name: Build
              env:
                  CI_PACKAGE: true
              working-directory: apps/web
              run: VERSION=$(scripts/get-version-from-git.sh) pnpm run build

            - name: Upload Artifact
              uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
              with:
                  name: webapp
                  path: apps/web/webapp
                  retention-days: 1

            - name: Calculate runner variables
              id: runner-vars
              uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
              with:
                  script: |
                      const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
                      const matrix = Array.from({ length: numRunners }, (_, i) => i + 1);
                      core.setOutput("matrix", JSON.stringify(matrix));

    playwright_ew:
        name: "Run Tests [${{ matrix.project }}] ${{ matrix.runner }}/${{ needs.build_ew.outputs.num-runners }}"
        needs: build_ew
        if: needs.build_ew.outputs.skip == 'false'
        runs-on: ubuntu-24.04
        permissions:
            actions: read
            issues: read
            pull-requests: read
        strategy:
            fail-fast: false
            matrix:
                # Run multiple instances in parallel to speed up the tests
                runner: ${{ fromJSON(needs.build_ew.outputs.runners-matrix) }}
                project:
                    - Chrome
                    - Firefox
                    - WebKit
                    - Dendrite
                    - Pinecone
                runAllTests:
                    - ${{ github.event_name == 'schedule' || contains(github.event.pull_request.labels.*.name, 'X-Run-All-Tests') }}
                # Skip the Firefox & Safari runs unless this was a cron trigger or PR has X-Run-All-Tests label
                exclude:
                    - runAllTests: false
                      project: Firefox
                    - runAllTests: false
                      project: WebKit
                    - runAllTests: false
                      project: Dendrite
                    - runAllTests: false
                      project: Pinecone
        steps:
            - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
              with:
                  persist-credentials: false
                  repository: element-hq/element-web

            - name: 📥 Download artifact
              uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
              with:
                  name: webapp
                  path: apps/web/webapp

            - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
            - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
              with:
                  cache: "pnpm"
                  cache-dependency-path: pnpm-lock.yaml
                  node-version: "lts/*"

            - name: Install dependencies
              run: pnpm install --frozen-lockfile

            - name: Setup playwright
              uses: ./.github/actions/setup-playwright
              with:
                  needs-webkit: ${{ matrix.project == 'WebKit' }}
                  write-cache: ${{ github.event_name != 'merge_group' }}

            # We skip tests tagged with @mergequeue when running on PRs, but run them in MQ and everywhere else
            - name: Run Playwright tests
              working-directory: apps/web
              run: |
                  pnpm test:playwright \
                      --shard "$SHARD" \
                      --project="${{ matrix.project }}" \
                      ${{ (github.event_name == 'pull_request' && matrix.runAllTests == false ) && '--grep-invert @mergequeue' || '' }}
              env:
                  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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
              with:
                  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_ew
        if: needs.build_ew.outputs.skip == 'false' && 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
            reporter: blob

    prepare_ed:
        name: "Prepare Element Desktop"
        uses: ./.github/workflows/build_desktop_prepare.yaml
        needs: build_ew
        if: needs.build_ew.outputs.skip == 'false'
        permissions:
            contents: read
        with:
            config: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'element.io/nightly' || 'element.io/release' }}
            version: ${{ case((github.event.pull_request.base.ref || github.ref_name) == 'develop' || github.event_name == 'merge_group', 'develop', '') }}
            webapp-artifact: webapp

    build_ed_windows:
        needs: prepare_ed
        name: "Desktop Windows"
        uses: ./.github/workflows/build_desktop_windows.yaml
        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
        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
        with:
            blob_report: true

    complete:
        name: end-to-end-tests
        needs:
            - build_ew
            - playwright_ew
            - downstream-modules
            - prepare_ed
            - build_ed_windows
            - build_ed_linux
            - build_ed_macos
        if: always()
        runs-on: ubuntu-24.04
        steps:
            - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
              if: needs.build_ew.outputs.skip == 'false'
              with:
                  persist-credentials: false
                  repository: element-hq/element-web

            - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
              if: needs.build_ew.outputs.skip == 'false'
            - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
              if: needs.build_ew.outputs.skip == 'false'
              with:
                  cache: "pnpm"
                  node-version: "lts/*"

            - name: Install dependencies
              if: needs.build_ew.outputs.skip == 'false'
              run: pnpm install --frozen-lockfile

            - name: Download blob reports from GitHub Actions Artifacts
              if: needs.build_ew.outputs.skip == 'false'
              uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
              with:
                  pattern: blob-report-*
                  path: all-blob-reports
                  merge-multiple: true

            - name: Merge into HTML Report
              if: needs.build_ew.outputs.skip == 'false'
              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('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
              if: always() && needs.build_ew.outputs.skip == 'false'
              uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
              with:
                  name: html-report
                  path: playwright-report
                  retention-days: 14
                  if-no-files-found: error

            - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
              run: exit 1
