From 6179470eb2c69ae9cd9f33c9070bf365afeec52d Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 21 Aug 2026 17:25:58 -0400 Subject: [PATCH] feat: default sandbox clone depth to 100 --- docs/public/api-reference/fabro-api.yaml | 4 ++- docs/public/execution/environments.mdx | 2 +- docs/public/execution/run-configuration.mdx | 4 +-- docs/public/integrations/daytona.mdx | 4 +-- .../fabro-sandbox/src/clone_source.rs | 20 +++++++++++- lib/components/fabro-sandbox/src/config.rs | 15 ++++++++- .../fabro-sandbox/src/daytona/mod.rs | 2 +- lib/components/fabro-sandbox/src/docker.rs | 27 +++++++++++++--- .../fabro-sandbox/src/from_environment.rs | 3 +- .../fabro-workflow/src/operations/start.rs | 32 +++++++++++++++++++ .../fabro-config/src/resolve/run.rs | 25 ++++++++------- .../fabro-config/src/tests/resolve_run.rs | 26 ++++++++++++--- .../fabro-types/src/settings/run.rs | 19 +++++++++-- .../src/models/run-clone-settings.ts | 3 ++ 14 files changed, 152 insertions(+), 34 deletions(-) diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index dafddf42f..9e0655ff7 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -14636,7 +14636,9 @@ components: depth: type: integer format: int32 - minimum: 1 + minimum: 0 + default: 100 + description: Git history depth. Set to 0 to clone full history. RunBranchSettings: type: object diff --git a/docs/public/execution/environments.mdx b/docs/public/execution/environments.mdx index c58e602b9..a97509bd9 100644 --- a/docs/public/execution/environments.mdx +++ b/docs/public/execution/environments.mdx @@ -258,7 +258,7 @@ memory = "4GB" mode = "block" ``` -Docker and Daytona are clone-based providers. When a run has a GitHub origin, Fabro clones it into the provider workspace. Set `[run.clone] enabled = false` to start with an empty workspace, or set a positive `[run.clone] depth` to limit the downloaded Git history. Docker and Daytona ignore `cwd`; use the provider-owned workspace layout and `run.working_dir` for repository-relative commands. +Docker and Daytona are clone-based providers. When a run has a GitHub origin, Fabro clones it into the provider workspace with a history depth of 100. Set `[run.clone] enabled = false` to start with an empty workspace. Set `[run.clone] depth = 0` to clone full history. Docker and Daytona ignore `cwd`; use the provider-owned workspace layout and `run.working_dir` for repository-relative commands. The image must provide `/bin/bash`; Fabro evaluates every sandbox command with it and has no `sh` fallback. Commands run in a **non-login** shell, so login profiles (`/etc/profile.d/*.sh`, `~/.bash_profile`, and `nvm`/`rbenv`/`sdkman` initializers) are not sourced — put anything they set into the Dockerfile's `ENV` instead. Fabro verifies Bash during initialization and again on resume, and fails with remediation rather than reporting the sandbox ready. diff --git a/docs/public/execution/run-configuration.mdx b/docs/public/execution/run-configuration.mdx index 14c6c1ff7..f77137ffc 100644 --- a/docs/public/execution/run-configuration.mdx +++ b/docs/public/execution/run-configuration.mdx @@ -241,7 +241,7 @@ Configure whether clone-based sandboxes clone the run's GitHub origin before exe ```toml title="run.toml" [run.clone] enabled = true -depth = 1 +depth = 100 ``` Set `enabled = false` to start Docker and Daytona runs with an empty provider workspace. Use [prepare steps](#runprepare) to clone or create any files the workflow needs. @@ -249,7 +249,7 @@ Set `enabled = false` to start Docker and Daytona runs with an empty provider wo | Field | Description | |---|---| | `enabled` | When `false`, Fabro skips the repository clone. Defaults to `true`. | -| `depth` | Optional positive Git history depth. Applies to Docker and Daytona. If omitted, Daytona clones full history and Docker uses its default depth of 10. | +| `depth` | Git history depth for Docker and Daytona. Defaults to `100`. Set it to `0` to clone full history. | ### `[run.run_branch]` diff --git a/docs/public/integrations/daytona.mdx b/docs/public/integrations/daytona.mdx index 05fa190e9..828debc0d 100644 --- a/docs/public/integrations/daytona.mdx +++ b/docs/public/integrations/daytona.mdx @@ -141,14 +141,14 @@ provider = "daytona" enabled = false ``` -For a faster clone that keeps only the newest commit, set a clone depth: +Daytona clones 100 commits by default. To keep only the newest commit, set a smaller clone depth: ```toml title="run.toml" [run.clone] depth = 1 ``` -If `depth` is omitted, Daytona clones the full repository history. +Set `depth = 0` to clone the full repository history. If the clone fails without GitHub access configured, Fabro suggests running the setup flow: diff --git a/lib/components/fabro-sandbox/src/clone_source.rs b/lib/components/fabro-sandbox/src/clone_source.rs index ea343bbf4..964084eb2 100644 --- a/lib/components/fabro-sandbox/src/clone_source.rs +++ b/lib/components/fabro-sandbox/src/clone_source.rs @@ -93,8 +93,13 @@ pub(crate) fn exact_fetch_command( commit_sha: &str, depth: usize, ) -> String { + let depth_arg = if depth == 0 { + String::new() + } else { + format!(" --depth {depth}") + }; format!( - "{git} -C {} fetch --depth {depth} --no-tags {} -- {}", + "{git} -C {} fetch{depth_arg} --no-tags {} -- {}", sandbox::shell_quote(checkout_path), sandbox::shell_quote(fetch_source), sandbox::shell_quote(commit_sha), @@ -493,6 +498,19 @@ mod tests { ); } + #[test] + fn exact_fetch_omits_depth_for_full_history() { + assert_eq!( + exact_fetch_command( + "/repos/acme/widgets", + "origin", + "0123456789abcdef0123456789abcdef01234567", + 0, + ), + "git -c maintenance.auto=0 -c gc.auto=0 -C /repos/acme/widgets fetch --no-tags origin -- 0123456789abcdef0123456789abcdef01234567" + ); + } + #[test] fn exact_checkout_verification_rejects_invalid_or_mismatched_head() { let expected = "0123456789abcdef0123456789abcdef01234567"; diff --git a/lib/components/fabro-sandbox/src/config.rs b/lib/components/fabro-sandbox/src/config.rs index 66f55354f..bddc7c369 100644 --- a/lib/components/fabro-sandbox/src/config.rs +++ b/lib/components/fabro-sandbox/src/config.rs @@ -12,7 +12,7 @@ use std::collections::HashMap; use serde::de::{self, MapAccess, Visitor}; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct DaytonaSettings { pub auto_stop_interval: Option, pub labels: Option>, @@ -23,6 +23,19 @@ pub struct DaytonaSettings { pub skip_clone: bool, } +impl Default for DaytonaSettings { + fn default() -> Self { + Self { + auto_stop_interval: None, + labels: None, + snapshot: None, + network: None, + clone_depth: Some(100), + skip_clone: false, + } + } +} + #[derive(Clone, Debug, PartialEq)] pub enum DaytonaNetwork { Block, diff --git a/lib/components/fabro-sandbox/src/daytona/mod.rs b/lib/components/fabro-sandbox/src/daytona/mod.rs index 725f110c8..1d1d8da23 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -3528,7 +3528,7 @@ mod tests { assert!(config.snapshot.is_none()); assert!(config.auto_stop_interval.is_none()); assert!(config.labels.is_none()); - assert!(config.clone_depth.is_none()); + assert_eq!(config.clone_depth, Some(100)); } #[test] diff --git a/lib/components/fabro-sandbox/src/docker.rs b/lib/components/fabro-sandbox/src/docker.rs index 4bc512d04..97e965ed8 100644 --- a/lib/components/fabro-sandbox/src/docker.rs +++ b/lib/components/fabro-sandbox/src/docker.rs @@ -48,7 +48,7 @@ const DOCKER_BASH_REQUIREMENT: &str = "Docker sandboxes require /bin/bash for ev pub(crate) const WORKING_DIRECTORY: &str = "/workspace"; pub(crate) const REPOS_ROOT: &str = "/repos"; -const DEFAULT_GIT_CLONE_DEPTH: usize = 10; +const DEFAULT_GIT_CLONE_DEPTH: usize = 100; const GIT_CLONE_TIMEOUT: Duration = Duration::from_mins(5); #[cfg(test)] const EXEC_STOP_POLL_SLEEP_SECONDS: &str = "0.005"; @@ -121,7 +121,8 @@ pub struct DockerSandboxOptions { pub auto_pull: bool, /// Additional `KEY=VALUE` environment variables for the container. pub env_vars: Vec, - /// Maximum Git history depth fetched during clone. + /// Maximum Git history depth fetched during clone. Zero fetches full + /// history. pub clone_depth: usize, /// Create an empty workspace instead of cloning even when an origin exists. pub skip_clone: bool, @@ -1548,8 +1549,10 @@ fn git_clone_command( command.push_str(&shell_quote(branch)); command.push_str(" --single-branch"); } - command.push_str(" --depth "); - command.push_str(&depth.to_string()); + if depth > 0 { + command.push_str(" --depth "); + command.push_str(&depth.to_string()); + } command.push_str(" --no-tags"); command.push_str(" -- "); command.push_str(&shell_quote(clone_url)); @@ -2596,7 +2599,21 @@ mod tests { ); assert_eq!( command, - "git -c maintenance.auto=0 -c gc.auto=0 clone --depth 10 --no-tags -- https://github.com/fabro-sh/fabro /repos/fabro-sh/fabro" + "git -c maintenance.auto=0 -c gc.auto=0 clone --depth 100 --no-tags -- https://github.com/fabro-sh/fabro /repos/fabro-sh/fabro" + ); + } + + #[test] + fn clone_command_omits_depth_for_full_clone() { + let command = git_clone_command( + "https://github.com/fabro-sh/fabro", + Some("main"), + "/repos/fabro-sh/fabro", + 0, + ); + assert_eq!( + command, + "git -c maintenance.auto=0 -c gc.auto=0 clone --branch main --single-branch --no-tags -- https://github.com/fabro-sh/fabro /repos/fabro-sh/fabro" ); } diff --git a/lib/components/fabro-sandbox/src/from_environment.rs b/lib/components/fabro-sandbox/src/from_environment.rs index c73023354..4bf76696e 100644 --- a/lib/components/fabro-sandbox/src/from_environment.rs +++ b/lib/components/fabro-sandbox/src/from_environment.rs @@ -61,7 +61,7 @@ pub fn daytona_config_from_environment( DaytonaNetwork::AllowList(settings.network.allow.clone()) } }), - clone_depth: clone.depth, + clone_depth: clone.depth.filter(|depth| *depth > 0), skip_clone: !clone.enabled, } } @@ -135,7 +135,6 @@ fn docker_config_from_environment_env( clone_depth: clone .depth .and_then(|depth| usize::try_from(depth).ok()) - .filter(|depth| *depth > 0) .unwrap_or(default_options.clone_depth), skip_clone: !clone.enabled, ..DockerSandboxOptions::default() diff --git a/lib/components/fabro-workflow/src/operations/start.rs b/lib/components/fabro-workflow/src/operations/start.rs index 54ace936c..9955f1a8d 100644 --- a/lib/components/fabro-workflow/src/operations/start.rs +++ b/lib/components/fabro-workflow/src/operations/start.rs @@ -1305,6 +1305,38 @@ reasoning = false ); } + #[test] + fn zero_clone_depth_requests_full_history_from_clone_providers() { + let settings = settings_from_run_layer(RunLayer { + clone: Some(RunCloneLayer { + enabled: None, + depth: Some(0), + }), + ..RunLayer::default() + }); + + assert_eq!(resolve_daytona_config(&settings.run).clone_depth, None); + assert_eq!( + resolve_docker_config(&settings.run, |_| None) + .unwrap() + .clone_depth, + 0 + ); + } + + #[test] + fn clone_providers_default_to_depth_100() { + let settings = settings_from_run_layer(RunLayer::default()); + + assert_eq!(resolve_daytona_config(&settings.run).clone_depth, Some(100)); + assert_eq!( + resolve_docker_config(&settings.run, |_| None) + .unwrap() + .clone_depth, + 100 + ); + } + #[test] fn runtime_mcp_server_wraps_resolve_error_source() { let settings = ResolvedMcpServerSettings { diff --git a/lib/foundation/fabro-config/src/resolve/run.rs b/lib/foundation/fabro-config/src/resolve/run.rs index b078c8dad..ba64fa67d 100644 --- a/lib/foundation/fabro-config/src/resolve/run.rs +++ b/lib/foundation/fabro-config/src/resolve/run.rs @@ -248,21 +248,24 @@ fn resolve_clone( clone: Option<&RunCloneLayer>, errors: &mut Vec, ) -> RunCloneSettings { - let depth = clone.and_then(|clone| clone.depth).and_then(|depth| { - if depth < 1 { - errors.push(ResolveError::Invalid { - path: "run.clone.depth".to_string(), - reason: "depth must be at least 1".to_string(), + let depth = + clone + .and_then(|clone| clone.depth) + .map_or(RunCloneSettings::DEFAULT_DEPTH, |depth| { + if depth < 0 { + errors.push(ResolveError::Invalid { + path: "run.clone.depth".to_string(), + reason: "depth must be at least 0".to_string(), + }); + RunCloneSettings::DEFAULT_DEPTH + } else { + depth + } }); - None - } else { - Some(depth) - } - }); RunCloneSettings { enabled: clone.and_then(|clone| clone.enabled).unwrap_or(true), - depth, + depth: Some(depth), } } diff --git a/lib/foundation/fabro-config/src/tests/resolve_run.rs b/lib/foundation/fabro-config/src/tests/resolve_run.rs index 890dd2028..d66b075ea 100644 --- a/lib/foundation/fabro-config/src/tests/resolve_run.rs +++ b/lib/foundation/fabro-config/src/tests/resolve_run.rs @@ -126,7 +126,7 @@ fn resolves_run_defaults_from_empty_settings() { assert!(!settings.environment.lifecycle.preserve); assert!(settings.environment.lifecycle.stop_on_terminal); assert!(settings.clone.enabled); - assert_eq!(settings.clone.depth, None); + assert_eq!(settings.clone.depth, Some(100)); assert!(settings.run_branch.enabled); assert!(settings.run_branch.push); assert!(settings.meta_branch.enabled); @@ -320,8 +320,8 @@ push = false } #[test] -fn rejects_non_positive_clone_depth() { - let error = super::workflow_settings_from_toml( +fn zero_clone_depth_requests_full_history() { + let settings = super::workflow_settings_from_toml( r" _version = 1 @@ -329,7 +329,23 @@ _version = 1 depth = 0 ", ) - .expect_err("zero clone depth should not resolve"); + .expect("zero clone depth should resolve") + .run; + + assert_eq!(settings.clone.depth, Some(0)); +} + +#[test] +fn rejects_negative_clone_depth() { + let error = super::workflow_settings_from_toml( + r" +_version = 1 + +[run.clone] +depth = -1 +", + ) + .expect_err("negative clone depth should not resolve"); let message = error.to_string(); assert!( @@ -337,7 +353,7 @@ depth = 0 "unexpected error: {message}" ); assert!( - message.contains("at least 1"), + message.contains("at least 0"), "unexpected error: {message}" ); } diff --git a/lib/foundation/fabro-types/src/settings/run.rs b/lib/foundation/fabro-types/src/settings/run.rs index 5d96e9ce2..a47cb48fb 100644 --- a/lib/foundation/fabro-types/src/settings/run.rs +++ b/lib/foundation/fabro-types/src/settings/run.rs @@ -961,19 +961,34 @@ impl Default for RunCheckpointSettings { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunCloneSettings { pub enabled: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde( + default = "default_clone_depth", + skip_serializing_if = "Option::is_none" + )] pub depth: Option, } +impl RunCloneSettings { + pub const DEFAULT_DEPTH: i32 = 100; +} + impl Default for RunCloneSettings { fn default() -> Self { Self { enabled: true, - depth: None, + depth: Some(Self::DEFAULT_DEPTH), } } } +#[expect( + clippy::unnecessary_wraps, + reason = "serde default provider must return the field's Option type" +)] +fn default_clone_depth() -> Option { + Some(RunCloneSettings::DEFAULT_DEPTH) +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunBranchSettings { pub enabled: bool, diff --git a/lib/packages/fabro-api-client/src/models/run-clone-settings.ts b/lib/packages/fabro-api-client/src/models/run-clone-settings.ts index 48c5959df..79791f502 100644 --- a/lib/packages/fabro-api-client/src/models/run-clone-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-clone-settings.ts @@ -16,5 +16,8 @@ export interface RunCloneSettings { 'enabled': boolean; + /** + * Git history depth. Set to 0 to clone full history. + */ 'depth'?: number; }