diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 81952ae98409d..1d535061c2f87 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -803,12 +803,17 @@ jobs: with: target: ${{ matrix.target }} cache-workspaces: rust/cube-cli - - name: Set crate version to the release version + # The CLI version comes from lerna.json at build time (see + # rust/cube-cli/build.rs); just guard against tag/lerna drift. + - name: Check release version matches lerna.json shell: bash run: | v="${GITHUB_REF_NAME#v}" - sed -i.bak "s/^version = .*/version = \"$v\"/" rust/cube-cli/Cargo.toml - rm -f rust/cube-cli/Cargo.toml.bak + l=$(jq -r .version lerna.json) + if [ "$v" != "$l" ]; then + echo "tag version $v does not match lerna.json version $l" >&2 + exit 1 + fi - name: Build working-directory: rust/cube-cli run: cargo build --release --target ${{ matrix.target }} diff --git a/rust/cube-cli/Cargo.lock b/rust/cube-cli/Cargo.lock index 68735fccb01d3..d9493ffe27bf2 100644 --- a/rust/cube-cli/Cargo.lock +++ b/rust/cube-cli/Cargo.lock @@ -283,7 +283,7 @@ dependencies = [ [[package]] name = "cube-cli" -version = "1.7.2" +version = "0.0.0" dependencies = [ "anyhow", "clap", diff --git a/rust/cube-cli/Cargo.toml b/rust/cube-cli/Cargo.toml index 9142ff06e353b..b41f3c526ec5d 100644 --- a/rust/cube-cli/Cargo.toml +++ b/rust/cube-cli/Cargo.toml @@ -1,9 +1,11 @@ [package] name = "cube-cli" -# Kept in lockstep with the Cube monorepo version (lerna.json). The release -# workflow overrides this from the pushed `v*` tag at build time. -version = "1.7.2" +# Placeholder — the real version is the Cube monorepo version, read from the +# repo-root lerna.json by build.rs at build time (CUBE_CLI_VERSION). This +# field is only the fallback for builds outside the monorepo. +version = "0.0.0" edition = "2021" +build = "build.rs" description = "Cube Cloud command line interface" license = "Apache-2.0" repository = "https://github.com/cube-js/cube" @@ -40,6 +42,9 @@ etcetera = "0.8" [target.'cfg(unix)'.dependencies] libc = "0.2" +[build-dependencies] +serde_json = "1" + [profile.release] lto = "thin" strip = true diff --git a/rust/cube-cli/README.md b/rust/cube-cli/README.md index ed45f6e28f5d7..67aede1a13849 100644 --- a/rust/cube-cli/README.md +++ b/rust/cube-cli/README.md @@ -58,9 +58,11 @@ no OpenSSL dependency and musl builds work out of the box. ## Versioning & releases -The CLI version tracks the Cube monorepo version (`lerna.json`); `Cargo.toml` -is kept in sync and the release build overrides it from the pushed tag, so -`cube --version` always matches the Cube release. +The CLI version tracks the Cube monorepo version: `build.rs` reads the +repo-root `lerna.json` at build time (the `Cargo.toml` version is only a +fallback for out-of-tree builds), so `cube --version` always reports the +real Cube version — for release builds and source builds alike. The release +workflow just verifies the pushed tag matches `lerna.json`. The CLI is built and published by the **same release workflow as the rest of Cube** (`.github/workflows/publish.yml`, on `v*.*.*` tags). Its `cube-cli` diff --git a/rust/cube-cli/build.rs b/rust/cube-cli/build.rs new file mode 100644 index 0000000000000..2f994a8db3674 --- /dev/null +++ b/rust/cube-cli/build.rs @@ -0,0 +1,21 @@ +use std::path::Path; + +/// The CLI version tracks the Cube monorepo version. The single source of +/// truth is the repo-root `lerna.json` (bumped by every release commit), so +/// read it at build time instead of keeping Cargo.toml in sync by hand — +/// source builds and release builds then always report the real version. +/// Falls back to the crate version if lerna.json isn't present (e.g. the +/// crate directory built outside the monorepo). +fn main() { + println!("cargo:rerun-if-changed=../../lerna.json"); + let version = lerna_version().unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); + println!("cargo:rustc-env=CUBE_CLI_VERSION={version}"); +} + +fn lerna_version() -> Option { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?; + let lerna = Path::new(&manifest_dir).join("../../lerna.json"); + let raw = std::fs::read_to_string(lerna).ok()?; + let json: serde_json::Value = serde_json::from_str(&raw).ok()?; + json.get("version")?.as_str().map(str::to_string) +} diff --git a/rust/cube-cli/src/client.rs b/rust/cube-cli/src/client.rs index a5ed881377a1f..f5a8475c47edc 100644 --- a/rust/cube-cli/src/client.rs +++ b/rust/cube-cli/src/client.rs @@ -39,7 +39,7 @@ impl Client { } Ok(Self { http: reqwest::Client::builder() - .user_agent(concat!("cube-cli/", env!("CARGO_PKG_VERSION"))) + .user_agent(concat!("cube-cli/", env!("CUBE_CLI_VERSION"))) .build()?, base_url, token: Mutex::new(token.to_string()), @@ -74,16 +74,16 @@ impl Client { self.token.lock().unwrap().clone() } - /// Authorization header value for a credential. JWTs (three dot-separated - /// segments — OAuth access tokens, legacy deploy JWTs) use the `Bearer` - /// scheme; opaque credentials are Cube Cloud API keys and use `Api-Key`. - /// `CUBE_AUTH_SCHEME=bearer|api-key` overrides the heuristic. + /// Authorization header value for a credential. Cube Cloud API keys are + /// prefixed `sk-` and use the `Api-Key` scheme; everything else (OAuth + /// access tokens — not necessarily JWTs — and legacy deploy JWTs) uses + /// `Bearer`. `CUBE_AUTH_SCHEME=bearer|api-key` overrides the detection. fn authorization(token: &str) -> String { let scheme = match std::env::var("CUBE_AUTH_SCHEME").as_deref() { Ok("bearer") | Ok("Bearer") => "Bearer", Ok("api-key") | Ok("Api-Key") => "Api-Key", - _ if token.split('.').count() == 3 => "Bearer", - _ => "Api-Key", + _ if token.starts_with("sk-") => "Api-Key", + _ => "Bearer", }; format!("{scheme} {token}") } diff --git a/rust/cube-cli/src/commands/login.rs b/rust/cube-cli/src/commands/login.rs index dba8f98fbc8ed..f50344848d900 100644 --- a/rust/cube-cli/src/commands/login.rs +++ b/rust/cube-cli/src/commands/login.rs @@ -25,7 +25,7 @@ pub async fn command(args: Args, ctx: &mut Ctx) -> Result<()> { .with_placeholder("https://.cubecloud.dev") .prompt()?, }; - let url = url.trim_end_matches('/').to_string(); + let url = url.trim().trim_end_matches('/').to_string(); let (token, refresh_token) = match args.api_key { Some(key) => (key, None), @@ -64,7 +64,7 @@ pub async fn command(args: Args, ctx: &mut Ctx) -> Result<()> { async fn device_login(url: &str) -> Result<(String, Option)> { let cfg = oauth::OAuthConfig::from_env(); let http = reqwest::Client::builder() - .user_agent(concat!("cube-cli/", env!("CARGO_PKG_VERSION"))) + .user_agent(concat!("cube-cli/", env!("CUBE_CLI_VERSION"))) .build()?; let device = oauth::request_device_code(&http, url, &cfg).await?; diff --git a/rust/cube-cli/src/commands/update.rs b/rust/cube-cli/src/commands/update.rs index f2a44f6d3a190..aca7b25bdd815 100644 --- a/rust/cube-cli/src/commands/update.rs +++ b/rust/cube-cli/src/commands/update.rs @@ -16,7 +16,7 @@ pub struct Args { pub async fn command(args: Args, _ctx: &Ctx) -> Result<()> { let http = reqwest::Client::builder() - .user_agent(concat!("cube-cli/", env!("CARGO_PKG_VERSION"))) + .user_agent(concat!("cube-cli/", env!("CUBE_CLI_VERSION"))) .build()?; let release = update::latest_release(&http).await?; @@ -24,7 +24,7 @@ pub async fn command(args: Args, _ctx: &Ctx) -> Result<()> { if !is_newer(latest, CURRENT_VERSION) { output::success(&format!( - "cube {CURRENT_VERSION} is up to date (latest release: {latest})" + "Cube CLI {CURRENT_VERSION} is up to date (latest release: {latest})" )); return Ok(()); } @@ -57,7 +57,7 @@ pub async fn command(args: Args, _ctx: &Ctx) -> Result<()> { let new_binary = extract_binary(&bytes)?; replace_current_exe(&new_binary)?; - output::success(&format!("Updated cube {CURRENT_VERSION} → {latest}")); + output::success(&format!("Updated Cube CLI {CURRENT_VERSION} → {latest}")); Ok(()) } diff --git a/rust/cube-cli/src/main.rs b/rust/cube-cli/src/main.rs index 3cdd4d51f26af..5b49cd63ba858 100644 --- a/rust/cube-cli/src/main.rs +++ b/rust/cube-cli/src/main.rs @@ -13,9 +13,9 @@ use clap::{Parser, Subcommand}; #[derive(Parser)] #[command( name = "cube", - version, - about = "Cube Cloud command line interface", - propagate_version = true + display_name = "Cube CLI", + version = env!("CUBE_CLI_VERSION"), + about = "Cube Cloud command line interface" )] struct Cli { #[command(flatten)] diff --git a/rust/cube-cli/src/telemetry.rs b/rust/cube-cli/src/telemetry.rs index e0fef8fa6d66e..f782e85be5186 100644 --- a/rust/cube-cli/src/telemetry.rs +++ b/rust/cube-cli/src/telemetry.rs @@ -37,7 +37,7 @@ pub fn event(name: &str, props: Map) { } let mut payload = json!({ "event": name, - "cliVersion": env!("CARGO_PKG_VERSION"), + "cliVersion": env!("CUBE_CLI_VERSION"), "clientTimestamp": timestamp(), "id": random_id(), "platform": platform(), @@ -58,7 +58,7 @@ pub async fn flush() { return; } let Ok(http) = reqwest::Client::builder() - .user_agent(concat!("cube-cli/", env!("CARGO_PKG_VERSION"))) + .user_agent(concat!("cube-cli/", env!("CUBE_CLI_VERSION"))) .timeout(Duration::from_secs(2)) .build() else { diff --git a/rust/cube-cli/src/update.rs b/rust/cube-cli/src/update.rs index b084fb00d5f04..bd6bf09679b3c 100644 --- a/rust/cube-cli/src/update.rs +++ b/rust/cube-cli/src/update.rs @@ -15,7 +15,7 @@ fn release_api_base() -> String { std::env::var("CUBE_UPDATE_API").unwrap_or_else(|_| "https://api.github.com".to_string()) } -pub const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const CURRENT_VERSION: &str = env!("CUBE_CLI_VERSION"); /// The release target triple this binary maps to. Linux always maps to the /// musl asset — that's the only Linux artifact we ship, and it runs anywhere. @@ -102,7 +102,7 @@ pub fn spawn_check() -> tokio::task::JoinHandle> { return None; } let http = reqwest::Client::builder() - .user_agent(concat!("cube-cli/", env!("CARGO_PKG_VERSION"))) + .user_agent(concat!("cube-cli/", env!("CUBE_CLI_VERSION"))) .build() .ok()?; let release = latest_release(&http).await.ok()?; @@ -110,7 +110,7 @@ pub fn spawn_check() -> tokio::task::JoinHandle> { if newer_than(&latest, CURRENT_VERSION) { Some(format!( "\n{} {} → {}\nRun {} to install it.", - "A new release of cube is available:".yellow(), + "A new release of Cube CLI is available:".yellow(), CURRENT_VERSION.dimmed(), latest.bold().green(), "cube update".bold().cyan(),