580 lines
17 KiB
YAML
580 lines
17 KiB
YAML
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: Runtime versions
|
|
run: |
|
|
node -v
|
|
npm -v
|
|
|
|
- name: Capture node path
|
|
run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"
|
|
|
|
- name: Enable corepack and pin pnpm
|
|
run: |
|
|
corepack enable
|
|
corepack prepare pnpm@10.23.0 --activate
|
|
pnpm -v
|
|
|
|
- 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 Bun
|
|
uses: oven-sh/setup-bun@v2
|
|
with:
|
|
bun-version: latest
|
|
|
|
- name: Runtime versions
|
|
run: |
|
|
node -v
|
|
npm -v
|
|
bun -v
|
|
|
|
- name: Capture node path
|
|
run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"
|
|
|
|
- name: Enable corepack and pin pnpm
|
|
run: |
|
|
corepack enable
|
|
corepack prepare pnpm@10.23.0 --activate
|
|
pnpm -v
|
|
|
|
- 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-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 Bun
|
|
uses: oven-sh/setup-bun@v2
|
|
with:
|
|
bun-version: latest
|
|
|
|
- name: Runtime versions
|
|
run: |
|
|
node -v
|
|
npm -v
|
|
bun -v
|
|
|
|
- name: Capture node path
|
|
run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"
|
|
|
|
- name: Enable corepack and pin pnpm
|
|
run: |
|
|
corepack enable
|
|
corepack prepare pnpm@10.23.0 --activate
|
|
pnpm -v
|
|
|
|
- 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: Runtime versions
|
|
run: |
|
|
node -v
|
|
npm -v
|
|
|
|
- name: Capture node path
|
|
run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"
|
|
|
|
- name: Enable corepack and pin pnpm
|
|
run: |
|
|
corepack enable
|
|
corepack prepare pnpm@10.23.0 --activate
|
|
pnpm -v
|
|
|
|
- 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 }}
|