Spaces:
Paused
Paused
| 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 (corepack retry) | |
| run: | | |
| set -euo pipefail | |
| corepack enable | |
| for attempt in 1 2 3; do | |
| if corepack prepare pnpm@10.23.0 --activate; then | |
| pnpm -v | |
| exit 0 | |
| fi | |
| echo "corepack prepare failed (attempt $attempt/3). Retrying..." | |
| sleep $((attempt * 10)) | |
| done | |
| exit 1 | |
| - 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 (corepack retry) | |
| run: | | |
| set -euo pipefail | |
| corepack enable | |
| for attempt in 1 2 3; do | |
| if corepack prepare pnpm@10.23.0 --activate; then | |
| pnpm -v | |
| exit 0 | |
| fi | |
| echo "corepack prepare failed (attempt $attempt/3). Retrying..." | |
| sleep $((attempt * 10)) | |
| done | |
| exit 1 | |
| - 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 | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=4096 | |
| 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 (corepack retry) | |
| run: | | |
| set -euo pipefail | |
| corepack enable | |
| for attempt in 1 2 3; do | |
| if corepack prepare pnpm@10.23.0 --activate; then | |
| pnpm -v | |
| exit 0 | |
| fi | |
| echo "corepack prepare failed (attempt $attempt/3). Retrying..." | |
| sleep $((attempt * 10)) | |
| done | |
| exit 1 | |
| - 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 (corepack retry) | |
| run: | | |
| set -euo pipefail | |
| corepack enable | |
| for attempt in 1 2 3; do | |
| if corepack prepare pnpm@10.23.0 --activate; then | |
| pnpm -v | |
| exit 0 | |
| fi | |
| echo "corepack prepare failed (attempt $attempt/3). Retrying..." | |
| sleep $((attempt * 10)) | |
| done | |
| exit 1 | |
| - 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 }} | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=4096 | |
| 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 | |
| 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 }} | |