diff --git a/.github/workflows/ci_rust.yml b/.github/workflows/ci_rust.yml index c76ee9d30..d2bede48c 100644 --- a/.github/workflows/ci_rust.yml +++ b/.github/workflows/ci_rust.yml @@ -254,6 +254,28 @@ jobs: cache-bin: false save-if: false + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + with: + version: ${{ steps.ci-config.outputs.uv_version }} + + - uses: taiki-e/install-action@c070f87102a1c75b3183910f391c1cb887fe13c8 # v2.77.6 + with: + tool: just@${{ steps.ci-config.outputs.just_version }} + + - name: Set CLI binary version + working-directory: ${{ env.NEMO_RELAY_CI_WORKSPACE }} + env: + REF_NAME: ${{ github.ref_name }} + REF_TYPE: ${{ github.ref_type }} + run: | + set -euo pipefail + version="$REF_NAME" + if [[ "$REF_TYPE" != "tag" ]]; then + version="$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -n1)+${GIT_COMMIT::8}" + fi + just set-cargo-version "$version" + printf 'NEMO_RELAY_CLI_PACKAGE_VERSION=%s\n' "$version" >> "$GITHUB_ENV" + - name: Install musl tools if: ${{ startsWith(matrix.target, 'x86_64-unknown-linux-musl') || startsWith(matrix.target, 'aarch64-unknown-linux-musl') }} run: | @@ -300,8 +322,17 @@ jobs: docker run --rm \ --volume "${{ env.NEMO_RELAY_CI_WORKSPACE }}:${{ env.NEMO_RELAY_CI_WORKSPACE }}" \ --workdir "${{ env.NEMO_RELAY_CI_WORKSPACE }}" \ + --env NEMO_RELAY_CI_WORKSPACE \ + --env NEMO_RELAY_CLI_PACKAGE_VERSION \ "${{ matrix.runtime_image }}" \ - "${{ env.NEMO_RELAY_CI_WORKSPACE }}/target/${{ matrix.target }}/release/nemo-relay" --version + /bin/sh -ec ' + actual="$("$NEMO_RELAY_CI_WORKSPACE/target/${{ matrix.target }}/release/nemo-relay" --version)" + expected="nemo-relay $NEMO_RELAY_CLI_PACKAGE_VERSION" + if [ "$actual" != "$expected" ]; then + echo "Error: expected CLI version \"$expected\", got \"$actual\"" >&2 + exit 1 + fi + ' - name: Stage CLI binary artifact working-directory: ${{ env.NEMO_RELAY_CI_WORKSPACE }} @@ -337,10 +368,7 @@ jobs: binary="${binary}.exe" fi source="${NEMO_RELAY_CI_WORKSPACE}/target/${target}/release/${binary}" - version="${{ github.ref_name }}" - if [ "${{ github.ref_type }}" != "tag" ]; then - version="$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -n1)+${GIT_COMMIT::8}" - fi + version="$NEMO_RELAY_CLI_PACKAGE_VERSION" rm -rf "${NEMO_RELAY_CI_WORKSPACE_TMP}/cli-packages" mkdir -p "${NEMO_RELAY_CI_WORKSPACE_TMP}/cli-packages" args=( @@ -369,7 +397,12 @@ jobs: venv_cli="${NEMO_RELAY_CI_WORKSPACE_TMP}/cli-wheel-venv/bin/nemo-relay" fi "$venv_python" -m pip install --force-reinstall --no-deps "${wheels[0]}" - "$venv_cli" --version + actual="$("$venv_cli" --version)" + expected="nemo-relay $NEMO_RELAY_CLI_PACKAGE_VERSION" + if [[ "$actual" != "$expected" ]]; then + echo "Error: expected CLI version '$expected', got '$actual'" >&2 + exit 1 + fi - name: Install and run CLI wheel on musllinux if: ${{ endsWith(matrix.target, '-musl') }} @@ -380,13 +413,19 @@ jobs: --volume "${{ env.NEMO_RELAY_CI_WORKSPACE }}:${{ env.NEMO_RELAY_CI_WORKSPACE }}" \ --workdir "${{ env.NEMO_RELAY_CI_WORKSPACE }}" \ --env NEMO_RELAY_CI_WORKSPACE_TMP \ + --env NEMO_RELAY_CLI_PACKAGE_VERSION \ "${{ matrix.runtime_image }}" \ /bin/sh -ec ' wheel="$(find "$NEMO_RELAY_CI_WORKSPACE_TMP/cli-packages" -maxdepth 1 -name "*.whl" -print -quit)" test -n "$wheel" /opt/python/cp311-cp311/bin/python -m venv /tmp/nemo-relay-cli-wheel-venv /tmp/nemo-relay-cli-wheel-venv/bin/python -m pip install --force-reinstall --no-deps "$wheel" - /tmp/nemo-relay-cli-wheel-venv/bin/nemo-relay --version + actual="$(/tmp/nemo-relay-cli-wheel-venv/bin/nemo-relay --version)" + expected="nemo-relay $NEMO_RELAY_CLI_PACKAGE_VERSION" + if [ "$actual" != "$expected" ]; then + echo "Error: expected CLI version \"$expected\", got \"$actual\"" >&2 + exit 1 + fi ' - name: Upload CLI binary artifact diff --git a/crates/cli/src/bootstrap/mod.rs b/crates/cli/src/bootstrap/mod.rs index fbd9142d4..79e8aad02 100644 --- a/crates/cli/src/bootstrap/mod.rs +++ b/crates/cli/src/bootstrap/mod.rs @@ -38,7 +38,7 @@ use state::{BOOTSTRAP_STATE_DIR_ENV, state_dir as bootstrap_state_dir}; pub(crate) const DEFAULT_BIND: &str = "127.0.0.1:47632"; pub(crate) const DEFAULT_URL: &str = "http://127.0.0.1:47632"; pub(crate) const HEALTHZ_TIMEOUT: Duration = Duration::from_millis(500); -pub(crate) const BOOTSTRAP_PROTOCOL_VERSION: u64 = 2; +pub(crate) const BOOTSTRAP_PROTOCOL_VERSION: u64 = 3; pub(super) const BOOTSTRAP_LOCK_TIMEOUT: Duration = Duration::from_secs(20); const BOOTSTRAP_START_TIMEOUT: Duration = Duration::from_secs(10); diff --git a/crates/cli/tests/cli_tests.rs b/crates/cli/tests/cli_tests.rs index 4fce1eea1..6cc3e6bc3 100644 --- a/crates/cli/tests/cli_tests.rs +++ b/crates/cli/tests/cli_tests.rs @@ -24,6 +24,7 @@ fn gateway_bin() -> &'static str { } const ACTIVE_GENERATION_TOKEN: &str = "active-generation"; +const BOOTSTRAP_PROTOCOL_VERSION: u64 = 3; const SIDECAR_PUBLICATION_TIMEOUT: Duration = Duration::from_secs(30); fn write_active_generation(temp: &std::path::Path) -> std::path::PathBuf { @@ -639,8 +640,9 @@ fn cli_mcp_starts_gateway_even_when_stdio_closes_before_request() { fn cli_mcp_rejects_an_unauthenticated_transparent_gateway() { let temp = tempfile::tempdir().unwrap(); let body = format!( - r#"{{"status":"ok","service":"nemo-relay","version":"{}","bootstrap_protocol":2,"instance_id":"transparent"}}"#, - env!("CARGO_PKG_VERSION") + r#"{{"status":"ok","service":"nemo-relay","version":"{}","bootstrap_protocol":{},"instance_id":"transparent"}}"#, + env!("CARGO_PKG_VERSION"), + BOOTSTRAP_PROTOCOL_VERSION, ); let (gateway_url, received) = spawn_single_request_server(200, &body); let mut child = Command::new(gateway_bin()) @@ -1127,8 +1129,9 @@ fn write_fake_bootstrap_health( let nonce = bootstrap_request_header(request, "x-nemo-relay-bootstrap-nonce").unwrap(); let proof_header = fake_bootstrap_proof_header(proof, key_path, fingerprint, nonce); let body = format!( - r#"{{"status":"ok","service":"nemo-relay","version":"{}","bootstrap_protocol":2,"instance_id":"test-instance"}}"#, - env!("CARGO_PKG_VERSION") + r#"{{"status":"ok","service":"nemo-relay","version":"{}","bootstrap_protocol":{},"instance_id":"test-instance"}}"#, + env!("CARGO_PKG_VERSION"), + BOOTSTRAP_PROTOCOL_VERSION, ); stream .write_all( @@ -1171,8 +1174,9 @@ fn handle_fake_bootstrap_tunnel( let health = read_http_request(&mut stream); requests.lock().unwrap().push(health); let body = format!( - r#"{{"status":"ok","service":"nemo-relay","version":"{}","bootstrap_protocol":2,"instance_id":"test-instance"}}"#, - env!("CARGO_PKG_VERSION") + r#"{{"status":"ok","service":"nemo-relay","version":"{}","bootstrap_protocol":{},"instance_id":"test-instance"}}"#, + env!("CARGO_PKG_VERSION"), + BOOTSTRAP_PROTOCOL_VERSION, ); stream .write_all( @@ -1635,7 +1639,7 @@ fn cli_mcp_clients_share_gateway_until_final_idle_shutdown() { let health = relay_health(address); assert_eq!(health["service"], "nemo-relay"); assert_eq!(health["version"], env!("CARGO_PKG_VERSION")); - assert_eq!(health["bootstrap_protocol"], 2); + assert_eq!(health["bootstrap_protocol"], BOOTSTRAP_PROTOCOL_VERSION); assert!( health["instance_id"] .as_str() @@ -4500,8 +4504,9 @@ fn write_phase_health_response( .ok_or_else(|| "health probe omitted its nonce".to_string())?; let proof = fake_bootstrap_proof(key, fingerprint, nonce); let body = format!( - r#"{{"status":"ok","service":"nemo-relay","version":"{}","bootstrap_protocol":2,"instance_id":"phase-health"}}"#, - env!("CARGO_PKG_VERSION") + r#"{{"status":"ok","service":"nemo-relay","version":"{}","bootstrap_protocol":{},"instance_id":"phase-health"}}"#, + env!("CARGO_PKG_VERSION"), + BOOTSTRAP_PROTOCOL_VERSION, ); stream .write_all( diff --git a/crates/cli/tests/coverage/shared/bootstrap_tests.rs b/crates/cli/tests/coverage/shared/bootstrap_tests.rs index 24f8feaf4..15ab05263 100644 --- a/crates/cli/tests/coverage/shared/bootstrap_tests.rs +++ b/crates/cli/tests/coverage/shared/bootstrap_tests.rs @@ -88,17 +88,21 @@ fn compatible_gateway_is_reused_without_starting_another_process() { #[test] fn foreign_and_incompatible_listeners_are_never_adopted() { crate::test_support::enable_operational_logs(); + let incompatible = format!( + "{{\"status\":\"incompatible\",\"service\":\"nemo-relay\",\"version\":\"other\",\"bootstrap_protocol\":{BOOTSTRAP_PROTOCOL_VERSION},\"instance_id\":\"other\"}}" + ); + let previous_protocol = format!( + "{{\"status\":\"ok\",\"service\":\"nemo-relay\",\"version\":\"{}\",\"bootstrap_protocol\":{},\"instance_id\":\"previous\"}}", + env!("CARGO_PKG_VERSION"), + BOOTSTRAP_PROTOCOL_VERSION - 1, + ); for (status, body, expected) in [ - ("200 OK", "{}", "not a compatible"), - ( - "409 Conflict", - "{\"status\":\"incompatible\",\"service\":\"nemo-relay\",\"version\":\"other\",\"bootstrap_protocol\":2,\"instance_id\":\"other\"}", - "different version", - ), + ("200 OK", "{}".to_string(), "not a compatible"), + ("409 Conflict", incompatible, "different version"), + ("200 OK", previous_protocol, "not a compatible"), ] { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let address = listener.local_addr().unwrap(); - let body = body.to_string(); listener.set_nonblocking(true).unwrap(); let (stop_tx, stop_rx) = std::sync::mpsc::channel(); let server = std::thread::spawn(move || { diff --git a/justfile b/justfile index 5f058e28c..311816d48 100644 --- a/justfile +++ b/justfile @@ -453,8 +453,17 @@ import sys version = sys.argv[1] if version.startswith("v"): raise SystemExit("Release tags must not start with 'v'; use raw SemVer such as 0.1.0") -if not re.fullmatch(r"\d+\.\d+\.\d+(?:-(?:alpha|beta|rc)\.\d+)?", version): - raise SystemExit(f"Unsupported release tag '{version}'; use 0.1.0 or prereleases like 0.1.0-rc.1") +numeric_identifier = r"(?:0|[1-9][0-9]*)" +if not re.fullmatch( + rf"{numeric_identifier}\.{numeric_identifier}\.{numeric_identifier}" + rf"(?:-(?:alpha|beta|rc)\.{numeric_identifier})?" + r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?", + version, +): + raise SystemExit( + f"Unsupported Cargo version '{version}'; use 0.1.0, prereleases like " + "0.1.0-rc.1, or build metadata like 0.1.0+deadbeef" + ) path = Path("Cargo.toml") text = path.read_text() @@ -1485,6 +1494,22 @@ set-version version="": cd "$NEMO_RELAY_REPO_ROOT" set_project_version "$version" +# Set only the Cargo workspace version for release artifact builds. +# [version] or --set ref_name= +set-cargo-version version="": + #!/usr/bin/env bash + {{ bash_helpers }} + version="{{ version }}" + if [[ -z "$version" ]]; then + version="{{ ref_name }}" + fi + if [[ -z "$version" ]]; then + echo "Error: version is required for set-cargo-version" >&2 + exit 1 + fi + cd "$NEMO_RELAY_REPO_ROOT" + set_cargo_workspace_version "$version" + # --set [output_dir=] [ref_name=] package-rust: #!/usr/bin/env bash