Skip to content

fix(broker): confirm fleet spawn success #4397

fix(broker): confirm fleet spawn success

fix(broker): confirm fleet spawn success #4397

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"