fix(broker): confirm fleet spawn success #4397
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: Package Validation | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| # Reusable workflows cannot elevate the caller's token permissions. | |
| # detect-changes.yml reads pull-request file lists and commit comparisons. | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| AGENT_RELAY_TELEMETRY_DISABLED: 1 | |
| jobs: | |
| changes: | |
| uses: ./.github/workflows/detect-changes.yml | |
| validate: | |
| name: Build & Validate | |
| needs: changes | |
| if: needs.changes.outputs.node_changed == 'true' | |
| runs-on: ubuntu-latest | |
| env: | |
| NPM_CONFIG_FUND: false | |
| TURBO_TELEMETRY_DISABLED: 1 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| # Restore node_modules — skip npm ci entirely if unchanged. Split | |
| # restore/save (instead of actions/cache@v4) so the cache is saved | |
| # right after `npm ci` produces a clean tree; otherwise the implicit | |
| # post-job save runs after `pack:validate`, which deletes | |
| # `packages/*/node_modules` and poisons the cache for the next run | |
| # (broker fails at runtime with `Cannot find package 'posthog-node'` | |
| # because the cached telemetry workspace has no nested deps). | |
| - name: Restore node_modules | |
| id: cache-modules | |
| uses: actions/cache/restore@v4 | |
| with: | |
| # Include nested workspace node_modules so unhoisted workspace deps | |
| # survive a cache hit — otherwise the cache restores only the root | |
| # tree and skipping `npm ci` can leave the bundler unable to resolve | |
| # them. | |
| path: | | |
| node_modules | |
| packages/*/node_modules | |
| key: modules-v3-${{ hashFiles('package-lock.json') }} | |
| - name: Install dependencies | |
| if: steps.cache-modules.outputs.cache-hit != 'true' | |
| run: npm ci | |
| # Save the freshly-installed node_modules immediately, before any | |
| # later step (notably `pack:validate`) mutates the tree. | |
| - name: Save node_modules | |
| if: steps.cache-modules.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: | | |
| node_modules | |
| packages/*/node_modules | |
| key: modules-v3-${{ hashFiles('package-lock.json') }} | |
| # Cache turbo build outputs for incremental builds | |
| - name: Cache turbo | |
| uses: actions/cache@v4 | |
| with: | |
| path: .turbo | |
| key: turbo-${{ github.sha }} | |
| restore-keys: turbo- | |
| - name: Build packages | |
| run: | | |
| npm run build | |
| # cloud sits outside the core build chain; | |
| # build it so package validation can verify its dist output. | |
| npm --prefix packages/cloud run build | |
| - name: Validate all | |
| run: | | |
| echo "=== Validating packages ===" | |
| ERRORS=0 | |
| # Check dist files exist (skip non-Node package directories). | |
| # broker-* packages ship a Rust-built binary in bin/, not a JS | |
| # dist — they live under packages/ only for workspace linkage. | |
| SKIP_PACKAGES="build-plans brand broker-darwin-arm64 broker-darwin-x64 broker-linux-arm64 broker-linux-x64 broker-win32-x64 personas" | |
| for pkg_dir in packages/*/; do | |
| pkg_name=$(basename "$pkg_dir") | |
| if [ ! -f "$pkg_dir/package.json" ]; then | |
| echo "⊘ $pkg_name - skipped (non-Node package)" | |
| continue | |
| fi | |
| if echo "$SKIP_PACKAGES" | grep -qw "$pkg_name"; then | |
| echo "⊘ $pkg_name - skipped (non-Node package)" | |
| continue | |
| fi | |
| if [ ! -f "$pkg_dir/dist/index.js" ] || [ ! -f "$pkg_dir/dist/index.d.ts" ]; then | |
| echo "✗ $pkg_name - missing dist files" | |
| ERRORS=$((ERRORS + 1)) | |
| else | |
| echo "✓ $pkg_name" | |
| fi | |
| done | |
| [ $ERRORS -gt 0 ] && exit 1 | |
| echo "" | |
| echo "=== Testing key imports ===" | |
| node --eval " | |
| const packages = [ | |
| '@agent-relay/sdk', | |
| '@agent-relay/config', | |
| '@agent-relay/utils', | |
| ]; | |
| (async () => { | |
| let errors = 0; | |
| for (const pkg of packages) { | |
| try { | |
| await import(pkg); | |
| console.log('✓', pkg); | |
| } catch (e) { | |
| console.error('✗', pkg, '-', e.message); | |
| errors++; | |
| } | |
| } | |
| if (errors) process.exit(1); | |
| })(); | |
| " | |
| echo "" | |
| echo "=== Testing main package ===" | |
| node --eval " | |
| import('./packages/cli/dist/index.js').then(m => { | |
| const required = ['AgentRelay', 'createLogger']; | |
| const missing = required.filter(r => !(r in m)); | |
| if (missing.length) { | |
| console.error('Missing exports:', missing); | |
| process.exit(1); | |
| } | |
| console.log('✓ Main package exports OK'); | |
| }); | |
| " | |
| echo "" | |
| echo "SUCCESS: All validations passed" | |
| - name: Test CLI startup | |
| run: | | |
| echo "=== Testing CLI broker lifecycle ===" | |
| BROKER_PORT=3899 | |
| API_PORT=$((BROKER_PORT + 1)) | |
| # Start the broker in background | |
| AGENT_RELAY_BROKER_PORT="$BROKER_PORT" node packages/cli/dist/cli/index.js node up & | |
| DAEMON_PID=$! | |
| # Wait for health endpoint (broker API is the base port + 1) | |
| for i in {1..20}; do | |
| if curl -sf "http://127.0.0.1:${API_PORT}/health" > /dev/null; then | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| # Verify broker is reachable | |
| if curl -sf "http://127.0.0.1:${API_PORT}/health" > /dev/null; then | |
| echo "✓ Broker health endpoint responding" | |
| AGENT_RELAY_SKIP_TMUX=1 node packages/cli/dist/cli/index.js node status || true | |
| else | |
| echo "✗ Broker health endpoint did not become ready" | |
| exit 1 | |
| fi | |
| # Cleanup through CLI down first, then hard-kill fallback | |
| node packages/cli/dist/cli/index.js node down --force --timeout 5000 || true | |
| kill $DAEMON_PID 2>/dev/null || true | |
| # Keep tarball validation after runtime smoke tests. The tarball | |
| # validator removes nested workspace node_modules before packing so the | |
| # package cannot accidentally include them. | |
| - name: Validate npm tarball artifact | |
| run: npm run pack:validate | |
| publish-fresh-install-build: | |
| name: Publish Fresh Install Build | |
| needs: changes | |
| if: needs.changes.outputs.node_changed == 'true' | |
| runs-on: ubuntu-latest | |
| env: | |
| NPM_CONFIG_FUND: false | |
| TURBO_TELEMETRY_DISABLED: 1 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22.14.0' | |
| # The publish workflow bumps versions, removes package-lock.json, then | |
| # runs npm install. This catches range-resolved dependency breaks before merge. | |
| - name: Install dependencies with publish resolution | |
| run: | | |
| rm -rf node_modules packages/*/node_modules package-lock.json | |
| npm install | |
| - name: Ensure rollup optional dependencies are installed | |
| run: npm install --no-save rollup || true | |
| - name: Build Rust broker | |
| run: npm run build:rust | |
| - name: Build TypeScript packages | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=4096 | |
| run: npm run clean && ./node_modules/.bin/turbo run build --filter='./packages/*' --concurrency=2 | |
| standalone-macos-smoke: | |
| name: Standalone macOS Smoke | |
| needs: changes | |
| if: needs.changes.outputs.node_changed == 'true' | |
| runs-on: macos-latest | |
| env: | |
| NPM_CONFIG_FUND: false | |
| TURBO_TELEMETRY_DISABLED: 1 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build packages | |
| run: npm run build | |
| - name: Build broker binary | |
| # Pin the broker's reported version to the same release line as | |
| # `agent-relay` / `@agent-relay/sdk`. `option_env!` in | |
| # crates/broker/src/util/version.rs consumes this at compile time | |
| # and falls back to `CARGO_PKG_VERSION` when unset. | |
| run: | | |
| AGENT_RELAY_VERSION="$(node -p "require('./package.json').version")" | |
| export AGENT_RELAY_VERSION | |
| echo "Building broker with AGENT_RELAY_VERSION=$AGENT_RELAY_VERSION" | |
| cargo build --release --bin agent-relay-broker | |
| - name: Verify broker binary | |
| run: | | |
| STANDALONE_BROKER="$PWD/target/release/agent-relay-broker" | |
| if [ ! -f "$STANDALONE_BROKER" ]; then | |
| echo "ERROR: Broker binary not found after build: $STANDALONE_BROKER" >&2 | |
| echo "Contents of target/release:" >&2 | |
| ls -la "$PWD/target/release" >&2 || true | |
| exit 1 | |
| fi | |
| if [ ! -x "$STANDALONE_BROKER" ]; then | |
| echo "ERROR: Broker binary is not executable: $STANDALONE_BROKER" >&2 | |
| ls -la "$STANDALONE_BROKER" >&2 | |
| exit 1 | |
| fi | |
| echo "STANDALONE_BROKER=$STANDALONE_BROKER" >> "$GITHUB_ENV" | |
| echo "Verified broker binary: $STANDALONE_BROKER" | |
| - name: Verify broker version matches release version | |
| # Acceptance criteria from #904: released broker binaries must | |
| # report the same version as the `agent-relay` / `@agent-relay/sdk` | |
| # release that shipped them. | |
| run: | | |
| EXPECTED_VERSION="$(node -p "require('./package.json').version")" | |
| REPORTED="$(./target/release/agent-relay-broker --version | awk '{print $NF}')" | |
| echo "expected: $EXPECTED_VERSION" | |
| echo "reported: $REPORTED" | |
| if [ "$EXPECTED_VERSION" != "$REPORTED" ]; then | |
| echo "ERROR: broker --version reported '$REPORTED' but package.json is '$EXPECTED_VERSION'." >&2 | |
| echo "AGENT_RELAY_VERSION did not propagate at compile time. See crates/broker/src/util/version.rs." >&2 | |
| exit 1 | |
| fi | |
| - name: Build standalone binary | |
| run: | | |
| mkdir -p release-binaries | |
| VERSION="$(node -p "require('./package.json').version")" | |
| ARCH="$(uname -m)" | |
| if [ "$ARCH" = "arm64" ]; then | |
| TARGET="bun-darwin-arm64" | |
| OUTPUT="agent-relay-darwin-arm64" | |
| EXPECTED_ARCH="arm64" | |
| elif [ "$ARCH" = "x86_64" ]; then | |
| TARGET="bun-darwin-x64" | |
| OUTPUT="agent-relay-darwin-x64" | |
| EXPECTED_ARCH="x86_64" | |
| else | |
| echo "Unsupported macOS runner architecture: $ARCH" | |
| exit 1 | |
| fi | |
| bun build \ | |
| --compile \ | |
| --minify \ | |
| --target="$TARGET" \ | |
| --external=better-sqlite3 \ | |
| --define="process.env.AGENT_RELAY_VERSION=\"${VERSION}\"" \ | |
| ./packages/cli/dist/cli/index.js \ | |
| --outfile "release-binaries/${OUTPUT}" | |
| scripts/sign-macos-binary.sh "release-binaries/${OUTPUT}" | |
| scripts/verify-macos-binary.sh "release-binaries/${OUTPUT}" "$EXPECTED_ARCH" -- --version | |
| echo "STANDALONE_CLI=$PWD/release-binaries/${OUTPUT}" >> "$GITHUB_ENV" | |
| - name: Smoke standalone lifecycle | |
| env: | |
| AGENT_RELAY_STARTUP_DEBUG: 1 | |
| run: bash scripts/ci-standalone-smoke.sh "$STANDALONE_CLI" "$STANDALONE_BROKER" |