test: consolidate redundant suites and speed attachment tests #4
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: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| # Detect docs-only changes to skip heavy jobs (test, build, Windows, macOS, Android). | |
| # Lint and format always run. Fail-safe: if detection fails, run everything. | |
| docs-scope: | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| outputs: | |
| docs_only: ${{ steps.check.outputs.docs_only }} | |
| docs_changed: ${{ steps.check.outputs.docs_changed }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| submodules: false | |
| - name: Detect docs-only changes | |
| id: check | |
| uses: ./.github/actions/detect-docs-changes | |
| # Detect which heavy areas are touched so PRs can skip unrelated expensive jobs. | |
| # Push to main keeps broad coverage. | |
| changed-scope: | |
| needs: [docs-scope] | |
| if: needs.docs-scope.outputs.docs_only != 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| outputs: | |
| run_node: ${{ steps.scope.outputs.run_node }} | |
| run_macos: ${{ steps.scope.outputs.run_macos }} | |
| run_android: ${{ steps.scope.outputs.run_android }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| submodules: false | |
| - name: Detect changed scopes | |
| id: scope | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ "${{ github.event_name }}" = "push" ]; then | |
| BASE="${{ github.event.before }}" | |
| else | |
| BASE="${{ github.event.pull_request.base.sha }}" | |
| fi | |
| CHANGED="$(git diff --name-only "$BASE" HEAD 2>/dev/null || echo "UNKNOWN")" | |
| if [ "$CHANGED" = "UNKNOWN" ] || [ -z "$CHANGED" ]; then | |
| # Fail-safe: run broad checks if detection fails. | |
| echo "run_node=true" >> "$GITHUB_OUTPUT" | |
| echo "run_macos=true" >> "$GITHUB_OUTPUT" | |
| echo "run_android=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| run_node=false | |
| run_macos=false | |
| run_android=false | |
| has_non_docs=false | |
| has_non_native_non_docs=false | |
| while IFS= read -r path; do | |
| [ -z "$path" ] && continue | |
| case "$path" in | |
| docs/*|*.md|*.mdx) | |
| continue | |
| ;; | |
| *) | |
| has_non_docs=true | |
| ;; | |
| esac | |
| case "$path" in | |
| # Generated protocol models are already covered by protocol:check and | |
| # should not force the full native macOS lane. | |
| apps/macos/Sources/OpenClawProtocol/*|apps/shared/OpenClawKit/Sources/OpenClawProtocol/*) | |
| ;; | |
| apps/macos/*|apps/ios/*|apps/shared/*|Swabble/*) | |
| run_macos=true | |
| ;; | |
| esac | |
| case "$path" in | |
| apps/android/*|apps/shared/*) | |
| run_android=true | |
| ;; | |
| esac | |
| case "$path" in | |
| src/*|test/*|extensions/*|packages/*|scripts/*|ui/*|.github/*|openclaw.mjs|package.json|pnpm-lock.yaml|pnpm-workspace.yaml|tsconfig*.json|vitest*.ts|tsdown.config.ts|.oxlintrc.json|.oxfmtrc.jsonc) | |
| run_node=true | |
| ;; | |
| esac | |
| case "$path" in | |
| apps/android/*|apps/ios/*|apps/macos/*|apps/shared/*|Swabble/*|appcast.xml) | |
| ;; | |
| *) | |
| has_non_native_non_docs=true | |
| ;; | |
| esac | |
| done <<< "$CHANGED" | |
| # If there are non-doc files outside native app trees, keep Node checks enabled. | |
| if [ "$run_node" = false ] && [ "$has_non_docs" = true ] && [ "$has_non_native_non_docs" = true ]; then | |
| run_node=true | |
| fi | |
| echo "run_node=${run_node}" >> "$GITHUB_OUTPUT" | |
| echo "run_macos=${run_macos}" >> "$GITHUB_OUTPUT" | |
| echo "run_android=${run_android}" >> "$GITHUB_OUTPUT" | |
| # Build dist once for Node-relevant changes and share it with downstream jobs. | |
| build-artifacts: | |
| needs: [docs-scope, changed-scope, check] | |
| if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| - name: Build dist | |
| run: pnpm build | |
| - name: Upload dist artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dist-build | |
| path: dist/ | |
| retention-days: 1 | |
| # Validate npm pack contents after build (only on push to main, not PRs). | |
| release-check: | |
| needs: [docs-scope, build-artifacts] | |
| if: github.event_name == 'push' && needs.docs-scope.outputs.docs_only != 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| - name: Download dist artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: dist-build | |
| path: dist/ | |
| - name: Check release contents | |
| run: pnpm release:check | |
| checks: | |
| needs: [docs-scope, changed-scope, check] | |
| if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - runtime: node | |
| task: test | |
| command: pnpm canvas:a2ui:bundle && pnpm test | |
| - runtime: node | |
| task: protocol | |
| command: pnpm protocol:check | |
| - runtime: bun | |
| task: test | |
| command: pnpm canvas:a2ui:bundle && bunx vitest run --config vitest.unit.config.ts | |
| steps: | |
| - name: Skip bun lane on push | |
| if: github.event_name == 'push' && matrix.runtime == 'bun' | |
| run: echo "Skipping bun test lane on push events." | |
| - name: Checkout | |
| if: github.event_name != 'push' || matrix.runtime != 'bun' | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Setup Node environment | |
| if: matrix.runtime != 'bun' || github.event_name != 'push' | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "${{ matrix.runtime == 'bun' }}" | |
| - name: Configure vitest JSON reports | |
| if: (github.event_name != 'push' || matrix.runtime != 'bun') && matrix.task == 'test' && matrix.runtime == 'node' | |
| run: echo "OPENCLAW_VITEST_REPORT_DIR=$RUNNER_TEMP/vitest-reports" >> "$GITHUB_ENV" | |
| - name: Configure Node test resources | |
| if: (github.event_name != 'push' || matrix.runtime != 'bun') && matrix.task == 'test' && matrix.runtime == 'node' | |
| run: | | |
| # `pnpm test` runs `scripts/test-parallel.mjs`, which spawns multiple Node processes. | |
| # Default heap limits have been too low on Linux CI (V8 OOM near 4GB). | |
| echo "OPENCLAW_TEST_WORKERS=2" >> "$GITHUB_ENV" | |
| echo "OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=6144" >> "$GITHUB_ENV" | |
| - name: Run ${{ matrix.task }} (${{ matrix.runtime }}) | |
| if: matrix.runtime != 'bun' || github.event_name != 'push' | |
| run: ${{ matrix.command }} | |
| - name: Summarize slowest tests | |
| if: (github.event_name != 'push' || matrix.runtime != 'bun') && matrix.task == 'test' && matrix.runtime == 'node' | |
| run: | | |
| node scripts/vitest-slowest.mjs --dir "$OPENCLAW_VITEST_REPORT_DIR" --top 50 --out "$RUNNER_TEMP/vitest-slowest.md" > /dev/null | |
| echo "Slowest test summary written to $RUNNER_TEMP/vitest-slowest.md" | |
| - name: Upload vitest reports | |
| if: (github.event_name != 'push' || matrix.runtime != 'bun') && matrix.task == 'test' && matrix.runtime == 'node' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: vitest-reports-${{ runner.os }}-${{ matrix.runtime }} | |
| path: | | |
| ${{ env.OPENCLAW_VITEST_REPORT_DIR }} | |
| ${{ runner.temp }}/vitest-slowest.md | |
| # Types, lint, and format check. | |
| check: | |
| name: "check" | |
| needs: [docs-scope] | |
| if: needs.docs-scope.outputs.docs_only != 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| - name: Check types and lint and oxfmt | |
| run: pnpm check | |
| # Report-only dead-code scans. Runs after scope detection and stores machine-readable | |
| # results as artifacts for later triage before we enable hard gates. | |
| # Temporarily disabled in CI while we process initial findings. | |
| deadcode: | |
| name: dead-code report | |
| needs: [docs-scope, changed-scope] | |
| # if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') | |
| if: false | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - tool: knip | |
| command: pnpm deadcode:report:ci:knip | |
| - tool: ts-prune | |
| command: pnpm deadcode:report:ci:ts-prune | |
| - tool: ts-unused-exports | |
| command: pnpm deadcode:report:ci:ts-unused | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| - name: Run ${{ matrix.tool }} dead-code scan | |
| run: ${{ matrix.command }} | |
| - name: Upload dead-code results | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dead-code-${{ matrix.tool }}-${{ github.run_id }} | |
| path: .artifacts/deadcode | |
| # Validate docs (format, lint, broken links) only when docs files changed. | |
| check-docs: | |
| needs: [docs-scope] | |
| if: needs.docs-scope.outputs.docs_changed == 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| - name: Check docs | |
| run: pnpm check:docs | |
| secrets: | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install detect-secrets | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install detect-secrets==1.5.0 | |
| - name: Detect secrets | |
| run: | | |
| if ! detect-secrets scan --baseline .secrets.baseline; then | |
| echo "::error::Secret scanning failed. See docs/gateway/security.md#secret-scanning-detect-secrets" | |
| exit 1 | |
| fi | |
| checks-windows: | |
| needs: [docs-scope, changed-scope, build-artifacts, check] | |
| if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') | |
| runs-on: blacksmith-16vcpu-windows-2025 | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=4096 | |
| # Keep total concurrency predictable on the 16 vCPU runner: | |
| # `scripts/test-parallel.mjs` runs some vitest suites in parallel processes. | |
| OPENCLAW_TEST_WORKERS: 2 | |
| defaults: | |
| run: | |
| shell: bash | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - runtime: node | |
| task: lint | |
| command: pnpm lint | |
| - runtime: node | |
| task: test | |
| command: pnpm canvas:a2ui:bundle && pnpm test | |
| - runtime: node | |
| task: protocol | |
| command: pnpm protocol:check | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Try to exclude workspace from Windows Defender (best-effort) | |
| shell: pwsh | |
| run: | | |
| $cmd = Get-Command Add-MpPreference -ErrorAction SilentlyContinue | |
| if (-not $cmd) { | |
| Write-Host "Add-MpPreference not available, skipping Defender exclusions." | |
| exit 0 | |
| } | |
| try { | |
| # Defender sometimes intercepts process spawning (vitest workers). If this fails | |
| # (eg hardened images), keep going and rely on worker limiting above. | |
| Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE" -ErrorAction Stop | |
| Add-MpPreference -ExclusionProcess "node.exe" -ErrorAction Stop | |
| Write-Host "Defender exclusions applied." | |
| } catch { | |
| Write-Warning "Failed to apply Defender exclusions, continuing. $($_.Exception.Message)" | |
| } | |
| - name: Download dist artifact (lint lane) | |
| if: matrix.task == 'lint' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: dist-build | |
| path: dist/ | |
| - name: Verify dist artifact (lint lane) | |
| if: matrix.task == 'lint' | |
| run: | | |
| set -euo pipefail | |
| test -s dist/index.js | |
| test -s dist/plugin-sdk/index.js | |
| - name: Setup Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: 22.x | |
| check-latest: true | |
| - name: Setup pnpm + cache store | |
| uses: ./.github/actions/setup-pnpm-store-cache | |
| with: | |
| pnpm-version: "10.23.0" | |
| cache-key-suffix: "node22" | |
| - name: Runtime versions | |
| run: | | |
| node -v | |
| npm -v | |
| pnpm -v | |
| - name: Capture node path | |
| run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV" | |
| - name: Install dependencies | |
| env: | |
| CI: true | |
| run: | | |
| export PATH="$NODE_BIN:$PATH" | |
| which node | |
| node -v | |
| pnpm -v | |
| pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true | |
| - name: Configure vitest JSON reports | |
| if: matrix.task == 'test' | |
| run: echo "OPENCLAW_VITEST_REPORT_DIR=$RUNNER_TEMP/vitest-reports" >> "$GITHUB_ENV" | |
| - name: Run ${{ matrix.task }} (${{ matrix.runtime }}) | |
| run: ${{ matrix.command }} | |
| - name: Summarize slowest tests | |
| if: matrix.task == 'test' | |
| run: | | |
| node scripts/vitest-slowest.mjs --dir "$OPENCLAW_VITEST_REPORT_DIR" --top 50 --out "$RUNNER_TEMP/vitest-slowest.md" > /dev/null | |
| echo "Slowest test summary written to $RUNNER_TEMP/vitest-slowest.md" | |
| - name: Upload vitest reports | |
| if: matrix.task == 'test' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: vitest-reports-${{ runner.os }}-${{ matrix.runtime }} | |
| path: | | |
| ${{ env.OPENCLAW_VITEST_REPORT_DIR }} | |
| ${{ runner.temp }}/vitest-slowest.md | |
| # Consolidated macOS job: runs TS tests + Swift lint/build/test sequentially | |
| # on a single runner. GitHub limits macOS concurrent jobs to 5 per org; | |
| # running 4 separate jobs per PR (as before) starved the queue. One job | |
| # per PR allows 5 PRs to run macOS checks simultaneously. | |
| macos: | |
| needs: [docs-scope, changed-scope, check] | |
| if: github.event_name == 'pull_request' && needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_macos == 'true' | |
| runs-on: macos-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| # --- Run all checks sequentially (fast gates first) --- | |
| - name: TS tests (macOS) | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=4096 | |
| run: pnpm test | |
| # --- Xcode/Swift setup --- | |
| - name: Select Xcode 26.1 | |
| run: | | |
| sudo xcode-select -s /Applications/Xcode_26.1.app | |
| xcodebuild -version | |
| - name: Install XcodeGen / SwiftLint / SwiftFormat | |
| run: brew install xcodegen swiftlint swiftformat | |
| - name: Show toolchain | |
| run: | | |
| sw_vers | |
| xcodebuild -version | |
| swift --version | |
| - name: Swift lint | |
| run: | | |
| swiftlint --config .swiftlint.yml | |
| swiftformat --lint apps/macos/Sources --config .swiftformat | |
| - name: Cache SwiftPM | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/Library/Caches/org.swift.swiftpm | |
| key: ${{ runner.os }}-swiftpm-${{ hashFiles('apps/macos/Package.resolved') }} | |
| restore-keys: | | |
| ${{ runner.os }}-swiftpm- | |
| - name: Swift build (release) | |
| run: | | |
| set -euo pipefail | |
| for attempt in 1 2 3; do | |
| if swift build --package-path apps/macos --configuration release; then | |
| exit 0 | |
| fi | |
| echo "swift build failed (attempt $attempt/3). Retrying…" | |
| sleep $((attempt * 20)) | |
| done | |
| exit 1 | |
| - name: Swift test | |
| run: | | |
| set -euo pipefail | |
| for attempt in 1 2 3; do | |
| if swift test --package-path apps/macos --parallel --enable-code-coverage --show-codecov-path; then | |
| exit 0 | |
| fi | |
| echo "swift test failed (attempt $attempt/3). Retrying…" | |
| sleep $((attempt * 20)) | |
| done | |
| exit 1 | |
| ios: | |
| if: false # ignore iOS in CI for now | |
| runs-on: macos-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Select Xcode 26.1 | |
| run: | | |
| sudo xcode-select -s /Applications/Xcode_26.1.app | |
| xcodebuild -version | |
| - name: Install XcodeGen | |
| run: brew install xcodegen | |
| - name: Install SwiftLint / SwiftFormat | |
| run: brew install swiftlint swiftformat | |
| - name: Show toolchain | |
| run: | | |
| sw_vers | |
| xcodebuild -version | |
| swift --version | |
| - name: Generate iOS project | |
| run: | | |
| cd apps/ios | |
| xcodegen generate | |
| - name: iOS tests | |
| run: | | |
| set -euo pipefail | |
| RESULT_BUNDLE_PATH="$RUNNER_TEMP/Clawdis-iOS.xcresult" | |
| DEST_ID="$( | |
| python3 - <<'PY' | |
| import json | |
| import subprocess | |
| import sys | |
| import uuid | |
| def sh(args: list[str]) -> str: | |
| return subprocess.check_output(args, text=True).strip() | |
| # Prefer an already-created iPhone simulator if it exists. | |
| devices = json.loads(sh(["xcrun", "simctl", "list", "devices", "-j"])) | |
| candidates: list[tuple[str, str]] = [] | |
| for runtime, devs in (devices.get("devices") or {}).items(): | |
| for dev in devs or []: | |
| if not dev.get("isAvailable"): | |
| continue | |
| name = str(dev.get("name") or "") | |
| udid = str(dev.get("udid") or "") | |
| if not udid or not name.startswith("iPhone"): | |
| continue | |
| candidates.append((name, udid)) | |
| candidates.sort(key=lambda it: (0 if "iPhone 16" in it[0] else 1, it[0])) | |
| if candidates: | |
| print(candidates[0][1]) | |
| sys.exit(0) | |
| # Otherwise, create one from the newest available iOS runtime. | |
| runtimes = json.loads(sh(["xcrun", "simctl", "list", "runtimes", "-j"])).get("runtimes") or [] | |
| ios = [rt for rt in runtimes if rt.get("platform") == "iOS" and rt.get("isAvailable")] | |
| if not ios: | |
| print("No available iOS runtimes found.", file=sys.stderr) | |
| sys.exit(1) | |
| def version_key(rt: dict) -> tuple[int, ...]: | |
| parts: list[int] = [] | |
| for p in str(rt.get("version") or "0").split("."): | |
| try: | |
| parts.append(int(p)) | |
| except ValueError: | |
| parts.append(0) | |
| return tuple(parts) | |
| ios.sort(key=version_key, reverse=True) | |
| runtime = ios[0] | |
| runtime_id = str(runtime.get("identifier") or "") | |
| if not runtime_id: | |
| print("Missing iOS runtime identifier.", file=sys.stderr) | |
| sys.exit(1) | |
| supported = runtime.get("supportedDeviceTypes") or [] | |
| iphones = [dt for dt in supported if dt.get("productFamily") == "iPhone"] | |
| if not iphones: | |
| print("No iPhone device types for iOS runtime.", file=sys.stderr) | |
| sys.exit(1) | |
| iphones.sort( | |
| key=lambda dt: ( | |
| 0 if "iPhone 16" in str(dt.get("name") or "") else 1, | |
| str(dt.get("name") or ""), | |
| ) | |
| ) | |
| device_type_id = str(iphones[0].get("identifier") or "") | |
| if not device_type_id: | |
| print("Missing iPhone device type identifier.", file=sys.stderr) | |
| sys.exit(1) | |
| sim_name = f"CI iPhone {uuid.uuid4().hex[:8]}" | |
| udid = sh(["xcrun", "simctl", "create", sim_name, device_type_id, runtime_id]) | |
| if not udid: | |
| print("Failed to create iPhone simulator.", file=sys.stderr) | |
| sys.exit(1) | |
| print(udid) | |
| PY | |
| )" | |
| echo "Using iOS Simulator id: $DEST_ID" | |
| xcodebuild test \ | |
| -project apps/ios/Clawdis.xcodeproj \ | |
| -scheme Clawdis \ | |
| -destination "platform=iOS Simulator,id=$DEST_ID" \ | |
| -resultBundlePath "$RESULT_BUNDLE_PATH" \ | |
| -enableCodeCoverage YES | |
| - name: iOS coverage summary | |
| run: | | |
| set -euo pipefail | |
| RESULT_BUNDLE_PATH="$RUNNER_TEMP/Clawdis-iOS.xcresult" | |
| xcrun xccov view --report --only-targets "$RESULT_BUNDLE_PATH" | |
| - name: iOS coverage gate (43%) | |
| run: | | |
| set -euo pipefail | |
| RESULT_BUNDLE_PATH="$RUNNER_TEMP/Clawdis-iOS.xcresult" | |
| RESULT_BUNDLE_PATH="$RESULT_BUNDLE_PATH" python3 - <<'PY' | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| target_name = "Clawdis.app" | |
| minimum = 0.43 | |
| report = json.loads( | |
| subprocess.check_output( | |
| ["xcrun", "xccov", "view", "--report", "--json", os.environ["RESULT_BUNDLE_PATH"]], | |
| text=True, | |
| ) | |
| ) | |
| target_coverage = None | |
| for target in report.get("targets", []): | |
| if target.get("name") == target_name: | |
| target_coverage = float(target["lineCoverage"]) | |
| break | |
| if target_coverage is None: | |
| print(f"Could not find coverage for target: {target_name}") | |
| sys.exit(1) | |
| print(f"{target_name} line coverage: {target_coverage * 100:.2f}% (min {minimum * 100:.2f}%)") | |
| if target_coverage + 1e-12 < minimum: | |
| sys.exit(1) | |
| PY | |
| android: | |
| needs: [docs-scope, changed-scope, check] | |
| if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_android == 'true') | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - task: test | |
| command: ./gradlew --no-daemon :app:testDebugUnitTest | |
| - task: build | |
| command: ./gradlew --no-daemon :app:assembleDebug | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Setup Java | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| # setup-android's sdkmanager currently crashes on JDK 21 in CI. | |
| java-version: 17 | |
| - name: Setup Android SDK | |
| uses: android-actions/setup-android@v3 | |
| with: | |
| accept-android-sdk-licenses: false | |
| - name: Setup Gradle | |
| uses: gradle/actions/setup-gradle@v4 | |
| with: | |
| gradle-version: 8.11.1 | |
| - name: Install Android SDK packages | |
| run: | | |
| yes | sdkmanager --licenses >/dev/null | |
| sdkmanager --install \ | |
| "platform-tools" \ | |
| "platforms;android-36" \ | |
| "build-tools;36.0.0" | |
| - name: Run Android ${{ matrix.task }} | |
| working-directory: apps/android | |
| run: ${{ matrix.command }} |