name: Unit Tests on: workflow_dispatch: push: branches: [develop] pull_request: permissions: {} jobs: tests: name: Tests # HealthChat: GitHub-hosted macos-26 could not finish this job inside # timeout-minutes: 60 (two attempts: 51m42s, then killed at 60m44s), and macOS # minutes bill at 10x. novaloop-mac-studio-001 is the org's Mac Studio, already # used by the release pipeline; its labels are [self-hosted, macOS, ARM64]. runs-on: [self-hosted, macOS] # Upstream's 60 is kept: with previews skipped (see Run tests) the build plus unit tests measured # ~16 min here. A run approaching this budget is a signal, not normal — the earlier 60m timeouts # were preview tests, not the build. timeout-minutes: 60 permissions: contents: read # HealthChat: this fork is INTERNAL, so a metadata-only token 404s on checkout (and on git lfs pull). Upstream is public, hence permissions: {} there. concurrency: # Only allow a single run of this workflow on each branch, automatically cancelling older runs. group: ${{ format('unit-tests-{0}', github.ref) }} cancel-in-progress: true steps: # HealthChat: upstream passes persist-credentials: false here, which the # composite forwards to actions/checkout — stripping the auth header before # its separate "Git LFS Pull" step runs. Upstream can afford that because # its repo is public and git lfs pull works anonymously; this fork is # INTERNAL, so the LFS batch endpoint would 404. Keep the composite's own # default (true) so the pull is authenticated with GITHUB_TOKEN. - uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 # HealthChat: verify only — never provision. Upstream's # setup_github_actions_environment cannot run on this runner: it calls # `sudo xcode-select -s /Applications/Xcode_26.5.0.app`, but the runner has no # passwordless sudo and no such path (only /Applications/Xcode.app exists), and # it calls `brew update && brew install ...` — novaloop-mac-studio-001 is the # shared release machine, so a per-run `brew update` could upgrade xcodegen # underneath the release pipeline. These tools are installed once by hand; CI # only asserts they are present and fails loudly, naming the fix, if not. - name: Verify environment run: | set -eu missing="" formulae="" for tool in xcodegen swiftlint swiftformat xcbeautify xcresultparser pkl git-lfs; do if ! command -v "$tool" >/dev/null 2>&1; then missing="$missing $tool" case "$tool" in # Not in homebrew-core; only this tap ships it. xcresultparser) formulae="$formulae a7ex/homebrew-formulae/xcresultparser" ;; *) formulae="$formulae $tool" ;; esac fi done if [ -n "$missing" ]; then echo "::error::Missing on self-hosted runner:${missing}. Install by hand on novaloop-mac-studio-001, then re-run: brew install${formulae}" exit 1 fi # The Xcode version is no longer pinned anywhere, so this is the only record # of which Xcode built a given run. xcodebuild -version - name: Restore compilation cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/Library/Developer/Xcode/CompilationCache.noindex key: compilation-cache-unit-tests-${{ github.sha }} restore-keys: compilation-cache-unit-tests- - name: Run tests # HealthChat: previews are skipped. PreviewTests renders a snapshot for every preview in the # app and was the entire reason this job could not fit in an hour — it never once finished, # on either runner. Unit tests alone take ~16 min here. Upstream skips them the same way in # unit-tests-enterprise.yml. Cost: preview snapshot regressions are not caught in CI, so # re-record and check them locally when changing UI. run: swift run -q tools ci unit-tests --skip-previews - name: Save compilation cache # Only develop populates the cache, all of the other runs simply read from it. if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/develop' }} uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/Library/Developer/Xcode/CompilationCache.noindex key: compilation-cache-unit-tests-${{ github.sha }} - name: Archive artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 # We only care about artefacts if the tests fail if: failure() with: name: Results path: test_output/UnitTests.zip retention-days: 1 if-no-files-found: ignore - name: Upload coverage to Codecov # Skip if not successful and in forks # HealthChat: the existing guard only asks whether the PR comes FROM a # fork, not whether this repo IS one, so it is true here and the step # runs without a CODECOV_TOKEN — which fails the job via # fail_ci_if_error. Gate on the upstream repo as post-release.yml does. if: ${{ success() && github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: report_type: coverage files: test_output/unit-cobertura.xml,test_output/preview-cobertura.xml disable_search: true fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} flags: unittests - name: Upload test results to Codecov # Skip if cancelled and in forks # HealthChat: gated on the upstream repo for the same reason as the # coverage step above — no CODECOV_TOKEN exists in this fork. if: ${{ !cancelled() && github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 continue-on-error: true with: report_type: test_results files: test_output/unit-junit.xml,test_output/preview-junit.xml disable_search: true fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} flags: unittests