diff --git a/.github/scripts/namespace-stack.sh b/.github/scripts/namespace-stack.sh new file mode 100755 index 000000000..ee2266276 --- /dev/null +++ b/.github/scripts/namespace-stack.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Runs a Docker Compose stack for a Namespace macOS runner on a separate Namespace Linux +# instance. macOS runners are VMs without nested virtualization, so Docker cannot run on them. +# +# namespace-stack.sh up create the instance, point docker at it, copy over; +# later steps get a docker wrapper that runs exec on the instance +# namespace-stack.sh forward ... expose instance ports on the runner's 127.0.0.1 under the same numbers +# namespace-stack.sh down destroy the instance +set -euo pipefail + +state_dir=/tmp/namespace-stack +instance_file="$state_dir/instance" + +install_compose() { + docker compose version >/dev/null 2>&1 && return + mkdir -p ~/.docker/cli-plugins + curl -fsSL -o ~/.docker/cli-plugins/docker-compose \ + https://github.com/docker/compose/releases/download/v5.6.0/docker-compose-darwin-aarch64 + chmod +x ~/.docker/cli-plugins/docker-compose +} + +up() { + local compose_dir + compose_dir="$(cd "$1" && pwd)" + mkdir -p "$state_dir/docker" + install_compose + + # The runner sets DOCKER_DEFAULT_PLATFORM=linux/arm64, so the instance must be arm64. + nsc create --bare --machine_type linux/arm64:8x16 --duration 4h --cidfile "$instance_file" + + # `attach-context --background` times out before its socket appears; the foreground form works. + nohup nsc docker attach-context --to "$(cat "$instance_file")" --name namespace-stack \ + --state "$state_dir/docker" --use > "$state_dir/attach.log" 2>&1 & + + local deadline=$((SECONDS + 300)) + until docker info >/dev/null 2>&1; do + if ((SECONDS > deadline)); then + cat "$state_dir/attach.log" >&2 + exit 1 + fi + sleep 5 + done + + # `docker exec` through the attached context returns no output, drops stdin and always exits 0. + # The same command run on the instance over `nsc ssh` behaves normally, except that any failure + # exits 1 rather than with the command's own code, so exec is routed there. + mkdir -p "$state_dir/bin" + cat > "$state_dir/bin/docker" <> "$GITHUB_PATH" + fi + + # Bind mounts resolve on the instance, so the compose directory has to exist there at the same path. + docker create --name namespace-stack-seed -v "$compose_dir:/seed" alpine:3 true >/dev/null + docker cp "$compose_dir/." namespace-stack-seed:/seed/ + docker rm namespace-stack-seed >/dev/null + docker run --rm -v "$compose_dir:/seed" alpine:3 chmod -R a+rwX /seed +} + +forward() { + local port + for port in "$@"; do + nohup nsc instance port-forward "$(cat "$instance_file")" --target_port "$port" \ + > "$state_dir/forward-$port.log" 2>&1 & + done + + local deadline=$((SECONDS + 300)) + for port in "$@"; do + until nc -z 127.0.0.1 "$port" 2>/dev/null; do + if ((SECONDS > deadline)); then + cat "$state_dir/forward-$port.log" >&2 + exit 1 + fi + sleep 3 + done + done +} + +down() { + pkill -f "nsc instance port-forward" || true + pkill -f "nsc docker attach-context" || true + docker context use default >/dev/null 2>&1 || true + if [[ -s "$instance_file" ]]; then + nsc destroy "$(cat "$instance_file")" --force || true + fi +} + +"$@" diff --git a/.github/workflows/ai-device-tests.yml b/.github/workflows/ai-device-tests.yml index f94661902..3c43c3f1b 100644 --- a/.github/workflows/ai-device-tests.yml +++ b/.github/workflows/ai-device-tests.yml @@ -32,7 +32,7 @@ jobs: trezor-emu: if: inputs.suite == 'trezor-emu' name: Trezor emulator dashboard - runs-on: [self-hosted, macOS] + runs-on: namespace-profile-mac timeout-minutes: 90 steps: @@ -46,6 +46,11 @@ jobs: ref: main path: bitkit-docker + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "26.2" + - name: Install xcbeautify run: | if ! command -v xcbeautify >/dev/null 2>&1; then @@ -56,25 +61,41 @@ jobs: run: | sw_vers xcodebuild -version - docker version - docker compose version - name: Resolve Swift packages run: | xcodebuild -resolvePackageDependencies -onlyUsePackageVersionsFromResolvedFile | xcbeautify + - name: Start remote Docker + run: | + awk '{ print } /^ trezor-user-env-mac:$/ { print " extra_hosts:\n - \"host.docker.internal:host-gateway\"" }' \ + bitkit-docker/docker-compose.yml > /tmp/docker-compose.yml + mv /tmp/docker-compose.yml bitkit-docker/docker-compose.yml + mkdir -p bitkit-docker/lnd bitkit-docker/.trezor-user-env/{trezor-suite,logs/screens,logs/mcp-screenshots,firmware/user_downloaded} + .github/scripts/namespace-stack.sh up bitkit-docker + .github/scripts/namespace-stack.sh forward 43782 39388 60001 21325 21328 9001 23902 + - name: Start regtest and Trezor emulator working-directory: bitkit-docker run: | - docker compose up -d + docker compose up -d bitcoind bitcoinsetup electrs darkhttpd lnd ldk-backup-server + until docker exec bitcoin bitcoin-cli -regtest -rpcport=43782 -rpcuser=polaruser -rpcpassword=polarpass -generate 101; do sleep 2; done ./scripts/trezor-emulator start ./scripts/trezor-emulator status - name: Boot simulator env: SIMULATOR_NAME: ${{ inputs.simulator_name }} + SIMULATOR_OS: ${{ inputs.simulator_os }} run: | - xcrun simctl boot "$SIMULATOR_NAME" || true + udid=$(xcrun simctl list devices available -j | python3 -c ' + import json, os, sys + runtime = "com.apple.CoreSimulator.SimRuntime.iOS-" + os.environ["SIMULATOR_OS"].replace(".", "-") + devices = json.load(sys.stdin)["devices"].get(runtime, []) + print(next(d["udid"] for d in devices if d["name"] == os.environ["SIMULATOR_NAME"])) + ') + xcrun simctl boot "$udid" || true + xcrun simctl bootstatus "$udid" -b - name: Run Trezor emulator UI tests env: @@ -128,9 +149,6 @@ jobs: path: ai-device-artifacts/trezor if-no-files-found: warn - - name: Stop emulator services + - name: Tear down emulator stack if: always() - working-directory: bitkit-docker - run: | - ./scripts/trezor-emulator stop || true - docker compose down || true + run: .github/scripts/namespace-stack.sh down diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 94da133c2..3c59e1d9d 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -46,6 +46,7 @@ jobs: - 'Bitkit.xcodeproj/**' - 'Package.swift' - '.github/workflows/e2e-tests.yml' + - '.github/scripts/**' build-local: needs: detect-changes @@ -149,8 +150,11 @@ jobs: e2e-tests-local: if: github.event.pull_request.draft == false && needs.detect-changes.outputs.code == 'true' - runs-on: [self-hosted, macOS] + runs-on: namespace-profile-mac needs: [detect-changes, build-local, e2e-branch] + env: + SIMULATOR_NAME: "iPhone 17" + SIMULATOR_OS_VERSION: "26.2" strategy: fail-fast: false @@ -165,6 +169,11 @@ jobs: name: e2e-tests-local - ${{ matrix.shard.name }} steps: + - name: Checkout stack scripts + uses: actions/checkout@v7 + with: + sparse-checkout: .github/scripts + - name: Resolve E2E tests ref id: e2e-ref uses: synonymdev/bitkit-e2e-tests/.github/actions/resolve-e2e-ref@main @@ -205,35 +214,30 @@ jobs: working-directory: bitkit-e2e-tests run: npm ci - - name: Docker info - run: | - docker --version - docker info - docker ps - docker compose version - - - name: Reset regtest environment - working-directory: bitkit-e2e-tests - continue-on-error: true - run: | - cd docker && docker compose --verbose down -v -t 300 - - name: Run regtest setup - working-directory: bitkit-e2e-tests run: | - cd docker - mkdir -p lnd && chmod 777 lnd - docker compose --verbose up --force-recreate -d + .github/scripts/namespace-stack.sh up bitkit-e2e-tests/docker + (cd bitkit-e2e-tests/docker && docker compose up --force-recreate -d) + .github/scripts/namespace-stack.sh forward 43782 39388 60001 8080 9735 10009 3003 9001 9002 21325 21328 - name: Wait for electrum server - working-directory: bitkit-e2e-tests run: | - echo "Waiting for Electrum server..." - while ! nc -z '127.0.0.1' 60001; do - echo "Electrum server not ready, waiting..." + for _ in $(seq 1 60); do + python3 -c 'import socket; s = socket.create_connection(("127.0.0.1", 60001), 10); s.sendall(b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"blockchain.headers.subscribe\",\"params\":[]}\n"); assert b"height" in s.recv(4096)' && exit 0 + sleep 5 + done + exit 1 + + - name: Copy LND credentials + working-directory: bitkit-e2e-tests/docker + run: | + mkdir -p lnd/data/chain/bitcoin/regtest + for _ in $(seq 1 60); do + docker cp lnd:/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon lnd/data/chain/bitcoin/regtest/ 2>/dev/null && break sleep 5 done - echo "Electrum server is ready!" + docker cp lnd:/home/lnd/.lnd/tls.cert lnd/tls.cert + test -s lnd/data/chain/bitcoin/regtest/admin.macaroon - name: Clear previous E2E artifacts working-directory: bitkit-e2e-tests @@ -243,11 +247,16 @@ jobs: - name: Clear iOS Simulator environment run: | - echo "🔧 Shutting down all running iOS simulators..." + udid=$(xcrun simctl list devices available -j | python3 -c ' + import json, os, sys + runtime = "com.apple.CoreSimulator.SimRuntime.iOS-" + os.environ["SIMULATOR_OS_VERSION"].replace(".", "-") + devices = json.load(sys.stdin)["devices"].get(runtime, []) + print(next(d["udid"] for d in devices if d["name"] == os.environ["SIMULATOR_NAME"])) + ') xcrun simctl shutdown all || true - echo "🔧 Erasing target simulator: iPhone 17..." - xcrun simctl erase "iPhone 17" || true - echo "🔧 Disabling iOS Simulator notifications..." + xcrun simctl erase "$udid" + xcrun simctl boot "$udid" + xcrun simctl bootstatus "$udid" -b defaults write com.apple.iphonesimulator DisableAllNotifications -bool true - name: Run E2E Tests 1 (${{ matrix.shard.name }}) @@ -285,11 +294,9 @@ jobs: name: e2e-artifacts_${{ matrix.shard.name }}_${{ github.run_number }} path: bitkit-e2e-tests/artifacts/ - - name: Cleanup Docker containers + - name: Tear down regtest stack if: always() - working-directory: bitkit-e2e-tests - run: | - cd docker && docker compose down -v || true + run: .github/scripts/namespace-stack.sh down e2e-status: if: always() && github.event.pull_request.draft == false diff --git a/.github/workflows/self-hosted-cleanup.yml b/.github/workflows/self-hosted-cleanup.yml deleted file mode 100644 index 21e563815..000000000 --- a/.github/workflows/self-hosted-cleanup.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: self-hosted-cleanup - -on: - workflow_dispatch: - -concurrency: - group: self-hosted-runner - cancel-in-progress: false - -jobs: - cleanup: - runs-on: [self-hosted, macOS] - - steps: - - name: Disk space before cleanup - run: | - echo "=== Disk Space Before Cleanup ===" - df -h / - echo "" - echo "=== Large directories ===" - du -sh ~/Library/Developer/Xcode/DerivedData 2>/dev/null || echo "No DerivedData" - du -sh ~/Library/Caches/org.swift.swiftpm 2>/dev/null || echo "No SPM cache" - du -sh ~/Library/Developer/CoreSimulator 2>/dev/null || echo "No Simulator data" - du -sh ~/.npm 2>/dev/null || echo "No npm cache" - - - name: Clean Xcode DerivedData - run: | - echo "Cleaning Xcode DerivedData..." - rm -rf ~/Library/Developer/Xcode/DerivedData/* - rm -rf ~/Library/Developer/Xcode/Archives/* - echo "✅ Xcode DerivedData cleaned" - - - name: Clean Swift Package Manager caches - run: | - echo "Cleaning Swift Package Manager caches..." - rm -rf ~/Library/Caches/org.swift.swiftpm - rm -rf ~/Library/org.swift.swiftpm - echo "✅ SPM caches cleaned" - - - name: Clean iOS Simulator caches - run: | - echo "Shutting down all simulators..." - xcrun simctl shutdown all || true - - echo "Deleting unavailable simulators..." - xcrun simctl delete unavailable || true - - echo "Cleaning simulator caches..." - rm -rf ~/Library/Developer/CoreSimulator/Caches/* - - echo "✅ Simulator caches cleaned" - - - name: Clean npm cache - run: | - echo "Cleaning npm cache..." - npm cache clean --force || true - rm -rf ~/.npm/_cacache - echo "✅ npm cache cleaned" - - - name: Clean Homebrew cache - run: | - echo "Cleaning Homebrew cache..." - brew cleanup -s || true - rm -rf ~/Library/Caches/Homebrew/* - echo "✅ Homebrew cache cleaned" - - - name: Clean GitHub Actions cache - run: | - echo "Cleaning old GitHub Actions artifacts..." - rm -rf ~/actions-runner/_work/_temp/* || true - rm -rf ~/actions-runner/_work/_actions/* || true - echo "✅ Actions temp files cleaned" - - - name: Clean system caches - run: | - echo "Cleaning system caches..." - rm -rf ~/Library/Caches/com.apple.dt.Xcode/* || true - echo "✅ System caches cleaned" - - - name: Disk space after cleanup - run: | - echo "=== Disk Space After Cleanup ===" - df -h / - echo "" - echo "=== Remaining large directories ===" - du -sh ~/Library/Developer/Xcode/DerivedData 2>/dev/null || echo "No DerivedData" - du -sh ~/Library/Caches/org.swift.swiftpm 2>/dev/null || echo "No SPM cache" - du -sh ~/Library/Developer/CoreSimulator 2>/dev/null || echo "No Simulator data" - du -sh ~/.npm 2>/dev/null || echo "No npm cache"