name: CI on: pull_request: branches: - main push: branches: - main jobs: check: name: Lint & Type Check runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.0 - name: Install dependencies env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: bun install - name: Lint & Type Check run: bun run check build: needs: check name: ${{ matrix.name }} runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - name: macOS arm64 (macos-14) runner: macos-14 - name: Linux x64 (ubuntu-24.04) runner: ubuntu-24.04 - name: Linux arm64 (ubuntu-24.04-arm) runner: ubuntu-24.04-arm - name: Windows x64 (windows-latest) runner: windows-latest defaults: run: shell: bash permissions: contents: read steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.0 - name: Install dependencies env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: bun install - name: Unlock GNOME Keyring if: runner.os == 'Linux' uses: t1m0thyj/unlock-keyring@v1 - name: Run tests (extended timeout) # Unit tests must pass for fork PRs (no secrets). Keep API-dependent tests # in a separate gated step. run: bun test src/tests --timeout 15000 - name: Run integration tests (API) # Only run on push to main or PRs from the same repo (not forks, to protect secrets) if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) }} env: LETTA_API_KEY: ${{ secrets.LETTA_API_KEY }} run: bun test src/integration-tests --timeout 15000 - name: Build bundle run: bun run build - name: CLI help smoke test run: ./letta.js --help - name: CLI version smoke test run: ./letta.js --version || true - name: Bundle size check run: | if [ "$RUNNER_OS" = "Windows" ]; then SIZE=$(powershell -command "(Get-Item letta.js).length") else SIZE=$(stat -f%z ./letta.js 2>/dev/null || stat -c%s ./letta.js 2>/dev/null) fi SIZE_MB=$((SIZE / 1024 / 1024)) echo "Bundle size: $SIZE bytes (~${SIZE_MB}MB)" # Warn if bundle is larger than 50MB if [ $SIZE -gt 52428800 ]; then echo "⚠️ Warning: Bundle size is larger than 50MB" else echo "✓ Bundle size is acceptable" fi # Test npm install flow with native shell to catch shebang issues # This uses PowerShell on Windows (not Git Bash) to match real user experience - name: Test npm install flow (Windows) if: runner.os == 'Windows' shell: pwsh run: | npm pack npm install -g (Get-Item letta-ai-letta-code-*.tgz).FullName letta --help - name: Test npm install flow (Unix) if: runner.os != 'Windows' shell: sh run: | npm pack npm install -g ./letta-ai-letta-code-*.tgz letta --help - name: Headless smoke test (API) # Only run on push to main or PRs from the same repo (not forks, to protect secrets) if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) }} env: LETTA_API_KEY: ${{ secrets.LETTA_API_KEY }} run: ./letta.js --prompt "ping" --tools "" --permission-mode plan - name: Windows integration test # Only run on Windows, with API key available if: ${{ runner.os == 'Windows' && (github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) }} env: LETTA_API_KEY: ${{ secrets.LETTA_API_KEY }} run: bun run src/tests/headless-windows.ts --model haiku - name: Publish dry-run if: ${{ github.event_name == 'push' }} env: NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} LETTA_API_KEY: dummy run: bun publish --dry-run - name: Pack (no auth available) if: ${{ github.event_name != 'push' }} run: bun pm pack headless: needs: check name: Headless / ${{ matrix.model }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: # Note: gemini-3-flash / glm-4.7 temporarily disabled due to instability model: [gpt-5-minimal, gpt-4.1, sonnet-4.5, gemini-pro, haiku] steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.0 - name: Install dependencies env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: bun install - name: Run headless scenario (all outputs) if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) }} env: LETTA_API_KEY: ${{ secrets.LETTA_API_KEY }} run: | bun run src/tests/headless-scenario.ts --model "${{ matrix.model }}" --output text --parallel on bun run src/tests/headless-scenario.ts --model "${{ matrix.model }}" --output json --parallel on bun run src/tests/headless-scenario.ts --model "${{ matrix.model }}" --output stream-json --parallel on docker: needs: check name: Docker Integration runs-on: ubuntu-latest # Only run on push to main or PRs from the same repo (not forks, to protect secrets) if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) }} steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.0 - name: Install dependencies env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: bun install - name: Build bundle run: bun run build - name: Start Letta Docker server run: | docker run -d \ --name letta-server \ -p 8283:8283 \ -e ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }} \ letta/letta:latest - name: Wait for Letta server to be ready run: | echo "Waiting for Letta server to be healthy..." timeout 120 bash -c 'until curl -sf http://localhost:8283/v1/health; do sleep 2; done' echo "Letta server is ready!" - name: Docker smoke test env: LETTA_BASE_URL: http://localhost:8283 ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} run: ./letta.js --prompt "ping" --tools "" --permission-mode plan --model haiku - name: Docker logs (on failure) if: failure() run: docker logs letta-server - name: Stop Letta Docker server if: always() run: docker stop letta-server || true