mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
* feat(secrets): expand secret target coverage and gateway tooling * docs(secrets): align gateway and CLI secret docs * chore(protocol): regenerate swift gateway models for secrets methods * fix(config): restore talk apiKey fallback and stabilize runner test * ci(windows): reduce test worker count for shard stability * ci(windows): raise node heap for test shard stability * test(feishu): make proxy env precedence assertion windows-safe * fix(gateway): resolve auth password SecretInput refs for clients * fix(gateway): resolve remote SecretInput credentials for clients * fix(secrets): skip inactive refs in command snapshot assignments * fix(secrets): scope gateway.remote refs to effective auth surfaces * fix(secrets): ignore memory defaults when enabled agents disable search * fix(secrets): honor Google Chat serviceAccountRef inheritance * fix(secrets): address tsgo errors in command and gateway collectors * fix(secrets): avoid auth-store load in providers-only configure * fix(gateway): defer local password ref resolution by precedence * fix(secrets): gate telegram webhook secret refs by webhook mode * fix(secrets): gate slack signing secret refs to http mode * fix(secrets): skip telegram botToken refs when tokenFile is set * fix(secrets): gate discord pluralkit refs by enabled flag * fix(secrets): gate discord voice tts refs by voice enabled * test(secrets): make runtime fixture modes explicit * fix(cli): resolve local qr password secret refs * fix(cli): fail when gateway leaves command refs unresolved * fix(gateway): fail when local password SecretRef is unresolved * fix(gateway): fail when required remote SecretRefs are unresolved * fix(gateway): resolve local password refs only when password can win * fix(cli): skip local password SecretRef resolution on qr token override * test(gateway): cast SecretRef fixtures to OpenClawConfig * test(secrets): activate mode-gated targets in runtime coverage fixture * fix(cron): support SecretInput webhook tokens safely * fix(bluebubbles): support SecretInput passwords across config paths * fix(msteams): make appPassword SecretInput-safe in onboarding/token paths * fix(bluebubbles): align SecretInput schema helper typing * fix(cli): clarify secrets.resolve version-skew errors * refactor(secrets): return structured inactive paths from secrets.resolve * refactor(gateway): type onboarding secret writes as SecretInput * chore(protocol): regenerate swift models for secrets.resolve * feat(secrets): expand extension credential secretref support * fix(secrets): gate web-search refs by active provider * fix(onboarding): detect SecretRef credentials in extension status * fix(onboarding): allow keeping existing ref in secret prompt * fix(onboarding): resolve gateway password SecretRefs for probe and tui * fix(onboarding): honor secret-input-mode for local gateway auth * fix(acp): resolve gateway SecretInput credentials * fix(secrets): gate gateway.remote refs to remote surfaces * test(secrets): cover pattern matching and inactive array refs * docs(secrets): clarify secrets.resolve and remote active surfaces * fix(bluebubbles): keep existing SecretRef during onboarding * fix(tests): resolve CI type errors in new SecretRef coverage * fix(extensions): replace raw fetch with SSRF-guarded fetch * test(secrets): mark gateway remote targets active in runtime coverage * test(infra): normalize home-prefix expectation across platforms * fix(cli): only resolve local qr password refs in password mode * test(cli): cover local qr token mode with unresolved password ref * docs(cli): clarify local qr password ref resolution behavior * refactor(extensions): reuse sdk SecretInput helpers * fix(wizard): resolve onboarding env-template secrets before plaintext * fix(cli): surface secrets.resolve diagnostics in memory and qr * test(secrets): repair post-rebase runtime and fixtures * fix(gateway): skip remote password ref resolution when token wins * fix(secrets): treat tailscale remote gateway refs as active * fix(gateway): allow remote password fallback when token ref is unresolved * fix(gateway): ignore stale local password refs for none and trusted-proxy * fix(gateway): skip remote secret ref resolution on local call paths * test(cli): cover qr remote tailscale secret ref resolution * fix(secrets): align gateway password active-surface with auth inference * fix(cli): resolve inferred local gateway password refs in qr * fix(gateway): prefer resolvable remote password over token ref pre-resolution * test(gateway): cover none and trusted-proxy stale password refs * docs(secrets): sync qr and gateway active-surface behavior * fix: restore stability blockers from pre-release audit * Secrets: fix collector/runtime precedence contradictions * docs: align secrets and web credential docs * fix(rebase): resolve integration regressions after main rebase * fix(node-host): resolve gateway secret refs for auth * fix(secrets): harden secretinput runtime readers * gateway: skip inactive auth secretref resolution * cli: avoid gateway preflight for inactive secret refs * extensions: allow unresolved refs in onboarding status * tests: fix qr-cli module mock hoist ordering * Security: align audit checks with SecretInput resolution * Gateway: resolve local-mode remote fallback secret refs * Node host: avoid resolving inactive password secret refs * Secrets runtime: mark Slack appToken inactive for HTTP mode * secrets: keep inactive gateway remote refs non-blocking * cli: include agent memory secret targets in runtime resolution * docs(secrets): sync docs with active-surface and web search behavior * fix(secrets): keep telegram top-level token refs active for blank account tokens * fix(daemon): resolve gateway password secret refs for probe auth * fix(secrets): skip IRC NickServ ref resolution when NickServ is disabled * fix(secrets): align token inheritance and exec timeout defaults * docs(secrets): clarify active-surface notes in cli docs * cli: require secrets.resolve gateway capability * gateway: log auth secret surface diagnostics * secrets: remove dead provider resolver module * fix(secrets): restore gateway auth precedence and fallback resolution * fix(tests): align plugin runtime mock typings --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
729 lines
23 KiB
YAML
729 lines
23 KiB
YAML
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
|
|
|
|
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
|
|
|
|
# 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"
|
|
use-sticky-disk: "true"
|
|
|
|
- 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"
|
|
use-sticky-disk: "true"
|
|
|
|
- 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' }}"
|
|
use-sticky-disk: "true"
|
|
|
|
- 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 }}
|
|
|
|
# Types, lint, and format check.
|
|
check:
|
|
name: "check"
|
|
needs: [docs-scope, changed-scope]
|
|
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"
|
|
use-sticky-disk: "true"
|
|
|
|
- name: Check types and lint and oxfmt
|
|
run: pnpm check
|
|
|
|
- name: Strict TS build smoke
|
|
run: pnpm build:strict-smoke
|
|
|
|
- name: Enforce safe external URL opening policy
|
|
run: pnpm lint:ui:no-raw-window-open
|
|
|
|
# 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"
|
|
use-sticky-disk: "true"
|
|
|
|
- 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"
|
|
use-sticky-disk: "true"
|
|
|
|
- name: Check docs
|
|
run: pnpm check:docs
|
|
|
|
skills-python:
|
|
needs: [docs-scope, changed-scope]
|
|
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 Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Install Python tooling
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
python -m pip install pytest ruff pyyaml
|
|
|
|
- name: Lint Python skill scripts
|
|
run: python -m ruff check skills
|
|
|
|
- name: Test skill Python scripts
|
|
run: python -m pytest -q skills
|
|
|
|
secrets:
|
|
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"
|
|
use-sticky-disk: "true"
|
|
|
|
- name: Setup Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Install pre-commit
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
python -m pip install pre-commit 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
|
|
|
|
- name: Detect committed private keys
|
|
run: pre-commit run --all-files detect-private-key
|
|
|
|
- name: Audit changed GitHub workflows with zizmor
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [ "${{ github.event_name }}" = "push" ]; then
|
|
BASE="${{ github.event.before }}"
|
|
else
|
|
BASE="${{ github.event.pull_request.base.sha }}"
|
|
fi
|
|
|
|
mapfile -t workflow_files < <(git diff --name-only "$BASE" HEAD -- '.github/workflows/*.yml' '.github/workflows/*.yaml')
|
|
if [ "${#workflow_files[@]}" -eq 0 ]; then
|
|
echo "No workflow changes detected; skipping zizmor."
|
|
exit 0
|
|
fi
|
|
|
|
pre-commit run zizmor --files "${workflow_files[@]}"
|
|
|
|
- name: Audit production dependencies
|
|
run: pre-commit run --all-files pnpm-audit-prod
|
|
|
|
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
|
|
timeout-minutes: 45
|
|
env:
|
|
NODE_OPTIONS: --max-old-space-size=6144
|
|
# Keep total concurrency predictable on the 16 vCPU runner.
|
|
# Windows shard 2 has shown intermittent instability at 2 workers.
|
|
OPENCLAW_TEST_WORKERS: 1
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- runtime: node
|
|
task: lint
|
|
shard_index: 0
|
|
shard_count: 1
|
|
command: pnpm lint
|
|
- runtime: node
|
|
task: test
|
|
shard_index: 1
|
|
shard_count: 2
|
|
command: pnpm canvas:a2ui:bundle && pnpm test
|
|
- runtime: node
|
|
task: test
|
|
shard_index: 2
|
|
shard_count: 2
|
|
command: pnpm canvas:a2ui:bundle && pnpm test
|
|
- runtime: node
|
|
task: protocol
|
|
shard_index: 0
|
|
shard_count: 1
|
|
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 test shard (Windows)
|
|
if: matrix.task == 'test'
|
|
run: |
|
|
echo "OPENCLAW_TEST_SHARDS=${{ matrix.shard_count }}" >> "$GITHUB_ENV"
|
|
echo "OPENCLAW_TEST_SHARD_INDEX=${{ matrix.shard_index }}" >> "$GITHUB_ENV"
|
|
|
|
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
|
|
run: ${{ matrix.command }}
|
|
|
|
# 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 }}
|