Skip to content

Commit 6ef888c

Browse files
committed
Fix dedupe.
1 parent f4fab33 commit 6ef888c

4 files changed

Lines changed: 192 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
#### 🐞 Fixes
6+
7+
- Fixed an issue where HTTP remote cache would deserialize manifests into the wrong shape, resulting
8+
in failed caching.
9+
- Fixed an issue where dependency deduping would run on fresh/initial installs.
10+
311
## 2.4.4
412

513
#### 🐞 Fixes

crates/actions/src/actions/install_dependencies.rs

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ pub async fn install_dependencies(
142142
None => None,
143143
};
144144

145+
// Determine this before any commands run, as the install itself
146+
// will create the vendor directory
147+
let first_install = toolchain.metadata.vendor_dir_name.is_some()
148+
&& !has_vendor_installed_dependencies(&toolchain, &deps_root);
149+
145150
// Create a lock if we haven't run before
146151
let Some(mut lock) = create_hash_and_return_lock_if_changed(
147152
action,
@@ -155,10 +160,7 @@ pub async fn install_dependencies(
155160
&input,
156161
)
157162
.await?,
158-
|| {
159-
toolchain.metadata.vendor_dir_name.is_some()
160-
&& !has_vendor_installed_dependencies(&toolchain, &deps_root)
161-
},
163+
|| first_install,
162164
)?
163165
else {
164166
debug!(
@@ -210,18 +212,30 @@ pub async fn install_dependencies(
210212
if !is_ci()
211213
&& let Some(mut dedupe) = output.dedupe_command
212214
{
213-
debug!(
214-
root = node.root.as_str(),
215-
toolchain_id = node.toolchain_id.as_str(),
216-
"Deduping {} dependencies",
217-
toolchain.metadata.name
218-
);
219-
220-
dedupe.cache = None; // Disable
221-
dedupe.command.stream = !hide_output;
222-
action
223-
.operations
224-
.extend(exec_plugin_command(app_context, &dedupe, &options).await?);
215+
// On the very first install there are no existing dependencies to
216+
// dedupe against, and worse, deduping would re-resolve the graph and
217+
// rewrite a pristine lockfile, since a cold cache always hashes as
218+
// "changed". Only dedupe once dependencies exist on disk.
219+
if first_install {
220+
debug!(
221+
root = node.root.as_str(),
222+
toolchain_id = node.toolchain_id.as_str(),
223+
"Dependencies were installed for the first time, skipping dedupe"
224+
);
225+
} else {
226+
debug!(
227+
root = node.root.as_str(),
228+
toolchain_id = node.toolchain_id.as_str(),
229+
"Deduping {} dependencies",
230+
toolchain.metadata.name
231+
);
232+
233+
dedupe.cache = None; // Disable
234+
dedupe.command.stream = !hide_output;
235+
action
236+
.operations
237+
.extend(exec_plugin_command(app_context, &dedupe, &options).await?);
238+
}
225239
}
226240

227241
finalize_action_operations(action, &toolchain, setup_op, output.operations, vec![])?;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use moon_action::{Action, ActionStatus, InstallDependenciesNode};
2+
use moon_action_context::ActionContext;
3+
use moon_actions::actions::install_dependencies;
4+
use moon_common::{Id, is_ci, path::WorkspaceRelativePathBuf};
5+
use moon_test_utils::WorkspaceMocker;
6+
use starbase_sandbox::{Sandbox, create_empty_sandbox};
7+
use starbase_utils::json::JsonValue;
8+
9+
fn create_workspace() -> (Sandbox, WorkspaceMocker) {
10+
let sandbox = create_empty_sandbox();
11+
12+
// The tier1 toolchain (inherited by tc-tier2) registers `tc.lock`
13+
// as its lockfile and `vendor` as its vendor directory
14+
sandbox.create_file("tc.lock", "v1");
15+
16+
let mocker = WorkspaceMocker::new(sandbox.path())
17+
.with_default_projects()
18+
.with_test_toolchains()
19+
// Opt-in to the install/dedupe commands returned by tc-tier2
20+
.update_toolchains_config(|config| {
21+
config
22+
.plugins
23+
.get_mut(&Id::raw("tc-tier2"))
24+
.unwrap()
25+
.config
26+
.insert("testInstallCommands".into(), JsonValue::Bool(true));
27+
})
28+
// Hash with the cache engine instead of the VCS,
29+
// as the sandbox is not a git repository
30+
.update_workspace_config(|config| {
31+
config.experiments.native_file_hashing = true;
32+
});
33+
34+
(sandbox, mocker)
35+
}
36+
37+
async fn run_action(ws: &WorkspaceMocker) -> (Action, ActionStatus) {
38+
let mut action = Action::default();
39+
let node = InstallDependenciesNode {
40+
members: None,
41+
project_id: None,
42+
root: WorkspaceRelativePathBuf::default(),
43+
toolchain_id: Id::raw("tc-tier2"),
44+
};
45+
46+
let status = install_dependencies(
47+
&mut action,
48+
ActionContext::default().into(),
49+
ws.mock_app_context().into(),
50+
ws.mock_workspace_graph().await.into(),
51+
&node,
52+
)
53+
.await
54+
.unwrap();
55+
56+
(action, status)
57+
}
58+
59+
fn count_execs(action: &Action) -> usize {
60+
action
61+
.operations
62+
.iter()
63+
.filter(|op| op.meta.is_process_execution())
64+
.count()
65+
}
66+
67+
mod install_dependencies {
68+
use super::*;
69+
70+
#[serial_test::serial]
71+
#[tokio::test(flavor = "multi_thread")]
72+
async fn does_not_dedupe_on_first_install() {
73+
let (_sandbox, ws) = create_workspace();
74+
75+
// No vendor directory exists yet, so only the install command must
76+
// run. Deduping would rewrite a pristine lockfile, since a cold
77+
// cache always hashes as "changed"
78+
let (action, status) = run_action(&ws).await;
79+
80+
assert_eq!(status, ActionStatus::Passed);
81+
assert_eq!(count_execs(&action), 1);
82+
}
83+
84+
#[serial_test::serial]
85+
#[tokio::test(flavor = "multi_thread")]
86+
async fn dedupes_when_dependencies_already_installed() {
87+
let (sandbox, ws) = create_workspace();
88+
sandbox.create_file("vendor/dep/manifest", "");
89+
90+
let (action, status) = run_action(&ws).await;
91+
92+
if is_ci() {
93+
// In CI, an existing vendor directory skips the action entirely
94+
assert_eq!(status, ActionStatus::Skipped);
95+
assert_eq!(count_execs(&action), 0);
96+
} else {
97+
// Install + dedupe
98+
assert_eq!(status, ActionStatus::Passed);
99+
assert_eq!(count_execs(&action), 2);
100+
}
101+
}
102+
103+
#[serial_test::serial]
104+
#[tokio::test(flavor = "multi_thread")]
105+
async fn dedupes_on_lockfile_change_after_first_install() {
106+
let (sandbox, ws) = create_workspace();
107+
108+
// Cold cache: install only, no dedupe
109+
let (action, status) = run_action(&ws).await;
110+
111+
assert_eq!(status, ActionStatus::Passed);
112+
assert_eq!(count_execs(&action), 1);
113+
114+
// Simulate the install having populated the vendor directory
115+
sandbox.create_file("vendor/dep/manifest", "");
116+
117+
// Nothing changed: nothing to install or dedupe
118+
let (action, status) = run_action(&ws).await;
119+
120+
assert_eq!(status, ActionStatus::Skipped);
121+
assert_eq!(count_execs(&action), 0);
122+
123+
// Lockfile changed: install and dedupe
124+
sandbox.create_file("tc.lock", "v2");
125+
126+
let (action, status) = run_action(&ws).await;
127+
128+
if is_ci() {
129+
assert_eq!(status, ActionStatus::Skipped);
130+
assert_eq!(count_execs(&action), 0);
131+
} else {
132+
assert_eq!(status, ActionStatus::Passed);
133+
assert_eq!(count_execs(&action), 2);
134+
}
135+
}
136+
}

wasm/tc-tier2/src/lib.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,25 @@ pub fn locate_dependencies_root(
5555

5656
#[plugin_fn]
5757
pub fn install_dependencies(
58-
Json(_): Json<InstallDependenciesInput>,
58+
Json(input): Json<InstallDependenciesInput>,
5959
) -> FnResult<Json<InstallDependenciesOutput>> {
60-
Ok(Json(InstallDependenciesOutput::default()))
60+
let mut output = InstallDependenciesOutput::default();
61+
62+
// Only return commands when a test opts in via toolchain config,
63+
// otherwise unrelated tests would execute real processes
64+
if input
65+
.toolchain_config
66+
.get("testInstallCommands")
67+
.and_then(|value| value.as_bool())
68+
.unwrap_or_default()
69+
{
70+
output.install_command =
71+
Some(ExecCommand::new(ExecCommandInput::pipe("git", ["--version"])).label("install"));
72+
output.dedupe_command =
73+
Some(ExecCommand::new(ExecCommandInput::pipe("git", ["--version"])).label("dedupe"));
74+
}
75+
76+
Ok(Json(output))
6177
}
6278

6379
#[plugin_fn]

0 commit comments

Comments
 (0)