Skip to content

Commit

Permalink
feat(turbo): add platform env support
Browse files Browse the repository at this point in the history
  • Loading branch information
tknickman committed Oct 11, 2024
1 parent 961ce00 commit 171d7b0
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 2 deletions.
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
178 changes: 178 additions & 0 deletions crates/turborepo-env/src/platform.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use turborepo_ci::Vendor;
use turborepo_ui::{cprint, cprintln, ColorConfig, BOLD, GREY, YELLOW};

use crate::EnvironmentVariableMap;

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

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 {

Check failure on line 14 in crates/turborepo-env/src/platform.rs

View workflow job for this annotation

GitHub Actions / Rust lints

you should consider adding a `Default` implementation for `PlatformEnv`
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

0 comments on commit 171d7b0

Please sign in to comment.