Live — agent channels (email + SMS) #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Live — agent channels (email + SMS) | |
| # Boots the agent-under-test (AUT) as a real gateway (opencode serve + the | |
| # sidecar: tunnel, webhook subscriptions), then a remote Inkbox identity | |
| # emails/texts it and waits for the reply. Two matrix legs: | |
| # mock — deterministic mock model; proves the pipe (no token spend). | |
| # real — real OpenAI key; proves the agent actually reasons (spends tokens). | |
| # Expensive (real gateway + tunnel + tokens), so on PRs it runs only once the | |
| # PR is READY (non-draft). The unattended cadence chains off the scheduled | |
| # canary via workflow_run, so live and the host stay in lock-step. | |
| on: | |
| pull_request: | |
| branches: [main] | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| workflow_dispatch: | |
| inputs: | |
| timeout_s: | |
| description: "Seconds to wait for the reply" | |
| default: "150" | |
| workflow_run: | |
| workflows: ["Canary — plugin vs opencode latest"] | |
| types: [completed] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| # Only ONE client may hold the AUT's Inkbox tunnel at a time, so EVERY live | |
| # tunnel workflow MUST use this exact group — they queue behind each other. | |
| group: inkbox-live-aut-tunnel | |
| cancel-in-progress: false | |
| jobs: | |
| live: | |
| runs-on: ubuntu-latest | |
| # Skip fork PRs (no secrets), draft PRs (expensive), and canary chains | |
| # where the canary FAILED (never take the tunnel against a broken host). | |
| if: >- | |
| (github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.draft == false)) && | |
| (github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success') | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 1 # legs share the AUT identity → one at a time | |
| matrix: | |
| mode: [mock, real] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - run: npm ci | |
| - run: npm install -g opencode-ai@latest | |
| - name: Start mock model | |
| if: matrix.mode == 'mock' | |
| run: | | |
| nohup node tests/live/mock-openai.mjs 8088 > "$RUNNER_TEMP/mock.log" 2>&1 & | |
| echo $! > "$RUNNER_TEMP/mock.pid" | |
| for i in $(seq 1 10); do | |
| curl -sf http://127.0.0.1:8088/v1/models >/dev/null && { echo "mock model ready"; exit 0; } | |
| sleep 1 | |
| done | |
| echo "::error::mock model did not start"; cat "$RUNNER_TEMP/mock.log"; exit 1 | |
| - name: Boot the AUT gateway (${{ matrix.mode }}) | |
| env: | |
| MODE: ${{ matrix.mode }} | |
| AUT_INKBOX_API_KEY: ${{ secrets.AUT_INKBOX_API_KEY }} | |
| AUT_INKBOX_SIGNING_KEY: ${{ secrets.AUT_INKBOX_SIGNING_KEY }} | |
| INKBOX_BASE_URL: ${{ vars.INKBOX_BASE_URL }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| run: bash scripts/live-aut.sh | |
| - name: Run live channel tests (${{ matrix.mode }}) | |
| env: | |
| AUT_INKBOX_API_KEY: ${{ secrets.AUT_INKBOX_API_KEY }} | |
| REMOTE_INKBOX_API_KEY: ${{ secrets.REMOTE_INKBOX_API_KEY }} | |
| INKBOX_BASE_URL: ${{ vars.INKBOX_BASE_URL }} | |
| LIVE_REAL_MODEL: ${{ matrix.mode == 'real' && '1' || '' }} | |
| LIVE_REPLY_TIMEOUT_S: ${{ github.event.inputs.timeout_s || '150' }} | |
| run: | | |
| npx vitest run --config vitest.live.config.ts \ | |
| tests/live/email-reply.test.ts tests/live/sms.test.ts | |
| # Failure-only: these logs carry live phone/email content and this | |
| # repo's Action logs are public. | |
| - name: Dump logs (on failure only) | |
| if: failure() | |
| run: | | |
| echo "=== gateway.log ==="; cat "$AUT_GATEWAY_LOG" || true | |
| echo "=== serve.log ==="; tail -n 100 "$AUT_SERVE_LOG" || true | |
| echo "=== mock.log ==="; cat "$RUNNER_TEMP/mock.log" 2>/dev/null || true | |
| - name: Tear down (always) | |
| if: always() | |
| run: | | |
| kill "$AUT_GATEWAY_PID" 2>/dev/null || true | |
| kill "$AUT_SERVE_PID" 2>/dev/null || true | |
| kill "$(cat "$RUNNER_TEMP/mock.pid" 2>/dev/null)" 2>/dev/null || true | |
| - name: Upload artifacts (on failure only) | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: live-logs-${{ matrix.mode }} | |
| retention-days: 5 | |
| path: | | |
| ${{ env.AUT_GATEWAY_LOG }} | |
| ${{ env.AUT_SERVE_LOG }} | |
| ${{ runner.temp }}/mock.log | |
| if-no-files-found: ignore | |
| # Alert only when an unattended (canary-chained) run fails; PRs and manual | |
| # dispatch stay silent — the check is visible inline there. | |
| notify: | |
| needs: [live] | |
| if: always() && needs.live.result == 'failure' && github.event_name == 'workflow_run' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Notify chat webhook on scheduled failure | |
| # Non-blocking: a flaky webhook must never flip the suite result. | |
| run: | | |
| [ -z "${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}" ] && exit 0 | |
| curl -sS --max-time 10 --retry 3 -X POST "${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}" \ | |
| -H 'Content-Type: application/json' \ | |
| -d '{"text": "⚠️ FAILED — Live channels (email + SMS) suite\n\nRun: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' || true |