diff --git a/.github/workflows/build-broker-binary.yml b/.github/workflows/build-broker-binary.yml index 26d259a22..28c419106 100644 --- a/.github/workflows/build-broker-binary.yml +++ b/.github/workflows/build-broker-binary.yml @@ -69,9 +69,28 @@ jobs: # crates/broker/src/telemetry.rs reads and bakes into the binary. Unset # variable → telemetry ships disabled (acceptable for forks and # pre-release pipelines). + # + # `AGENT_RELAY_VERSION` is consumed by `option_env!` in + # crates/broker/src/util/version.rs. The standalone build is fed + # from the dispatch tag input (or the repo's package.json version + # for branch pushes) so the broker reports a release-line version + # rather than the Cargo crate version. env: AGENT_RELAY_POSTHOG_KEY: ${{ vars.POSTHOG_PROJECT_KEY }} run: | + # Resolve a release-line version for AGENT_RELAY_VERSION. Prefer the + # workflow_dispatch tag (e.g. "v6.2.2" → "6.2.2"); fall back to + # package.json on branch pushes. + TAG_INPUT="${{ inputs.tag }}" + if [[ -n "$TAG_INPUT" && "$TAG_INPUT" != "latest" ]]; then + VERSION="${TAG_INPUT#v}" + else + # `npm pkg get` is available on the default GitHub runner image; + # the value is quoted JSON, so strip the quotes. + VERSION="$(npm pkg get version | tr -d '"')" + fi + export AGENT_RELAY_VERSION="$VERSION" + echo "AGENT_RELAY_VERSION=$VERSION" if [[ "${{ matrix.target }}" == "aarch64-unknown-linux-musl" ]]; then RUSTFLAGS="-C target-feature=+crt-static" cross build --release --target ${{ matrix.target }} --bin agent-relay-broker aarch64-linux-gnu-strip target/${{ matrix.target }}/release/agent-relay-broker 2>/dev/null || true diff --git a/.github/workflows/package-validation.yml b/.github/workflows/package-validation.yml index 07b0af3d7..d0fe16dc0 100644 --- a/.github/workflows/package-validation.yml +++ b/.github/workflows/package-validation.yml @@ -266,7 +266,15 @@ jobs: run: npm run build - name: Build broker binary - run: cargo build --release --bin agent-relay-broker + # Pin the broker's reported version to the same release line as + # `agent-relay` / `@agent-relay/sdk`. `option_env!` in + # crates/broker/src/util/version.rs consumes this at compile time + # and falls back to `CARGO_PKG_VERSION` when unset. + run: | + AGENT_RELAY_VERSION="$(node -p "require('./package.json').version")" + export AGENT_RELAY_VERSION + echo "Building broker with AGENT_RELAY_VERSION=$AGENT_RELAY_VERSION" + cargo build --release --bin agent-relay-broker - name: Verify broker binary run: | @@ -285,6 +293,21 @@ jobs: echo "STANDALONE_BROKER=$STANDALONE_BROKER" >> "$GITHUB_ENV" echo "Verified broker binary: $STANDALONE_BROKER" + - name: Verify broker version matches release version + # Acceptance criteria from #904: released broker binaries must + # report the same version as the `agent-relay` / `@agent-relay/sdk` + # release that shipped them. + run: | + EXPECTED_VERSION="$(node -p "require('./package.json').version")" + REPORTED="$(./target/release/agent-relay-broker --version | awk '{print $NF}')" + echo "expected: $EXPECTED_VERSION" + echo "reported: $REPORTED" + if [ "$EXPECTED_VERSION" != "$REPORTED" ]; then + echo "ERROR: broker --version reported '$REPORTED' but package.json is '$EXPECTED_VERSION'." >&2 + echo "AGENT_RELAY_VERSION did not propagate at compile time. See crates/broker/src/util/version.rs." >&2 + exit 1 + fi + - name: Build standalone binary run: | mkdir -p release-binaries diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ea6d1dead..9d2815da4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -75,6 +75,11 @@ 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: @@ -141,8 +146,15 @@ jobs: # `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 }} @@ -156,6 +168,7 @@ jobs: 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 }} diff --git a/Cross.toml b/Cross.toml index 16d272174..31f012b8c 100644 --- a/Cross.toml +++ b/Cross.toml @@ -1,9 +1,10 @@ [build.env] -# Forward the PostHog write key from the host into the cross-compile -# container so `option_env!("AGENT_RELAY_POSTHOG_KEY")` in src/telemetry.rs -# resolves the same way it does for native cargo builds. Unset on forks / -# CI, set via secret on release pipelines. -passthrough = ["AGENT_RELAY_POSTHOG_KEY"] +# Forward the PostHog write key and product-release version from the host +# into the cross-compile container so `option_env!` in +# src/telemetry.rs and src/util/version.rs resolves the same way it does +# for native cargo builds. Unset on forks / CI, set via secret / +# release pipelines. +passthrough = ["AGENT_RELAY_POSTHOG_KEY", "AGENT_RELAY_VERSION"] [target.aarch64-unknown-linux-musl] image = "ghcr.io/cross-rs/aarch64-unknown-linux-musl:main" diff --git a/crates/broker/src/cli/mod.rs b/crates/broker/src/cli/mod.rs index eb7002909..1bfe0f144 100644 --- a/crates/broker/src/cli/mod.rs +++ b/crates/broker/src/cli/mod.rs @@ -14,6 +14,7 @@ pub(crate) mod command_parse; #[derive(Debug, Parser)] #[command(name = "agent-relay-broker")] #[command(about = "Agent relay broker and worker runtime")] +#[command(version = crate::util::version::BROKER_VERSION)] struct Cli { #[command(subcommand)] command: Commands, diff --git a/crates/broker/src/listen_api.rs b/crates/broker/src/listen_api.rs index d0e0bc591..30a525248 100644 --- a/crates/broker/src/listen_api.rs +++ b/crates/broker/src/listen_api.rs @@ -309,7 +309,7 @@ fn listen_api_router_with_auth( .filter(|value| !value.is_empty()), memberships: config.memberships, default_workspace_id: config.default_workspace_id, - broker_version: env!("CARGO_PKG_VERSION").to_string(), + broker_version: crate::util::version::broker_version().to_string(), persist: config.persist, started_at: std::time::Instant::now(), }; @@ -403,7 +403,7 @@ pub(crate) fn listen_api_health_payload( json!({ "status": status, "service": "agent-relay-listen", - "version": env!("CARGO_PKG_VERSION"), + "version": crate::util::version::broker_version(), "uptimeMs": 0, "workspaceId": workspace_id, "defaultWorkspaceId": default_workspace_id, diff --git a/crates/broker/src/swarm.rs b/crates/broker/src/swarm.rs index 304bcb51f..69e13b473 100644 --- a/crates/broker/src/swarm.rs +++ b/crates/broker/src/swarm.rs @@ -1437,7 +1437,7 @@ impl BrokerClient { "hello", json!({ "client_name": "broker-swarm", - "client_version": env!("CARGO_PKG_VERSION"), + "client_version": crate::util::version::broker_version(), }), ) .await?; diff --git a/crates/broker/src/telemetry.rs b/crates/broker/src/telemetry.rs index 9a51fff04..7f34d2ca0 100644 --- a/crates/broker/src/telemetry.rs +++ b/crates/broker/src/telemetry.rs @@ -442,7 +442,7 @@ impl TelemetryClient { // which component emitted the event. `agent_relay_version` is kept // as a back-compat alias that mirrors `broker_version` here. if let Some(obj) = props.as_object_mut() { - let broker_version = env!("CARGO_PKG_VERSION"); + let broker_version = crate::util::version::broker_version(); obj.insert("agent_relay_version".to_string(), json!(broker_version)); obj.insert("broker_version".to_string(), json!(broker_version)); if let Some(ref v) = self.cli_version { diff --git a/crates/broker/src/util/mod.rs b/crates/broker/src/util/mod.rs index 21af3dd2e..f12c053e0 100644 --- a/crates/broker/src/util/mod.rs +++ b/crates/broker/src/util/mod.rs @@ -1,2 +1,3 @@ pub(crate) mod ansi; pub(crate) mod terminal; +pub(crate) mod version; diff --git a/crates/broker/src/util/version.rs b/crates/broker/src/util/version.rs new file mode 100644 index 000000000..a6711e687 --- /dev/null +++ b/crates/broker/src/util/version.rs @@ -0,0 +1,48 @@ +/// The broker version reported in health/session/telemetry payloads and +/// by `agent-relay-broker --version`. +/// +/// Released binaries are compiled with `AGENT_RELAY_VERSION` set to the +/// shipped `agent-relay` / `@agent-relay/sdk` product version so that +/// `broker_version` in telemetry, `/api/config`, and the health/session +/// responses matches the artifact users installed. Local `cargo build` +/// invocations have no such env var; in that case we fall back to the +/// Rust crate's `CARGO_PKG_VERSION`, which is a reasonable developer-build +/// label rather than a release-line identifier. +pub const BROKER_VERSION: &str = match option_env!("AGENT_RELAY_VERSION") { + // Treat an empty `AGENT_RELAY_VERSION` as unset — CI workflows that + // forward an unresolved expression (e.g. `${{ needs.build.outputs.new_version }}` + // before the upstream job ran) can leave the var defined but blank, + // which would otherwise surface as an empty broker version string. + Some(v) if !v.is_empty() => v, + _ => env!("CARGO_PKG_VERSION"), +}; + +/// Returns the broker version. Prefer this helper over `env!("CARGO_PKG_VERSION")` +/// so all components report the same release-line version. +pub fn broker_version() -> &'static str { + BROKER_VERSION +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn broker_version_is_non_empty() { + // Either AGENT_RELAY_VERSION (release builds) or CARGO_PKG_VERSION + // (developer builds) must produce a non-empty string. + assert!(!broker_version().is_empty()); + assert_eq!(broker_version(), BROKER_VERSION); + } + + #[test] + fn broker_version_matches_compile_time_env() { + // When AGENT_RELAY_VERSION is set to a non-empty string at build + // time, the helper surfaces that exact value; empty or unset + // falls back to the Cargo crate version. + match option_env!("AGENT_RELAY_VERSION") { + Some(v) if !v.is_empty() => assert_eq!(broker_version(), v), + _ => assert_eq!(broker_version(), env!("CARGO_PKG_VERSION")), + } + } +}