Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 21 additions & 43 deletions crates/turborepo-cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,41 +134,6 @@ fn initialize_deferred_telemetry_client(
}
}

#[derive(PartialEq)]
enum PrintVersionState {
Enabled,
Disabled,
}

fn get_print_version_state() -> PrintVersionState {
env::var("TURBO_PRINT_VERSION_DISABLED")
.map(|var| match var.as_str() {
"1" | "true" => PrintVersionState::Disabled,
_ => PrintVersionState::Enabled,
})
.unwrap_or(PrintVersionState::Enabled)
}

#[derive(PartialEq)]
enum CIState {
Inside,
Outside,
}

fn get_ci_state() -> CIState {
match turborepo_ci::is_ci() {
true => CIState::Inside,
_ => CIState::Outside,
}
}

fn should_print_version() -> bool {
let print_version_state = get_print_version_state();
let ci_state = get_ci_state();

print_version_state == PrintVersionState::Enabled && ci_state == CIState::Outside
}

fn set_run_flags<'a>(
command: &'a mut Command,
repo_state: &'a Option<RepoState>,
Expand Down Expand Up @@ -340,14 +305,27 @@ async fn run_main(

let mut command = get_command(&mut cli_args)?;

// Suppress the version banner in --json mode — all output on stdout
// must be machine-readable NDJSON.
let is_json_mode = matches!(
&command,
Command::Run { execution_args, .. } | Command::Watch { execution_args, .. }
if execution_args.json
);
if should_print_version() && !is_json_mode {
// Skip the standalone version banner when the command reports the version
// itself: `run`/`watch` print it as the first bullet of the run prelude
// (and omit it entirely in --json mode), and `turbo query` executions that
// print a JSON document embed it in that document.
let reports_own_version = matches!(&command, Command::Run { .. } | Command::Watch { .. })
|| matches!(
&command,
Command::Query {
subcommand: Some(QuerySubcommand::Affected(_)),
..
} | Command::Query {
subcommand: None,
query: Some(_),
..
} | Command::Query {
subcommand: None,
schema: true,
..
}
);
if turborepo_run::should_print_version() && !reports_own_version {
eprintln!("{}", GREY.apply_to(format!("• turbo {}", get_version())));
}

Expand Down
34 changes: 30 additions & 4 deletions crates/turborepo-cli/src/commands/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,32 +60,51 @@ async fn execute_query_and_print(
query_server: &dyn QueryServer,
query: &str,
variables_json: Option<&str>,
version: &str,
) -> Result<(i32, String), cli::Error> {
execute_query_and_write(
run,
query_server,
query,
variables_json,
version,
&mut std::io::stdout(),
&mut std::io::stderr(),
)
.await
}

/// Prepend a `version` key to a JSON object so the output stays a single
/// parseable document (the version banner is suppressed for query output).
/// Non-object or unparseable input is returned unchanged.
fn with_version_key(result_json: &str, version: &str) -> String {
match serde_json::from_str::<serde_json::Value>(result_json) {
Ok(serde_json::Value::Object(fields)) => {
let mut output = serde_json::Map::with_capacity(fields.len() + 1);
output.insert("version".to_string(), version.into());
output.extend(fields.into_iter().filter(|(key, _)| key != "version"));
serde_json::to_string_pretty(&serde_json::Value::Object(output))
.unwrap_or_else(|_| result_json.to_string())
}
_ => result_json.to_string(),
}
}

/// The same query adapter with injectable output streams for in-process tests.
async fn execute_query_and_write(
run: Arc<dyn QueryRun>,
query_server: &dyn QueryServer,
query: &str,
variables_json: Option<&str>,
version: &str,
stdout: &mut (impl IoWrite + Send),
stderr: &mut (impl IoWrite + Send),
) -> Result<(i32, String), cli::Error> {
let result = query_server
.execute_query(run, query, variables_json)
.await?;

writeln!(stdout, "{}", result.result_json)?;
writeln!(stdout, "{}", with_version_key(&result.result_json, version))?;
if !result.errors.is_empty() {
for error in result.errors {
let error = QueryError::from_query_error(error, query.to_string());
Expand Down Expand Up @@ -119,6 +138,7 @@ pub async fn run(
return Ok(0);
}

let version = base.version();
let signal = get_signal()?;
let handler = SignalHandler::new(signal);

Expand Down Expand Up @@ -146,7 +166,7 @@ pub async fn run(
};
let query = build_affected_query(&input);
let (exit_code, result_json) =
execute_query_and_print(run, query_server, &query, None).await?;
execute_query_and_print(run, query_server, &query, None, version).await?;

if exit_code != 0 {
return Ok(exit_code);
Expand Down Expand Up @@ -198,7 +218,8 @@ pub async fn run(
.map_err(turborepo_query_api::Error::Server)?;

let (exit_code, _) =
execute_query_and_print(run, query_server, query, variables_json.as_deref()).await?;
execute_query_and_print(run, query_server, query, variables_json.as_deref(), version)
.await?;
Ok(exit_code)
} else {
query_server.run_query_server(run, handler).await?;
Expand Down Expand Up @@ -385,14 +406,19 @@ mod tests {
&server,
"query { version }",
Some(r#"{"name":"app"}"#),
"1.2.3",
&mut stdout,
&mut stderr,
)
.await
.unwrap();
assert_eq!(exit, expected_exit);
assert_eq!(json, r#"{"data":{"version":"fixture"}}"#);
assert_eq!(String::from_utf8(stdout).unwrap(), format!("{json}\n"));
assert_eq!(
String::from_utf8(stdout).unwrap(),
"{\n \"version\": \"1.2.3\",\n \"data\": {\n \"version\": \"fixture\"\n \
}\n}\n"
);
let diagnostic = String::from_utf8(stderr).unwrap();
if fail {
assert!(diagnostic.contains("fixture error"), "{diagnostic}");
Expand Down
16 changes: 16 additions & 0 deletions crates/turborepo-run/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ use turborepo_ui::{ColorConfig, LIGHT_GREY, TerminalSink, sender::UISender, tui,

pub use crate::error::Error;

/// Whether the `• turbo <version>` line should be shown. Disabled in CI and
/// when `TURBO_PRINT_VERSION_DISABLED` is `1` or `true`.
pub fn should_print_version() -> bool {
let disabled = std::env::var("TURBO_PRINT_VERSION_DISABLED")
.is_ok_and(|var| matches!(var.as_str(), "1" | "true"));
!disabled && !turborepo_ci::is_ci()
}

/// Live status of the remote cache, determined by a preflight API check
/// that runs concurrently with graph building.
#[derive(Debug, Clone, Copy)]
Expand Down Expand Up @@ -426,6 +434,14 @@ impl Run {
)
.emit();

if should_print_version() {
turborepo_log::info(
turborepo_log::Source::turbo(turborepo_log::Subsystem::Run),
format!("{pad}• turbo {}", self.repo.version),
)
.emit();
}

let targets_list = self.execution.opts.run_opts.tasks.join(", ");
if self.execution.opts.run_opts.single_package {
turborepo_log::info(
Expand Down
20 changes: 19 additions & 1 deletion crates/turborepo/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,25 @@ macro_rules! check_json_output {
stderr,
);

let query_output: serde_json::Value = serde_json::from_str(&stdout)?;
let mut query_output: serde_json::Value = serde_json::from_str(&stdout)?;
// `turbo query` embeds the CLI version as the first key. Verify
// it, then drop it so snapshots stay stable across releases.
if $command == "query" {
let object = query_output
.as_object_mut()
.expect("turbo query output should be a JSON object");
assert_eq!(
object.keys().next().map(String::as_str),
Some("version"),
"turbo query output should start with a version key:\n{}",
stdout,
);
let version = object.shift_remove("version").unwrap();
assert!(
version.as_str().is_some_and(|v| !v.is_empty()),
"version should be a non-empty string: {version}"
);
}
let test_name = format!(
"{}_{}_({})",
$fixture,
Expand Down
Loading