Skip to content

Live — agent channels (email + SMS) #4

Live — agent channels (email + SMS)

Live — agent channels (email + SMS) #4

Workflow file for this run

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