Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(turbo): add platform env support #9122

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/turborepo-env/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ regex = { workspace = true }
serde = { workspace = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
turborepo-ci = { workspace = true }
turborepo-ui = { workspace = true }

[dev-dependencies]
test-case = { workspace = true }
2 changes: 2 additions & 0 deletions crates/turborepo-env/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use serde::Serialize;
use sha2::{Digest, Sha256};
use thiserror::Error;

pub mod platform;

const DEFAULT_ENV_VARS: [&str; 1] = ["VERCEL_ANALYTICS_ID"];

#[derive(Clone, Debug, Error)]
Expand Down
184 changes: 184 additions & 0 deletions crates/turborepo-env/src/platform.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
use turborepo_ci::Vendor;
use turborepo_ui::{cprint, cprintln, ColorConfig, BOLD, GREY, YELLOW};

use crate::EnvironmentVariableMap;

pub struct PlatformEnv {
env_keys: Vec<String>,
}

impl Default for PlatformEnv {
fn default() -> Self {
Self::new()
}
}

const TURBO_PLATFORM_ENV_KEY: &str = "TURBO_PLATFORM_ENV";
const TURBO_PLATFORM_ENV_DISABLED_KEY: &str = "TURBO_PLATFORM_ENV_DISABLED";

impl PlatformEnv {
pub fn new() -> Self {
let env_keys = std::env::var(TURBO_PLATFORM_ENV_KEY)
.unwrap_or_default()
.split(',')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();

Self { env_keys }
}

pub fn disabled() -> bool {
let turbo_platform_env_disabled =
std::env::var(TURBO_PLATFORM_ENV_DISABLED_KEY).unwrap_or_default();
turbo_platform_env_disabled == "1" || turbo_platform_env_disabled == "true"
}

pub fn validate(&self, execution_env: &EnvironmentVariableMap) -> Vec<String> {
if Self::disabled() {
return vec![];
}

self.diff(execution_env)
}

pub fn diff(&self, execution_env: &EnvironmentVariableMap) -> Vec<String> {
self.env_keys
.iter()
.filter(|key| !execution_env.contains_key(*key))
.map(|s| s.to_string())
.collect()
}

pub fn output_header(is_strict: bool, color_config: ColorConfig) {
let ci = Vendor::get_constant().unwrap_or("unknown");

let strict_message = if is_strict {
"These variables WILL NOT be available to your application and may cause your build to \
fail."
} else {
"These variables WILL NOT be considered in your cache key and could cause inadvertent \
cache hits."
};

match ci {
"VERCEL" => {
cprintln!(
color_config,
BOLD,
"The following environment variables are set on your Vercel project, but \
missing from \"turbo.json\". {}",
strict_message
);
}
_ => {
cprintln!(
color_config,
BOLD,
"The following environment variables are missing from \"turbo.json\". {}",
strict_message
);
}
}
}

pub fn output_for_task(
missing: Vec<String>,
task_id_for_display: &str,
color_config: ColorConfig,
) {
cprintln!(color_config, YELLOW, "{}", task_id_for_display);
for key in missing {
cprint!(color_config, GREY, " - ");
cprint!(color_config, GREY, "{}\n", key);
}
println!();
}
}

#[cfg(test)]
mod tests {
use std::collections::HashMap;

use super::*;

fn set_env_var(key: &str, value: &str) {
std::env::set_var(key, value);
}

fn clear_env_var(key: &str) {
std::env::remove_var(key);
}

#[test]
fn test_platform_env_new() {
set_env_var(TURBO_PLATFORM_ENV_KEY, "VAR1,VAR2,VAR3");
let platform_env = PlatformEnv::new();
assert_eq!(platform_env.env_keys, vec!["VAR1", "VAR2", "VAR3"]);
clear_env_var(TURBO_PLATFORM_ENV_KEY);
}

#[test]
fn test_platform_env_new_empty() {
set_env_var(TURBO_PLATFORM_ENV_KEY, "");
let platform_env = PlatformEnv::new();
assert!(platform_env.env_keys.is_empty());
clear_env_var(TURBO_PLATFORM_ENV_KEY);
}

#[test]
fn test_disabled_true() {
set_env_var(TURBO_PLATFORM_ENV_DISABLED_KEY, "1");
assert!(PlatformEnv::disabled());
clear_env_var(TURBO_PLATFORM_ENV_DISABLED_KEY);
}

#[test]
fn test_disabled_false() {
set_env_var(TURBO_PLATFORM_ENV_DISABLED_KEY, "0");
assert!(!PlatformEnv::disabled());
clear_env_var(TURBO_PLATFORM_ENV_DISABLED_KEY);
}

#[test]
fn test_validate_disabled() {
set_env_var(TURBO_PLATFORM_ENV_DISABLED_KEY, "1");
let platform_env = PlatformEnv::new();
let execution_env = EnvironmentVariableMap(HashMap::new());
let missing = platform_env.validate(&execution_env);
assert!(missing.is_empty());
clear_env_var(TURBO_PLATFORM_ENV_DISABLED_KEY);
}

#[test]
fn test_validate_missing_keys() {
set_env_var(TURBO_PLATFORM_ENV_KEY, "VAR1,VAR2");
clear_env_var(TURBO_PLATFORM_ENV_DISABLED_KEY);

let platform_env = PlatformEnv::new();

let mut execution_env = EnvironmentVariableMap(HashMap::new());
execution_env.insert("VAR2".to_string(), "value".to_string());

let missing = platform_env.validate(&execution_env);

assert_eq!(missing, vec!["VAR1".to_string()]);

clear_env_var(TURBO_PLATFORM_ENV_KEY);
}

#[test]
fn test_diff_all_keys_present() {
set_env_var(TURBO_PLATFORM_ENV_KEY, "VAR1,VAR2");
let platform_env = PlatformEnv::new();

let mut execution_env = EnvironmentVariableMap(HashMap::new());
execution_env.insert("VAR1".to_string(), "value1".to_string());
execution_env.insert("VAR2".to_string(), "value2".to_string());

let missing = platform_env.diff(&execution_env);
assert!(missing.is_empty());

clear_env_var(TURBO_PLATFORM_ENV_KEY);
}
}
4 changes: 4 additions & 0 deletions crates/turborepo-lib/src/run/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ impl TaskCache {
self.task_output_logs
}

pub fn is_caching_disabled(&self) -> bool {
self.caching_disabled
}

/// Will read log file and write to output a line at a time
pub fn replay_log_file(&self, output: &mut impl CacheOutput) -> Result<(), Error> {
if self.log_file_path.exists() {
Expand Down
59 changes: 57 additions & 2 deletions crates/turborepo-lib/src/task_graph/visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ use itertools::Itertools;
use miette::{Diagnostic, NamedSource, SourceSpan};
use regex::Regex;
use tokio::sync::{mpsc, oneshot};
use tracing::{debug, error, Instrument, Span};
use tracing::{debug, error, warn, Instrument, Span};
use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf, AnchoredSystemPath};
use turborepo_ci::{Vendor, VendorBehavior};
use turborepo_env::EnvironmentVariableMap;
use turborepo_env::{platform::PlatformEnv, EnvironmentVariableMap};
use turborepo_repository::{
package_graph::{PackageGraph, PackageName, ROOT_PKG_NAME},
package_manager::PackageManager,
Expand Down Expand Up @@ -68,6 +68,7 @@ pub struct Visitor<'a> {
color_config: ColorConfig,
is_watch: bool,
ui_sender: Option<UISender>,
warnings: Arc<Mutex<Vec<TaskWarning>>>,
}

#[derive(Debug, thiserror::Error, Diagnostic)]
Expand Down Expand Up @@ -153,6 +154,7 @@ impl<'a> Visitor<'a> {
global_env,
ui_sender,
is_watch,
warnings: Default::default(),
}
}

Expand Down Expand Up @@ -293,6 +295,7 @@ impl<'a> Visitor<'a> {
} else {
TaskOutput::Direct(self.output_client(&info, vendor_behavior))
};

let tracker = self.run_tracker.track_task(info.clone().into_owned());
let spaces_client = self.run_tracker.spaces_task_client();
let parent_span = Span::current();
Expand Down Expand Up @@ -381,6 +384,35 @@ impl<'a> Visitor<'a> {

let global_hash_summary = GlobalHashSummary::try_from(global_hash_inputs)?;

// output any warnings that we collected while running tasks
if let Ok(warnings) = self.warnings.lock() {
if !warnings.is_empty() {
println!();
warn!("finished with warnings");
println!();

let has_missing_platform_env: bool = warnings
.iter()
.any(|warning| !warning.missing_platform_env.is_empty());
if has_missing_platform_env {
PlatformEnv::output_header(
global_env_mode == EnvMode::Strict,
self.color_config,
);

for warning in warnings.iter() {
if !warning.missing_platform_env.is_empty() {
PlatformEnv::output_for_task(
warning.missing_platform_env.clone(),
&warning.task_id,
self.color_config,
)
}
}
}
}
}

Ok(self
.run_tracker
.finish(
Expand Down Expand Up @@ -564,6 +596,13 @@ fn turbo_regex() -> &'static Regex {
RE.get_or_init(|| Regex::new(r"(?:^|\s)turbo(?:$|\s)").unwrap())
}

// Warning that comes from the execution of the task
#[derive(Debug, Clone)]
pub struct TaskWarning {
task_id: String,
missing_platform_env: Vec<String>,
}

// Error that comes from the execution of the task
#[derive(Debug, thiserror::Error, Clone)]
#[error("{task_id}: {cause}")]
Expand Down Expand Up @@ -691,8 +730,10 @@ impl<'a> ExecContextFactory<'a> {
continue_on_error: self.visitor.run_opts.continue_on_error,
pass_through_args,
errors: self.errors.clone(),
warnings: self.visitor.warnings.clone(),
takes_input,
task_access,
platform_env: PlatformEnv::new(),
}
}

Expand Down Expand Up @@ -727,8 +768,10 @@ struct ExecContext {
continue_on_error: bool,
pass_through_args: Option<Vec<String>>,
errors: Arc<Mutex<Vec<TaskError>>>,
warnings: Arc<Mutex<Vec<TaskWarning>>>,
takes_input: bool,
task_access: TaskAccess,
platform_env: PlatformEnv,
}

enum ExecOutcome {
Expand Down Expand Up @@ -881,6 +924,18 @@ impl ExecContext {
}
}

if !self.task_cache.is_caching_disabled() {
let missing_platform_env = self.platform_env.validate(&self.execution_env);
if !missing_platform_env.is_empty() {
let _ = self.warnings.lock().map(|mut warnings| {
warnings.push(TaskWarning {
task_id: self.task_id_for_display.clone(),
missing_platform_env,
});
});
}
}

match self
.task_cache
.restore_outputs(&mut prefixed_ui, telemetry)
Expand Down
Loading