diff --git a/crates/turbo-trace/src/main.rs b/crates/turbo-trace/src/main.rs index be8c5cd79857c..5c5f10473ede9 100644 --- a/crates/turbo-trace/src/main.rs +++ b/crates/turbo-trace/src/main.rs @@ -30,7 +30,7 @@ fn main() -> Result<(), PathError> { .map(|f| AbsoluteSystemPathBuf::from_unknown(&abs_cwd, f)) .collect(); - let tracer = Tracer::new(abs_cwd, files, args.ts_config)?; + let tracer = Tracer::new(abs_cwd, files, args.ts_config); let result = tracer.trace(); diff --git a/crates/turborepo-lib/src/config/env.rs b/crates/turborepo-lib/src/config/env.rs index 84601bf9f1da8..e05d448e60600 100644 --- a/crates/turborepo-lib/src/config/env.rs +++ b/crates/turborepo-lib/src/config/env.rs @@ -63,7 +63,7 @@ impl EnvVars { impl ResolvedConfigurationOptions for EnvVars { fn get_configuration_options( &self, - _existing_config: &ConfigurationOptions, + _existing_config: Option<&ConfigurationOptions>, ) -> Result { // Process signature let signature = self @@ -189,60 +189,7 @@ impl ResolvedConfigurationOptions for EnvVars { } } -const VERCEL_ARTIFACTS_MAPPING: &[(&str, &str)] = [ - ("vercel_artifacts_token", "token"), - ("vercel_artifacts_owner", "team_id"), -] -.as_slice(); - -pub struct OverrideEnvVars<'a> { - environment: &'a HashMap, - output_map: HashMap<&'static str, String>, -} - -impl<'a> OverrideEnvVars<'a> { - pub fn new(environment: &'a HashMap) -> Result { - let vercel_artifacts_mapping: HashMap<_, _> = - VERCEL_ARTIFACTS_MAPPING.iter().copied().collect(); - - let output_map = map_environment(vercel_artifacts_mapping, environment)?; - Ok(Self { - environment, - output_map, - }) - } - - fn ui(&self) -> Option { - let value = self - .environment - .get(OsStr::new("ci")) - .or_else(|| self.environment.get(OsStr::new("no_color")))?; - match truth_env_var(value.to_str()?)? { - true => Some(UIMode::Stream), - false => None, - } - } -} - -impl<'a> ResolvedConfigurationOptions for OverrideEnvVars<'a> { - fn get_configuration_options( - &self, - _existing_config: &ConfigurationOptions, - ) -> Result { - let ui = self.ui(); - let output = ConfigurationOptions { - team_id: self.output_map.get("team_id").cloned(), - token: self.output_map.get("token").cloned(), - api_url: None, - ui, - ..Default::default() - }; - - Ok(output) - } -} - -fn truth_env_var(s: &str) -> Option { +pub fn truth_env_var(s: &str) -> Option { match s { "true" | "1" => Some(true), "false" | "0" => Some(false), @@ -251,7 +198,13 @@ fn truth_env_var(s: &str) -> Option { } fn map_environment<'a>( + // keys are environment variable names + // values are properties of ConfigurationOptions we want to store the + // values in mapping: HashMap<&str, &'a str>, + + // keys are environment variable names + // values are the values of those environment variables environment: &HashMap, ) -> Result, Error> { let mut output_map = HashMap::new(); @@ -324,7 +277,7 @@ mod test { let config = EnvVars::new(&env) .unwrap() - .get_configuration_options(&ConfigurationOptions::default()) + .get_configuration_options(None) .unwrap(); assert!(config.preflight()); assert!(config.force()); @@ -374,7 +327,7 @@ mod test { let config = EnvVars::new(&env) .unwrap() - .get_configuration_options(&ConfigurationOptions::default()) + .get_configuration_options(None) .unwrap(); assert_eq!(config.api_url(), DEFAULT_API_URL); assert_eq!(config.login_url(), DEFAULT_LOGIN_URL); @@ -395,30 +348,4 @@ mod test { assert!(!config.run_summary()); assert!(!config.allow_no_turbo_json()); } - - #[test] - fn test_override_env_setting() { - let mut env: HashMap = HashMap::new(); - - let vercel_artifacts_token = "correct-horse-battery-staple"; - let vercel_artifacts_owner = "bobby_tables"; - - env.insert( - "vercel_artifacts_token".into(), - vercel_artifacts_token.into(), - ); - env.insert( - "vercel_artifacts_owner".into(), - vercel_artifacts_owner.into(), - ); - env.insert("ci".into(), "1".into()); - - let config = OverrideEnvVars::new(&env) - .unwrap() - .get_configuration_options(&ConfigurationOptions::default()) - .unwrap(); - assert_eq!(vercel_artifacts_token, config.token.unwrap()); - assert_eq!(vercel_artifacts_owner, config.team_id.unwrap()); - assert_eq!(Some(UIMode::Stream), config.ui); - } } diff --git a/crates/turborepo-lib/src/config/file.rs b/crates/turborepo-lib/src/config/file.rs index d3f889bd293dc..e00bcd631c2be 100644 --- a/crates/turborepo-lib/src/config/file.rs +++ b/crates/turborepo-lib/src/config/file.rs @@ -23,7 +23,7 @@ impl ConfigFile { impl ResolvedConfigurationOptions for ConfigFile { fn get_configuration_options( &self, - _existing_config: &ConfigurationOptions, + _existing_config: Option<&ConfigurationOptions>, ) -> Result { let contents = self .path @@ -55,7 +55,7 @@ impl AuthFile { impl ResolvedConfigurationOptions for AuthFile { fn get_configuration_options( &self, - _existing_config: &ConfigurationOptions, + _existing_config: Option<&ConfigurationOptions>, ) -> Result { let token = match turborepo_auth::Token::from_file(&self.path) { Ok(token) => token, diff --git a/crates/turborepo-lib/src/config/mod.rs b/crates/turborepo-lib/src/config/mod.rs index c7ad713ef48ed..cfa22a63d0fc2 100644 --- a/crates/turborepo-lib/src/config/mod.rs +++ b/crates/turborepo-lib/src/config/mod.rs @@ -1,5 +1,6 @@ mod env; mod file; +mod override_env; mod turbo_json; use std::{collections::HashMap, ffi::OsString, io}; @@ -7,10 +8,11 @@ use std::{collections::HashMap, ffi::OsString, io}; use camino::{Utf8Path, Utf8PathBuf}; use convert_case::{Case, Casing}; use derive_setters::Setters; -use env::{EnvVars, OverrideEnvVars}; +use env::EnvVars; use file::{AuthFile, ConfigFile}; use merge::Merge; use miette::{Diagnostic, NamedSource, SourceSpan}; +use override_env::OverrideEnvVars; use serde::Deserialize; use struct_iterable::Iterable; use thiserror::Error; @@ -219,11 +221,14 @@ pub struct ConfigurationOptions { #[serde(alias = "teamslug")] #[serde(alias = "TeamSlug")] #[serde(alias = "TEAMSLUG")] + /// corresponds to env var TURBO_TEAM pub(crate) team_slug: Option, #[serde(alias = "teamid")] #[serde(alias = "TeamId")] #[serde(alias = "TEAMID")] + /// corresponds to env var TURBO_TEAMID pub(crate) team_id: Option, + /// corresponds to env var TURBO_TOKEN pub(crate) token: Option, pub(crate) signature: Option, pub(crate) preflight: Option, @@ -402,7 +407,7 @@ fn non_empty_str(s: Option<&str>) -> Option<&str> { trait ResolvedConfigurationOptions { fn get_configuration_options( &self, - existing_config: &ConfigurationOptions, + existing_config: Option<&ConfigurationOptions>, ) -> Result; } @@ -410,7 +415,7 @@ trait ResolvedConfigurationOptions { impl<'a> ResolvedConfigurationOptions for &'a ConfigurationOptions { fn get_configuration_options( &self, - _existing_config: &ConfigurationOptions, + _existing_config: Option<&ConfigurationOptions>, ) -> Result { Ok((*self).clone()) } @@ -467,9 +472,9 @@ impl TurborepoConfigBuilder { // These are ordered from highest to lowest priority let sources: [Box; 7] = [ - Box::new(override_env_var_config), Box::new(&self.override_config), Box::new(env_var_config), + Box::new(override_env_var_config), Box::new(local_config), Box::new(global_auth), Box::new(global_config), @@ -479,7 +484,7 @@ impl TurborepoConfigBuilder { let config = sources.into_iter().try_fold( ConfigurationOptions::default(), |mut acc, current_source| { - let current_source_config = current_source.get_configuration_options(&acc)?; + let current_source_config = current_source.get_configuration_options(Some(&acc))?; acc.merge(current_source_config); Ok(acc) }, @@ -561,22 +566,16 @@ mod test { vercel_artifacts_owner.into(), ); - let override_config = ConfigurationOptions { - token: Some("unseen".into()), - team_id: Some("unseen".into()), - ..Default::default() - }; - let builder = TurborepoConfigBuilder { repo_root, - override_config, + override_config: Default::default(), global_config_path: Some(global_config_path), environment: Some(env), }; let config = builder.build().unwrap(); - assert_eq!(config.team_id().unwrap(), vercel_artifacts_owner); - assert_eq!(config.token().unwrap(), vercel_artifacts_token); + assert_eq!(config.team_id().unwrap(), turbo_teamid); + assert_eq!(config.token().unwrap(), turbo_token); assert_eq!(config.spaces_id().unwrap(), "my-spaces-id"); } diff --git a/crates/turborepo-lib/src/config/override_env.rs b/crates/turborepo-lib/src/config/override_env.rs new file mode 100644 index 0000000000000..fe0ab0e3ccdbb --- /dev/null +++ b/crates/turborepo-lib/src/config/override_env.rs @@ -0,0 +1,493 @@ +use std::{ + collections::HashMap, + ffi::{OsStr, OsString}, +}; + +use super::{env::truth_env_var, ConfigurationOptions, Error, ResolvedConfigurationOptions}; +use crate::turbo_json::UIMode; + +/* +Hi! If you're new here: +1. The general pattern is that: + - ConfigurationOptions.token corresponds to TURBO_TOKEN or VERCEL_ARTIFACTS_TOKEN + - ConfigurationOptions.team_id corresponds to TURBO_TEAMID or VERCEL_ARTIFACTS_OWNER + - ConfigurationOptions.team_slug corresponds to TURBO_TEAM +1. We're ultimately poking around the env vars looking for _paris_ that make sense. + Since we presume that users are the only ones sending TURBO_* and Vercel is the only one sending VERCEL_*, we can make some assumptions. Namely, we assume that if we have one of VERCEL_ARTIFACTS_OWNER or VERCEL_ARTIFACTS_TOKEN we will always have both. +1. Watch out for mixing up `TURBO_TEAM` and `TURBO_TEAMID`. Same for ConfigurationOptions.team_id and ConfigurationOptions.team_slug. +*/ + +/// these correspond directly to the environment variables that this module +/// needs to do it's work +#[allow(non_snake_case)] +struct Input { + TURBO_TEAM: Option, + TURBO_TEAMID: Option, + TURBO_TOKEN: Option, + VERCEL_ARTIFACTS_OWNER: Option, + VERCEL_ARTIFACTS_TOKEN: Option, +} + +impl Input { + fn new() -> Self { + Self { + TURBO_TEAM: None, + TURBO_TEAMID: None, + TURBO_TOKEN: None, + VERCEL_ARTIFACTS_OWNER: None, + VERCEL_ARTIFACTS_TOKEN: None, + } + } +} + +impl<'a> From<&'a HashMap> for Input { + fn from(environment: &'a HashMap) -> Self { + Self { + TURBO_TEAM: environment + .get(OsStr::new("turbo_team")) + .map(|s| s.to_str().unwrap().to_string()), + TURBO_TEAMID: environment + .get(OsStr::new("turbo_teamid")) + .map(|s| s.to_str().unwrap().to_string()), + TURBO_TOKEN: environment + .get(OsStr::new("turbo_token")) + .map(|s| s.to_str().unwrap().to_string()), + VERCEL_ARTIFACTS_OWNER: environment + .get(OsStr::new("vercel_artifacts_owner")) + .map(|s| s.to_str().unwrap().to_string()), + VERCEL_ARTIFACTS_TOKEN: environment + .get(OsStr::new("vercel_artifacts_token")) + .map(|s| s.to_str().unwrap().to_string()), + } + } +} + +// this is an internal structure (that's a partial of ConfigurationOptions) that +// we use to store +pub struct Output { + /// maps to ConfigurationOptions.team_id + pub team_id: Option, + // maps to ConfigurationOptions.team_slug + pub team_slug: Option, + // maps to ConfigurationOptions.token + pub token: Option, +} + +impl Output { + fn new() -> Self { + Self { + team_id: None, + team_slug: None, + token: None, + } + } +} + +// get Output from Input +impl From for Output { + fn from(input: Input) -> Self { + let mut output = Output::new(); + + // TURBO_TEAMID+TURBO_TOKEN + if input.TURBO_TEAMID.is_some() && input.TURBO_TOKEN.is_some() { + output.team_id = input.TURBO_TEAMID; + output.token = input.TURBO_TOKEN; + + if input.TURBO_TEAM.is_some() { + // there can also be a TURBO_TEAM, so we'll use that as well + output.team_slug = input.TURBO_TEAM; + } + + return output; + } + + // TURBO_TEAM+TURBO_TOKEN + if input.TURBO_TEAM.is_some() && input.TURBO_TOKEN.is_some() { + output.team_slug = input.TURBO_TEAM; + output.token = input.TURBO_TOKEN; + + if input.TURBO_TEAMID.is_some() { + // there can also be a TURBO_TEAMID, so we'll use that as well + output.team_id = input.TURBO_TEAMID; + } + + return output; + } + + // if there's both Vercel items, we use those next + if input.VERCEL_ARTIFACTS_OWNER.is_some() && input.VERCEL_ARTIFACTS_TOKEN.is_some() { + output.team_id = input.VERCEL_ARTIFACTS_OWNER; + output.token = input.VERCEL_ARTIFACTS_TOKEN; + return output; + } + + // from this point below, there's no token we can do anything with + // ------------------------------------------------ + + // if there's no token, this is also permissible + if input.TURBO_TEAMID.is_some() && input.TURBO_TEAM.is_some() { + output.team_id = input.TURBO_TEAMID; + output.team_slug = input.TURBO_TEAM; + return output; + } + + // handle "only" cases + // ------------------------------------------------ + if input.TURBO_TEAMID.is_some() { + output.team_id = input.TURBO_TEAMID; + return output; + } + + if input.TURBO_TEAM.is_some() { + output.team_slug = input.TURBO_TEAM; + + if input.VERCEL_ARTIFACTS_OWNER.is_some() { + output.team_id = input.VERCEL_ARTIFACTS_OWNER; + } + + return output; + } + + if input.VERCEL_ARTIFACTS_OWNER.is_some() { + output.team_id = input.VERCEL_ARTIFACTS_OWNER; + return output; + } + + Output::new() + } +} + +pub struct OverrideEnvVars<'a> { + environment: &'a HashMap, + output: Output, +} + +impl<'a> OverrideEnvVars<'a> { + pub fn new(environment: &'a HashMap) -> Result { + let input = Input::from(environment); + let output = Output::from(input); + + Ok(Self { + environment, + output, + }) + } + + fn ui(&self) -> Option { + // TODO double check on what's going on here + let value = self + .environment + .get(OsStr::new("ci")) + .or_else(|| self.environment.get(OsStr::new("no_color")))?; + match truth_env_var(value.to_str()?)? { + true => Some(UIMode::Stream), + false => None, + } + } +} + +impl<'a> ResolvedConfigurationOptions for OverrideEnvVars<'a> { + fn get_configuration_options( + &self, + _existing_config: Option<&ConfigurationOptions>, + ) -> Result { + let output = ConfigurationOptions { + team_id: self.output.team_id.clone(), + token: self.output.token.clone(), + team_slug: self.output.team_slug.clone(), + ui: self.ui(), + api_url: None, + ..Default::default() + }; + Ok(output) + } +} + +#[cfg(test)] +mod test { + use lazy_static::lazy_static; + + use super::*; + + lazy_static! { + static ref VERCEL_ARTIFACTS_OWNER: String = String::from("valueof:VERCEL_ARTIFACTS_OWNER"); + static ref VERCEL_ARTIFACTS_TOKEN: String = String::from("valueof:VERCEL_ARTIFACTS_TOKEN"); + static ref TURBO_TEAMID: String = String::from("valueof:TURBO_TEAMID"); + static ref TURBO_TEAM: String = String::from("valueof:TURBO_TEAM"); + static ref TURBO_TOKEN: String = String::from("valueof:TURBO_TOKEN"); + } + + struct TestCase { + input: Input, + output: Output, + reason: &'static str, + } + + impl TestCase { + fn new() -> Self { + Self { + input: Input::new(), + output: Output::new(), + reason: "missing", + } + } + + fn reason(mut self, reason: &'static str) -> Self { + self.reason = reason; + self + } + + #[allow(non_snake_case)] + fn VERCEL_ARTIFACTS_OWNER(mut self) -> Self { + self.input.VERCEL_ARTIFACTS_OWNER = Some(VERCEL_ARTIFACTS_OWNER.clone()); + self + } + + #[allow(non_snake_case)] + fn VERCEL_ARTIFACTS_TOKEN(mut self) -> Self { + self.input.VERCEL_ARTIFACTS_TOKEN = Some(VERCEL_ARTIFACTS_TOKEN.clone()); + self + } + + #[allow(non_snake_case)] + fn TURBO_TEAMID(mut self) -> Self { + self.input.TURBO_TEAMID = Some(TURBO_TEAMID.clone()); + self + } + + #[allow(non_snake_case)] + fn TURBO_TEAM(mut self) -> Self { + self.input.TURBO_TEAM = Some(TURBO_TEAM.clone()); + self + } + + #[allow(non_snake_case)] + fn TURBO_TOKEN(mut self) -> Self { + self.input.TURBO_TOKEN = Some(TURBO_TOKEN.clone()); + self + } + + fn team_id(mut self, value: String) -> Self { + self.output.team_id = Some(value); + self + } + + fn team_slug(mut self, value: String) -> Self { + self.output.team_slug = Some(value); + self + } + + fn token(mut self, value: String) -> Self { + self.output.token = Some(value); + self + } + } + + #[test] + fn test_all_the_combos() { + let cases: &[TestCase] = &[ + // + // Get nothing back + // ------------------------------ + TestCase::new().reason("no env vars set"), + TestCase::new() + .reason("just VERCEL_ARTIFACTS_TOKEN") + .VERCEL_ARTIFACTS_TOKEN(), + TestCase::new().reason("just TURBO_TOKEN").TURBO_TOKEN(), + // + // When 3rd Party Wins with all three + // ------------------------------ + TestCase::new() + .reason("we can use all of TURBO_TEAM, TURBO_TEAMID, and TURBO_TOKEN") + .TURBO_TEAM() + .TURBO_TEAMID() + .TURBO_TOKEN() + .team_id(TURBO_TEAMID.clone()) + .team_slug(TURBO_TEAM.clone()) + .token(TURBO_TOKEN.clone()), + TestCase::new() + .reason("if we have a 3rd party trifecta, that wins, even against a Vercel Pair") + .TURBO_TEAM() + .TURBO_TEAMID() + .TURBO_TOKEN() + .VERCEL_ARTIFACTS_OWNER() + .VERCEL_ARTIFACTS_TOKEN() + .team_id(TURBO_TEAMID.clone()) + .team_slug(TURBO_TEAM.clone()) + .token(TURBO_TOKEN.clone()), + TestCase::new() + .reason("a 3rd party trifecta wins against a partial Vercel (just artifacts token)") + .TURBO_TEAM() + .TURBO_TEAMID() + .TURBO_TOKEN() + .VERCEL_ARTIFACTS_TOKEN() + .team_id(TURBO_TEAMID.clone()) + .team_slug(TURBO_TEAM.clone()) + .token(TURBO_TOKEN.clone()), + TestCase::new() + .reason("a 3rd party trifecta wins against a partial Vercel (just artifacts owner)") + .TURBO_TEAM() + .TURBO_TEAMID() + .TURBO_TOKEN() + .VERCEL_ARTIFACTS_OWNER() + .team_id(TURBO_TEAMID.clone()) + .team_slug(TURBO_TEAM.clone()) + .token(TURBO_TOKEN.clone()), + // + // When 3rd Party Wins with team_slug + // ------------------------------ + TestCase::new() + .reason("golden path for 3rd party, not deployed on Vercel") + .TURBO_TEAM() + .TURBO_TOKEN() + .team_slug(TURBO_TEAM.clone()) + .token(TURBO_TOKEN.clone()), + TestCase::new() + .reason( + "a TURBO_TEAM+TURBO_TOKEN pair wins against an incomplete Vercel (just \ + artifacts token)", + ) + .TURBO_TEAM() + .TURBO_TOKEN() + .VERCEL_ARTIFACTS_TOKEN() // disregarded + .team_slug(TURBO_TEAM.clone()) + .token(TURBO_TOKEN.clone()), + TestCase::new() + .reason("golden path for 3rd party, deployed on Vercel") + .TURBO_TEAM() + .TURBO_TOKEN() + .VERCEL_ARTIFACTS_OWNER() // normally this would map to team_id, but not with a complete 3rd party pair + .VERCEL_ARTIFACTS_TOKEN() + .team_slug(TURBO_TEAM.clone()) + .token(TURBO_TOKEN.clone()), + // + // When 3rd Party Wins with team_id + // ------------------------------ + TestCase::new() + .reason("if they pass a TURBO_TEAMID and a TURBO_TOKEN, we use them") + .TURBO_TEAMID() + .TURBO_TOKEN() + .team_id(TURBO_TEAMID.clone()) + .token(TURBO_TOKEN.clone()), + TestCase::new() + .reason("a TURBO_TEAMID+TURBO_TOKEN pair will also win against a Vercel pair") + .TURBO_TEAMID() + .TURBO_TOKEN() + .VERCEL_ARTIFACTS_OWNER() + .VERCEL_ARTIFACTS_TOKEN() + .team_id(TURBO_TEAMID.clone()) + .token(TURBO_TOKEN.clone()), + TestCase::new() + .reason( + "a TURBO_TEAMID+TURBO_TOKEN pair wins against an incomplete Vercel (just \ + artifacts token)", + ) + .TURBO_TEAMID() + .TURBO_TOKEN() + .VERCEL_ARTIFACTS_TOKEN() + .team_id(TURBO_TEAMID.clone()) + .token(TURBO_TOKEN.clone()), + // + // When Vercel Wins + // ------------------------------ + TestCase::new() + .reason("golden path on Vercel zero config") + .VERCEL_ARTIFACTS_OWNER() + .VERCEL_ARTIFACTS_TOKEN() + .team_id(VERCEL_ARTIFACTS_OWNER.clone()) + .token(VERCEL_ARTIFACTS_TOKEN.clone()), + TestCase::new() + .reason("Vercel wins: disregard just TURBO_TOKEN") + .TURBO_TOKEN() + .VERCEL_ARTIFACTS_OWNER() + .VERCEL_ARTIFACTS_TOKEN() + .team_id(VERCEL_ARTIFACTS_OWNER.clone()) + .token(VERCEL_ARTIFACTS_TOKEN.clone()), + TestCase::new() + .reason("Vercel wins: disregard just TURBO_TEAM") + .TURBO_TEAM() + .VERCEL_ARTIFACTS_OWNER() + .VERCEL_ARTIFACTS_TOKEN() + .team_id(VERCEL_ARTIFACTS_OWNER.clone()) + .token(VERCEL_ARTIFACTS_TOKEN.clone()), + TestCase::new() + .reason("Vercel wins: disregard just TURBO_TEAMID") + .TURBO_TEAMID() + .VERCEL_ARTIFACTS_OWNER() + .VERCEL_ARTIFACTS_TOKEN() + .team_id(VERCEL_ARTIFACTS_OWNER.clone()) + .token(VERCEL_ARTIFACTS_TOKEN.clone()), + TestCase::new() + .reason("Vercel wins if TURBO_TOKEN is missing") + .TURBO_TEAM() + .TURBO_TEAMID() + .VERCEL_ARTIFACTS_OWNER() + .VERCEL_ARTIFACTS_TOKEN() + .team_id(VERCEL_ARTIFACTS_OWNER.clone()) + .token(VERCEL_ARTIFACTS_TOKEN.clone()), + // + // Just get a team_id + // ------------------------------ + TestCase::new() + .reason("just VERCEL_ARTIFACTS_OWNER") + .VERCEL_ARTIFACTS_OWNER() + .team_id(VERCEL_ARTIFACTS_OWNER.clone()), + TestCase::new() + .reason("just TURBO_TEAMID") + .TURBO_TEAMID() + .team_id(TURBO_TEAMID.clone()), + // + // Just get a team_slug + // ------------------------------ + TestCase::new() + .reason("just TURBO_TEAM") + .TURBO_TEAM() + .team_slug(TURBO_TEAM.clone()), + // + // just team_slug and team_id + // ------------------------------ + TestCase::new() + .reason("if we just have TURBO_TEAM+TURBO_TEAMID, that's ok") + .TURBO_TEAM() + .TURBO_TEAMID() + .team_slug(TURBO_TEAM.clone()) + .team_id(TURBO_TEAMID.clone()), + // + // just set team_id and team_slug + // ------------------------------ + TestCase::new() + .reason("if we just have a TURBO_TEAM and TURBO_TEAMID we can use them both") + .TURBO_TEAM() + .TURBO_TEAMID() + .team_id(TURBO_TEAMID.clone()) + .team_slug(TURBO_TEAM.clone()), + ]; + + for case in cases { + let mut env: HashMap = HashMap::new(); + + if let Some(value) = &case.input.TURBO_TEAM { + env.insert("turbo_team".into(), value.into()); + } + if let Some(value) = &case.input.TURBO_TEAMID { + env.insert("turbo_teamid".into(), value.into()); + } + if let Some(value) = &case.input.TURBO_TOKEN { + env.insert("turbo_token".into(), value.into()); + } + if let Some(value) = &case.input.VERCEL_ARTIFACTS_OWNER { + env.insert("vercel_artifacts_owner".into(), value.into()); + } + if let Some(value) = &case.input.VERCEL_ARTIFACTS_TOKEN { + env.insert("vercel_artifacts_token".into(), value.into()); + } + + let config = OverrideEnvVars::new(&env).unwrap(); + let reason = case.reason; + + assert_eq!(case.output.team_id, config.output.team_id, "{reason}"); + assert_eq!(case.output.team_slug, config.output.team_slug, "{reason}"); + assert_eq!(case.output.token, config.output.token, "{reason}"); + } + } +} diff --git a/crates/turborepo-lib/src/config/turbo_json.rs b/crates/turborepo-lib/src/config/turbo_json.rs index e4cf24c1182cd..e903a9cb14b4c 100644 --- a/crates/turborepo-lib/src/config/turbo_json.rs +++ b/crates/turborepo-lib/src/config/turbo_json.rs @@ -17,9 +17,11 @@ impl<'a> TurboJsonReader<'a> { impl<'a> ResolvedConfigurationOptions for TurboJsonReader<'a> { fn get_configuration_options( &self, - existing_config: &ConfigurationOptions, + existing_config: Option<&ConfigurationOptions>, ) -> Result { - let turbo_json_path = existing_config.root_turbo_json_path(self.repo_root); + let turbo_json_path = existing_config + .unwrap_or(&ConfigurationOptions::default()) + .root_turbo_json_path(self.repo_root); let turbo_json = RawTurboJson::read(self.repo_root, &turbo_json_path).or_else(|e| { if let Error::Io(e) = &e { if matches!(e.kind(), std::io::ErrorKind::NotFound) { @@ -89,7 +91,9 @@ mod test { .unwrap(); let reader = TurboJsonReader::new(repo_root); - let config = reader.get_configuration_options(&existing_config).unwrap(); + let config = reader + .get_configuration_options(Some(&existing_config)) + .unwrap(); // Make sure we read the default turbo.json assert_eq!(config.daemon(), Some(false)); } @@ -119,7 +123,9 @@ mod test { .unwrap(); let reader = TurboJsonReader::new(repo_root); - let config = reader.get_configuration_options(&existing_config).unwrap(); + let config = reader + .get_configuration_options(Some(&existing_config)) + .unwrap(); // Make sure we read the correct turbo.json assert_eq!(config.daemon(), Some(false)); }