diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..1bedc51d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + checks: + name: Typecheck and unit tests + runs-on: ubuntu-latest + timeout-minutes: 25 + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.13" + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Typecheck + run: | + bun run --cwd packages/protocol check + bun run --cwd packages/agent-sessions check + bun run --cwd packages/runtime check + bun run --cwd packages/session-trace check + bun run --cwd packages/session-trace-react check + bun run --cwd apps/desktop check + bun run --cwd apps/cloud check + bun run --cwd apps/mesh-front-door check + + - name: Unit tests + run: bun run test:unit diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cfad882a..e924428b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,7 +21,7 @@ jobs: - uses: oven-sh/setup-bun@v2 with: - bun-version: "1.3.11" + bun-version: "1.3.13" - run: cd landing && bun install && bun run build diff --git a/.github/workflows/heavy-ci.yml b/.github/workflows/heavy-ci.yml new file mode 100644 index 00000000..7481e26c --- /dev/null +++ b/.github/workflows/heavy-ci.yml @@ -0,0 +1,361 @@ +name: Heavy CI + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + - labeled + - unlabeled + workflow_dispatch: + inputs: + target: + description: Heavy lane to run. + type: choice + required: true + default: full + options: + - native + - web-build + - e2e + - full + live_harness: + description: Run the live agent harness when the e2e lane is selected. + type: boolean + required: false + default: false + +permissions: + contents: read + pull-requests: read + +concurrency: + group: heavy-ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + plan: + name: Plan heavy lanes + runs-on: ubuntu-latest + outputs: + native: ${{ steps.plan.outputs.native }} + web_build: ${{ steps.plan.outputs.web_build }} + e2e: ${{ steps.plan.outputs.e2e }} + live_harness: ${{ steps.plan.outputs.live_harness }} + steps: + - name: Resolve labels, dispatch inputs, and touched paths + id: plan + env: + GH_TOKEN: ${{ github.token }} + DISPATCH_TARGET: ${{ github.event.inputs.target || '' }} + DISPATCH_LIVE_HARNESS: ${{ github.event.inputs.live_harness || 'false' }} + run: | + set -euo pipefail + + native=false + web_build=false + e2e=false + live_harness=false + reasons=() + + labels_file="$(mktemp)" + jq -r '.pull_request.labels[]?.name // empty' "$GITHUB_EVENT_PATH" > "$labels_file" + + has_label() { + grep -Fxq "$1" "$labels_file" + } + + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + target="${DISPATCH_TARGET:-full}" + reasons+=("workflow_dispatch:${target}") + + case "$target" in + native) + native=true + ;; + web-build) + web_build=true + ;; + e2e) + e2e=true + ;; + full) + native=true + web_build=true + e2e=true + ;; + *) + echo "::error::Unsupported heavy CI target: $target" + exit 1 + ;; + esac + + if [[ "$DISPATCH_LIVE_HARNESS" == "true" ]]; then + live_harness=true + e2e=true + reasons+=("workflow_dispatch:live_harness") + fi + else + if has_label "ci:full"; then + native=true + web_build=true + e2e=true + live_harness=true + reasons+=("label:ci:full") + fi + + if has_label "ci:native"; then + native=true + reasons+=("label:ci:native") + fi + + if has_label "ci:web-build"; then + web_build=true + reasons+=("label:ci:web-build") + fi + + if has_label "ci:e2e"; then + e2e=true + live_harness=true + reasons+=("label:ci:e2e") + fi + + pr_number="$(jq -r '.pull_request.number // empty' "$GITHUB_EVENT_PATH")" + if [[ -n "$pr_number" ]]; then + files_file="$(mktemp)" + gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}/files" \ + --jq '.[].filename' > "$files_file" + + if grep -Eq '^(apps/ios|apps/macos)/' "$files_file"; then + native=true + reasons+=("paths:apps/ios-or-apps/macos") + fi + fi + fi + + if [[ "$e2e" != "true" ]]; then + live_harness=false + fi + + if ((${#reasons[@]} == 0)); then + reasons+=("no heavy lanes selected") + fi + + { + echo "native=$native" + echo "web_build=$web_build" + echo "e2e=$e2e" + echo "live_harness=$live_harness" + } >> "$GITHUB_OUTPUT" + + { + echo "### Heavy CI plan" + echo + echo "- native: \`$native\`" + echo "- web/CLI build: \`$web_build\`" + echo "- e2e: \`$e2e\`" + echo "- live harness: \`$live_harness\`" + echo "- reasons: \`${reasons[*]}\`" + } >> "$GITHUB_STEP_SUMMARY" + + native-macos: + name: Native macOS build/test + needs: plan + if: needs.plan.outputs.native == 'true' + runs-on: macos-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v5 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.13" + + - name: Show Swift toolchain + run: swift --version + + - name: Build macOS Swift package + run: swift build --package-path apps/macos -c debug + + - name: Test macOS Swift package when tests exist + run: | + set -euo pipefail + + if find apps/macos -path '*/Tests/*' -type f | grep -q .; then + swift test --package-path apps/macos + else + echo "::notice::No Swift test targets found under apps/macos; skipping swift test." + fi + + - name: Build signed macOS app bundle + run: bun ./apps/macos/bin/openscout-menu.ts build + + native-ios: + name: Native iOS build/test + needs: plan + if: needs.plan.outputs.native == 'true' + runs-on: macos-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + + - name: Show Xcode toolchain + run: | + xcodebuild -version + xcrun simctl list runtimes available + + - name: Resolve iOS package dependencies + run: | + set -euo pipefail + + xcodebuild \ + -resolvePackageDependencies \ + -project apps/ios/Scout.xcodeproj \ + -scheme ScoutApp \ + -derivedDataPath "$RUNNER_TEMP/Scout-iOS-DerivedData" \ + -skipPackagePluginValidation + + - name: Build iOS app for Simulator + run: | + set -euo pipefail + + xcodebuild \ + -project apps/ios/Scout.xcodeproj \ + -scheme ScoutApp \ + -configuration Debug \ + -destination "generic/platform=iOS Simulator" \ + -derivedDataPath "$RUNNER_TEMP/Scout-iOS-DerivedData" \ + -skipPackagePluginValidation \ + CODE_SIGNING_ALLOWED=NO \ + build + + - name: Test iOS app on an available Simulator + run: | + set -euo pipefail + + destination_id="$( + xcodebuild -showdestinations \ + -project apps/ios/Scout.xcodeproj \ + -scheme ScoutApp 2>/dev/null | + grep 'platform:iOS Simulator' | + grep -v 'placeholder' | + sed -n 's/.* id:\([^,}]*\).*/\1/p' | + head -n 1 | + tr -d ' ' + )" + + if [[ -z "$destination_id" ]]; then + echo "::error::No concrete iOS Simulator destination is available on this runner." + xcodebuild -showdestinations \ + -project apps/ios/Scout.xcodeproj \ + -scheme ScoutApp || true + exit 1 + fi + + xcodebuild \ + -project apps/ios/Scout.xcodeproj \ + -scheme ScoutApp \ + -configuration Debug \ + -destination "id=${destination_id}" \ + -derivedDataPath "$RUNNER_TEMP/Scout-iOS-DerivedData" \ + -skipPackagePluginValidation \ + CODE_SIGNING_ALLOWED=NO \ + test + + web-cli-package-build: + name: Web and CLI packaged build + needs: plan + if: needs.plan.outputs.web_build == 'true' + runs-on: ubuntu-latest + timeout-minutes: 35 + steps: + - uses: actions/checkout@v5 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.13" + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build web and CLI packages + run: bun run build + + - name: Dry-run package CLI + run: npm --prefix packages/cli pack --dry-run + + - name: Dry-run package web + run: npm --prefix packages/web pack --dry-run + + - name: Run package smoke tests + run: | + set -euo pipefail + bun run --cwd packages/cli test:happy + bun run --cwd packages/web test:happy + + e2e-scenarios: + name: Runtime e2e scenarios + needs: plan + if: needs.plan.outputs.e2e == 'true' + runs-on: ubuntu-latest + timeout-minutes: 35 + steps: + - uses: actions/checkout@v5 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.13" + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build runtime + run: bun run --cwd packages/runtime build + + - name: Run broker scenario suite + run: bun run test:scenarios + + live-harness: + name: Live local harness pass + needs: plan + if: needs.plan.outputs.live_harness == 'true' + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.13" + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run live Codex/Claude broker pass when harness CLIs exist + env: + OPENSCOUT_KEEP_LIVE_PASS: "1" + OPENSCOUT_LIVE_PASS_ROOT: ${{ runner.temp }}/openscout-live-harness + OPENSCOUT_E2E_MISSION: "Run the GitHub Actions live harness smoke pass without editing files." + run: | + set -euo pipefail + + missing=() + command -v codex >/dev/null 2>&1 || missing+=("codex") + command -v claude >/dev/null 2>&1 || missing+=("claude") + + if ((${#missing[@]} > 0)); then + echo "::notice::Skipping live harness pass; missing CLI(s): ${missing[*]}." + exit 0 + fi + + bun run --cwd packages/runtime test:live:local-agent-pass + + - name: Upload live harness artifacts + if: always() + uses: actions/upload-artifact@v5 + with: + name: live-harness-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/openscout-live-harness + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 9983f4a7..b1e9180e 100644 --- a/.gitignore +++ b/.gitignore @@ -105,6 +105,9 @@ apps/ios/build/ !.agents/ !.agents/skills/ !.agents/skills/** +!.github/ +!.github/workflows/ +!.github/workflows/*.yml !landing/public/.well-known/ !landing/public/.well-known/scout.json diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 10230478..44e96b0b 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -32,6 +32,7 @@ "scripts": { "check": "bunx tsc -p tsconfig.json", "scout": "bun ./bin/scout.ts", + "test": "bun test --isolate ./src", "test:happy": "bun test src/core/pairing/runtime/bridge/fileserver.test.ts" }, "devDependencies": { diff --git a/apps/desktop/src/cli/commands/ask.test.ts b/apps/desktop/src/cli/commands/ask.test.ts index 60f8619c..6c5a3c5a 100644 --- a/apps/desktop/src/cli/commands/ask.test.ts +++ b/apps/desktop/src/cli/commands/ask.test.ts @@ -13,6 +13,8 @@ describe("renderAskCommandHelp", () => { expect(help).toContain("--reply-mode notify"); expect(help).toContain("--label