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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions crates/turborepo/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,11 @@ pub fn turbo_command(test_dir: &Path) -> assert_cmd::Command {
.env("DO_NOT_TRACK", "1")
.env("NPM_CONFIG_UPDATE_NOTIFIER", "false")
// Corepack intercepts package manager calls (yarn, pnpm) and can
// either prompt for download confirmation or fetch an exact version
// from the network, both of which hang in non-interactive CI.
// COREPACK_ENABLE_DOWNLOAD_PROMPT=0 auto-approves downloads.
// COREPACK_ENABLE_STRICT=0 lets corepack fall back to whatever
// version is already installed instead of downloading the exact
// version from packageManager field.
// prompt for download confirmation, which hangs in non-interactive CI.
// The test setup pre-warms the corepack cache via `corepack prepare`
// so the correct version is available locally without network access.
// DOWNLOAD_PROMPT=0 is a safety net in case the cache is somehow cold.
.env("COREPACK_ENABLE_DOWNLOAD_PROMPT", "0")
.env("COREPACK_ENABLE_STRICT", "0")
.env_remove("CI")
.env_remove("GITHUB_ACTIONS")
.current_dir(test_dir);
Expand Down Expand Up @@ -147,6 +144,10 @@ pub fn setup_lockfile_test(dir: &Path, pm_name: &str) {
setup::copy_dir_all(&base_fixture, dir).unwrap();
setup::copy_dir_all(&pm_overlay, dir).unwrap();

// Pre-warm the corepack cache for the declared packageManager version so
// that turbo's PM invocations resolve locally without network access.
setup::prepare_corepack_from_package_json(dir);

// Lockfile tests need a minimal git init without .npmrc or extra .gitignore
// entries that setup::setup_git() creates, since those would appear in git
// diffs and affect the filter results.
Expand Down
76 changes: 73 additions & 3 deletions crates/turborepo/tests/common/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,13 @@ pub fn setup_package_manager(
&format!("Updated package manager to {package_manager}"),
)?;

// Enable corepack for this package manager
// Corepack only manages yarn and pnpm. npm ships with node and bun is
// its own runtime — neither needs corepack setup.
let pm_name = package_manager.split('@').next().unwrap_or(package_manager);
if !corepack_supports(pm_name) {
return Ok(());
}

fs::create_dir_all(corepack_dir)?;

let status = cmd("corepack")
Expand All @@ -146,9 +151,69 @@ pub fn setup_package_manager(
anyhow::bail!("corepack enable {} failed with {}", pm_name, status);
}

// Pre-download the exact PM version into corepack's cache so that
// subsequent invocations (yarn install, turbo run build → yarn run build)
// resolve locally without any network access. Without this, every
// corepack-intercepted PM call can trigger a slow download that causes
// tests to timeout in CI.
let status = cmd("corepack")
.arg("prepare")
.arg(package_manager) // e.g. "yarn@1.22.17"
.arg("--activate")
.current_dir(target_dir)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.status()
.map_err(|e| anyhow::anyhow!("failed to run corepack prepare: {e}"))?;

if !status.success() {
anyhow::bail!(
"corepack prepare {} failed with {}",
package_manager,
status
);
}

Ok(())
}

/// Read the `packageManager` field from `package.json` in `dir` and run
/// `corepack prepare <value> --activate` to pre-warm the corepack cache.
/// This is used by test setups that copy fixtures with a pre-existing
/// `packageManager` field (e.g. lockfile-aware-caching tests) and don't go
/// through `setup_package_manager`.
pub fn prepare_corepack_from_package_json(dir: &Path) {
let pkg_json_path = dir.join("package.json");
let contents = fs::read_to_string(&pkg_json_path).expect("failed to read package.json");
let pkg: serde_json::Value =
serde_json::from_str(&contents).expect("failed to parse package.json");

let pm = match pkg.get("packageManager").and_then(|v| v.as_str()) {
Some(pm) => pm.to_string(),
None => return,
};

let pm_name = pm.split('@').next().unwrap_or(&pm);
if !corepack_supports(pm_name) {
return;
}

let status = cmd("corepack")
.arg("prepare")
.arg(&pm)
.arg("--activate")
.current_dir(dir)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.status()
.unwrap_or_else(|e| panic!("failed to run corepack prepare {pm}: {e}"));

assert!(
status.success(),
"corepack prepare {pm} failed with {status}"
);
}

/// Install dependencies using the specified package manager.
pub fn install_deps(
target_dir: &Path,
Expand Down Expand Up @@ -244,9 +309,10 @@ fn run_cmd(dir: &Path, program: &str, args: &[&str], path_env: &str) -> Result<(
let output = cmd_with_path(program, path_env)
.args(args)
.current_dir(dir)
// Prevent corepack from prompting or downloading exact versions
// Safety net: auto-approve any corepack download prompt in case the
// cache is somehow cold. The setup pre-warms the cache via
// `corepack prepare` so this should rarely be needed.
.env("COREPACK_ENABLE_DOWNLOAD_PROMPT", "0")
.env("COREPACK_ENABLE_STRICT", "0")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.output()
Expand Down Expand Up @@ -282,6 +348,10 @@ fn git_commit_if_changed(dir: &Path, message: &str) -> Result<(), anyhow::Error>
Ok(())
}

fn corepack_supports(pm_name: &str) -> bool {
matches!(pm_name, "npm" | "yarn" | "pnpm" | "berry")
}

fn prepend_to_path(dir: &Path) -> String {
let current = std::env::var("PATH").unwrap_or_default();
let sep = if cfg!(windows) { ";" } else { ":" };
Expand Down
Loading