diff --git a/Cargo.lock b/Cargo.lock index a6b5a9f8..dae60f3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1780,7 +1780,6 @@ dependencies = [ "serde", "serde_json", "serde_yaml_ng", - "temp-env", "tempfile", "tokio", "url", @@ -3108,7 +3107,6 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050" dependencies = [ - "futures", "parking_lot", ] diff --git a/crates/mergify-ci/Cargo.toml b/crates/mergify-ci/Cargo.toml index 4bcb2609..562bc9eb 100644 --- a/crates/mergify-ci/Cargo.toml +++ b/crates/mergify-ci/Cargo.toml @@ -30,7 +30,6 @@ url = { workspace = true } [dev-dependencies] mergify-test-support = { path = "../mergify-test-support" } tempfile = { workspace = true } -temp-env = { workspace = true, features = ["async_closure"] } tokio = { workspace = true } wiremock = { workspace = true } diff --git a/crates/mergify-ci/src/detector.rs b/crates/mergify-ci/src/detector.rs index b2e020c8..56001a1a 100644 --- a/crates/mergify-ci/src/detector.rs +++ b/crates/mergify-ci/src/detector.rs @@ -6,7 +6,7 @@ //! Rust commands are mirrored; the rest stays in Python until its //! command is ported. -use std::env; +use mergify_core::env; use mergify_core::CliError; use mergify_core::auth; @@ -38,16 +38,16 @@ impl CIProvider { #[must_use] pub fn get_ci_provider() -> Option { - if env::var("JENKINS_URL").is_ok_and(|v| !v.is_empty()) { + if env::var_non_empty("JENKINS_URL").is_some() { return Some(CIProvider::Jenkins); } - if env::var("GITHUB_ACTIONS").as_deref() == Ok("true") { + if env::var("GITHUB_ACTIONS").as_deref() == Some("true") { return Some(CIProvider::GithubActions); } - if env::var("CIRCLECI").as_deref() == Ok("true") { + if env::var("CIRCLECI").as_deref() == Some("true") { return Some(CIProvider::CircleCi); } - if env::var("BUILDKITE").as_deref() == Ok("true") { + if env::var("BUILDKITE").as_deref() == Some("true") { return Some(CIProvider::Buildkite); } None @@ -58,7 +58,7 @@ pub fn get_ci_provider() -> Option { /// repository URL into ``owner/repo``. Returns ``None`` when the var /// is unset or the value doesn't parse. fn get_github_repository_from_env(env_name: &str) -> Option { - let raw = env::var(env_name).ok()?; + let raw = env::var(env_name)?; parse_repository_url(&raw) } @@ -151,7 +151,7 @@ pub fn split_owner_repo(value: &str) -> Result<(&str, &str), CliError> { #[must_use] pub fn get_github_repository() -> Option { match get_ci_provider()? { - CIProvider::GithubActions => env::var("GITHUB_REPOSITORY").ok().filter(|s| !s.is_empty()), + CIProvider::GithubActions => env::var_non_empty("GITHUB_REPOSITORY"), CIProvider::CircleCi => get_github_repository_from_env("CIRCLE_REPOSITORY_URL"), CIProvider::Jenkins => get_github_repository_from_env("GIT_URL"), CIProvider::Buildkite => get_github_repository_from_env("BUILDKITE_REPO"), @@ -199,9 +199,13 @@ pub fn get_github_pull_request_number() -> Result, CliError> { match get_ci_provider() { Some(CIProvider::GithubActions) => read_github_event_pull_request_number(), Some(CIProvider::Buildkite) => match env::var("BUILDKITE_PULL_REQUEST") { - Ok(pr) if !pr.is_empty() && pr != "false" => pr.parse::().map(Some).map_err(|e| { - CliError::Configuration(format!("BUILDKITE_PULL_REQUEST is not an integer: {e}")) - }), + Some(pr) if !pr.is_empty() && pr != "false" => { + pr.parse::().map(Some).map_err(|e| { + CliError::Configuration(format!( + "BUILDKITE_PULL_REQUEST is not an integer: {e}" + )) + }) + } _ => Ok(None), }, _ => Ok(None), @@ -216,7 +220,7 @@ fn read_github_event_pull_request_number() -> Result, CliError> { // [`read_github_event_pull_request_head_sha`]) stays lenient // because every one of its callers has somewhere to go without // an answer. - let Some(event_path) = env::var("GITHUB_EVENT_PATH").ok().filter(|s| !s.is_empty()) else { + let Some(event_path) = env::var_non_empty("GITHUB_EVENT_PATH") else { return Ok(None); }; let content = match std::fs::read_to_string(&event_path) { @@ -276,8 +280,8 @@ pub fn get_github_pull_request_head_sha() -> Option { // Buildkite and CircleCI both build a pull request from its head // commit, so their revision var *is* that head (`git_refs` reads // `BUILDKITE_COMMIT` for the same purpose). - CIProvider::Buildkite => non_empty_env("BUILDKITE_COMMIT"), - CIProvider::CircleCi => non_empty_env("CIRCLE_SHA1"), + CIProvider::Buildkite => env::var_non_empty("BUILDKITE_COMMIT"), + CIProvider::CircleCi => env::var_non_empty("CIRCLE_SHA1"), CIProvider::Jenkins => None, }?; is_sha1_object_name(&sha).then_some(sha) @@ -310,19 +314,18 @@ pub fn get_pipeline_name() -> Option { CIProvider::Buildkite => "BUILDKITE_PIPELINE_SLUG", CIProvider::CircleCi => return None, }; - non_empty_env(var) + env::var_non_empty(var) } /// `cicd.pipeline.task.name` — the job within a pipeline. #[must_use] pub fn get_job_name() -> Option { match get_ci_provider()? { - CIProvider::GithubActions => non_empty_env("GITHUB_JOB"), - CIProvider::CircleCi => non_empty_env("CIRCLE_JOB"), - CIProvider::Jenkins => non_empty_env("JOB_NAME"), - CIProvider::Buildkite => { - non_empty_env("BUILDKITE_LABEL").or_else(|| non_empty_env("BUILDKITE_STEP_KEY")) - } + CIProvider::GithubActions => env::var_non_empty("GITHUB_JOB"), + CIProvider::CircleCi => env::var_non_empty("CIRCLE_JOB"), + CIProvider::Jenkins => env::var_non_empty("JOB_NAME"), + CIProvider::Buildkite => env::var_non_empty("BUILDKITE_LABEL") + .or_else(|| env::var_non_empty("BUILDKITE_STEP_KEY")), } } @@ -334,10 +337,10 @@ pub fn get_head_ref_name() -> Option { // GitHub Actions sets `GITHUB_HEAD_REF` only on PR // events. Fall back to `GITHUB_REF_NAME` everywhere // else (the bare branch name, not `/merge`). - non_empty_env("GITHUB_HEAD_REF").or_else(|| non_empty_env("GITHUB_REF_NAME")) + env::var_non_empty("GITHUB_HEAD_REF").or_else(|| env::var_non_empty("GITHUB_REF_NAME")) } - CIProvider::CircleCi => non_empty_env("CIRCLE_BRANCH"), - CIProvider::Jenkins => non_empty_env("GIT_BRANCH").map(|raw| { + CIProvider::CircleCi => env::var_non_empty("CIRCLE_BRANCH"), + CIProvider::Jenkins => env::var_non_empty("GIT_BRANCH").map(|raw| { // Jenkins' Git plugin sets `GIT_BRANCH` to // `/` (or `refs/heads/` when // the job's configured for a refspec). Strip the @@ -350,7 +353,7 @@ pub fn get_head_ref_name() -> Option { } raw }), - CIProvider::Buildkite => non_empty_env("BUILDKITE_BRANCH"), + CIProvider::Buildkite => env::var_non_empty("BUILDKITE_BRANCH"), } } @@ -358,9 +361,9 @@ pub fn get_head_ref_name() -> Option { #[must_use] pub fn get_base_ref_name() -> Option { match get_ci_provider()? { - CIProvider::GithubActions => non_empty_env("GITHUB_BASE_REF"), - CIProvider::Jenkins => non_empty_env("CHANGE_TARGET"), - CIProvider::Buildkite => non_empty_env("BUILDKITE_PULL_REQUEST_BASE_BRANCH"), + CIProvider::GithubActions => env::var_non_empty("GITHUB_BASE_REF"), + CIProvider::Jenkins => env::var_non_empty("CHANGE_TARGET"), + CIProvider::Buildkite => env::var_non_empty("BUILDKITE_PULL_REQUEST_BASE_BRANCH"), CIProvider::CircleCi => None, } } @@ -369,9 +372,9 @@ pub fn get_base_ref_name() -> Option { #[must_use] pub fn get_cicd_pipeline_runner_name() -> Option { match get_ci_provider()? { - CIProvider::GithubActions => non_empty_env("RUNNER_NAME"), - CIProvider::Jenkins => non_empty_env("NODE_NAME"), - CIProvider::Buildkite => non_empty_env("BUILDKITE_AGENT_NAME"), + CIProvider::GithubActions => env::var_non_empty("RUNNER_NAME"), + CIProvider::Jenkins => env::var_non_empty("NODE_NAME"), + CIProvider::Buildkite => env::var_non_empty("BUILDKITE_AGENT_NAME"), CIProvider::CircleCi => None, } } @@ -382,10 +385,10 @@ pub fn get_cicd_pipeline_runner_name() -> Option { #[must_use] pub fn get_cicd_pipeline_run_id() -> Option { match get_ci_provider()? { - CIProvider::GithubActions => non_empty_env("GITHUB_RUN_ID"), - CIProvider::CircleCi => non_empty_env("CIRCLE_WORKFLOW_ID"), - CIProvider::Jenkins => non_empty_env("BUILD_ID"), - CIProvider::Buildkite => non_empty_env("BUILDKITE_BUILD_ID"), + CIProvider::GithubActions => env::var_non_empty("GITHUB_RUN_ID"), + CIProvider::CircleCi => env::var_non_empty("CIRCLE_WORKFLOW_ID"), + CIProvider::Jenkins => env::var_non_empty("BUILD_ID"), + CIProvider::Buildkite => env::var_non_empty("BUILDKITE_BUILD_ID"), } } @@ -393,11 +396,11 @@ pub fn get_cicd_pipeline_run_id() -> Option { #[must_use] pub fn get_cicd_pipeline_run_attempt() -> Option { match get_ci_provider()? { - CIProvider::GithubActions => non_empty_env("GITHUB_RUN_ATTEMPT")?.parse().ok(), - CIProvider::CircleCi => non_empty_env("CIRCLE_BUILD_NUM")?.parse().ok(), + CIProvider::GithubActions => env::var_non_empty("GITHUB_RUN_ATTEMPT")?.parse().ok(), + CIProvider::CircleCi => env::var_non_empty("CIRCLE_BUILD_NUM")?.parse().ok(), // Buildkite uses 0-indexed retries; add 1 so a fresh run // reads as attempt 1 (matching the GHA/CircleCI semantics). - CIProvider::Buildkite => non_empty_env("BUILDKITE_RETRY_COUNT")? + CIProvider::Buildkite => env::var_non_empty("BUILDKITE_RETRY_COUNT")? .parse::() .ok() .map(|n| n + 1), @@ -409,7 +412,7 @@ pub fn get_cicd_pipeline_run_attempt() -> Option { #[must_use] pub fn get_cicd_pipeline_run_url() -> Option { match get_ci_provider()? { - CIProvider::Buildkite => non_empty_env("BUILDKITE_BUILD_URL"), + CIProvider::Buildkite => env::var_non_empty("BUILDKITE_BUILD_URL"), _ => None, } } @@ -420,9 +423,9 @@ pub fn get_cicd_pipeline_run_url() -> Option { #[must_use] pub fn get_repository_url() -> Option { match get_ci_provider()? { - CIProvider::Buildkite => non_empty_env("BUILDKITE_REPO"), - CIProvider::CircleCi => non_empty_env("CIRCLE_REPOSITORY_URL"), - CIProvider::Jenkins => non_empty_env("GIT_URL"), + CIProvider::Buildkite => env::var_non_empty("BUILDKITE_REPO"), + CIProvider::CircleCi => env::var_non_empty("CIRCLE_REPOSITORY_URL"), + CIProvider::Jenkins => env::var_non_empty("GIT_URL"), CIProvider::GithubActions => None, } } @@ -444,19 +447,19 @@ pub fn get_repository_url() -> Option { pub fn get_head_sha() -> Option { match get_ci_provider()? { CIProvider::GithubActions => get_github_actions_head_sha(), - CIProvider::CircleCi => non_empty_env("CIRCLE_SHA1"), - CIProvider::Jenkins => non_empty_env("GIT_COMMIT"), - CIProvider::Buildkite => non_empty_env("BUILDKITE_COMMIT"), + CIProvider::CircleCi => env::var_non_empty("CIRCLE_SHA1"), + CIProvider::Jenkins => env::var_non_empty("GIT_COMMIT"), + CIProvider::Buildkite => env::var_non_empty("BUILDKITE_COMMIT"), } } fn get_github_actions_head_sha() -> Option { - if env::var("GITHUB_EVENT_NAME").as_deref() == Ok("pull_request") + if env::var("GITHUB_EVENT_NAME").as_deref() == Some("pull_request") && let Some(sha) = read_github_event_pull_request_head_sha() { return Some(sha); } - non_empty_env("GITHUB_SHA") + env::var_non_empty("GITHUB_SHA") } /// Read `GITHUB_EVENT_PATH` and pluck the @@ -475,7 +478,7 @@ fn read_github_event_pull_request_head_sha() -> Option { } fn read_github_event_json() -> Option { - let event_path = env::var("GITHUB_EVENT_PATH").ok()?; + let event_path = env::var("GITHUB_EVENT_PATH")?; if event_path.is_empty() { return None; } @@ -483,10 +486,6 @@ fn read_github_event_json() -> Option { serde_json::from_str(&content).ok() } -fn non_empty_env(name: &str) -> Option { - env::var(name).ok().filter(|s| !s.is_empty()) -} - /// Branch the quarantine API should look up tests for. Mirrors /// Python's `get_tests_target_branch`: the PR base branch when /// available, otherwise the head branch — i.e. "the branch the @@ -500,13 +499,12 @@ pub fn get_tests_target_branch() -> Option { #[cfg(test)] mod tests { use super::*; - use crate::testing::with_ci_env; use crate::testing::write_github_event; #[test] fn ci_provider_jenkins_takes_precedence() { - with_ci_env( - &[ + env::testing::with_vars( + [ ("JENKINS_URL", Some("http://jenkins")), ("GITHUB_ACTIONS", Some("true")), ("CIRCLECI", Some("true")), @@ -520,15 +518,15 @@ mod tests { #[test] fn ci_provider_returns_none_when_unset() { - with_ci_env(&[], || { + env::testing::with_no_vars(|| { assert_eq!(get_ci_provider(), None); }); } #[test] fn github_repository_github_actions() { - with_ci_env( - &[ + env::testing::with_vars( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_REPOSITORY", Some("owner/repo")), ], @@ -540,8 +538,8 @@ mod tests { #[test] fn github_repository_buildkite_ssh() { - with_ci_env( - &[ + env::testing::with_vars( + [ ("BUILDKITE", Some("true")), ("BUILDKITE_REPO", Some("git@github.com:owner/repo.git")), ], @@ -553,8 +551,8 @@ mod tests { #[test] fn github_repository_buildkite_https() { - with_ci_env( - &[ + env::testing::with_vars( + [ ("BUILDKITE", Some("true")), ("BUILDKITE_REPO", Some("https://github.com/owner/repo")), ], @@ -566,8 +564,8 @@ mod tests { #[test] fn github_repository_circleci() { - with_ci_env( - &[ + env::testing::with_vars( + [ ("CIRCLECI", Some("true")), ( "CIRCLE_REPOSITORY_URL", @@ -582,8 +580,8 @@ mod tests { #[test] fn github_repository_jenkins() { - with_ci_env( - &[ + env::testing::with_vars( + [ ("JENKINS_URL", Some("http://jenkins")), ("GIT_URL", Some("https://github.com/owner/repo.git")), ], @@ -595,15 +593,15 @@ mod tests { #[test] fn github_repository_returns_none_with_no_provider() { - with_ci_env(&[("GITHUB_REPOSITORY", Some("owner/repo"))], || { + env::testing::with_vars([("GITHUB_REPOSITORY", Some("owner/repo"))], || { assert_eq!(get_github_repository(), None); }); } #[test] fn resolve_repository_prefers_flag_over_env() { - with_ci_env( - &[ + env::testing::with_vars( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_REPOSITORY", Some("env/env")), ], @@ -619,8 +617,8 @@ mod tests { // used — even though this test runs inside a git checkout whose // `origin` would otherwise resolve to a different slug. Asserts // both the CI fallback and its precedence over the git remote. - with_ci_env( - &[ + env::testing::with_vars( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_REPOSITORY", Some("owner/repo")), ], @@ -636,7 +634,7 @@ mod tests { // git-remote fallback (`parse_slug`) accepts multi-segment // paths that would inject extra request-path segments; an // explicit value exercises the same guard deterministically. - with_ci_env(&[], || { + env::testing::with_no_vars(|| { assert!(matches!( resolve_repository(Some("owner/repo/extra")), Err(CliError::Configuration(_)) @@ -652,8 +650,8 @@ mod tests { #[test] fn pull_request_buildkite_reads_env() { - with_ci_env( - &[ + env::testing::with_vars( + [ ("BUILDKITE", Some("true")), ("BUILDKITE_PULL_REQUEST", Some("42")), ], @@ -665,8 +663,8 @@ mod tests { #[test] fn pull_request_buildkite_returns_none_when_false() { - with_ci_env( - &[ + env::testing::with_vars( + [ ("BUILDKITE", Some("true")), ("BUILDKITE_PULL_REQUEST", Some("false")), ], @@ -678,14 +676,14 @@ mod tests { #[test] fn pull_request_buildkite_returns_none_when_unset() { - with_ci_env(&[("BUILDKITE", Some("true"))], || { + env::testing::with_vars([("BUILDKITE", Some("true"))], || { assert_eq!(get_github_pull_request_number().unwrap(), None); }); } #[test] fn pull_request_returns_none_with_no_provider() { - with_ci_env(&[], || { + env::testing::with_no_vars(|| { assert_eq!(get_github_pull_request_number().unwrap(), None); }); } @@ -699,8 +697,8 @@ mod tests { serde_json::json!({ "pull_request": { "number": 123 } }).to_string(), ) .unwrap(); - with_ci_env( - &[ + env::testing::with_vars( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_EVENT_PATH", Some(event_path.to_str().unwrap())), ], @@ -714,8 +712,8 @@ mod tests { fn pull_request_github_actions_missing_event_file_returns_none() { let tmp = tempfile::tempdir().unwrap(); let missing = tmp.path().join("nope.json"); - with_ci_env( - &[ + env::testing::with_vars( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_EVENT_PATH", Some(missing.to_str().unwrap())), ], @@ -764,8 +762,8 @@ mod tests { ); for event_name in ["pull_request", "pull_request_target"] { - with_ci_env( - &[ + env::testing::with_vars( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_EVENT_NAME", Some(event_name)), ("GITHUB_EVENT_PATH", Some(event_path.to_str().unwrap())), @@ -793,8 +791,8 @@ mod tests { // not. let tmp = tempfile::tempdir().unwrap(); let event_path = write_github_event(tmp.path(), &serde_json::json!({})); - with_ci_env( - &[ + env::testing::with_vars( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_EVENT_NAME", Some("push")), ("GITHUB_EVENT_PATH", Some(event_path.to_str().unwrap())), @@ -811,8 +809,8 @@ mod tests { #[test] fn pull_request_head_sha_uses_buildkite_commit() { - with_ci_env( - &[ + env::testing::with_vars( + [ ("BUILDKITE", Some("true")), ( "BUILDKITE_COMMIT", @@ -832,8 +830,8 @@ mod tests { fn pull_request_head_sha_drops_a_value_that_is_not_a_revision() { // An unset `BUILDKITE_COMMIT` is what `git_refs` reads as the // literal `HEAD`; callers must not have to re-check the shape. - with_ci_env( - &[ + env::testing::with_vars( + [ ("BUILDKITE", Some("true")), ("BUILDKITE_COMMIT", Some("HEAD")), ], @@ -845,8 +843,8 @@ mod tests { #[test] fn pull_request_head_sha_uses_circle_sha1() { - with_ci_env( - &[ + env::testing::with_vars( + [ ("CIRCLECI", Some("true")), ( "CIRCLE_SHA1", @@ -868,8 +866,8 @@ mod tests { // plugin builds a pull request merged into its target, which is // its default, and nothing distinguishes that from the head-only // configuration. No answer beats the wrong one. - with_ci_env( - &[ + env::testing::with_vars( + [ ("JENKINS_URL", Some("http://ci")), ( "GIT_COMMIT", @@ -976,8 +974,8 @@ mod tests { ) .unwrap(); - with_ci_env( - &[ + env::testing::with_vars( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_EVENT_NAME", Some("pull_request")), ("GITHUB_EVENT_PATH", Some(event_path.to_str().unwrap())), @@ -1003,8 +1001,8 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let event_path = tmp.path().join("event.json"); std::fs::write(&event_path, serde_json::json!({}).to_string()).unwrap(); - with_ci_env( - &[ + env::testing::with_vars( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_EVENT_NAME", Some("push")), ("GITHUB_EVENT_PATH", Some(event_path.to_str().unwrap())), @@ -1021,8 +1019,8 @@ mod tests { // Workflows without an event file (e.g. local // `act` runs) still set GITHUB_SHA — we must not regress // to `None` just because the JSON file isn't there. - with_ci_env( - &[ + env::testing::with_vars( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_EVENT_NAME", Some("pull_request")), ("GITHUB_EVENT_PATH", Some("/this/path/does/not/exist")), diff --git a/crates/mergify-ci/src/git_refs.rs b/crates/mergify-ci/src/git_refs.rs index 2ca9ab59..24c46ef9 100644 --- a/crates/mergify-ci/src/git_refs.rs +++ b/crates/mergify-ci/src/git_refs.rs @@ -27,7 +27,7 @@ //! `BUILDKITE=true` it invokes `buildkite-agent meta-data set` for //! base/head/source. -use std::env; +use mergify_core::env; use std::io::Write; use std::process::Command; @@ -152,7 +152,7 @@ pub fn detect( output: &mut dyn Output, notes_reader: NotesReader<'_>, ) -> Result { - if env::var("BUILDKITE").as_deref() == Ok("true") + if env::var("BUILDKITE").as_deref() == Some("true") && let Some(refs) = detect_from_buildkite(notes_reader) { return Ok(refs); @@ -188,16 +188,12 @@ pub fn detect( } fn detect_from_buildkite(notes_reader: NotesReader<'_>) -> Option { - let pr = env::var("BUILDKITE_PULL_REQUEST").ok()?; + let pr = env::var("BUILDKITE_PULL_REQUEST")?; if pr.is_empty() || pr == "false" { return None; } - let commit = env::var("BUILDKITE_COMMIT") - .ok() - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| "HEAD".to_string()); - if let Ok(branch) = env::var("BUILDKITE_BRANCH") - && !branch.is_empty() + let commit = env::var_non_empty("BUILDKITE_COMMIT").unwrap_or_else(|| "HEAD".to_string()); + if let Some(branch) = env::var_non_empty("BUILDKITE_BRANCH") && let Some(base) = notes_reader(&branch, &commit) { return Some(References { @@ -206,9 +202,7 @@ fn detect_from_buildkite(notes_reader: NotesReader<'_>) -> Option { source: ReferencesSource::MergeQueue, }); } - let base_branch = env::var("BUILDKITE_PULL_REQUEST_BASE_BRANCH") - .ok() - .filter(|s| !s.is_empty())?; + let base_branch = env::var_non_empty("BUILDKITE_PULL_REQUEST_BASE_BRANCH")?; Some(References { base: Some(base_branch), head: commit, @@ -471,7 +465,7 @@ fn write_github_output(refs: &References) -> Result<(), CliError> { } fn write_buildkite_metadata(refs: &References) -> std::io::Result<()> { - if env::var("BUILDKITE").as_deref() != Ok("true") { + if env::var("BUILDKITE").as_deref() != Some("true") { return Ok(()); } if let Some(base) = refs.base.as_deref() { @@ -582,10 +576,9 @@ mod tests { #[test] fn falls_back_to_head_pair_when_no_event() { let mut cap = Captured::human(); - let refs = temp_env::with_vars_unset( - ["GITHUB_EVENT_NAME", "GITHUB_EVENT_PATH", "BUILDKITE"], - || detect(&mut cap.output, &no_notes).unwrap(), - ); + // An empty overlay is the empty environment: no provider + // variable is visible, whatever the host exports. + let refs = env::testing::with_no_vars(|| detect(&mut cap.output, &no_notes).unwrap()); assert_eq!(refs.base.as_deref(), Some("HEAD^")); assert_eq!(refs.head, "HEAD"); assert_eq!(refs.source, ReferencesSource::FallbackLastCommit); @@ -604,7 +597,7 @@ mod tests { }), ); let mut cap = Captured::human(); - let refs = temp_env::with_vars( + let refs = env::testing::with_vars( [ ("GITHUB_EVENT_NAME", Some("pull_request")), ("GITHUB_EVENT_PATH", Some(path.to_str().unwrap())), @@ -625,7 +618,7 @@ mod tests { &serde_json::json!({"before": "old-sha", "after": "new-sha"}), ); let mut cap = Captured::human(); - let refs = temp_env::with_vars( + let refs = env::testing::with_vars( [ ("GITHUB_EVENT_NAME", Some("push")), ("GITHUB_EVENT_PATH", Some(path.to_str().unwrap())), @@ -652,7 +645,7 @@ mod tests { }), ); let mut cap = Captured::human(); - let refs = temp_env::with_vars( + let refs = env::testing::with_vars( [ ("GITHUB_EVENT_NAME", Some("pull_request")), ("GITHUB_EVENT_PATH", Some(path.to_str().unwrap())), @@ -688,7 +681,7 @@ mod tests { }), ); let mut cap = Captured::human(); - let refs = temp_env::with_vars( + let refs = env::testing::with_vars( [ ("GITHUB_EVENT_NAME", Some("pull_request")), ("GITHUB_EVENT_PATH", Some(path.to_str().unwrap())), @@ -719,7 +712,7 @@ mod tests { }), ); let mut cap = Captured::human(); - let refs = temp_env::with_vars( + let refs = env::testing::with_vars( [ ("GITHUB_EVENT_NAME", Some("pull_request")), ("GITHUB_EVENT_PATH", Some(path.to_str().unwrap())), @@ -757,7 +750,7 @@ mod tests { }), ); let mut cap = Captured::human(); - let refs = temp_env::with_vars( + let refs = env::testing::with_vars( [ ("GITHUB_EVENT_NAME", Some("pull_request")), ("GITHUB_EVENT_PATH", Some(path.to_str().unwrap())), @@ -793,7 +786,7 @@ mod tests { }), ); let mut cap = Captured::human(); - let refs = temp_env::with_vars( + let refs = env::testing::with_vars( [ ("GITHUB_EVENT_NAME", Some("pull_request")), ("GITHUB_EVENT_PATH", Some(path.to_str().unwrap())), @@ -834,7 +827,7 @@ mod tests { } }; let mut cap = Captured::human(); - let refs = temp_env::with_vars( + let refs = env::testing::with_vars( [ ("GITHUB_EVENT_NAME", Some("pull_request")), ("GITHUB_EVENT_PATH", Some(path.to_str().unwrap())), @@ -853,7 +846,7 @@ mod tests { &serde_json::json!({"pull_request": {"head": {"sha": "h"}}}), ); let mut cap = Captured::human(); - let err = temp_env::with_vars( + let err = env::testing::with_vars( [ ("GITHUB_EVENT_NAME", Some("pull_request")), ("GITHUB_EVENT_PATH", Some(path.to_str().unwrap())), @@ -867,7 +860,7 @@ mod tests { #[test] fn detects_buildkite_pull_request() { let mut cap = Captured::human(); - let refs = temp_env::with_vars( + let refs = env::testing::with_vars( [ ("BUILDKITE", Some("true")), ("BUILDKITE_PULL_REQUEST", Some("42")), @@ -966,7 +959,7 @@ mod tests { head: NOTE_BASE.into(), source: ReferencesSource::MergeQueue, }; - temp_env::with_var("GITHUB_OUTPUT", Some(path.to_str().unwrap()), || { + env::testing::with_var("GITHUB_OUTPUT", Some(path.to_str().unwrap()), || { write_github_output(&refs).unwrap(); }); let written = std::fs::read_to_string(&path).unwrap(); diff --git a/crates/mergify-ci/src/github_event.rs b/crates/mergify-ci/src/github_event.rs index f725225b..4495e9fc 100644 --- a/crates/mergify-ci/src/github_event.rs +++ b/crates/mergify-ci/src/github_event.rs @@ -5,7 +5,7 @@ //! `deny_unknown_fields` on purpose) so the payload's superset of //! fields doesn't break us. -use std::env; +use mergify_core::env; use std::path::PathBuf; use serde::Deserialize; @@ -65,12 +65,8 @@ pub const PULL_REQUEST_EVENTS: &[&str] = &[ /// `GitHubEventNotFoundError` being converted to a fallback. #[must_use] pub fn load() -> Option<(String, GitHubEvent)> { - let event_name = env::var("GITHUB_EVENT_NAME") - .ok() - .filter(|s| !s.is_empty())?; - let event_path = env::var("GITHUB_EVENT_PATH") - .ok() - .filter(|s| !s.is_empty())?; + let event_name = env::var_non_empty("GITHUB_EVENT_NAME")?; + let event_path = env::var_non_empty("GITHUB_EVENT_PATH")?; let path = PathBuf::from(event_path); if !path.is_file() { return None; diff --git a/crates/mergify-ci/src/github_output.rs b/crates/mergify-ci/src/github_output.rs index 001fdeec..9a9f725e 100644 --- a/crates/mergify-ci/src/github_output.rs +++ b/crates/mergify-ci/src/github_output.rs @@ -33,7 +33,7 @@ //! ahead of the `<<`) would let the runner read the block as //! something else. -use std::env; +use mergify_core::env; use std::fmt::Write as _; use std::fs::OpenOptions; use std::io::Write as _; @@ -45,7 +45,7 @@ use mergify_core::CliError; /// output. No-op when the variable is unset or empty — i.e. anywhere /// but a GitHub Actions runner. pub(crate) fn append(outputs: &[(&'static str, &str)]) -> Result<(), CliError> { - let Some(path) = env::var("GITHUB_OUTPUT").ok().filter(|s| !s.is_empty()) else { + let Some(path) = env::var_non_empty("GITHUB_OUTPUT") else { return Ok(()); }; // Assembled first, then written once. Three `writeln!` calls on an @@ -90,13 +90,13 @@ mod tests { #[test] fn append_is_a_noop_outside_github_actions() { - temp_env::with_var("GITHUB_OUTPUT", None::<&str>, || { + env::testing::with_var("GITHUB_OUTPUT", None::<&str>, || { append(&[("k", "v")]).unwrap(); }); // An empty value is treated the same as unset: the runner // exports `GITHUB_OUTPUT=` in some contexts, and an empty // path is not openable. - temp_env::with_var("GITHUB_OUTPUT", Some(""), || { + env::testing::with_var("GITHUB_OUTPUT", Some(""), || { append(&[("k", "v")]).unwrap(); }); } @@ -105,7 +105,7 @@ mod tests { fn append_wraps_every_output_in_its_own_heredoc() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("gha_output"); - temp_env::with_var("GITHUB_OUTPUT", Some(path.to_str().unwrap()), || { + env::testing::with_var("GITHUB_OUTPUT", Some(path.to_str().unwrap()), || { append(&[("base", "cafef00d"), ("head", "0badc0de")]).unwrap(); }); let written = std::fs::read_to_string(&path).unwrap(); @@ -134,7 +134,7 @@ mod tests { // step output (MRGFY-8845). let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("gha_output"); - temp_env::with_var("GITHUB_OUTPUT", Some(path.to_str().unwrap()), || { + env::testing::with_var("GITHUB_OUTPUT", Some(path.to_str().unwrap()), || { append(&[("base", "cafef00d\nevil=1")]).unwrap(); }); let written = std::fs::read_to_string(&path).unwrap(); @@ -152,7 +152,7 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("gha_output"); std::fs::write(&path, "earlier=1\n").unwrap(); - temp_env::with_var("GITHUB_OUTPUT", Some(path.to_str().unwrap()), || { + env::testing::with_var("GITHUB_OUTPUT", Some(path.to_str().unwrap()), || { append(&[("base", "cafef00d")]).unwrap(); }); let written = std::fs::read_to_string(&path).unwrap(); diff --git a/crates/mergify-ci/src/junit_process/command.rs b/crates/mergify-ci/src/junit_process/command.rs index 2f8b96aa..3780b675 100644 --- a/crates/mergify-ci/src/junit_process/command.rs +++ b/crates/mergify-ci/src/junit_process/command.rs @@ -25,7 +25,7 @@ use std::path::{Path, PathBuf}; use mergify_core::auth; -use mergify_core::env::var_non_empty; +use mergify_core::env; use mergify_core::{CliError, ExitCode, Output}; use crate::detector; @@ -164,7 +164,7 @@ async fn run_with_cap( let metadata = UploadMetadata { test_framework: opts.test_framework.map(str::to_string), test_language: opts.test_language.map(str::to_string), - mergify_test_job_name: var_non_empty("MERGIFY_TEST_JOB_NAME"), + mergify_test_job_name: env::var_non_empty("MERGIFY_TEST_JOB_NAME"), quarantined: quarantine_result .quarantined .iter() @@ -337,7 +337,7 @@ fn resolve_test_exit_code(explicit: Option) -> Result, CliError if explicit.is_some() { return Ok(explicit); } - let Some(raw) = var_non_empty("MERGIFY_TEST_EXIT_CODE") else { + let Some(raw) = env::var_non_empty("MERGIFY_TEST_EXIT_CODE") else { return Ok(None); }; raw.parse::().map(Some).map_err(|e| { @@ -606,10 +606,7 @@ fn upload_status_label( /// on stderr instead of erroring. fn maybe_write_github_output(status: &str) { use std::io::Write as _; - let Some(path) = std::env::var("GITHUB_OUTPUT") - .ok() - .filter(|s| !s.is_empty()) - else { + let Some(path) = env::var_non_empty("GITHUB_OUTPUT") else { return; }; let result = std::fs::OpenOptions::new() @@ -634,7 +631,7 @@ fn maybe_write_github_output(status: &str) { /// they're permanent misconfiguration; transient failures (5xx, /// 408, 429, network) as warnings. fn gha_upload_annotation(error: &upload::UploadError) -> Option { - if std::env::var("GITHUB_ACTIONS").as_deref() != Ok("true") { + if env::var("GITHUB_ACTIONS").as_deref() != Some("true") { return None; } Some(if error.is_rejection() { @@ -659,7 +656,7 @@ fn gha_upload_annotation(error: &upload::UploadError) -> Option { /// summary / checks UI — otherwise it only appears in the human /// report prose. Never an error: the CI outcome is unaffected. fn gha_oversized_annotation(names: &[String]) -> Option { - if std::env::var("GITHUB_ACTIONS").as_deref() != Ok("true") { + if env::var("GITHUB_ACTIONS").as_deref() != Some("true") { return None; } Some(format!( @@ -890,7 +887,7 @@ mod tests { // `gha-mergify-ci` action uses when no runner exit code is // available. Pin so a future refactor can't accidentally // invert the precedence. - let got = temp_env::with_var("MERGIFY_TEST_EXIT_CODE", Some("42"), || { + let got = env::testing::with_var("MERGIFY_TEST_EXIT_CODE", Some("42"), || { resolve_test_exit_code(Some(0)).unwrap() }); assert_eq!(got, Some(0)); @@ -909,7 +906,7 @@ mod tests { // fix drops the clap `env` hook and routes the env var // through here — empty must collapse to `None`, the // same shape no env var would produce. - let got = temp_env::with_var("MERGIFY_TEST_EXIT_CODE", Some(""), || { + let got = env::testing::with_var("MERGIFY_TEST_EXIT_CODE", Some(""), || { resolve_test_exit_code(None).unwrap() }); assert_eq!(got, None); @@ -917,7 +914,7 @@ mod tests { #[test] fn resolve_test_exit_code_parses_non_empty_env_var() { - let got = temp_env::with_var("MERGIFY_TEST_EXIT_CODE", Some("7"), || { + let got = env::testing::with_var("MERGIFY_TEST_EXIT_CODE", Some("7"), || { resolve_test_exit_code(None).unwrap() }); assert_eq!(got, Some(7)); @@ -929,7 +926,7 @@ mod tests { // real misconfiguration, not a "no value" sentinel — // error loudly with the offending value in the message so // the user can spot the typo without having to dig. - let err = temp_env::with_var("MERGIFY_TEST_EXIT_CODE", Some("not-an-int"), || { + let err = env::testing::with_var("MERGIFY_TEST_EXIT_CODE", Some("not-an-int"), || { resolve_test_exit_code(None).unwrap_err() }); let msg = err.to_string(); @@ -1098,13 +1095,13 @@ mod tests { #[test] fn gha_oversized_annotation_lists_names_only_on_gha() { // Outside GitHub Actions: no annotation. - let none = temp_env::with_var("GITHUB_ACTIONS", None::<&str>, || { + let none = env::testing::with_var("GITHUB_ACTIONS", None::<&str>, || { gha_oversized_annotation(&["a.big".to_string()]) }); assert!(none.is_none()); // On GitHub Actions: a warning naming the dropped tests, never // an error (CI outcome is unaffected). - let ann = temp_env::with_var("GITHUB_ACTIONS", Some("true"), || { + let ann = env::testing::with_var("GITHUB_ACTIONS", Some("true"), || { gha_oversized_annotation(&["a.big".to_string(), "b.huge".to_string()]) }) .unwrap(); @@ -1134,7 +1131,7 @@ mod tests { write_oversized_cases(&mut report, std::slice::from_ref(&long)); assert!(!report.contains(&long), "the report printed the whole name"); - let ann = temp_env::with_var("GITHUB_ACTIONS", Some("true"), || { + let ann = env::testing::with_var("GITHUB_ACTIONS", Some("true"), || { gha_oversized_annotation(std::slice::from_ref(&long)) }) .unwrap(); @@ -1161,7 +1158,6 @@ mod tests { // banner text drifting). mod orchestrator { use super::*; - use crate::testing::with_ci_env_async; use mergify_core::{OutputMode, StdioOutput}; use std::sync::{Arc, Mutex}; use wiremock::matchers::{method, path}; @@ -1240,7 +1236,7 @@ mod tests { let api_url = server.uri(); let mut cap = captured(); - let code = with_ci_env_async(&[], async { + let code = env::testing::with_no_vars_async(async { let opts = JunitProcessOptions { api_url: Some(&api_url), token: Some("secret"), @@ -1281,7 +1277,7 @@ mod tests { let api_url = server.uri(); let mut cap = captured(); let cap_bytes = 4 * 1024; - let code = with_ci_env_async(&[], async { + let code = env::testing::with_no_vars_async(async { let opts = JunitProcessOptions { api_url: Some(&api_url), token: Some("secret"), @@ -1422,8 +1418,8 @@ mod tests { // checks survive stamping. With the environment scrubbed // it would only ever emit `test.run.id`, and the check // would pass on an empty resource. - with_ci_env_async( - &[ + env::testing::with_vars_async( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_REPOSITORY", Some("owner/repo")), ( @@ -1525,7 +1521,7 @@ mod tests { let file = write_xml(&tmp, "report.xml", &incompressible_failures_xml(30, 2048)); let api_url = server.uri(); let mut cap = captured(); - with_ci_env_async(&[], async { + env::testing::with_no_vars_async(async { let opts = JunitProcessOptions { api_url: Some(&api_url), token: Some("secret"), @@ -1566,7 +1562,7 @@ mod tests { let api_url = server.uri(); let mut cap = captured(); - with_ci_env_async(&[], async { + env::testing::with_no_vars_async(async { let opts = JunitProcessOptions { api_url: Some(&api_url), token: Some("secret"), @@ -1604,7 +1600,7 @@ mod tests { let api_url = server.uri(); let mut cap = captured(); - with_ci_env_async(&[], async { + env::testing::with_no_vars_async(async { let opts = JunitProcessOptions { api_url: Some(&api_url), token: Some("secret"), @@ -1640,7 +1636,7 @@ mod tests { let api_url = server.uri(); let mut cap = captured(); - with_ci_env_async(&[], async { + env::testing::with_no_vars_async(async { let opts = JunitProcessOptions { api_url: Some(&api_url), token: Some("secret"), @@ -1732,7 +1728,7 @@ mod tests { let api_url = server.uri(); let mut cap = captured(); - with_ci_env_async(&[], async { + env::testing::with_no_vars_async(async { let opts = JunitProcessOptions { api_url: Some(&api_url), token: Some("secret"), @@ -1761,7 +1757,7 @@ mod tests { let api_url = server.uri(); let mut cap = captured(); - with_ci_env_async(&[], async { + env::testing::with_no_vars_async(async { let opts = JunitProcessOptions { api_url: Some(&api_url), token: Some("secret"), @@ -1829,8 +1825,8 @@ mod tests { let output_path = github_output.to_string_lossy().into_owned(); let mut cap = captured(); // 64-byte cap: smaller than any single case's gzipped span. - let code = with_ci_env_async( - &[ + let code = env::testing::with_vars_async( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_OUTPUT", Some(&output_path)), ], @@ -1872,7 +1868,7 @@ mod tests { let api_url = server.uri(); let mut cap = captured(); - let code = with_ci_env_async(&[], async { + let code = env::testing::with_no_vars_async(async { let opts = JunitProcessOptions { api_url: Some(&api_url), token: Some("secret"), @@ -1908,7 +1904,7 @@ mod tests { let api_url = server.uri(); let mut cap = captured(); - let code = with_ci_env_async(&[], async { + let code = env::testing::with_no_vars_async(async { let opts = JunitProcessOptions { api_url: Some(&api_url), token: Some("secret"), @@ -1952,7 +1948,7 @@ mod tests { let api_url = server.uri(); let mut cap = captured(); - let code = with_ci_env_async(&[], async { + let code = env::testing::with_no_vars_async(async { let opts = JunitProcessOptions { api_url: Some(&api_url), token: Some("secret"), @@ -2011,8 +2007,8 @@ mod tests { ) -> (ExitCode, String) { let mut cap = captured(); let output_path = github_output.to_string_lossy().into_owned(); - let code = with_ci_env_async( - &[ + let code = env::testing::with_vars_async( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_OUTPUT", Some(&output_path)), ], @@ -2131,7 +2127,7 @@ mod tests { let api_url = server.uri(); let mut cap = captured(); - let code = with_ci_env_async(&[], async { + let code = env::testing::with_no_vars_async(async { let opts = JunitProcessOptions { api_url: Some(&api_url), token: Some("secret"), @@ -2179,7 +2175,7 @@ mod tests { // No mock server: if the orchestrator skips the early // exit and tries to reach the API, the bogus URL will // fail the test loudly. - let code = with_ci_env_async(&[], async { + let code = env::testing::with_no_vars_async(async { let opts = JunitProcessOptions { api_url: Some("http://127.0.0.1:1"), token: Some("secret"), diff --git a/crates/mergify-ci/src/junit_process/spans.rs b/crates/mergify-ci/src/junit_process/spans.rs index ae988ec2..4d863bea 100644 --- a/crates/mergify-ci/src/junit_process/spans.rs +++ b/crates/mergify-ci/src/junit_process/spans.rs @@ -446,9 +446,10 @@ impl RandomBytes for OsRandom { #[cfg(test)] mod tests { + use mergify_core::env; + use super::*; use crate::junit_process::junit::Failure; - use crate::testing::with_ci_env; /// Deterministic byte source for tests. Bytes are consumed /// in order. Tests provide enough buffer for the spans they @@ -538,7 +539,8 @@ mod tests { let now: u64 = 1_700_000_000_000_000_000; let metadata = UploadMetadata::default(); - let built = with_ci_env(&[], || build_traces_with(&parsed, &metadata, now, &mut rng)); + let built = + env::testing::with_no_vars(|| build_traces_with(&parsed, &metadata, now, &mut rng)); assert_eq!(built.oversized_case_names, vec![oversized.clone()]); @@ -589,7 +591,7 @@ mod tests { bytes.extend(std::iter::repeat_n(0x55, 8)); let mut rng = FixedRng::new(bytes); - let built = with_ci_env(&[], || { + let built = env::testing::with_no_vars(|| { build_traces_with( &parsed, &UploadMetadata::default(), @@ -618,7 +620,7 @@ mod tests { let now: u64 = 1_700_000_000_000_000_000; let metadata = UploadMetadata::default(); - let built = with_ci_env(&[], || { + let built = env::testing::with_no_vars(|| { build_traces_with(&sample_parsed(), &metadata, now, &mut rng) }); @@ -661,7 +663,7 @@ mod tests { let mut rng = FixedRng::new(vec![0xFF; 256]); let now: u64 = 1_700_000_000_000_000_000; let metadata = UploadMetadata::default(); - let built = with_ci_env(&[], || { + let built = env::testing::with_no_vars(|| { build_traces_with(&sample_parsed(), &metadata, now, &mut rng) }); let spans = &built.request.resource_spans[0].scope_spans[0].spans; @@ -696,7 +698,7 @@ mod tests { fn case_attributes_include_file_line_and_code_function() { let mut rng = FixedRng::new(vec![0xFF; 256]); let metadata = UploadMetadata::default(); - let built = with_ci_env(&[], || { + let built = env::testing::with_no_vars(|| { build_traces_with(&sample_parsed(), &metadata, 0, &mut rng) }); let spans = &built.request.resource_spans[0].scope_spans[0].spans; @@ -737,8 +739,8 @@ mod tests { fn resource_attributes_carry_ci_env_when_set() { let mut rng = FixedRng::new(vec![0xFF; 256]); let metadata = UploadMetadata::default(); - let built = with_ci_env( - &[ + let built = env::testing::with_vars( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_REPOSITORY", Some("owner/repo")), ("GITHUB_WORKFLOW", Some("CI")), @@ -795,7 +797,7 @@ mod tests { mergify_test_job_name: None, quarantined: BTreeSet::new(), }; - let built = with_ci_env(&[], || { + let built = env::testing::with_no_vars(|| { build_traces_with(&sample_parsed(), &metadata, 0, &mut rng) }); let spans = &built.request.resource_spans[0].scope_spans[0].spans; @@ -822,7 +824,7 @@ mod tests { let mut rng = FixedRng::new(vec![0xFF; 256]); let now: u64 = 1_000_000_000_000_000_000; let metadata = UploadMetadata::default(); - let built = with_ci_env(&[], || { + let built = env::testing::with_no_vars(|| { build_traces_with(&sample_parsed(), &metadata, now, &mut rng) }); let spans = &built.request.resource_spans[0].scope_spans[0].spans; diff --git a/crates/mergify-ci/src/junit_process/split.rs b/crates/mergify-ci/src/junit_process/split.rs index 54d8442d..bfc8170a 100644 --- a/crates/mergify-ci/src/junit_process/split.rs +++ b/crates/mergify-ci/src/junit_process/split.rs @@ -555,7 +555,8 @@ mod tests { use super::*; use crate::junit_process::junit::{Failure, ParseResult, TestCase, TestStatus}; use crate::junit_process::spans::{UploadMetadata, build_traces}; - use crate::testing::{incompressible, with_ci_env}; + use crate::testing::incompressible; + use mergify_core::env; use std::collections::BTreeSet; use std::time::Duration; @@ -595,7 +596,7 @@ mod tests { mergify_test_job_name: None, quarantined: BTreeSet::new(), }; - with_ci_env(&[], || build_traces(&parsed, &metadata)).request + env::testing::with_no_vars(|| build_traces(&parsed, &metadata)).request } /// Collect the `test.case.name` of every case span in a chunk. diff --git a/crates/mergify-ci/src/queue_info.rs b/crates/mergify-ci/src/queue_info.rs index d2db01e2..7c2b845a 100644 --- a/crates/mergify-ci/src/queue_info.rs +++ b/crates/mergify-ci/src/queue_info.rs @@ -109,6 +109,7 @@ fn write_github_output(metadata: &Value) -> Result<(), CliError> { #[cfg(test)] mod tests { use mergify_core::ExitCode; + use mergify_core::env; use mergify_test_support::Captured; use serde_json::json; @@ -141,7 +142,7 @@ mod tests { fn prints_whole_note_payload() { let note = || Some(sample()); let mut cap = Captured::human(); - temp_env::with_var("GITHUB_OUTPUT", None::<&str>, || { + env::testing::with_var("GITHUB_OUTPUT", None::<&str>, || { run_with_reader(&mut cap.output, ¬e).unwrap(); }); let stdout = cap.stdout(); @@ -161,7 +162,7 @@ mod tests { let gha_output = dir.path().join("gha_output"); let note = || Some(sample()); let mut cap = Captured::human(); - temp_env::with_var("GITHUB_OUTPUT", Some(gha_output.to_str().unwrap()), || { + env::testing::with_var("GITHUB_OUTPUT", Some(gha_output.to_str().unwrap()), || { run_with_reader(&mut cap.output, ¬e).unwrap(); }); let written = std::fs::read_to_string(&gha_output).unwrap(); diff --git a/crates/mergify-ci/src/scopes_detect/mod.rs b/crates/mergify-ci/src/scopes_detect/mod.rs index b313e346..b85e34f3 100644 --- a/crates/mergify-ci/src/scopes_detect/mod.rs +++ b/crates/mergify-ci/src/scopes_detect/mod.rs @@ -25,14 +25,13 @@ pub mod config; pub mod matching; pub mod outputs; -use std::env; use std::io::Write; use std::path::Path; use std::path::PathBuf; use mergify_core::CliError; use mergify_core::Output; -use mergify_core::env::var_non_empty; +use mergify_core::env; use serde::Serialize; use crate::git_refs; @@ -128,7 +127,7 @@ fn resolve_config_path(explicit: Option<&Path>) -> Result { path.display(), ))); } - if let Some(env_path) = var_non_empty("MERGIFY_CONFIG_PATH") { + if let Some(env_path) = env::var_non_empty("MERGIFY_CONFIG_PATH") { let p = PathBuf::from(&env_path); if !p.is_file() { return Err(CliError::Configuration(format!( @@ -240,7 +239,7 @@ fn emit_scopes_listing( by_scope: &std::collections::BTreeMap>, output: &mut dyn Output, ) -> Result<(), CliError> { - let actions_debug = env::var("ACTIONS_STEP_DEBUG").as_deref() == Ok("true"); + let actions_debug = env::var("ACTIONS_STEP_DEBUG").as_deref() == Some("true"); if hit.is_empty() { output.status("No scopes matched.")?; return Ok(()); @@ -289,7 +288,6 @@ fn write_detected_scopes( #[cfg(test)] mod tests { use super::*; - use crate::testing::with_ci_env; use mergify_test_support::Captured; #[test] @@ -330,7 +328,7 @@ mod tests { // so this function owns the lookup — and the empty branch // here must fall through to autodetect rather than report // a malformed env var. - let result = temp_env::with_var("MERGIFY_CONFIG_PATH", Some(""), || { + let result = env::testing::with_var("MERGIFY_CONFIG_PATH", Some(""), || { resolve_config_path(None) }); // Either autodetect found a real config (cargo test runs @@ -354,9 +352,10 @@ mod tests { // value that doesn't exist, the error must name the env // var + the bogus path so the user can spot the typo // without having to dig. - let err = temp_env::with_var("MERGIFY_CONFIG_PATH", Some("/no/such/.mergify.yml"), || { - resolve_config_path(None).unwrap_err() - }); + let err = + env::testing::with_var("MERGIFY_CONFIG_PATH", Some("/no/such/.mergify.yml"), || { + resolve_config_path(None).unwrap_err() + }); let msg = err.to_string(); assert!(msg.contains("MERGIFY_CONFIG_PATH="), "got: {msg}"); assert!(msg.contains("/no/such/.mergify.yml"), "got: {msg}"); @@ -402,11 +401,10 @@ mod tests { // "select all scopes" branch and reports every // configured scope as touched. No git operations. // - // The `with_ci_env` wrapper scrubs `GITHUB_OUTPUT` so a - // GHA runner executing the suite doesn't see `run()` - // append a heredoc to its real step-output file (which - // would break the runner step with "Matching delimiter - // not found"). + // The empty overlay hides `GITHUB_OUTPUT` so a GHA runner + // executing the suite doesn't see `run()` append a heredoc + // to its real step-output file (which would break the + // runner step with "Matching delimiter not found"). let tmp = tempfile::tempdir().unwrap(); let cfg = tmp.path().join("mergify.yml"); std::fs::write( @@ -415,7 +413,7 @@ mod tests { ) .unwrap(); let mut cap = Captured::human(); - with_ci_env(&[], || { + env::testing::with_no_vars(|| { run( ScopesOptions { config: Some(&cfg), @@ -444,7 +442,7 @@ mod tests { let cfg = tmp.path().join("mergify.yml"); std::fs::write(&cfg, "scopes:\n source:\n manual: null\n").unwrap(); let mut cap = Captured::human(); - let err = with_ci_env(&[], || { + let err = env::testing::with_no_vars(|| { run( ScopesOptions { config: Some(&cfg), @@ -475,7 +473,7 @@ mod tests { .unwrap(); let out = tmp.path().join("detected.json"); let mut cap = Captured::human(); - with_ci_env(&[], || { + env::testing::with_no_vars(|| { run( ScopesOptions { config: Some(&cfg), diff --git a/crates/mergify-ci/src/scopes_detect/outputs.rs b/crates/mergify-ci/src/scopes_detect/outputs.rs index d3582beb..da3bacd4 100644 --- a/crates/mergify-ci/src/scopes_detect/outputs.rs +++ b/crates/mergify-ci/src/scopes_detect/outputs.rs @@ -5,8 +5,8 @@ //! `mergify_cli/ci/scopes/cli.py` and stay quiet when their //! respective environment knob is absent. +use mergify_core::env; use std::collections::BTreeSet; -use std::env; use std::fmt::Write as _; use std::fs::OpenOptions; use std::io::Write; @@ -64,7 +64,7 @@ pub fn maybe_write_buildkite_metadata( all: &BTreeSet, hit: &BTreeSet, ) -> Result<(), CliError> { - if env::var("BUILDKITE").as_deref() != Ok("true") { + if env::var("BUILDKITE").as_deref() != Some("true") { return Ok(()); } let payload = scopes_dict_json(all, hit); @@ -114,10 +114,7 @@ pub fn maybe_write_github_step_summary( all: &BTreeSet, hit: &BTreeSet, ) -> Result<(), CliError> { - let Some(path) = env::var("GITHUB_STEP_SUMMARY") - .ok() - .filter(|s| !s.is_empty()) - else { + let Some(path) = env::var_non_empty("GITHUB_STEP_SUMMARY") else { return Ok(()); }; let md = build_summary_markdown(refs, all, hit); @@ -138,7 +135,7 @@ pub fn maybe_write_buildkite_annotation( all: &BTreeSet, hit: &BTreeSet, ) { - if env::var("BUILDKITE").as_deref() != Ok("true") { + if env::var("BUILDKITE").as_deref() != Some("true") { return; } let md = build_summary_markdown(refs, all, hit); diff --git a/crates/mergify-ci/src/scopes_send.rs b/crates/mergify-ci/src/scopes_send.rs index 06a1a265..5fba0e37 100644 --- a/crates/mergify-ci/src/scopes_send.rs +++ b/crates/mergify-ci/src/scopes_send.rs @@ -228,6 +228,7 @@ struct SendScopesRequest<'a> { mod tests { use std::fs; + use mergify_core::env; use mergify_test_support::Captured; use wiremock::Mock; use wiremock::MockServer; @@ -238,13 +239,11 @@ mod tests { use wiremock::matchers::path; use super::*; - use crate::testing::with_ci_env; - use crate::testing::with_ci_env_async; use crate::testing::write_github_event; #[test] fn resolve_pull_request_prefers_explicit() { - with_ci_env(&[], || { + env::testing::with_no_vars(|| { assert_eq!(resolve_pull_request(Some(7)).unwrap(), Some(7)); }); } @@ -287,7 +286,7 @@ mod tests { #[tokio::test] async fn run_skips_when_no_pull_request_detected() { let mut cap = Captured::human(); - with_ci_env_async(&[("GITHUB_REPOSITORY", Some("owner/repo"))], async { + env::testing::with_vars_async([("GITHUB_REPOSITORY", Some("owner/repo"))], async { run( ScopesSendOptions { repository: None, @@ -331,8 +330,8 @@ mod tests { let api_url = server.uri(); let direct = vec!["a".to_string()]; - with_ci_env_async( - &[ + env::testing::with_vars_async( + [ ("BUILDKITE", Some("true")), ("BUILDKITE_REPO", Some("git@github.com:owner/repo.git")), ("BUILDKITE_PULL_REQUEST", Some("99")), @@ -554,8 +553,8 @@ mod tests { let api_url = server.uri(); let direct = vec!["a".to_string()]; - with_ci_env_async( - &[ + env::testing::with_vars_async( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_EVENT_NAME", Some("pull_request")), ("GITHUB_EVENT_PATH", Some(event_path.to_str().unwrap())), @@ -620,8 +619,8 @@ mod tests { let api_url = server.uri(); let direct = vec!["backend".to_string()]; - with_ci_env_async( - &[ + env::testing::with_vars_async( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_EVENT_NAME", Some("pull_request")), ("GITHUB_EVENT_PATH", Some(event_path.to_str().unwrap())), diff --git a/crates/mergify-ci/src/testing.rs b/crates/mergify-ci/src/testing.rs index db729e76..1a496126 100644 --- a/crates/mergify-ci/src/testing.rs +++ b/crates/mergify-ci/src/testing.rs @@ -1,130 +1,9 @@ //! Test-only helpers shared across the CI-aware command modules //! (`detector`, `scopes_send`, `tests_show`, `tests_quarantine`). -//! -//! These modules test CI-provider-aware code paths and need to scrub -//! the host's CI env vars before running each case — otherwise a -//! test running on a real Buildkite/Actions/Circle/Jenkins host -//! inherits provider state and the detector picks the wrong branch. -//! Two flavors: a sync `with_ci_env` and an async `with_ci_env_async` -//! (used by the `#[tokio::test]` cases). -use std::future::Future; use std::path::Path; use std::path::PathBuf; -/// Env vars the CI-provider detection chain inspects. Clear every -/// one of them before applying the test-specific overrides, so the -/// host environment can't leak into the test — running the test -/// suite *on* a real GitHub Actions / `CircleCI` / Jenkins / Buildkite -/// host would otherwise produce `vcs.ref.head.name` etc. values -/// taken from the runner instead of the test's explicit override -/// and silently fail. -/// -/// `GITHUB_OUTPUT` belongs on this list too — when the suite runs -/// on a GHA runner that var points at the runner's real -/// step-output file, and any test that exercises a code path -/// appending a heredoc (e.g. `ci scopes` → -/// `MERGIFY_SCOPES<)]) -> Vec<(String, Option)> { - let mut vars: Vec<(String, Option)> = CI_ENV_VARS - .iter() - .map(|k| ((*k).to_string(), None)) - .collect(); - for (k, v) in extra { - vars.push(((*k).to_string(), v.map(ToString::to_string))); - } - vars -} - -/// Run `f` with the CI-provider env vars cleared, plus the -/// `extra` overrides applied on top. -pub(crate) fn with_ci_env(extra: &[(&str, Option<&str>)], f: F) -> R -where - F: FnOnce() -> R, -{ - temp_env::with_vars(merged_overrides(extra), f) -} - -/// Async counterpart to [`with_ci_env`]. Used by `#[tokio::test]` -/// cases in `scopes_send` — the sync variant can't bridge `.await` -/// points. -pub(crate) async fn with_ci_env_async(extra: &[(&str, Option<&str>)], f: F) -> R -where - F: Future, -{ - temp_env::async_with_vars(merged_overrides(extra), f).await -} - /// Write `payload` as a GitHub Actions event file under `dir` and /// return its path, ready to point `GITHUB_EVENT_PATH` at. The /// payload-reading helpers across `detector` and `scopes_send` all diff --git a/crates/mergify-ci/src/tests_quarantine.rs b/crates/mergify-ci/src/tests_quarantine.rs index df934ce5..fd85ee1b 100644 --- a/crates/mergify-ci/src/tests_quarantine.rs +++ b/crates/mergify-ci/src/tests_quarantine.rs @@ -401,6 +401,7 @@ mod tests { use mergify_core::OutputMode; use mergify_core::StdioOutput; + use mergify_core::env; use serde_json::json; use wiremock::Mock; use wiremock::MockServer; @@ -410,7 +411,6 @@ mod tests { use wiremock::matchers::path as path_matcher; use super::*; - use crate::testing::with_ci_env_async; type SharedBytes = Arc>>; @@ -805,8 +805,8 @@ mod tests { // With no `--repository`, the command resolves the repository // from the CI environment — here GitHub Actions' // `GITHUB_REPOSITORY` — and queries that repository's endpoint. - with_ci_env_async( - &[ + env::testing::with_vars_async( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_REPOSITORY", Some("owner/repo")), ], diff --git a/crates/mergify-ci/src/tests_show.rs b/crates/mergify-ci/src/tests_show.rs index 227dfbe0..ad049a49 100644 --- a/crates/mergify-ci/src/tests_show.rs +++ b/crates/mergify-ci/src/tests_show.rs @@ -370,6 +370,7 @@ mod tests { use mergify_core::OutputMode; use mergify_core::StdioOutput; + use mergify_core::env; use serde_json::json; use wiremock::Mock; use wiremock::MockServer; @@ -378,7 +379,6 @@ mod tests { use wiremock::matchers::path as path_matcher; use super::*; - use crate::testing::with_ci_env_async; type SharedBytes = Arc>>; @@ -522,8 +522,8 @@ mod tests { // With no `--repository`, the command resolves the repository // from the CI environment — here GitHub Actions' // `GITHUB_REPOSITORY` — and queries that repository's endpoint. - with_ci_env_async( - &[ + env::testing::with_vars_async( + [ ("GITHUB_ACTIONS", Some("true")), ("GITHUB_REPOSITORY", Some("owner/repo")), ],