Skip to content

Publish Package

Publish Package #399

Workflow file for this run

name: Publish Package
on:
workflow_dispatch:
inputs:
package:
description: 'Package to publish'
required: true
type: choice
options:
- all
- main
- cli-prerelease
- sdk
- sdk-py
- brand
default: 'all'
version:
description: 'Version bump type'
required: true
type: choice
options:
- patch
- minor
- major
- prepatch
- preminor
- premajor
- prerelease
custom_version:
description: 'Custom version (optional, overrides version type)'
required: false
type: string
preid:
description: 'Prerelease identifier (used with pre* version types)'
required: false
type: choice
options:
- beta
- alpha
- rc
- next
default: 'beta'
dry_run:
description: 'Dry run (do not actually publish)'
required: false
type: boolean
default: false
tag:
description: 'NPM dist-tag'
required: false
type: choice
options:
- latest
- next
- beta
- alpha
default: 'latest'
# Prevent concurrent publishes
concurrency:
group: publish-package
cancel-in-progress: false
permissions:
contents: write
id-token: write
env:
NPM_CONFIG_FUND: false
AGENT_RELAY_TELEMETRY_DISABLED: 1
WITHDRAWN_STABLE_TAG_PATTERN: '^(v6\.3\.6)$'
jobs:
# Build Rust broker binary for all platforms (needed by SDK's AgentRelayClient)
build-broker:
name: Build broker (${{ matrix.target }})
# Depends on `build` so we can pass the shipped product version into the
# broker at compile time via `AGENT_RELAY_VERSION`. See
# crates/broker/src/util/version.rs — released binaries report this
# version through health/session/telemetry payloads.
needs: build
runs-on: ${{ matrix.os }}
if: github.event.inputs.package == 'all' || github.event.inputs.package == 'main' || github.event.inputs.package == 'cli-prerelease' || github.event.inputs.package == 'sdk' || github.event.inputs.package == 'sdk-py'
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
target: aarch64-apple-darwin
broker_pkg: broker-darwin-arm64
binary_name: agent-relay-broker-darwin-arm64
binary_file: agent-relay-broker
- os: macos-latest
target: x86_64-apple-darwin
broker_pkg: broker-darwin-x64
binary_name: agent-relay-broker-darwin-x64
binary_file: agent-relay-broker
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
broker_pkg: broker-linux-x64
binary_name: agent-relay-broker-linux-x64
binary_file: agent-relay-broker
- os: ubuntu-latest
target: aarch64-unknown-linux-musl
broker_pkg: broker-linux-arm64
binary_name: agent-relay-broker-linux-arm64
binary_file: agent-relay-broker
- os: windows-latest
target: x86_64-pc-windows-msvc
broker_pkg: broker-win32-x64
binary_name: agent-relay-broker-win32-x64.exe
binary_file: agent-relay-broker.exe
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install musl tools (x86_64)
if: matrix.target == 'x86_64-unknown-linux-musl'
run: |
sudo apt-get update
sudo apt-get install -y musl-tools
- name: Install cross (aarch64)
if: matrix.target == 'aarch64-unknown-linux-musl'
uses: taiki-e/install-action@cross
- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
key: broker-${{ matrix.target }}
cache-bin: false
- name: Build broker binary (unix)
if: runner.os != 'Windows'
# POSTHOG_PROJECT_KEY is a repository *variable* (Settings →
# Variables, NOT Secrets) — the PostHog ingest key is
# public-by-design (same category as Sentry DSN). We map it into
# the code-side env var `AGENT_RELAY_POSTHOG_KEY`, which
# `option_env!` in crates/broker/src/telemetry.rs consumes at compile time.
# Unset variable → telemetry ships disabled (acceptable for
# forks / pre-release pipelines).
#
# `AGENT_RELAY_VERSION` is consumed by `option_env!` in
# crates/broker/src/util/version.rs and becomes the broker version
# reported through health/session/telemetry payloads. We pin it to
# the release-line version so a `6.2.x` artifact does not report
# the Cargo crate version (`3.0.0`).
env:
AGENT_RELAY_POSTHOG_KEY: ${{ vars.POSTHOG_PROJECT_KEY }}
AGENT_RELAY_VERSION: ${{ needs.build.outputs.new_version }}
run: |
if [[ "${{ matrix.target }}" == "aarch64-unknown-linux-musl" ]]; then
RUSTFLAGS="-C target-feature=+crt-static" cross build --release --bin agent-relay-broker --target ${{ matrix.target }}
else
RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --bin agent-relay-broker --target ${{ matrix.target }}
fi
strip target/${{ matrix.target }}/release/${{ matrix.binary_file }} 2>/dev/null || true
- name: Build broker binary (windows)
if: runner.os == 'Windows'
shell: pwsh
env:
AGENT_RELAY_POSTHOG_KEY: ${{ vars.POSTHOG_PROJECT_KEY }}
AGENT_RELAY_VERSION: ${{ needs.build.outputs.new_version }}
run: |
$env:RUSTFLAGS = "-C target-feature=+crt-static"
cargo build --release --bin agent-relay-broker --target ${{ matrix.target }}
- name: Copy binary with platform name (unix)
if: runner.os != 'Windows'
run: |
mkdir -p release-binaries
cp target/${{ matrix.target }}/release/${{ matrix.binary_file }} release-binaries/${{ matrix.binary_name }}
- name: Copy binary with platform name (windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path release-binaries | Out-Null
Copy-Item "target/${{ matrix.target }}/release/${{ matrix.binary_file }}" "release-binaries/${{ matrix.binary_name }}"
# Ad-hoc sign macOS binaries at build time so the Python SDK wheel
# doesn't have to invoke codesign at install time.
- name: Ad-hoc sign macOS broker
if: runner.os == 'macOS'
run: codesign --force --sign - release-binaries/${{ matrix.binary_name }}
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.binary_name }}
path: release-binaries/${{ matrix.binary_name }}
retention-days: 1
# Build standalone binaries using bun compile (cross-platform, no Node.js required)
build-standalone:
name: Build standalone (${{ matrix.target }})
needs: build # Wait for version bump
runs-on: ${{ matrix.os }}
if: github.event.inputs.package == 'all' || github.event.inputs.package == 'main'
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
target: bun-darwin-arm64
binary_name: agent-relay-darwin-arm64
- os: macos-latest
target: bun-darwin-x64
binary_name: agent-relay-darwin-x64
- os: ubuntu-latest
target: bun-linux-x64
binary_name: agent-relay-linux-x64
- os: ubuntu-latest
target: bun-linux-arm64
binary_name: agent-relay-linux-arm64
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.14.0'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build TypeScript
run: npm run build
# Belt-and-braces with the `--define` below: the define only substitutes a
# literal `process.env.AGENT_RELAY_POSTHOG_KEY`, so this also bakes the key
# into the compiled posthog-key module the CLI falls back to.
- name: Bake PostHog key into built CLI
env:
AGENT_RELAY_POSTHOG_KEY: ${{ vars.POSTHOG_PROJECT_KEY }}
run: node scripts/inject-posthog-key.mjs
- name: Build standalone binary
# `AGENT_RELAY_POSTHOG_KEY` (the code-side name read by
# packages/cli/src/cli/telemetry/posthog-config.ts via process.env) is
# populated from the `POSTHOG_PROJECT_KEY` repository *variable*
# (Settings → Variables, NOT Secrets). The bun standalone is an
# end-user-shipped artifact, so we bake the key in via --define
# rather than expecting it in the runtime env. Empty variable →
# the define evaluates to "" and the telemetry no-op path takes
# over (see readKey() in posthog-config.ts).
env:
AGENT_RELAY_POSTHOG_KEY: ${{ vars.POSTHOG_PROJECT_KEY }}
run: |
mkdir -p release-binaries
# Exclude native modules - bun runtime has built-in bun:sqlite
bun build \
--compile \
--minify \
--target=${{ matrix.target }} \
--external=better-sqlite3 \
--define="process.env.AGENT_RELAY_VERSION=\"${{ needs.build.outputs.new_version }}\"" \
--define="process.env.AGENT_RELAY_POSTHOG_KEY=\"${AGENT_RELAY_POSTHOG_KEY:-}\"" \
./packages/cli/dist/cli/index.js \
--outfile release-binaries/${{ matrix.binary_name }}
- name: Verify ssh2 is bundled into binary
run: |
BIN="release-binaries/${{ matrix.binary_name }}"
if ! strings "$BIN" | grep -q 'ssh-userauth'; then
echo "FATAL: ssh2 protocol symbols missing from $BIN"
echo "The cloud connect command will hang in the fallback path."
echo "Check that --external=ssh2 is NOT passed to bun build."
exit 1
fi
echo "OK: ssh2 is bundled (ssh-userauth symbol present)"
- name: Sign macOS binary
if: startsWith(matrix.target, 'bun-darwin-')
run: scripts/sign-macos-binary.sh release-binaries/${{ matrix.binary_name }}
- name: Verify Linux binary
if: matrix.target == 'bun-linux-x64'
run: |
chmod +x release-binaries/${{ matrix.binary_name }}
./release-binaries/${{ matrix.binary_name }} --version
- name: Verify macOS binary
if: startsWith(matrix.target, 'bun-darwin-')
run: |
if [ "${{ matrix.target }}" = "bun-darwin-arm64" ]; then
EXPECTED_ARCH="arm64"
else
EXPECTED_ARCH="x86_64"
fi
scripts/verify-macos-binary.sh release-binaries/${{ matrix.binary_name }} "$EXPECTED_ARCH" -- --version
- name: Compress binary with gzip
run: |
gzip -9 -k release-binaries/${{ matrix.binary_name }}
echo "Uncompressed: $(du -h release-binaries/${{ matrix.binary_name }} | cut -f1)"
echo "Compressed: $(du -h release-binaries/${{ matrix.binary_name }}.gz | cut -f1)"
- name: Upload compressed binary
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.binary_name }}.gz
path: release-binaries/${{ matrix.binary_name }}.gz
retention-days: 1
- name: Upload uncompressed binary
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.binary_name }}
path: release-binaries/${{ matrix.binary_name }}
retention-days: 1
# Build all packages once, version them, and upload
build:
name: Build & Version
runs-on: ubuntu-latest
outputs:
new_version: ${{ steps.bump.outputs.new_version }}
is_prerelease: ${{ steps.bump.outputs.is_prerelease }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.14.0'
cache: 'npm'
cache-dependency-path: package-lock.json
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Ensure rollup optional dependencies are installed
run: npm install --no-save rollup || true
- name: Version all packages
id: bump
run: |
CUSTOM_VERSION="${{ github.event.inputs.custom_version }}"
VERSION_TYPE="${{ github.event.inputs.version }}"
PREID="${{ github.event.inputs.preid }}"
CURRENT_VERSION=$(node -p "require('./package.json').version")
echo "Current version: $CURRENT_VERSION"
if [ -n "$CUSTOM_VERSION" ]; then
echo "Setting version to custom value: $CUSTOM_VERSION"
npm version "$CUSTOM_VERSION" --no-git-tag-version --allow-same-version
else
echo "Bumping version: $VERSION_TYPE (preid=$PREID)"
npm version "$VERSION_TYPE" --no-git-tag-version --preid="$PREID"
fi
NEW_VERSION=$(node -p "require('./package.json').version")
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "New version: $NEW_VERSION"
# Detect if this is a prerelease version (contains hyphen, e.g. 2.2.0-beta.1)
if [[ "$NEW_VERSION" == *"-"* ]]; then
echo "is_prerelease=true" >> $GITHUB_OUTPUT
echo "This is a PRERELEASE version"
else
echo "is_prerelease=false" >> $GITHUB_OUTPUT
echo "This is a STABLE version"
fi
# Sync Python SDK version in pyproject.toml
sed -i "s/^version = .*/version = \"$NEW_VERSION\"/" packages/sdk-py/pyproject.toml
echo "Python SDK version set to $NEW_VERSION"
# Sync all package versions and internal dependencies using node script
# (avoids npm version which validates dependencies against registry)
node -e "
const fs = require('fs');
const path = require('path');
const version = '$NEW_VERSION';
// Update @agent-relay/* references across every dep section so
// sibling packages stay pinned to the same version we're about
// to publish. optionalDependencies is critical here: @agent-relay/sdk
// pins the per-platform broker packages by exact version, and
// forgetting this section leaves a freshly-published SDK pointing
// at the prior release's broker packages.
const DEP_SECTIONS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
const rewriteInternalRefs = (pkg) => {
for (const depType of DEP_SECTIONS) {
for (const dep of Object.keys(pkg[depType] || {})) {
if (dep.startsWith('@agent-relay/')) {
pkg[depType][dep] = version;
}
}
}
};
// Update root package internal dependencies
const rootPkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
rewriteInternalRefs(rootPkg);
fs.writeFileSync('package.json', JSON.stringify(rootPkg, null, 2) + '\n');
// Update all sub-packages: version + internal dependencies
const packagesDir = 'packages';
for (const dir of fs.readdirSync(packagesDir)) {
const pkgPath = path.join(packagesDir, dir, 'package.json');
if (fs.existsSync(pkgPath)) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
// Update package version
pkg.version = version;
console.log('@agent-relay/' + dir);
console.log('v' + version);
rewriteInternalRefs(pkg);
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
}
}
"
- name: Clean reinstall after version bump
run: |
# Clear npm cache and node_modules to ensure fresh platform-specific deps
npm cache clean --force
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 all packages
run: npm run build
- name: Run tests
run: npm test
# MUST come after `npm test` — its `pretest` hook reruns `npm run build`,
# which would overwrite the injected module. And it must come before the
# artifact upload, because that dist is what the publish matrix ships.
#
# The npm tarball is plain tsc output and publish runs with
# --ignore-scripts, so this is the only chance to embed the key. Without
# it the published CLI resolves no key and drops every event it emits.
# POSTHOG_PROJECT_KEY is a repository *variable* (not a secret) — a
# PostHog ingest key is public by design, same as a Sentry DSN.
- name: Bake PostHog key into built CLI
env:
AGENT_RELAY_POSTHOG_KEY: ${{ vars.POSTHOG_PROJECT_KEY }}
run: node scripts/inject-posthog-key.mjs
- name: Verify PostHog key was baked in
if: vars.POSTHOG_PROJECT_KEY != ''
run: node scripts/inject-posthog-key.mjs --check
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: |
package.json
package-lock.json
packages/*/package.json
packages/sdk-py/pyproject.toml
packages/*/dist/
dist/
retention-days: 1
# End-to-end smoke test of the optional-dep broker pattern on every target
# platform, using locally-packed tarballs — no registry round-trip, no
# mocking. Each matrix leg:
# 1. Stages the platform's broker binary into its package tree and
# injects os/cpu so the package validates like the published one.
# 2. `npm pack`s the harness driver and SDK (with packages/sdk/bin emptied
# so the SDK tarball cannot fall back to a bundled binary).
# 3. `npm pack`s the matching broker package.
# 4. Installs the tarballs into a scratch project.
# 5. Asserts getBrokerBinaryPath() resolves through the optional-dep
# package and returns an executable file.
# 6. Runs HarnessDriverClient.spawn() end-to-end and shuts it down.
# Gates publish-broker-packages — we do not ship if any platform fails.
smoke-broker-packages:
name: Smoke ${{ matrix.platform }}
needs: [build, build-broker]
if: github.event.inputs.package == 'all' || github.event.inputs.package == 'main' || github.event.inputs.package == 'cli-prerelease' || github.event.inputs.package == 'sdk'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- platform: darwin-arm64
os: macos-14
broker_pkg: broker-darwin-arm64
binary_name: agent-relay-broker-darwin-arm64
binary_file: agent-relay-broker
pkg_os: darwin
pkg_cpu: arm64
# darwin-x64 smoke is intentionally omitted: GitHub's macos-13
# (Intel) runner pool is capacity-constrained and routinely queues
# for hours. The x86_64-apple-darwin broker binary is still
# cross-compiled and published; we just don't exercise it in CI.
- platform: linux-x64
os: ubuntu-latest
broker_pkg: broker-linux-x64
binary_name: agent-relay-broker-linux-x64
binary_file: agent-relay-broker
pkg_os: linux
pkg_cpu: x64
- platform: linux-arm64
os: ubuntu-24.04-arm
broker_pkg: broker-linux-arm64
binary_name: agent-relay-broker-linux-arm64
binary_file: agent-relay-broker
pkg_os: linux
pkg_cpu: arm64
- platform: win32-x64
os: windows-latest
broker_pkg: broker-win32-x64
binary_name: agent-relay-broker-win32-x64.exe
binary_file: agent-relay-broker.exe
pkg_os: win32
pkg_cpu: x64
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.14.0'
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .
- name: Download broker binary
uses: actions/download-artifact@v4
with:
name: ${{ matrix.binary_name }}
path: /tmp/broker
- name: Stage broker binary (unix)
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
mkdir -p "packages/${{ matrix.broker_pkg }}/bin"
cp "/tmp/broker/${{ matrix.binary_name }}" "packages/${{ matrix.broker_pkg }}/bin/${{ matrix.binary_file }}"
chmod +x "packages/${{ matrix.broker_pkg }}/bin/${{ matrix.binary_file }}"
- name: Stage broker binary (windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path "packages/${{ matrix.broker_pkg }}/bin" | Out-Null
Copy-Item "/tmp/broker/${{ matrix.binary_name }}" "packages/${{ matrix.broker_pkg }}/bin/${{ matrix.binary_file }}"
- name: Inject os/cpu for ${{ matrix.pkg_os }}-${{ matrix.pkg_cpu }}
shell: bash
run: |
node -e "
const fs = require('fs');
const p = 'packages/${{ matrix.broker_pkg }}/package.json';
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
pkg.os = ['${{ matrix.pkg_os }}'];
pkg.cpu = ['${{ matrix.pkg_cpu }}'];
fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n');
"
- name: Pack harness driver, SDK, and broker
shell: bash
run: |
set -euo pipefail
TARBALLS="$RUNNER_TEMP/tarballs"
mkdir -p "$TARBALLS"
echo "TARBALLS=$TARBALLS" >> "$GITHUB_ENV"
# Empty packages/sdk/bin so the SDK tarball has NO bundled broker
# binary. This forces the smoke test to exercise the optional-dep
# resolution path instead of the legacy bundled fallback.
rm -rf packages/sdk/bin
mkdir -p packages/sdk/bin
(cd packages/harness-driver && npm pack --ignore-scripts --pack-destination "$TARBALLS")
(cd packages/sdk && npm pack --ignore-scripts --pack-destination "$TARBALLS")
(cd "packages/${{ matrix.broker_pkg }}" && npm pack --ignore-scripts --pack-destination "$TARBALLS")
ls -lh "$TARBALLS"
- name: Install tarballs into scratch project
shell: bash
run: |
set -euo pipefail
SCRATCH="$RUNNER_TEMP/smoke"
mkdir -p "$SCRATCH"
echo "SCRATCH=$SCRATCH" >> "$GITHUB_ENV"
cd "$SCRATCH"
npm init -y --silent >/dev/null
HARNESS_DRIVER_TGZ=$(ls "$TARBALLS"/agent-relay-harness-driver-*.tgz | head -n1)
SDK_TGZ=$(ls "$TARBALLS"/agent-relay-sdk-*.tgz | head -n1)
BROKER_TGZ=$(ls "$TARBALLS"/agent-relay-broker-${{ matrix.platform }}-*.tgz | head -n1)
echo "Installing $HARNESS_DRIVER_TGZ + $SDK_TGZ + $BROKER_TGZ"
npm install --ignore-scripts --no-audit --no-fund \
"$HARNESS_DRIVER_TGZ" "$SDK_TGZ" "$BROKER_TGZ"
ls node_modules/@agent-relay/
- name: Resolver smoke — getBrokerBinaryPath()
shell: bash
run: |
cd "$SCRATCH"
node --input-type=module -e "
import { getBrokerBinaryPath, getOptionalDepPackageName } from '@agent-relay/harness-driver/broker-path';
import { accessSync, constants } from 'node:fs';
const expectedPkg = getOptionalDepPackageName();
const p = getBrokerBinaryPath();
console.log('expected pkg:', expectedPkg);
console.log('resolved:', p);
if (!p) { console.error('FAIL: resolver returned null'); process.exit(1); }
if (!p.includes('${{ matrix.broker_pkg }}')) {
console.error('FAIL: expected path through ${{ matrix.broker_pkg }}, got', p);
process.exit(1);
}
accessSync(p, constants.X_OK);
console.log('OK: resolver returned executable binary from optional-dep package');
"
- name: Spawn smoke — HarnessDriverClient.spawn()
shell: bash
run: |
cd "$SCRATCH"
node --input-type=module -e "
import { HarnessDriverClient } from '@agent-relay/harness-driver';
const client = await HarnessDriverClient.spawn({
cwd: process.cwd(),
channels: ['general'],
startupTimeoutMs: 45000,
onStderr: (line) => console.error('[broker]', line),
});
console.log('OK: HarnessDriverClient.spawn() returned');
await client.shutdown();
console.log('OK: client.shutdown() completed');
" || { echo 'SPAWN_FAILED'; exit 1; }
- name: Negative smoke — optional dep missing (linux-x64 only)
if: matrix.platform == 'linux-x64'
shell: bash
run: |
set -euo pipefail
NEGATIVE="$RUNNER_TEMP/smoke-negative"
mkdir -p "$NEGATIVE"
cd "$NEGATIVE"
npm init -y --silent >/dev/null
HARNESS_DRIVER_TGZ=$(ls "$TARBALLS"/agent-relay-harness-driver-*.tgz | head -n1)
SDK_TGZ=$(ls "$TARBALLS"/agent-relay-sdk-*.tgz | head -n1)
# Install harness driver and SDK, but skip the broker optional deps
# entirely. The resolver should return null and spawn() should throw
# the clear error.
npm install --ignore-scripts --no-audit --no-fund --no-optional \
"$HARNESS_DRIVER_TGZ" "$SDK_TGZ"
node --input-type=module -e "
import { HarnessDriverClient } from '@agent-relay/harness-driver';
try {
await HarnessDriverClient.spawn({ cwd: process.cwd() });
console.error('FAIL: spawn() should have thrown');
process.exit(1);
} catch (err) {
const msg = err && err.message ? err.message : String(err);
console.log('got error:', msg);
const expected = 'couldn\\'t find an agent-relay-broker binary for linux-x64';
if (!msg.includes(expected)) {
console.error('FAIL: error message does not name platform/package');
process.exit(1);
}
console.log('OK: spawn() threw the expected clear error');
}
"
# Publish the SDK's exact-version internal runtime dependencies before the
# SDK. @agent-relay/sdk imports these packages at runtime and pins them to
# the release version, so they must exist on the registry before the SDK can
# be installed from npm. The root CLI depends on @agent-relay/sdk too, so
# package=main and package=cli-prerelease also need this chain.
publish-sdk-internal-deps:
name: Publish SDK internal dep ${{ matrix.package }}
needs: [build, smoke-broker-packages]
runs-on: ubuntu-latest
if: github.event.inputs.package == 'all' || github.event.inputs.package == 'main' || github.event.inputs.package == 'cli-prerelease' || github.event.inputs.package == 'sdk'
strategy:
fail-fast: false
max-parallel: 3
matrix:
package:
- cloud
- config
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Dry run check
if: github.event.inputs.dry_run == 'true'
working-directory: packages/${{ matrix.package }}
run: npm publish --dry-run --access public --tag ${{ github.event.inputs.tag }} --ignore-scripts
- name: Publish to NPM
if: github.event.inputs.dry_run != 'true'
working-directory: packages/${{ matrix.package }}
run: |
set -euo pipefail
PKG_NAME=$(node -p "require('./package.json').name")
PKG_VERSION=$(node -p "require('./package.json').version")
if npm view "${PKG_NAME}@${PKG_VERSION}" version >/dev/null 2>&1; then
echo "${PKG_NAME}@${PKG_VERSION} already exists on npm; skipping publish"
exit 0
fi
npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts
# Publish the per-platform broker packages first. @agent-relay/sdk declares
# these as exact-version optionalDependencies, so they must exist on the
# registry at the matching version before the SDK is published — otherwise
# `npm install @agent-relay/sdk@<v>` races the registry for the broker
# package at the same version.
publish-broker-packages:
name: Publish ${{ matrix.broker_pkg }}
needs: [build, build-broker, smoke-broker-packages]
runs-on: ubuntu-latest
if: github.event.inputs.package == 'all' || github.event.inputs.package == 'main' || github.event.inputs.package == 'cli-prerelease' || github.event.inputs.package == 'sdk'
strategy:
fail-fast: false
max-parallel: 5
matrix:
include:
- broker_pkg: broker-darwin-arm64
binary_name: agent-relay-broker-darwin-arm64
binary_file: agent-relay-broker
os: darwin
cpu: arm64
- broker_pkg: broker-darwin-x64
binary_name: agent-relay-broker-darwin-x64
binary_file: agent-relay-broker
os: darwin
cpu: x64
- broker_pkg: broker-linux-arm64
binary_name: agent-relay-broker-linux-arm64
binary_file: agent-relay-broker
os: linux
cpu: arm64
- broker_pkg: broker-linux-x64
binary_name: agent-relay-broker-linux-x64
binary_file: agent-relay-broker
os: linux
cpu: x64
- broker_pkg: broker-win32-x64
binary_name: agent-relay-broker-win32-x64.exe
binary_file: agent-relay-broker.exe
os: win32
cpu: x64
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .
- name: Download broker binary
uses: actions/download-artifact@v4
with:
name: ${{ matrix.binary_name }}
path: /tmp/broker
- name: Stage broker binary into package tree
run: |
set -euo pipefail
mkdir -p packages/${{ matrix.broker_pkg }}/bin
cp "/tmp/broker/${{ matrix.binary_name }}" "packages/${{ matrix.broker_pkg }}/bin/${{ matrix.binary_file }}"
chmod +x "packages/${{ matrix.broker_pkg }}/bin/${{ matrix.binary_file }}"
ls -lh packages/${{ matrix.broker_pkg }}/bin/
# os/cpu constraints are injected at publish time. Keeping them out of
# the committed package.json lets these packages live as plain
# workspaces during development — otherwise npm install trips
# EBADPLATFORM on the machines that don't match every platform.
- name: Inject os/cpu for ${{ matrix.os }}-${{ matrix.cpu }}
run: |
node -e "
const fs = require('fs');
const p = 'packages/${{ matrix.broker_pkg }}/package.json';
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
pkg.os = ['${{ matrix.os }}'];
pkg.cpu = ['${{ matrix.cpu }}'];
fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n');
console.log('Staged', pkg.name, 'for', pkg.os, pkg.cpu);
"
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Dry run check
if: github.event.inputs.dry_run == 'true'
working-directory: packages/${{ matrix.broker_pkg }}
run: |
echo "Dry run - would publish @agent-relay/${{ matrix.broker_pkg }}"
npm publish --dry-run --access public --tag ${{ github.event.inputs.tag }} --ignore-scripts
# Retry up to 3 times — npm registry occasionally flakes and we cannot
# reuse the version on rerun, so transient failures must be absorbed
# here rather than failing the whole release.
- name: Publish to NPM (attempt 1)
id: publish_1
if: github.event.inputs.dry_run != 'true'
continue-on-error: true
working-directory: packages/${{ matrix.broker_pkg }}
run: |
set -euo pipefail
PKG_NAME=$(node -p "require('./package.json').name")
PKG_VERSION=$(node -p "require('./package.json').version")
if npm view "${PKG_NAME}@${PKG_VERSION}" version >/dev/null 2>&1; then
echo "${PKG_NAME}@${PKG_VERSION} already exists on npm; skipping publish"
exit 0
fi
npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts
- name: Wait before retry
if: github.event.inputs.dry_run != 'true' && steps.publish_1.outcome == 'failure'
run: sleep 30
- name: Publish to NPM (attempt 2)
id: publish_2
if: github.event.inputs.dry_run != 'true' && steps.publish_1.outcome == 'failure'
continue-on-error: true
working-directory: packages/${{ matrix.broker_pkg }}
run: |
set -euo pipefail
PKG_NAME=$(node -p "require('./package.json').name")
PKG_VERSION=$(node -p "require('./package.json').version")
if npm view "${PKG_NAME}@${PKG_VERSION}" version >/dev/null 2>&1; then
echo "${PKG_NAME}@${PKG_VERSION} already exists on npm; skipping publish"
exit 0
fi
npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts
- name: Wait before retry
if: github.event.inputs.dry_run != 'true' && steps.publish_2.outcome == 'failure'
run: sleep 60
- name: Publish to NPM (attempt 3)
id: publish_3
if: github.event.inputs.dry_run != 'true' && steps.publish_1.outcome == 'failure' && steps.publish_2.outcome == 'failure'
working-directory: packages/${{ matrix.broker_pkg }}
run: |
set -euo pipefail
PKG_NAME=$(node -p "require('./package.json').name")
PKG_VERSION=$(node -p "require('./package.json').version")
if npm view "${PKG_NAME}@${PKG_VERSION}" version >/dev/null 2>&1; then
echo "${PKG_NAME}@${PKG_VERSION} already exists on npm; skipping publish"
exit 0
fi
npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts
- name: Fail if all publish attempts failed
if: >-
github.event.inputs.dry_run != 'true' &&
steps.publish_1.outcome == 'failure' &&
steps.publish_2.outcome == 'failure' &&
steps.publish_3.outcome == 'failure'
run: exit 1
# Publish remaining packages in parallel. SDK runtime deps that are pinned by
# exact version are published by publish-sdk-internal-deps before this matrix
# can publish @agent-relay/sdk.
publish-packages:
name: Publish ${{ matrix.package }}
needs: [build, build-broker, publish-broker-packages, publish-sdk-internal-deps]
runs-on: ubuntu-latest
if: github.event.inputs.package == 'all'
strategy:
fail-fast: false
max-parallel: 10
matrix:
package:
# All publishable npm packages - published in parallel
- policy
- utils
- cloud
- sdk
- brand
- harness-driver
- integration-prompts
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .
# Keep bundling all platform broker binaries into the SDK tarball for
# one release cycle. The SDK prefers the optional-dep package but falls
# back to this bundled copy so mixed-version installs keep working
# during the migration. Delete this step in the next major.
- name: Download broker binaries (SDK only)
if: matrix.package == 'sdk'
uses: actions/download-artifact@v4
with:
pattern: agent-relay-broker-*
path: packages/sdk/bin/
merge-multiple: true
- name: Make broker binaries executable (SDK only)
if: matrix.package == 'sdk'
run: |
chmod +x packages/sdk/bin/agent-relay-broker-* || true
# Remove stale generic binary — SDK resolves platform-specific names at runtime
rm -f packages/sdk/bin/agent-relay-broker
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Dry run check
if: github.event.inputs.dry_run == 'true'
working-directory: packages/${{ matrix.package }}
run: |
if [ "${{ matrix.package }}" = "brand" ]; then
echo "Dry run - would publish @agent-relay/brand"
else
echo "Dry run - would publish @agent-relay/${{ matrix.package }}"
fi
npm publish --dry-run --access public --tag ${{ github.event.inputs.tag }} --ignore-scripts
- name: Publish to NPM
if: github.event.inputs.dry_run != 'true'
working-directory: packages/${{ matrix.package }}
run: |
set -euo pipefail
PKG_NAME=$(node -p "require('./package.json').name")
PKG_VERSION=$(node -p "require('./package.json').version")
if npm view "${PKG_NAME}@${PKG_VERSION}" version >/dev/null 2>&1; then
echo "${PKG_NAME}@${PKG_VERSION} already exists on npm; skipping publish"
exit 0
fi
npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts
# Publish @agent-relay/harnesses and @agent-relay/evals after the
# publish-packages matrix, which is where their exact-version workspace deps
# (@agent-relay/sdk and @agent-relay/harness-driver) land on the registry.
# Publishing these before those exist would leave a window where
# `npm install @agent-relay/harnesses@<v>` or `@agent-relay/evals@<v>`
# cannot resolve their dependencies — the same install race the broker/sdk
# ordering above is built to avoid.
publish-harnesses:
name: Publish ${{ matrix.package }}
needs: [build, publish-packages]
runs-on: ubuntu-latest
if: github.event.inputs.package == 'all'
strategy:
fail-fast: false
max-parallel: 2
matrix:
package:
- harnesses
- evals
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Dry run check
if: github.event.inputs.dry_run == 'true'
working-directory: packages/${{ matrix.package }}
run: npm publish --dry-run --access public --tag ${{ github.event.inputs.tag }} --ignore-scripts
- name: Publish to NPM
if: github.event.inputs.dry_run != 'true'
working-directory: packages/${{ matrix.package }}
run: |
set -euo pipefail
PKG_NAME=$(node -p "require('./package.json').name")
PKG_VERSION=$(node -p "require('./package.json').version")
if npm view "${PKG_NAME}@${PKG_VERSION}" version >/dev/null 2>&1; then
echo "${PKG_NAME}@${PKG_VERSION} already exists on npm; skipping publish"
exit 0
fi
npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts
# Publish @agent-relay/fleet after @agent-relay/harnesses lands on the
# registry. fleet pins @agent-relay/{harnesses,harness-driver,sdk} by exact
# version, so publishing it before harnesses exists would leave a window where
# `npm install @agent-relay/fleet@<v>` cannot resolve its dependencies — the
# same install race the broker/sdk/harnesses ordering above is built to avoid.
# The root CLI depends on @agent-relay/fleet, so this must finish before
# publish-main (wired via publish-main's needs).
publish-fleet:
name: Publish fleet
needs: [build, publish-harnesses]
runs-on: ubuntu-latest
if: github.event.inputs.package == 'all'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Dry run check
if: github.event.inputs.dry_run == 'true'
working-directory: packages/fleet
run: npm publish --dry-run --access public --tag ${{ github.event.inputs.tag }} --ignore-scripts
- name: Publish to NPM
if: github.event.inputs.dry_run != 'true'
working-directory: packages/fleet
run: |
set -euo pipefail
PKG_NAME=$(node -p "require('./package.json').name")
PKG_VERSION=$(node -p "require('./package.json').version")
if npm view "${PKG_NAME}@${PKG_VERSION}" version >/dev/null 2>&1; then
echo "${PKG_NAME}@${PKG_VERSION} already exists on npm; skipping publish"
exit 0
fi
npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts
# package=main publishes only the root `agent-relay` tarball, but that
# tarball pins several @agent-relay/* runtime dependencies to the freshly
# bumped version. Publish those direct deps first so a main-only release
# cannot point npm at versions that do not exist.
publish-main-runtime-deps:
name: Publish main runtime dep ${{ matrix.package }}
needs: [build, build-broker, publish-broker-packages, publish-sdk-internal-deps]
runs-on: ubuntu-latest
if: github.event.inputs.package == 'main' || github.event.inputs.package == 'cli-prerelease'
strategy:
fail-fast: false
max-parallel: 6
matrix:
package:
- cloud
- config
- sdk
- utils
- harness-driver
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .
# Keep the SDK tarball equivalent to the package=all path.
- name: Download broker binaries (SDK only)
if: matrix.package == 'sdk'
uses: actions/download-artifact@v4
with:
pattern: agent-relay-broker-*
path: packages/sdk/bin/
merge-multiple: true
- name: Make broker binaries executable (SDK only)
if: matrix.package == 'sdk'
run: |
chmod +x packages/sdk/bin/agent-relay-broker-* || true
rm -f packages/sdk/bin/agent-relay-broker
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Dry run check
if: github.event.inputs.dry_run == 'true'
working-directory: packages/${{ matrix.package }}
run: npm publish --dry-run --access public --tag ${{ github.event.inputs.tag }} --ignore-scripts
- name: Publish to NPM
if: github.event.inputs.dry_run != 'true'
working-directory: packages/${{ matrix.package }}
run: |
set -euo pipefail
PKG_NAME=$(node -p "require('./package.json').name")
PKG_VERSION=$(node -p "require('./package.json').version")
if npm view "${PKG_NAME}@${PKG_VERSION}" version >/dev/null 2>&1; then
echo "${PKG_NAME}@${PKG_VERSION} already exists on npm; skipping publish"
exit 0
fi
npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts
# Publish brand package only (when selected)
publish-brand-only:
name: Publish Brand to NPM
needs: build
runs-on: ubuntu-latest
if: github.event.inputs.package == 'brand'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Dry run check
if: github.event.inputs.dry_run == 'true'
working-directory: packages/brand
run: |
echo "Dry run - would publish @agent-relay/brand"
npm publish --dry-run --access public --tag ${{ github.event.inputs.tag }} --ignore-scripts
- name: Publish Brand to NPM
if: github.event.inputs.dry_run != 'true'
working-directory: packages/brand
run: npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts
# Publish SDK only (when selected)
publish-sdk-only:
name: Publish SDK to NPM
needs: [build, build-broker, publish-broker-packages, publish-sdk-internal-deps]
runs-on: ubuntu-latest
if: github.event.inputs.package == 'sdk'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .
# Bundled broker binaries in packages/sdk/bin/ are kept for one release
# cycle as a fallback. New installs resolve the binary via the
# per-platform @agent-relay/broker-* optional deps published by the
# publish-broker-packages job above.
- name: Download broker binaries
uses: actions/download-artifact@v4
with:
pattern: agent-relay-broker-*
path: packages/sdk/bin/
merge-multiple: true
- name: Make broker binaries executable
run: |
chmod +x packages/sdk/bin/agent-relay-broker-* || true
# Remove stale generic binary — SDK resolves platform-specific names at runtime
rm -f packages/sdk/bin/agent-relay-broker
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Dry run check
if: github.event.inputs.dry_run == 'true'
working-directory: packages/sdk
run: npm publish --dry-run --access public --tag ${{ github.event.inputs.tag }} --ignore-scripts
- name: Publish SDK to NPM
if: github.event.inputs.dry_run != 'true'
working-directory: packages/sdk
run: npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts
# Publish Python SDK to PyPI as per-platform wheels with the broker binary
# embedded. One wheel per (broker_artifact, plat_tag) — see issue #769.
publish-sdk-py:
name: Publish Python SDK wheel (${{ matrix.plat_tag }})
needs: [build, build-broker]
runs-on: ubuntu-latest
if: github.event.inputs.package == 'all' || github.event.inputs.package == 'sdk-py'
environment:
name: pypi
url: https://pypi.org/project/agent-relay-sdk/
strategy:
fail-fast: false
matrix:
include:
- binary_name: agent-relay-broker-darwin-arm64
plat_tag: macosx_11_0_arm64
- binary_name: agent-relay-broker-darwin-x64
plat_tag: macosx_10_12_x86_64
- binary_name: agent-relay-broker-linux-x64
plat_tag: manylinux_2_17_x86_64.manylinux2014_x86_64
- binary_name: agent-relay-broker-linux-arm64
plat_tag: manylinux_2_17_aarch64.manylinux2014_aarch64
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install build tools
run: pip install hatchling build wheel
- name: Download broker binary
uses: actions/download-artifact@v4
with:
name: ${{ matrix.binary_name }}
path: /tmp/broker
- name: Stage broker binary into wheel tree
run: |
mkdir -p packages/sdk-py/src/agent_relay/bin
cp /tmp/broker/${{ matrix.binary_name }} packages/sdk-py/src/agent_relay/bin/agent-relay-broker
chmod +x packages/sdk-py/src/agent_relay/bin/agent-relay-broker
- name: Build wheel
working-directory: packages/sdk-py
run: python -m build --wheel
# `python -m build` produces a `py3-none-any` wheel. Retag it with the
# actual platform we just embedded a binary for; --remove drops the old
# any-platform wheel atomically.
- name: Retag wheel to ${{ matrix.plat_tag }}
working-directory: packages/sdk-py
run: |
python -m wheel tags --platform-tag=${{ matrix.plat_tag }} --remove dist/*.whl
ls -lh dist/
- name: Dry run summary
if: github.event.inputs.dry_run == 'true'
working-directory: packages/sdk-py
run: |
echo "Dry run - would publish agent-relay-sdk==${{ needs.build.outputs.new_version }} (${{ matrix.plat_tag }}) to PyPI"
ls -lh dist/
- name: Publish to PyPI (attempt 1)
id: pypi_publish_1
if: github.event.inputs.dry_run != 'true'
continue-on-error: true
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: packages/sdk-py/dist/
skip-existing: true
- name: Clean up stale attestations before retry
if: github.event.inputs.dry_run != 'true' && steps.pypi_publish_1.outcome == 'failure'
run: |
# pypa/gh-action-pypi-publish writes <wheel>.publish.attestation
# next to each wheel on first attempt. If upload then fails, the
# attestation file is left behind and the next attempt aborts with
# "already have publish attestations". Clear them so we can retry.
rm -f packages/sdk-py/dist/*.publish.attestation
sleep 60
- name: Publish to PyPI (attempt 2)
id: pypi_publish_2
if: github.event.inputs.dry_run != 'true' && steps.pypi_publish_1.outcome == 'failure'
continue-on-error: true
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: packages/sdk-py/dist/
skip-existing: true
- name: Clean up stale attestations before final retry
if: github.event.inputs.dry_run != 'true' && steps.pypi_publish_2.outcome == 'failure'
run: |
rm -f packages/sdk-py/dist/*.publish.attestation
sleep 120
- name: Publish to PyPI (attempt 3)
id: pypi_publish_3
if: github.event.inputs.dry_run != 'true' && steps.pypi_publish_2.outcome == 'failure'
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: packages/sdk-py/dist/
skip-existing: true
- name: Fail if all PyPI publish attempts failed
if: >-
github.event.inputs.dry_run != 'true' &&
steps.pypi_publish_1.outcome == 'failure' &&
steps.pypi_publish_2.outcome == 'failure' &&
steps.pypi_publish_3.outcome == 'failure'
run: exit 1
# Verify standalone binaries on Linux
verify-standalone-linux:
name: Verify Standalone (Linux)
needs: [build-standalone]
runs-on: ubuntu-latest
if: github.event.inputs.package == 'all' || github.event.inputs.package == 'main'
steps:
- name: Download Linux binary
uses: actions/download-artifact@v4
with:
name: agent-relay-linux-x64
path: bin/
- name: Download compressed binary
uses: actions/download-artifact@v4
with:
name: agent-relay-linux-x64.gz
path: bin/
- name: Verify uncompressed binary
run: |
chmod +x bin/agent-relay-linux-x64
echo "Testing uncompressed binary..."
./bin/agent-relay-linux-x64 --version
echo "✓ Uncompressed binary works"
- name: Verify compressed binary
run: |
echo "Testing compressed binary decompression..."
gunzip -c bin/agent-relay-linux-x64.gz > /tmp/agent-relay-test
chmod +x /tmp/agent-relay-test
/tmp/agent-relay-test --version
echo "✓ Compressed binary decompresses and works"
- name: Verify compression ratio
run: |
UNCOMPRESSED=$(stat -c%s bin/agent-relay-linux-x64)
COMPRESSED=$(stat -c%s bin/agent-relay-linux-x64.gz)
RATIO=$(echo "scale=0; 100 - ($COMPRESSED * 100 / $UNCOMPRESSED)" | bc)
echo "Compression ratio: ${RATIO}% reduction"
echo "Uncompressed: $(echo "scale=2; $UNCOMPRESSED / 1048576" | bc)MB"
echo "Compressed: $(echo "scale=2; $COMPRESSED / 1048576" | bc)MB"
# Verify reasonable compression (should be at least 50%)
if [ "$RATIO" -lt 50 ]; then
echo "WARNING: Compression ratio is lower than expected"
else
echo "✓ Compression ratio is good"
fi
# Verify standalone binaries on macOS
verify-standalone-macos:
name: Verify Standalone (macOS)
needs: [build-standalone, build-broker]
runs-on: macos-15
if: github.event.inputs.package == 'all' || github.event.inputs.package == 'main'
strategy:
fail-fast: false
matrix:
include:
- binary_name: agent-relay-darwin-arm64
broker_name: agent-relay-broker-darwin-arm64
expected_arch: arm64
- binary_name: agent-relay-darwin-x64
broker_name: agent-relay-broker-darwin-x64
expected_arch: x86_64
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download macOS binary
uses: actions/download-artifact@v4
with:
name: ${{ matrix.binary_name }}
path: bin/
- name: Download broker binary
uses: actions/download-artifact@v4
with:
name: ${{ matrix.broker_name }}
path: broker/
- name: Download compressed binary
uses: actions/download-artifact@v4
with:
name: ${{ matrix.binary_name }}.gz
path: bin/
- name: Verify uncompressed binary
run: |
echo "Testing uncompressed binary..."
scripts/verify-macos-binary.sh "bin/${{ matrix.binary_name }}" "${{ matrix.expected_arch }}" -- --version
echo "✓ Uncompressed binary verified"
- name: Smoke standalone lifecycle
run: |
if [ "$(uname -m)" != "${{ matrix.expected_arch }}" ]; then
echo "Skipping lifecycle smoke for ${{ matrix.expected_arch }} on $(uname -m) host"
exit 0
fi
chmod +x "broker/${{ matrix.broker_name }}"
bash scripts/ci-standalone-smoke.sh "$PWD/bin/${{ matrix.binary_name }}" "$PWD/broker/${{ matrix.broker_name }}"
- name: Verify compressed binary
run: |
echo "Testing compressed binary decompression..."
gunzip -c "bin/${{ matrix.binary_name }}.gz" > "/tmp/${{ matrix.binary_name }}-test"
scripts/verify-macos-binary.sh "/tmp/${{ matrix.binary_name }}-test" "${{ matrix.expected_arch }}" -- --version
echo "✓ Compressed binary decompresses and verifies"
# Gate job that requires both Linux and macOS verification to pass
verify-binaries:
name: All Binaries Verified
needs: [verify-standalone-linux, verify-standalone-macos]
runs-on: ubuntu-latest
if: github.event.inputs.package == 'all' || github.event.inputs.package == 'main'
steps:
- name: All binary checks passed
run: |
echo "All binary verification checks passed!"
{
echo ""
echo "## Binary Verification Gate"
echo "All platform binaries verified and ready for publish"
echo ""
echo "### Verified Binaries"
echo "- agent-relay-broker: Linux x64, Linux ARM64, macOS x64, macOS ARM64"
echo "- agent-relay: Linux x64, macOS x64, macOS ARM64 (compressed and uncompressed)"
} >> "$GITHUB_STEP_SUMMARY"
# Publish main package
publish-main:
name: Publish Main Package
needs: [build, verify-binaries, publish-packages, publish-fleet, publish-main-runtime-deps]
runs-on: ubuntu-latest
outputs:
published: ${{ steps.publish_root.outputs.published }}
if: |
always() &&
(github.event.inputs.package == 'all' || github.event.inputs.package == 'main' || github.event.inputs.package == 'cli-prerelease') &&
needs.build.result == 'success' &&
(needs.verify-binaries.result == 'success' || (needs.verify-binaries.result == 'skipped' && github.event.inputs.package == 'cli-prerelease')) &&
(needs.publish-packages.result == 'success' || needs.publish-packages.result == 'skipped') &&
(needs.publish-fleet.result == 'success' || needs.publish-fleet.result == 'skipped') &&
(needs.publish-main-runtime-deps.result == 'success' || needs.publish-main-runtime-deps.result == 'skipped')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .
# NOTE: the root `agent-relay` CLI no longer bundles broker binaries in
# its own tarball. Brokers ship as `@agent-relay/broker-<platform>-<arch>`
# optional-deps of `@agent-relay/sdk`, which the root package installs as
# an exact-version runtime dependency.
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Wait for CLI internal dependencies
if: github.event.inputs.dry_run != 'true'
shell: bash
run: |
set -euo pipefail
mapfile -t INTERNAL_DEPS < <(node --input-type=module -e "
import fs from 'node:fs';
const pkg = JSON.parse(fs.readFileSync('packages/cli/package.json', 'utf8'));
const deps = Object.entries(pkg.dependencies ?? {})
.filter(([name]) => name.startsWith('@agent-relay/'))
.map(([name, version]) => name + '@' + version);
console.log(deps.join('\n'));
")
if [ "${#INTERNAL_DEPS[@]}" -eq 0 ]; then
echo "No @agent-relay/* CLI dependencies to verify."
exit 0
fi
for _ in {1..12}; do
missing=()
for spec in "${INTERNAL_DEPS[@]}"; do
if npm view "$spec" version >/dev/null 2>&1; then
echo "✓ $spec is available"
else
missing+=("$spec")
fi
done
if [ "${#missing[@]}" -eq 0 ]; then
echo "All CLI @agent-relay/* dependencies are available on npm."
exit 0
fi
echo "Waiting for npm registry propagation: ${missing[*]}"
sleep 10
done
echo "Timed out waiting for CLI @agent-relay/* dependencies:"
printf ' - %s\n' "${missing[@]}"
exit 1
# bundledDependencies requires workspace packages to exist in
# node_modules/ at pack time so npm can include them in the tarball.
- name: Install workspace dependencies for bundling
run: npm ci --omit=dev --ignore-scripts
- name: Clean publish artifacts before packing
shell: bash
run: |
set -euo pipefail
# Keep root node_modules for bundledDependencies, but remove nested
# workspace installs and caches so they cannot leak into the tarball.
find packages -mindepth 2 -type d \( \
-name node_modules -o \
-name .npm -o \
-name .cache -o \
-name .parcel-cache -o \
-name .turbo \
\) -prune -exec rm -rf {} +
rm -rf .npm-tarball agent-relay-*.tgz
- name: Pack npm tarball
shell: bash
working-directory: packages/cli
run: |
set -euo pipefail
TARBALL_DIR="$(mktemp -d "${RUNNER_TEMP:-/tmp}/agent-relay-pack.XXXXXX")"
npm pack --ignore-scripts --pack-destination "$TARBALL_DIR"
mapfile -t TARBALLS < <(find "$TARBALL_DIR" -maxdepth 1 -type f -name '*.tgz' -print)
if [ "${#TARBALLS[@]}" -ne 1 ]; then
echo "Expected exactly one npm tarball in $TARBALL_DIR, found ${#TARBALLS[@]}"
printf '%s\n' "${TARBALLS[@]}"
exit 1
fi
echo "NPM_TARBALL=${TARBALLS[0]}" >> "$GITHUB_ENV"
echo "Packed tarball: ${TARBALLS[0]}"
ls -lh "${TARBALLS[0]}"
- name: Dry run check
if: github.event.inputs.dry_run == 'true'
run: |
echo "Dry run - would publish agent-relay@${{ needs.build.outputs.new_version }}"
npm publish "$NPM_TARBALL" --dry-run --access public --tag "${{ github.event.inputs.tag }}"
- name: Publish to NPM
id: publish_root
if: github.event.inputs.dry_run != 'true'
run: |
set -euo pipefail
PKG_VERSION=$(node -p "require('./packages/cli/package.json').version")
if npm view "agent-relay@${PKG_VERSION}" version >/dev/null 2>&1; then
echo "agent-relay@${PKG_VERSION} already exists on npm; skipping publish"
echo "published=false" >> "$GITHUB_OUTPUT"
exit 0
fi
npm publish "$NPM_TARBALL" --access public --provenance --tag "${{ github.event.inputs.tag }}"
echo "published=true" >> "$GITHUB_OUTPUT"
# Create git tag and release
create-release:
name: Create Release
needs: [build, build-broker, build-standalone, verify-binaries, publish-main, publish-harnesses]
runs-on: ubuntu-latest
# publish-harnesses only runs for package=all; for a package=main release it
# is skipped, which must not block the tag. Gate on "not failed" rather than
# "succeeded" so a real harness publish failure stops the release but a
# skipped one does not.
if: |
always() &&
github.event.inputs.package != 'cli-prerelease' &&
github.event.inputs.dry_run != 'true' &&
needs.publish-main.result == 'success' &&
(needs.publish-harnesses.result == 'success' || needs.publish-harnesses.result == 'skipped') &&
needs.publish-main.outputs.published == 'true'
steps:
- name: Setup Github App
uses: actions/create-github-app-token@v2
id: create_github_app_token
with:
app-id: ${{ secrets.GH_APP_PUSHER_ID }}
private-key: ${{ secrets.GH_APP_PUSHER_PRIVATE_KEY }}
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.create_github_app_token.outputs.token }}
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .
- name: Download standalone binaries (uncompressed)
uses: actions/download-artifact@v4
with:
pattern: agent-relay-*
path: release-binaries/
merge-multiple: true
- name: Download broker binaries
uses: actions/download-artifact@v4
with:
pattern: agent-relay-broker-*
path: release-binaries/
merge-multiple: true
- name: Make binaries executable
run: |
# Make uncompressed binaries executable (skip .gz files)
for f in release-binaries/*; do
if [[ "$f" != *.gz ]]; then
chmod +x "$f"
fi
done
- name: Verify all binaries present
run: |
echo "=== Verifying release binaries ==="
ls -la release-binaries/
MISSING=0
# Check standalone binaries (both compressed and uncompressed)
for BINARY in agent-relay-darwin-arm64 agent-relay-darwin-x64 agent-relay-linux-x64 agent-relay-linux-arm64; do
# Check uncompressed
if [ -f "release-binaries/$BINARY" ]; then
echo "✓ $BINARY"
else
echo "✗ MISSING: $BINARY"
MISSING=1
fi
# Check compressed
if [ -f "release-binaries/${BINARY}.gz" ]; then
echo "✓ ${BINARY}.gz"
else
echo "⚠ MISSING: ${BINARY}.gz (compressed version)"
# Don't fail on missing .gz - uncompressed is fallback
fi
done
if [ $MISSING -eq 1 ]; then
echo ""
echo "ERROR: Some required binaries are missing!"
exit 1
fi
echo ""
echo "All required binaries present."
- name: Setup Node.js for changelog
uses: actions/setup-node@v4
with:
node-version: '22.14.0'
- name: Generate changelog entry
run: |
NEW_VERSION="${{ needs.build.outputs.new_version }}"
IS_PRERELEASE="${{ needs.build.outputs.is_prerelease }}"
TODAY=$(date -u +%Y-%m-%d)
# Skip changelog generation for prereleases
if [ "$IS_PRERELEASE" = "true" ]; then
echo "Skipping changelog generation for prerelease v${NEW_VERSION}"
exit 0
fi
# Get last published stable tag (semver only, excluding prerelease
# tags like v2.1.0-beta.1 and withdrawn stable tags).
LAST_TAG=$(
git tag -l --sort=-v:refname |
grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' |
grep -Ev "$WITHDRAWN_STABLE_TAG_PATTERN" |
head -n1
)
if [ -z "$LAST_TAG" ]; then
echo "No previous tag found, skipping changelog generation"
exit 0
fi
echo "Generating changelog from ${LAST_TAG} to HEAD for v${NEW_VERSION}"
cat > /tmp/gen-changelog.mjs << 'GENEOF'
import { execSync } from 'node:child_process';
import { readFileSync, writeFileSync } from 'node:fs';
const [,, lastTag, newVersion, today] = process.argv;
const log = execSync(
`git log ${lastTag}..HEAD --pretty=format:"%H|%s|%b%x00" --no-merges`,
{ encoding: 'utf-8' }
).trim();
if (!log) {
console.log('No commits since last tag, skipping changelog');
process.exit(0);
}
const commits = log.split('\0').filter(Boolean).map(record => {
const normalized = record.trimStart();
const idx = normalized.indexOf('|');
const idx2 = normalized.indexOf('|', idx + 1);
const hash = normalized.slice(0, 8);
const files = execSync(`git show --pretty=format: --name-only ${hash}`, {
encoding: 'utf-8',
})
.split('\n')
.map(file => file.trim())
.filter(Boolean);
return {
hash,
subject: normalized.slice(idx + 1, idx2).trim(),
body: normalized.slice(idx2 + 1).trim(),
files,
};
});
function parseSubject(subject) {
const conventional = subject.match(
/^(feat|fix|refactor|perf|chore|test|ci|docs|build|style|security|deprecate|deprecated|remove|removed)(\(([^)]+)\))?(!)?:\s*(.*)$/i
);
if (!conventional) {
return {
type: 'changed',
scope: '',
title: cleanTitle(subject),
breaking: false,
};
}
const [, typeRaw, , scopeRaw = '', bang = '', titleRaw] = conventional;
const type = typeRaw.toLowerCase();
return {
type,
scope: scopeRaw.toLowerCase(),
title: cleanTitle(titleRaw),
breaking: bang === '!',
};
}
function cleanTitle(title) {
const cleaned = title
.replace(/\s*\(#[0-9]+(?:[^)]*)?\)/g, '')
.replace(/\s+#\d+\b/g, '')
.replace(/\s+/g, ' ')
.trim();
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
}
function shouldSkip({ type, scope, title }, files) {
const text = title.toLowerCase();
if (type === 'chore' && (scope === 'release' || scope === 'prerelease')) return true;
if (scope === 'trajectories' || scope === 'comments') return true;
// relay-feature-guardian is an internal Slack feature-check agent, not a
// user-facing Relay surface — keep its changes out of the release changelog.
if (scope === 'feature-guardian' || scope === 'relay-feature-guardian') return true;
if (text.includes('relay-feature-guardian')) return true;
if (
files.length > 0 &&
files.every(file => file.startsWith('.agentworkforce/agents/relay-feature-guardian/'))
) {
return true;
}
if (text.includes('compact trajectories')) return true;
if (text.includes('record ') && text.includes('trajectory')) return true;
if (text.includes('address pr review')) return true;
if (text.includes('review feedback')) return true;
if (text.includes('retrigger flaky')) return true;
if (text === 'clean up skills' || text === 'bump skills') return true;
if (
(type === 'chore' || type === 'style') &&
(text.startsWith('auto-format ') || text.startsWith('format '))
) {
return true;
}
if (text.startsWith('revert version bump')) return true;
return title.length === 0;
}
function sectionFor(commit) {
if (commit.breaking) return 'Breaking Changes';
if (commit.type === 'feat') return 'Added';
if (commit.type === 'fix') return 'Fixed';
if (commit.type === 'security') return 'Security';
if (commit.type === 'deprecate' || commit.type === 'deprecated') return 'Deprecated';
if (commit.type === 'remove' || commit.type === 'removed') return 'Removed';
return 'Changed';
}
const sections = new Map([
['Breaking Changes', []],
['Added', []],
['Changed', []],
['Deprecated', []],
['Removed', []],
['Fixed', []],
['Security', []],
]);
for (const c of commits) {
const parsed = parseSubject(c.subject);
if (shouldSkip(parsed, c.files)) continue;
const section = sectionFor(parsed);
const entries = sections.get(section);
if (!entries.includes(parsed.title)) entries.push(parsed.title);
}
const changelog = readFileSync('CHANGELOG.md', 'utf-8');
// Curated [Unreleased] entries are authoritative (AGENTS.md "Changelog"):
// move them under the new version and restore a bare, empty [Unreleased]
// heading. Commit subjects are only the fallback for an empty block.
const unreleased = changelog.match(
/^## \[Unreleased(?: - (?:Patch|Minor|Major))?\][ \t]*\n([\s\S]*?)(?=^## \[|(?![\s\S]))/m
);
const curated = unreleased ? unreleased[1].trim() : '';
let body = curated;
if (!body) {
if ([...sections.values()].every(entries => entries.length === 0)) {
console.log('No curated entries and no changelog-worthy commits since last tag, skipping changelog');
process.exit(0);
}
const lines = [];
for (const [section, entries] of sections) {
if (entries.length === 0) continue;
lines.push(`### ${section}`);
lines.push('');
for (const entry of entries) lines.push(`- ${entry}`);
lines.push('');
}
body = lines.join('\n').trimEnd();
}
const newEntry = `## [${newVersion}] - ${today}\n\n${body}\n\n`;
if (unreleased) {
const start = unreleased.index;
const end = start + unreleased[0].length;
writeFileSync(
'CHANGELOG.md',
changelog.slice(0, start) + '## [Unreleased]\n\n' + newEntry + changelog.slice(end)
);
} else {
// No [Unreleased] heading: insert before the first versioned entry
// Use \d to match versioned headings (e.g. ## [2.1.5])
const match = changelog.match(/\n## \[\d/);
const insertAt = match ? match.index : -1;
if (insertAt === -1) {
writeFileSync('CHANGELOG.md', changelog.trimEnd() + '\n\n' + newEntry);
} else {
writeFileSync('CHANGELOG.md', changelog.slice(0, insertAt + 1) + newEntry + changelog.slice(insertAt + 1));
}
}
console.log(`Changelog updated for v${newVersion}`);
GENEOF
node /tmp/gen-changelog.mjs "$LAST_TAG" "$NEW_VERSION" "$TODAY"
- name: Compact trajectories
run: |
if command -v npx &>/dev/null && [ -d ".trajectories" ]; then
LAST_TAG=$(
git tag -l --sort=-v:refname |
grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' |
grep -Ev "$WITHDRAWN_STABLE_TAG_PATTERN" |
head -n1
)
if [ -n "$LAST_TAG" ]; then
RELEASE_COMMITS=$(git log "${LAST_TAG}..HEAD" --format=%H | paste -sd, -)
if [ -n "$RELEASE_COMMITS" ]; then
npx agent-trajectories compact \
--commits "$RELEASE_COMMITS" \
--output ".trajectories/compacted/release-${{ needs.build.outputs.new_version }}.json" || true
fi
fi
fi
# Commit compacted trajectories so the working tree is clean for the release commit
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add .trajectories/ 2>/dev/null || true
if ! git diff --staged --quiet; then
git commit -m "chore: compact trajectories for v${{ needs.build.outputs.new_version }}"
fi
# Discard package-lock.json changes from npx side-effects
# (version-bumped files and CHANGELOG must be preserved for the release commit)
git checkout -- package-lock.json 2>/dev/null || true
- name: Commit and tag
env:
GITHUB_TOKEN: ${{ steps.create_github_app_token.outputs.token }}
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
NEW_VERSION="${{ needs.build.outputs.new_version }}"
IS_PRERELEASE="${{ needs.build.outputs.is_prerelease }}"
# Stage version-bumped files (must be separate from optional paths
# because git add fails entirely if any path doesn't exist)
git add package.json package-lock.json packages/*/package.json packages/sdk-py/pyproject.toml CHANGELOG.md
# Stage optional paths that may not exist (trajectories already committed in prior step)
git add .trajectories/ 2>/dev/null || true
if ! git diff --staged --quiet; then
if [ "$IS_PRERELEASE" = "true" ]; then
git commit -m "chore(prerelease): v${NEW_VERSION}"
# Prerelease: push to staging branch, not main
git push origin HEAD:staging
else
git commit -m "chore(release): v${NEW_VERSION}"
# Rebase on latest main in case it advanced during the pipeline
git pull --rebase origin main
git push origin HEAD:main
fi
fi
git tag -a "v${NEW_VERSION}" -m "Release v${NEW_VERSION}"
git push origin "v${NEW_VERSION}"
- name: Create GitHub Release (stable)
if: needs.build.outputs.is_prerelease != 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.build.outputs.new_version }}
name: v${{ needs.build.outputs.new_version }}
body: |
## agent-relay v${{ needs.build.outputs.new_version }}
### Quick Install (no Node.js required!)
```bash
curl -fsSL https://raw.githubusercontent.com/AgentWorkforce/relay/main/install.sh | bash
```
### npm install
```bash
npm install -g agent-relay@${{ needs.build.outputs.new_version }}
npm install @agent-relay/sdk@${{ needs.build.outputs.new_version }}
npm install @agent-relay/brand@${{ needs.build.outputs.new_version }}
```
### Standalone binaries
Self-contained executables (no runtime dependencies).
**Use `.gz` versions for faster downloads (~60-70% smaller).**
| Platform | Compressed (recommended) | Uncompressed |
|----------|--------------------------|--------------|
| Linux x64 | `agent-relay-linux-x64.gz` | `agent-relay-linux-x64` |
| Linux ARM64 | `agent-relay-linux-arm64.gz` | `agent-relay-linux-arm64` |
| macOS Intel | `agent-relay-darwin-x64.gz` | `agent-relay-darwin-x64` |
| macOS Apple Silicon | `agent-relay-darwin-arm64.gz` | `agent-relay-darwin-arm64` |
### agent-relay-broker binaries
Broker binary for spawning and managing agents:
- `agent-relay-broker-linux-x64` - Linux x86_64
- `agent-relay-broker-linux-arm64` - Linux ARM64
- `agent-relay-broker-darwin-x64` - macOS Intel
- `agent-relay-broker-darwin-arm64` - macOS Apple Silicon
- `agent-relay-broker-win32-x64.exe` - Windows x86_64
files: |
release-binaries/*
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ steps.create_github_app_token.outputs.token }}
- name: Create GitHub Release (prerelease)
if: needs.build.outputs.is_prerelease == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.build.outputs.new_version }}
name: v${{ needs.build.outputs.new_version }} (prerelease)
prerelease: true
body: |
## agent-relay v${{ needs.build.outputs.new_version }} (prerelease)
> This is a **prerelease** version published under the `${{ github.event.inputs.tag }}` npm dist-tag.
> It is not installed by default. Use `npm install agent-relay@${{ github.event.inputs.tag }}` to test.
### Install this prerelease
```bash
npm install -g agent-relay@${{ needs.build.outputs.new_version }}
npm install @agent-relay/sdk@${{ needs.build.outputs.new_version }}
npm install @agent-relay/brand@${{ needs.build.outputs.new_version }}
```
Or by dist-tag:
```bash
npm install -g agent-relay@${{ github.event.inputs.tag }}
```
### Standalone binaries
| Platform | Compressed | Uncompressed |
|----------|------------|--------------|
| Linux x64 | `agent-relay-linux-x64.gz` | `agent-relay-linux-x64` |
| Linux ARM64 | `agent-relay-linux-arm64.gz` | `agent-relay-linux-arm64` |
| macOS Intel | `agent-relay-darwin-x64.gz` | `agent-relay-darwin-x64` |
| macOS Apple Silicon | `agent-relay-darwin-arm64.gz` | `agent-relay-darwin-arm64` |
### agent-relay-broker binaries
- `agent-relay-broker-{linux,darwin}-{x64,arm64}`
### Next Steps
1. Deploy to staging: Run **Deploy Staging** workflow in relay-cloud with `relay_version=${{ needs.build.outputs.new_version }}`
2. Test on staging environment
3. If validated, run this workflow again with `tag=latest` for the stable release
files: |
release-binaries/*
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ steps.create_github_app_token.outputs.token }}
# Trigger post-publish verification
verify-publish:
name: Verify Published Package
needs: [build, publish-main]
if: |
always() &&
github.event.inputs.dry_run != 'true' &&
needs.publish-main.result == 'success'
uses: ./.github/workflows/verify-publish.yml
with:
version: ${{ needs.build.outputs.new_version }}
# Post-publish cross-platform verification of @agent-relay/harness-driver and
# its per-platform broker optional deps. Installs from the registry on every
# target (macOS arm64, Linux x64/arm64, Windows x64) and runs spawn
# end-to-end. Catches registry-round-trip failures that smoke-broker-packages
# can't see (missing/wrong os/cpu on published manifests, CDN propagation).
verify-publish-sdk:
name: Verify Published Harness Driver (Cross-Platform)
needs: [build, publish-broker-packages, publish-packages, publish-sdk-only]
if: |
always() &&
github.event.inputs.dry_run != 'true' &&
github.event.inputs.package == 'all' &&
needs.publish-broker-packages.result == 'success' &&
needs.publish-packages.result == 'success'
uses: ./.github/workflows/verify-publish-sdk.yml
with:
version: ${{ needs.build.outputs.new_version }}
summary:
name: Summary
needs:
[
build,
build-broker,
build-standalone,
verify-binaries,
verify-standalone-linux,
verify-standalone-macos,
smoke-broker-packages,
publish-sdk-internal-deps,
publish-broker-packages,
publish-packages,
publish-harnesses,
publish-brand-only,
publish-sdk-py,
publish-main,
verify-publish,
verify-publish-sdk,
]
runs-on: ubuntu-latest
if: always()
steps:
- name: Summary
run: |
IS_PRERELEASE="${{ needs.build.outputs.is_prerelease }}"
if [ "$IS_PRERELEASE" = "true" ]; then
echo "## Prerelease Publish Summary" >> $GITHUB_STEP_SUMMARY
else
echo "## NPM Publish Summary" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version**: \`${{ needs.build.outputs.new_version }}\`" >> $GITHUB_STEP_SUMMARY
echo "**NPM Tag**: \`${{ github.event.inputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Prerelease**: \`$IS_PRERELEASE\`" >> $GITHUB_STEP_SUMMARY
echo "**Dry Run**: \`${{ github.event.inputs.dry_run }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$IS_PRERELEASE" = "true" ]; then
echo "> Users running \`npm install agent-relay\` or \`install.sh\` are **NOT** affected by this prerelease." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
echo "### Results" >> $GITHUB_STEP_SUMMARY
echo "| Stage | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Build | ${{ needs.build.result == 'success' && '✅' || '❌' }} ${{ needs.build.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Build Broker (Rust) | ${{ needs.build-broker.result == 'success' && '✅' || (needs.build-broker.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.build-broker.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Build Standalone (Bun) | ${{ needs.build-standalone.result == 'success' && '✅' || (needs.build-standalone.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.build-standalone.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Verify Binaries | ${{ needs.verify-binaries.result == 'success' && '✅' || (needs.verify-binaries.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.verify-binaries.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Verify Standalone (Linux) | ${{ needs.verify-standalone-linux.result == 'success' && '✅' || (needs.verify-standalone-linux.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.verify-standalone-linux.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Verify Standalone (macOS) | ${{ needs.verify-standalone-macos.result == 'success' && '✅' || (needs.verify-standalone-macos.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.verify-standalone-macos.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Smoke Broker Packages | ${{ needs.smoke-broker-packages.result == 'success' && '✅' || (needs.smoke-broker-packages.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.smoke-broker-packages.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Publish SDK Internal Deps | ${{ needs.publish-sdk-internal-deps.result == 'success' && '✅' || (needs.publish-sdk-internal-deps.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-sdk-internal-deps.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Publish Broker Packages | ${{ needs.publish-broker-packages.result == 'success' && '✅' || (needs.publish-broker-packages.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-broker-packages.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Publish Packages | ${{ needs.publish-packages.result == 'success' && '✅' || (needs.publish-packages.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-packages.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Publish Harnesses | ${{ needs.publish-harnesses.result == 'success' && '✅' || (needs.publish-harnesses.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-harnesses.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Publish Brand | ${{ needs.publish-brand-only.result == 'success' && '✅' || (needs.publish-brand-only.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-brand-only.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Publish Python SDK | ${{ needs.publish-sdk-py.result == 'success' && '✅' || (needs.publish-sdk-py.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-sdk-py.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Publish Main | ${{ needs.publish-main.result == 'success' && '✅' || (needs.publish-main.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-main.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Post-Publish Verify | ${{ needs.verify-publish.result == 'success' && '✅' || (needs.verify-publish.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.verify-publish.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Post-Publish Verify SDK (cross-platform) | ${{ needs.verify-publish-sdk.result == 'success' && '✅' || (needs.verify-publish-sdk.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.verify-publish-sdk.result }} |" >> $GITHUB_STEP_SUMMARY
if [ "$IS_PRERELEASE" = "true" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Next Steps" >> $GITHUB_STEP_SUMMARY
echo "1. Deploy to staging: Run **Deploy Staging** workflow in relay-cloud with \`relay_version=${{ needs.build.outputs.new_version }}\`" >> $GITHUB_STEP_SUMMARY
echo "2. Test on staging environment" >> $GITHUB_STEP_SUMMARY
echo "3. If validated, run this workflow again with \`version=patch/minor/major\` and \`tag=latest\` for the stable release" >> $GITHUB_STEP_SUMMARY
fi