name: CI on: push: pull_request: jobs: install-check: runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout uses: actions/checkout@v4 with: submodules: false - name: Checkout submodules (retry) run: | set -euo pipefail git submodule sync --recursive for attempt in 1 2 3 4 5; do if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then exit 0 fi echo "Submodule update failed (attempt $attempt/5). Retrying…" sleep $((attempt * 10)) done exit 1 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22.x check-latest: true - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10.23.0 run_install: false - 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 (frozen) 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 checks: runs-on: blacksmith-4vcpu-ubuntu-2404 strategy: fail-fast: false matrix: include: - runtime: node task: lint command: pnpm lint - runtime: node task: test command: pnpm test - runtime: node task: build command: pnpm build - runtime: node task: protocol command: pnpm protocol:check - runtime: node task: format command: pnpm format - runtime: bun task: test command: bunx vitest run - runtime: bun task: build command: bunx tsc -p tsconfig.json steps: - name: Checkout uses: actions/checkout@v4 with: submodules: false - name: Checkout submodules (retry) run: | set -euo pipefail git submodule sync --recursive for attempt in 1 2 3 4 5; do if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then exit 0 fi echo "Submodule update failed (attempt $attempt/5). Retrying…" sleep $((attempt * 10)) done exit 1 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22.x check-latest: true - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10.23.0 run_install: false - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Runtime versions run: | node -v npm -v bun -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: Run ${{ matrix.task }} (${{ matrix.runtime }}) run: ${{ matrix.command }} secrets: runs-on: blacksmith-4vcpu-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: runs-on: blacksmith-4vcpu-windows-2025 defaults: run: shell: bash strategy: fail-fast: false matrix: include: - runtime: node task: lint command: pnpm lint - runtime: node task: test command: pnpm test - runtime: node task: build command: pnpm build - runtime: node task: protocol command: pnpm protocol:check steps: - name: Checkout uses: actions/checkout@v4 with: submodules: false - name: Checkout submodules (retry) run: | set -euo pipefail git submodule sync --recursive for attempt in 1 2 3 4 5; do if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then exit 0 fi echo "Submodule update failed (attempt $attempt/5). Retrying…" sleep $((attempt * 10)) done exit 1 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22.x check-latest: true - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10.23.0 run_install: false - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Runtime versions run: | node -v npm -v bun -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: Run ${{ matrix.task }} (${{ matrix.runtime }}) run: ${{ matrix.command }} checks-macos: if: github.event_name == 'pull_request' runs-on: macos-latest strategy: fail-fast: false matrix: include: - task: test command: pnpm test steps: - name: Checkout uses: actions/checkout@v4 with: submodules: false - name: Checkout submodules (retry) run: | set -euo pipefail git submodule sync --recursive for attempt in 1 2 3 4 5; do if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then exit 0 fi echo "Submodule update failed (attempt $attempt/5). Retrying…" sleep $((attempt * 10)) done exit 1 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22.x check-latest: true - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10.23.0 run_install: false - 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: Run ${{ matrix.task }} run: ${{ matrix.command }} macos-app: if: github.event_name == 'pull_request' runs-on: macos-latest strategy: fail-fast: false matrix: include: - task: lint command: | swiftlint --config .swiftlint.yml swiftformat --lint apps/macos/Sources --config .swiftformat - task: build command: | 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 - task: test command: | 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 steps: - name: Checkout uses: actions/checkout@v4 with: submodules: false - name: Checkout submodules (retry) run: | set -euo pipefail git submodule sync --recursive for attempt in 1 2 3 4 5; do if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then exit 0 fi echo "Submodule update failed (attempt $attempt/5). Retrying…" sleep $((attempt * 10)) done exit 1 - 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: Run ${{ matrix.task }} run: ${{ matrix.command }} ios: if: false # ignore iOS in CI for now runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: false - name: Checkout submodules (retry) run: | set -euo pipefail git submodule sync --recursive for attempt in 1 2 3 4 5; do if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then exit 0 fi echo "Submodule update failed (attempt $attempt/5). Retrying…" sleep $((attempt * 10)) done exit 1 - 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: runs-on: blacksmith-4vcpu-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: Checkout submodules (retry) run: | set -euo pipefail git submodule sync --recursive for attempt in 1 2 3 4 5; do if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then exit 0 fi echo "Submodule update failed (attempt $attempt/5). Retrying…" sleep $((attempt * 10)) done exit 1 - name: Setup Java uses: actions/setup-java@v4 with: distribution: temurin java-version: 21 - 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 - 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 }}