docs: add Chinese README link to README.md #3
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: | |
| 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 canvas:a2ui:bundle && 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: pnpm canvas:a2ui:bundle && 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 | |
| CLAWDBOT_TEST_WORKERS: 1 | |
| 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: 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 }} |