From a462a4f38b696fa30168ae5b77b16be443eea6fa Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sun, 5 Jul 2026 16:53:00 -0700 Subject: [PATCH 1/8] [eric] build: VMP_REQUIRE_SIGN=0 override for cred-less local cuts (CI keeps the hard gate) --- scripts/build-app.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/build-app.sh b/scripts/build-app.sh index 88b92e42..bca64678 100755 --- a/scripts/build-app.sh +++ b/scripts/build-app.sh @@ -69,7 +69,8 @@ if $SIGN_MODE; then # A signed build is a build users actually run, so its Widevine VMP signature # is mandatory: the afterPack hook hard-fails on a missing/failed signature # instead of shipping a DMG whose Spotify/Netflix audio is silently dead. - export VMP_REQUIRE_SIGN=1 + # Respect an explicit outer VMP_REQUIRE_SIGN=0: a cred-less local cut may consciously ship DRM-limited (CI always has EVS secrets, so releases from CI keep the hard gate). + export VMP_REQUIRE_SIGN=${VMP_REQUIRE_SIGN:-1} fi # Step 0: Ensure bundled uv + uvx binaries exist. From 7d5833817cf5a68681576a00af45bbfd8a32d3c8 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 6 Jul 2026 14:17:19 -0700 Subject: [PATCH 2/8] [eric] build: per-arch bundled python-env; x64 DMG shipped arm64 python, bricking Intel Macs --- electron/package.json | 23 +++++--- scripts/build-app.sh | 85 +++++++++++++++--------------- scripts/build-python-env.sh | 39 +++++++++----- scripts/ci/verify-python-health.js | 27 ++++++++++ 4 files changed, 112 insertions(+), 62 deletions(-) diff --git a/electron/package.json b/electron/package.json index 87c0541e..72213ded 100644 --- a/electron/package.json +++ b/electron/package.json @@ -72,6 +72,13 @@ "filter": [ "**/*" ] + }, + { + "from": "build-staging/python-env/${arch}", + "to": "python-env", + "filter": [ + "**/*" + ] } ] }, @@ -94,6 +101,15 @@ }, "win": { "icon": "build/icon.ico", + "extraResources": [ + { + "from": "python-env", + "to": "python-env", + "filter": [ + "**/*" + ] + } + ], "target": [ { "target": "squirrel", @@ -148,13 +164,6 @@ "**/*" ] }, - { - "from": "python-env", - "to": "python-env", - "filter": [ - "**/*" - ] - }, { "from": "build-staging/router", "to": "router", diff --git a/scripts/build-app.sh b/scripts/build-app.sh index bca64678..87f73ce9 100755 --- a/scripts/build-app.sh +++ b/scripts/build-app.sh @@ -26,6 +26,22 @@ elif [[ "${1:-}" == "--sign" ]]; then SIGN_MODE=true fi +# Arch targets for this run. Publish always builds both DMGs; otherwise +# OSW_BUILD_ARCH (arm64|x64|both) overrides, defaulting to the host. Node, +# python-env, and the electron-builder flags below all derive from this ONE +# list, so a staged-arch vs packed-arch mismatch can't happen (the class of +# bug that shipped arm64 python inside the x64 DMG). +if $PUBLISH_MODE; then + BUILD_ARCHS=(arm64 x64) +else + case "${OSW_BUILD_ARCH:-host}" in + both) BUILD_ARCHS=(arm64 x64) ;; + x64) BUILD_ARCHS=(x64) ;; + arm64) BUILD_ARCHS=(arm64) ;; + *) if [[ "$(uname -m)" == "x86_64" ]]; then BUILD_ARCHS=(x64); else BUILD_ARCHS=(arm64); fi ;; + esac +fi + # Defensive: detach any leftover OpenSwarm DMG volumes from prior failed builds. # hdiutil's "Resource busy" / volume-name-collision errors almost always trace # back to a stale mount in /Volumes (e.g. after a build crash or a still-open @@ -265,17 +281,6 @@ fi echo "Frontend build complete." echo "" -# Step 2: Build Python environment -echo "[2/4] Building Python environment..." -bash "$SCRIPT_DIR/build-python-env.sh" - -if [[ ! -d "$PROJECT_ROOT/electron/python-env" ]]; then - echo "ERROR: Python environment not found at electron/python-env/" - exit 1 -fi -echo "Python environment ready." -echo "" - # Step 3: Fetch Router from npm # The 9router Next.js server is published as an npm package with a pre-built # standalone output. We install it into a scratch dir and stage it directly @@ -293,6 +298,19 @@ fi echo "Router staged." echo "" +# Step 3a: Bundled Python env, one per target arch (must run AFTER the +# build-staging reset above or the freshly staged envs get wiped). +echo "[3a] Building bundled Python env(s): ${BUILD_ARCHS[*]}" +for A in "${BUILD_ARCHS[@]}"; do + bash "$SCRIPT_DIR/build-python-env.sh" "$A" + if [[ ! -f "$STAGING_DIR/python-env/$A/bin/python3.13" ]]; then + echo "ERROR: python-env ($A) missing at $STAGING_DIR/python-env/$A" + exit 1 + fi +done +echo "Python environment(s) ready." +echo "" + # Step 3b: Bundle a real Node.js binary so 9Router and MCP servers don't # fall back to ELECTRON_RUN_AS_NODE on user machines without system node. # Two wins: @@ -351,21 +369,10 @@ NPMSH echo "[3b] Node $NODE_VERSION ($arch) staged ($(du -h "$out_dir/bin/node" | cut -f1))" } -# Publish mode builds both DMGs from one invocation, so always stage both. -# Single-arch local/sign builds only need the host arch. -if $PUBLISH_MODE; then - download_node_for_arch arm64 - download_node_for_arch x64 -else - HOST_ARCH=$(uname -m) - if [[ "$HOST_ARCH" == "arm64" ]]; then - download_node_for_arch arm64 - elif [[ "$HOST_ARCH" == "x86_64" ]]; then - download_node_for_arch x64 - else - echo "WARNING: unknown host arch $HOST_ARCH — skipping node bundle (will fall back to ELECTRON_RUN_AS_NODE)" - fi -fi +# Stage node for every arch this run packs (BUILD_ARCHS decides, top of file). +for A in "${BUILD_ARCHS[@]}"; do + download_node_for_arch "$A" +done echo "" # Step 3c: Pre-build the webapp-template node_modules archive so first-app @@ -488,27 +495,19 @@ fi # Caller's NODE_OPTIONS is respected if already set. export NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=12288}" +# Pack exactly the arches we staged for (BUILD_ARCHS, top of file). +EB_ARCH_FLAGS=() +for A in "${BUILD_ARCHS[@]}"; do + EB_ARCH_FLAGS+=("--$A") +done + if $PUBLISH_MODE; then - npx electron-builder --mac --arm64 --x64 --publish always + npx electron-builder --mac "${EB_ARCH_FLAGS[@]}" --publish always elif $SIGN_MODE; then - ARCH=$(uname -m) - if [[ "$ARCH" == "arm64" ]]; then - npx electron-builder --mac --arm64 --publish never - elif [[ "$ARCH" == "x86_64" ]]; then - npx electron-builder --mac --x64 --publish never - else - npx electron-builder --mac --publish never - fi + npx electron-builder --mac "${EB_ARCH_FLAGS[@]}" --publish never else export CSC_IDENTITY_AUTO_DISCOVERY=false - ARCH=$(uname -m) - if [[ "$ARCH" == "arm64" ]]; then - npx electron-builder --mac --arm64 --publish never - elif [[ "$ARCH" == "x86_64" ]]; then - npx electron-builder --mac --x64 --publish never - else - npx electron-builder --mac --publish never - fi + npx electron-builder --mac "${EB_ARCH_FLAGS[@]}" --publish never fi rm -rf "$PROJECT_ROOT/electron/build-staging" diff --git a/scripts/build-python-env.sh b/scripts/build-python-env.sh index 9dbe5c90..0de2a7bc 100755 --- a/scripts/build-python-env.sh +++ b/scripts/build-python-env.sh @@ -1,28 +1,42 @@ #!/bin/bash set -euo pipefail -# Build an embedded Python environment for the Electron app. +# Build an embedded Python environment for the Electron app (macOS). # # Downloads a standalone Python build from python-build-standalone, # creates a venv, and installs all backend dependencies. -# The resulting python-env/ directory is bundled into the Electron app. +# +# Usage: build-python-env.sh [arm64|x64] (default: host arch) +# +# The env stages under electron/build-staging/python-env/ so +# electron-builder's ${arch} macro bundles the MATCHING env per pack, same +# pattern as node/${arch}. Never bundle one host-arch env into both DMGs: +# that shipped arm64 python inside the x64 app and killed every Intel Mac. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" ELECTRON_DIR="$PROJECT_ROOT/electron" -PYTHON_ENV_DIR="$ELECTRON_DIR/python-env" PYTHON_VERSION="3.13" PYTHON_FULL_VERSION="3.13.2" -ARCH="$(uname -m)" +ARCH="${1:-$(uname -m)}" -if [[ "$ARCH" == "arm64" ]]; then - PLATFORM_TAG="aarch64-apple-darwin" -elif [[ "$ARCH" == "x86_64" ]]; then - PLATFORM_TAG="x86_64-apple-darwin" -else - echo "Unsupported architecture: $ARCH" - exit 1 +case "$ARCH" in + arm64|aarch64) ARCH="arm64"; PLATFORM_TAG="aarch64-apple-darwin" ;; + x64|x86_64) ARCH="x64"; PLATFORM_TAG="x86_64-apple-darwin" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; +esac + +PYTHON_ENV_DIR="$ELECTRON_DIR/build-staging/python-env/$ARCH" + +# Cross-building x64 on Apple Silicon runs the x64 python under Rosetta +# (pip then resolves x86_64 wheels, including the SDK's bundled claude CLI). +if [[ "$ARCH" == "x64" && "$(uname -m)" == "arm64" ]]; then + if ! arch -x86_64 /usr/bin/true 2>/dev/null; then + echo "ERROR: building the x64 python-env on Apple Silicon requires Rosetta 2." + echo " Install it with: softwareupdate --install-rosetta --agree-to-license" + exit 1 + fi fi RELEASE_TAG="20250212" @@ -44,6 +58,7 @@ if [[ -d "$PYTHON_ENV_DIR" ]]; then echo "Removing old python-env..." rm -rf "$PYTHON_ENV_DIR" fi +mkdir -p "$(dirname "$PYTHON_ENV_DIR")" # Download standalone Python echo "Downloading standalone Python from python-build-standalone..." @@ -301,7 +316,7 @@ PLIST # python-env/ via realpath, and libpython loads via the rewritten # @executable_path path. if ! "$PY_APP/Contents/MacOS/python3" -c \ - "import sys; assert sys.prefix.endswith('python-env'), sys.prefix" 2>/dev/null; then + "import sys, os; assert os.path.realpath(sys.prefix) == os.path.realpath('$PYTHON_ENV_DIR'), sys.prefix" 2>/dev/null; then echo "ERROR: Python.app wrapper failed self-test (libpython or stdlib not findable)" >&2 echo " Try: $PY_APP/Contents/MacOS/python3 -c 'import sys; print(sys.prefix)'" >&2 exit 1 diff --git a/scripts/ci/verify-python-health.js b/scripts/ci/verify-python-health.js index 43cf626b..02ec7d75 100644 --- a/scripts/ci/verify-python-health.js +++ b/scripts/ci/verify-python-health.js @@ -54,6 +54,33 @@ function main() { } process.stdout.write(` ok python ${versionLine}\n`); + // 1b) macOS: the bundled python's arch slices must cover the app's. An arm64 + // python inside the x64 app RUNS on an arm64 build host (native, not Rosetta), + // so --version alone can never catch the cross-arch bundle bug that bricked + // every Intel Mac. lipo compares what the file IS, not what the host can run. + if (process.platform === 'darwin') { + const i = appExe.indexOf('.app'); + const appRoot = i === -1 ? appExe : appExe.slice(0, i + 4); + const mainBin = path.join(appRoot, 'Contents', 'MacOS', path.basename(appRoot, '.app')); + const archsOf = (bin) => { + const r = spawnSync('lipo', ['-archs', bin], { encoding: 'utf8', timeout: 15000 }); + if (r.status !== 0) return null; + return (r.stdout || '').trim().split(/\s+/).filter(Boolean); + }; + const appArchs = archsOf(mainBin); + const pyArchs = archsOf(fs.realpathSync(py)); + if (!appArchs || !pyArchs) { + process.stderr.write(`\nPYTHON-HEALTH FAIL: lipo could not read archs (app=${appArchs}, python=${pyArchs})\n`); + process.exit(1); + } + const missing = appArchs.filter((a) => !pyArchs.includes(a)); + if (missing.length > 0) { + process.stderr.write(`\nPYTHON-HEALTH FAIL: app is [${appArchs}] but bundled python is [${pyArchs}] (missing ${missing}). This build would brick ${missing.join('/')} Macs.\n`); + process.exit(1); + } + process.stdout.write(` ok arch match (app [${appArchs}] / python [${pyArchs}])\n`); + } + // 2) Import smoke: load the heaviest deps to catch a half-extracted site-packages tree (rare but lethal). const smoke = spawnSync(py, ['-c', 'import sys, fastapi, anthropic, pydantic, httpx, jsonschema; print(sys.version_info[:3])'], { encoding: 'utf8', timeout: 30000 }); if (smoke.status !== 0) { From 45d0bc1466c02409300e279b740bb6b9a8a1f0ef Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 6 Jul 2026 15:10:31 -0700 Subject: [PATCH 3/8] [eric] ci: one-off intel-x64-verify workflow (real Intel runner proves the published x64 DMG) --- .github/workflows/intel-x64-verify.yml | 96 ++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 .github/workflows/intel-x64-verify.yml diff --git a/.github/workflows/intel-x64-verify.yml b/.github/workflows/intel-x64-verify.yml new file mode 100644 index 00000000..ac9a631d --- /dev/null +++ b/.github/workflows/intel-x64-verify.yml @@ -0,0 +1,96 @@ +name: intel-x64-verify + +# One-off, manually dispatched: prove the published x64 DMG on REAL Intel mac +# hardware (the arm64 build host can't; Rosetta lacks AVX so the bundled Bun +# claude CLI is untestable there). Downloads the live release asset, checks +# every binary's arch, runs the bundled python + CLI, then boots the whole app +# and polls backend health. +on: + workflow_dispatch: + inputs: + runner: + description: 'runner label (must be an Intel x64 mac)' + default: 'macos-15-large' + required: true + +jobs: + verify: + runs-on: ${{ inputs.runner }} + timeout-minutes: 25 + steps: + - name: prove this runner is real Intel silicon + run: | + set -x + uname -m + sysctl -n machdep.cpu.brand_string + sysctl hw.optional.avx1_0 hw.optional.avx2_0 + test "$(uname -m)" = "x86_64" + test "$(sysctl -n hw.optional.avx1_0)" = "1" + + - name: download published x64 DMG + run: | + curl -sSL -o /tmp/x64.dmg "https://github.com/${{ github.repository }}/releases/latest/download/OpenSwarm-x64.dmg" + ls -la /tmp/x64.dmg + hdiutil attach -nobrowse -readonly -mountpoint /tmp/oswmnt /tmp/x64.dmg + mkdir -p /tmp/oswapp + ditto /tmp/oswmnt/OpenSwarm.app /tmp/oswapp/OpenSwarm.app + hdiutil detach /tmp/oswmnt + + - name: gatekeeper + signature + run: | + codesign --verify --deep --strict /tmp/oswapp/OpenSwarm.app + spctl -a -t exec -vv /tmp/oswapp/OpenSwarm.app + xcrun stapler validate /tmp/oswapp/OpenSwarm.app + + - name: binary arch census + run: | + R=/tmp/oswapp/OpenSwarm.app/Contents/Resources + for b in \ + /tmp/oswapp/OpenSwarm.app/Contents/MacOS/OpenSwarm \ + "$R/python-env/bin/python3.13" \ + "$R/python-env/lib/python3.13/site-packages/claude_agent_sdk/_bundled/claude" \ + "$R/node/x64/bin/node" \ + "$R/backend/uv-bin/uv"; do + A=$(lipo -archs "$b") + echo "$A $b" + case "$A" in *x86_64*) ;; *) echo "WRONG ARCH"; exit 1;; esac + done + + - name: bundled python runs natively + backend deps import + run: | + R=/tmp/oswapp/OpenSwarm.app/Contents/Resources + "$R/python-env/bin/python3" --version + "$R/python-env/bin/python3" -c "import fastapi, anthropic, pydantic, httpx, jsonschema, claude_agent_sdk; print('deps ok')" + + - name: bundled claude CLI runs natively (the AVX gate Rosetta could not test) + run: | + CLI=/tmp/oswapp/OpenSwarm.app/Contents/Resources/python-env/lib/python3.13/site-packages/claude_agent_sdk/_bundled/claude + OUT=$("$CLI" --version 2>&1); echo "$OUT" + echo "$OUT" | grep -q "Claude Code" + if echo "$OUT" | grep -qi "lacks AVX"; then echo "AVX warning on real Intel = fail"; exit 1; fi + "$CLI" --help > /dev/null + # a real invocation exercises the JIT/network paths; a clean auth + # error (not a SIGILL/crash) is the pass condition + set +e + ANTHROPIC_API_KEY=sk-ant-invalid timeout 90 "$CLI" -p "hi" --model claude-haiku-4-5-20251001 > /tmp/cli-run.out 2>&1 + CODE=$? + set -e + cat /tmp/cli-run.out + echo "exit=$CODE" + # 132=SIGILL 139=SIGSEGV 134=SIGABRT: any of those = AVX/crash class + if [ $CODE -eq 132 ] || [ $CODE -eq 139 ] || [ $CODE -eq 134 ]; then exit 1; fi + + - name: boot the full app, poll backend health + run: | + cd /tmp/oswapp + OPENSWARM_E2E=1 ./OpenSwarm.app/Contents/MacOS/OpenSwarm > /tmp/boot.log 2>&1 & + APP_PID=$! + for i in $(seq 1 60); do + CODE=$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 http://127.0.0.1:8324/api/health/check || true) + [ "$CODE" = "200" ] && break + sleep 2 + done + echo "health=$CODE after ~$((i*2))s" + kill $APP_PID 2>/dev/null || true + tail -30 /tmp/boot.log || true + test "$CODE" = "200" From 8d5a8856648a23414c12cedfd39ae8831f40c1b2 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 6 Jul 2026 15:11:06 -0700 Subject: [PATCH 4/8] [eric] ci: intel verify fires on push (dispatch needs default-branch registration) --- .github/workflows/intel-x64-verify.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/intel-x64-verify.yml b/.github/workflows/intel-x64-verify.yml index ac9a631d..58fefb81 100644 --- a/.github/workflows/intel-x64-verify.yml +++ b/.github/workflows/intel-x64-verify.yml @@ -6,6 +6,9 @@ name: intel-x64-verify # every binary's arch, runs the bundled python + CLI, then boots the whole app # and polls backend health. on: + push: + branches: [eric/intel-x64-fix] + paths: ['.github/workflows/intel-x64-verify.yml'] workflow_dispatch: inputs: runner: @@ -15,7 +18,7 @@ on: jobs: verify: - runs-on: ${{ inputs.runner }} + runs-on: ${{ inputs.runner || 'macos-15-large' }} timeout-minutes: 25 steps: - name: prove this runner is real Intel silicon From 4d9687ec462ab448b74ed01e8966ae097e0487e9 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 6 Jul 2026 15:11:51 -0700 Subject: [PATCH 5/8] [eric] ci: use free macos-15-intel runner (large runners blocked on billing) --- .github/workflows/intel-x64-verify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/intel-x64-verify.yml b/.github/workflows/intel-x64-verify.yml index 58fefb81..ab507fc3 100644 --- a/.github/workflows/intel-x64-verify.yml +++ b/.github/workflows/intel-x64-verify.yml @@ -18,7 +18,7 @@ on: jobs: verify: - runs-on: ${{ inputs.runner || 'macos-15-large' }} + runs-on: ${{ inputs.runner || 'macos-15-intel' }} timeout-minutes: 25 steps: - name: prove this runner is real Intel silicon From ece22d98cbf4eb4627b3d6cd384750103cc102bf Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 6 Jul 2026 15:14:39 -0700 Subject: [PATCH 6/8] [eric] ci: portable timeout for the CLI exercise (macOS runners lack coreutils timeout) --- .github/workflows/intel-x64-verify.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/intel-x64-verify.yml b/.github/workflows/intel-x64-verify.yml index ab507fc3..313f58a6 100644 --- a/.github/workflows/intel-x64-verify.yml +++ b/.github/workflows/intel-x64-verify.yml @@ -75,7 +75,8 @@ jobs: # a real invocation exercises the JIT/network paths; a clean auth # error (not a SIGILL/crash) is the pass condition set +e - ANTHROPIC_API_KEY=sk-ant-invalid timeout 90 "$CLI" -p "hi" --model claude-haiku-4-5-20251001 > /tmp/cli-run.out 2>&1 + # macOS has no `timeout`; perl alarm is the portable equivalent + ANTHROPIC_API_KEY=sk-ant-invalid perl -e 'alarm 90; exec @ARGV' -- "$CLI" -p "hi" --model claude-haiku-4-5-20251001 > /tmp/cli-run.out 2>&1 CODE=$? set -e cat /tmp/cli-run.out From c3bfef841ac5346e1f9d04172a801e09089eacad Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 6 Jul 2026 15:21:35 -0700 Subject: [PATCH 7/8] [eric] ci: verify-windows job (published Setup.exe silent-install + boot on real win x64) --- .github/workflows/intel-x64-verify.yml | 52 ++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/.github/workflows/intel-x64-verify.yml b/.github/workflows/intel-x64-verify.yml index 313f58a6..5f30f061 100644 --- a/.github/workflows/intel-x64-verify.yml +++ b/.github/workflows/intel-x64-verify.yml @@ -98,3 +98,55 @@ jobs: kill $APP_PID 2>/dev/null || true tail -30 /tmp/boot.log || true test "$CODE" = "200" + + verify-windows: + runs-on: windows-latest + timeout-minutes: 30 + steps: + - name: download published Setup.exe + shell: pwsh + run: | + curl.exe -sSL -o $env:TEMP\OpenSwarm-Setup-x64.exe "https://github.com/${{ github.repository }}/releases/latest/download/OpenSwarm-Setup-x64.exe" + Get-Item $env:TEMP\OpenSwarm-Setup-x64.exe | Select-Object Name,Length + + - name: silent install (Squirrel) + shell: pwsh + run: | + Start-Process -FilePath "$env:TEMP\OpenSwarm-Setup-x64.exe" -ArgumentList "--silent" + $deadline = (Get-Date).AddMinutes(8) + do { + Start-Sleep -Seconds 5 + $exe = Get-ChildItem "$env:LOCALAPPDATA" -Recurse -Depth 2 -Filter "OpenSwarm.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 + } until ($exe -or (Get-Date) -gt $deadline) + if (-not $exe) { Get-ChildItem "$env:LOCALAPPDATA" -Depth 1 | Select-Object FullName; throw "installed OpenSwarm.exe not found" } + echo "APP_EXE=$($exe.FullName)" >> $env:GITHUB_ENV + echo "APP_DIR=$($exe.DirectoryName)" >> $env:GITHUB_ENV + echo "installed at $($exe.FullName)" + + - name: bundled python + claude CLI run on real Windows x64 + shell: pwsh + run: | + $py = Join-Path $env:APP_DIR "resources\python-env\python.exe" + & $py --version + if ($LASTEXITCODE -ne 0) { throw "python --version failed" } + & $py -c "import fastapi, anthropic, pydantic, httpx, jsonschema, claude_agent_sdk; print('deps ok')" + if ($LASTEXITCODE -ne 0) { throw "import smoke failed" } + $cli = Get-ChildItem (Join-Path $env:APP_DIR "resources\python-env") -Recurse -Filter "claude*" -ErrorAction SilentlyContinue | Where-Object { $_.Directory.Name -eq "_bundled" } | Select-Object -First 1 + if (-not $cli) { throw "bundled claude CLI not found" } + & $cli.FullName --version + if ($LASTEXITCODE -ne 0) { throw "claude --version failed" } + + - name: boot the installed app, poll backend health + shell: pwsh + run: | + $env:OPENSWARM_E2E = "1" + Start-Process -FilePath $env:APP_EXE + $code = 0 + foreach ($i in 1..60) { + Start-Sleep -Seconds 3 + try { $code = (Invoke-WebRequest -Uri "http://127.0.0.1:8324/api/health/check" -UseBasicParsing -TimeoutSec 2).StatusCode } catch { $code = 0 } + if ($code -eq 200) { break } + } + echo "health=$code" + Stop-Process -Name "OpenSwarm" -Force -ErrorAction SilentlyContinue + if ($code -ne 200) { throw "backend never became healthy" } From fb874fd2e184c4e8385153f9c3305fd80f4f65b0 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 6 Jul 2026 15:23:48 -0700 Subject: [PATCH 8/8] [eric] ci: windows verify must target app- dir, root exe is the squirrel stub --- .github/workflows/intel-x64-verify.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/intel-x64-verify.yml b/.github/workflows/intel-x64-verify.yml index 5f30f061..a9f7b089 100644 --- a/.github/workflows/intel-x64-verify.yml +++ b/.github/workflows/intel-x64-verify.yml @@ -113,15 +113,18 @@ jobs: shell: pwsh run: | Start-Process -FilePath "$env:TEMP\OpenSwarm-Setup-x64.exe" -ArgumentList "--silent" - $deadline = (Get-Date).AddMinutes(8) + # the root OpenSwarm.exe is Squirrel's stub; the real app + resources + # live in app-\. python.exe appearing = install truly done. + $deadline = (Get-Date).AddMinutes(10) do { Start-Sleep -Seconds 5 - $exe = Get-ChildItem "$env:LOCALAPPDATA" -Recurse -Depth 2 -Filter "OpenSwarm.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 - } until ($exe -or (Get-Date) -gt $deadline) - if (-not $exe) { Get-ChildItem "$env:LOCALAPPDATA" -Depth 1 | Select-Object FullName; throw "installed OpenSwarm.exe not found" } - echo "APP_EXE=$($exe.FullName)" >> $env:GITHUB_ENV - echo "APP_DIR=$($exe.DirectoryName)" >> $env:GITHUB_ENV - echo "installed at $($exe.FullName)" + $py = Get-ChildItem "$env:LOCALAPPDATA\openswarm\app-*\resources\python-env\python.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 + } until ($py -or (Get-Date) -gt $deadline) + if (-not $py) { Get-ChildItem "$env:LOCALAPPDATA\openswarm" -Recurse -Depth 2 -ErrorAction SilentlyContinue | Select-Object FullName -First 40; throw "installed python-env not found" } + $appDir = $py.FullName -replace '\\resources\\python-env\\python\.exe$', '' + echo "APP_EXE=$appDir\OpenSwarm.exe" >> $env:GITHUB_ENV + echo "APP_DIR=$appDir" >> $env:GITHUB_ENV + echo "installed at $appDir" - name: bundled python + claude CLI run on real Windows x64 shell: pwsh