Publish Package #399
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: 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 |