From 438bab29f08f8d15120ee5eea7a99b5eabf1f323 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 21 Aug 2026 17:11:24 -0400 Subject: [PATCH 1/5] feat: support shallow sandbox clones --- Cargo.lock | 76 +++++++++++++++---- Cargo.toml | 4 +- docs/public/api-reference/fabro-api.yaml | 4 + docs/public/execution/environments.mdx | 2 +- docs/public/execution/run-configuration.mdx | 6 ++ docs/public/integrations/daytona.mdx | 9 +++ lib/apps/fabro-server/src/run_manifest.rs | 4 +- lib/components/fabro-sandbox/src/config.rs | 1 + .../fabro-sandbox/src/daytona/mod.rs | 32 +++++--- lib/components/fabro-sandbox/src/details.rs | 30 ++++++-- lib/components/fabro-sandbox/src/docker.rs | 31 ++++++-- .../fabro-sandbox/src/from_environment.rs | 42 +++++----- .../fabro-workflow/src/operations/start.rs | 12 ++- lib/foundation/fabro-config/src/layers/run.rs | 2 + .../fabro-config/src/resolve/run.rs | 20 ++++- .../fabro-config/src/tests/resolve_run.rs | 26 +++++++ .../fabro-types/src/settings/run.rs | 7 +- .../src/models/run-clone-settings.ts | 1 + 18 files changed, 241 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 800846e58..4a8172d93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1870,7 +1870,7 @@ checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "daytona-api-client" version = "0.1.0" -source = "git+https://github.com/brynary/daytona-sdk-rust?rev=73c9c458dd1a1d096afd3521175637af82afd8d8#73c9c458dd1a1d096afd3521175637af82afd8d8" +source = "git+https://github.com/brynary/daytona-sdk-rust?rev=be2c7b7272740d47c023cac8abc9f63c1a51a511#be2c7b7272740d47c023cac8abc9f63c1a51a511" dependencies = [ "reqwest 0.13.2", "reqwest-middleware", @@ -1884,7 +1884,7 @@ dependencies = [ [[package]] name = "daytona-sdk" version = "0.1.0" -source = "git+https://github.com/brynary/daytona-sdk-rust?rev=73c9c458dd1a1d096afd3521175637af82afd8d8#73c9c458dd1a1d096afd3521175637af82afd8d8" +source = "git+https://github.com/brynary/daytona-sdk-rust?rev=be2c7b7272740d47c023cac8abc9f63c1a51a511#be2c7b7272740d47c023cac8abc9f63c1a51a511" dependencies = [ "daytona-api-client", "daytona-toolbox-client", @@ -1904,13 +1904,15 @@ dependencies = [ [[package]] name = "daytona-toolbox-client" version = "0.1.0" -source = "git+https://github.com/brynary/daytona-sdk-rust?rev=73c9c458dd1a1d096afd3521175637af82afd8d8#73c9c458dd1a1d096afd3521175637af82afd8d8" +source = "git+https://github.com/brynary/daytona-sdk-rust?rev=be2c7b7272740d47c023cac8abc9f63c1a51a511#be2c7b7272740d47c023cac8abc9f63c1a51a511" dependencies = [ "reqwest 0.13.2", "reqwest-middleware", "serde", "serde_json", "serde_repr", + "tokio", + "tokio-util", "url", ] @@ -2072,7 +2074,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2199,7 +2201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4388,6 +4390,22 @@ dependencies = [ "webpki-roots 1.0.6", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -4440,7 +4458,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -5315,6 +5333,23 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe 0.2.1", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -5389,7 +5424,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6386,7 +6421,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -6671,11 +6706,13 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", "mime", "mime_guess", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -6687,6 +6724,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls", "tokio-util", "tower", @@ -6860,7 +6898,7 @@ dependencies = [ "errno 0.3.14", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6919,7 +6957,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -7443,7 +7481,7 @@ version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ - "errno 0.3.14", + "errno 0.2.8", "libc", ] @@ -8033,7 +8071,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -8079,7 +8117,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -8231,6 +8269,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -9132,7 +9180,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4a6437546..ebf4764e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,8 +97,8 @@ twin-openai = { path = "test/twin/openai" } twin-github = { path = "test/twin/github" } tokio-tungstenite = { version = "0.26", features = ["rustls-tls-webpki-roots"] } futures-util = "0.3" -daytona-sdk = { git = "https://github.com/brynary/daytona-sdk-rust", rev = "73c9c458dd1a1d096afd3521175637af82afd8d8", package = "daytona-sdk" } -daytona-api-client = { git = "https://github.com/brynary/daytona-sdk-rust", rev = "73c9c458dd1a1d096afd3521175637af82afd8d8", package = "daytona-api-client" } +daytona-sdk = { git = "https://github.com/brynary/daytona-sdk-rust", rev = "be2c7b7272740d47c023cac8abc9f63c1a51a511", package = "daytona-sdk" } +daytona-api-client = { git = "https://github.com/brynary/daytona-sdk-rust", rev = "be2c7b7272740d47c023cac8abc9f63c1a51a511", package = "daytona-api-client" } sentry = { version = "0.35", default-features = false, features = ["backtrace", "contexts", "ureq", "rustls"] } fork = "0.2" exec = "0.3" diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index ee2eb143e..dafddf42f 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -14633,6 +14633,10 @@ components: properties: enabled: type: boolean + depth: + type: integer + format: int32 + minimum: 1 RunBranchSettings: type: object diff --git a/docs/public/execution/environments.mdx b/docs/public/execution/environments.mdx index 82a72d5ed..c58e602b9 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. 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. 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. 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 d2ce9bd5e..14c6c1ff7 100644 --- a/docs/public/execution/run-configuration.mdx +++ b/docs/public/execution/run-configuration.mdx @@ -241,10 +241,16 @@ Configure whether clone-based sandboxes clone the run's GitHub origin before exe ```toml title="run.toml" [run.clone] enabled = true +depth = 1 ``` 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. +| 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. | + ### `[run.run_branch]` Configure Fabro's managed `fabro/run/` checkpoint branch. diff --git a/docs/public/integrations/daytona.mdx b/docs/public/integrations/daytona.mdx index 1a2f25790..05fa190e9 100644 --- a/docs/public/integrations/daytona.mdx +++ b/docs/public/integrations/daytona.mdx @@ -141,6 +141,15 @@ provider = "daytona" enabled = false ``` +For a faster clone that keeps only the newest commit, set a clone depth: + +```toml title="run.toml" +[run.clone] +depth = 1 +``` + +If `depth` is omitted, Daytona clones the full repository history. + If the clone fails without GitHub access configured, Fabro suggests running the setup flow: ``` diff --git a/lib/apps/fabro-server/src/run_manifest.rs b/lib/apps/fabro-server/src/run_manifest.rs index fda5f8087..6a5045e81 100644 --- a/lib/apps/fabro-server/src/run_manifest.rs +++ b/lib/apps/fabro-server/src/run_manifest.rs @@ -669,11 +669,11 @@ pub(crate) fn effective_sandbox_provider(settings: &RunNamespace) -> SandboxProv } fn resolve_daytona_config(settings: &RunNamespace) -> DaytonaConfig { - daytona_config_from_environment(&settings.environment, !settings.clone.enabled) + daytona_config_from_environment(&settings.environment, &settings.clone) } fn resolve_docker_config(settings: &RunNamespace) -> DockerSandboxOptions { - docker_config_from_environment(&settings.environment, !settings.clone.enabled) + docker_config_from_environment(&settings.environment, &settings.clone) } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/lib/components/fabro-sandbox/src/config.rs b/lib/components/fabro-sandbox/src/config.rs index c1304db5a..66f55354f 100644 --- a/lib/components/fabro-sandbox/src/config.rs +++ b/lib/components/fabro-sandbox/src/config.rs @@ -18,6 +18,7 @@ pub struct DaytonaSettings { pub labels: Option>, pub snapshot: Option, pub network: Option, + pub clone_depth: Option, #[serde(default)] pub skip_clone: bool, } diff --git a/lib/components/fabro-sandbox/src/daytona/mod.rs b/lib/components/fabro-sandbox/src/daytona/mod.rs index a932ae826..725f110c8 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -106,12 +106,15 @@ fn daytona_git_clone_options( commit_id: Option, username: Option, password: Option, + depth: Option, ) -> GitCloneOptions { GitCloneOptions { branch, commit_id, username, password, + depth, + ..GitCloneOptions::default() } } @@ -121,10 +124,10 @@ pub(crate) fn daytona_not_found(err: &DaytonaError) -> bool { /// Permissions a Daytona API key needs for Fabro's snapshot and sandbox flow. pub const REQUIRED_DAYTONA_PERMISSIONS: &[Permissions] = &[ - Permissions::WriteColonSnapshots, - Permissions::DeleteColonSnapshots, - Permissions::WriteColonSandboxes, - Permissions::DeleteColonSandboxes, + Permissions::WRITE_SNAPSHOTS, + Permissions::DELETE_SNAPSHOTS, + Permissions::WRITE_SANDBOXES, + Permissions::DELETE_SANDBOXES, ]; pub use crate::config::{ @@ -258,10 +261,10 @@ fn join_perms(perms: &[Permissions]) -> String { fn perm_wire_str(permission: Permissions) -> &'static str { match permission { - Permissions::WriteColonSnapshots => "write:snapshots", - Permissions::DeleteColonSnapshots => "delete:snapshots", - Permissions::WriteColonSandboxes => "write:sandboxes", - Permissions::DeleteColonSandboxes => "delete:sandboxes", + Permissions::WRITE_SNAPSHOTS => "write:snapshots", + Permissions::DELETE_SNAPSHOTS => "delete:snapshots", + Permissions::WRITE_SANDBOXES => "write:sandboxes", + Permissions::DELETE_SANDBOXES => "delete:sandboxes", _ => "unknown", } } @@ -1625,6 +1628,7 @@ impl Sandbox for DaytonaSandbox { commit_sha.clone(), username.clone(), password.clone(), + self.config.clone_depth, ); async move { git_svc.clone(origin, target, options).await } }, @@ -3145,6 +3149,7 @@ mod tests { Some("0123456789abcdef0123456789abcdef01234567".to_string()), Some("x-access-token".to_string()), Some("secret".to_string()), + Some(1), ); assert_eq!(options.branch.as_deref(), Some("feature/work")); @@ -3154,6 +3159,7 @@ mod tests { ); assert_eq!(options.username.as_deref(), Some("x-access-token")); assert_eq!(options.password.as_deref(), Some("secret")); + assert_eq!(options.depth, Some(1)); } fn mock_sandbox_body(sandbox_id: &str) -> serde_json::Value { @@ -3171,6 +3177,7 @@ mod tests { "gpu": 0.0, "memory": 4.0, "disk": 20.0, + "toolboxProxyUrl": "https://proxy.example.com/toolbox", "state": "started" }) } @@ -3488,6 +3495,7 @@ mod tests { "size": null, "entrypoint": null, "errorReason": null, + "sourceSandboxId": null, "lastUsedAt": null, "createdAt": "2026-05-01T00:00:00Z", "updatedAt": "2026-05-01T00:00:00Z" @@ -3509,6 +3517,7 @@ mod tests { "gpu": 0.0, "memory": 4.0, "disk": 20.0, + "toolboxProxyUrl": "https://proxy.example.com/toolbox", "state": state.to_string() }) } @@ -3519,6 +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()); } #[test] @@ -4195,10 +4205,7 @@ mod tests { fn missing_display_uses_daytona_wire_scope_names() { let check = DaytonaKeyCheck { key_name: "delete-only".to_string(), - missing: vec![ - Permissions::WriteColonSnapshots, - Permissions::WriteColonSandboxes, - ], + missing: vec![Permissions::WRITE_SNAPSHOTS, Permissions::WRITE_SANDBOXES], }; assert_eq!(check.missing_display(), "write:snapshots, write:sandboxes"); @@ -4337,6 +4344,7 @@ mod tests { "gpu": 0.0, "memory": 4.0, "disk": 20.0, + "toolboxProxyUrl": "https://proxy.example.com/toolbox", "state": "started" })); }) diff --git a/lib/components/fabro-sandbox/src/details.rs b/lib/components/fabro-sandbox/src/details.rs index 2ada66652..0d7fe6390 100644 --- a/lib/components/fabro-sandbox/src/details.rs +++ b/lib/components/fabro-sandbox/src/details.rs @@ -672,18 +672,22 @@ pub(crate) mod daytona { DaytonaState::Creating | DaytonaState::PendingBuild | DaytonaState::BuildingSnapshot - | DaytonaState::PullingSnapshot => SandboxState::Provisioning, - DaytonaState::Starting => SandboxState::Starting, - DaytonaState::Started => SandboxState::Running, - DaytonaState::Stopping | DaytonaState::Archiving => SandboxState::Stopping, + | DaytonaState::PullingSnapshot + | DaytonaState::Forking => SandboxState::Provisioning, + DaytonaState::Starting | DaytonaState::Resuming => SandboxState::Starting, + DaytonaState::Started | DaytonaState::Snapshotting => SandboxState::Running, + DaytonaState::Stopping | DaytonaState::Archiving | DaytonaState::Pausing => { + SandboxState::Stopping + } DaytonaState::Stopped => SandboxState::Stopped, + DaytonaState::Paused => SandboxState::Paused, DaytonaState::Restoring => SandboxState::Restoring, DaytonaState::Resizing => SandboxState::Resizing, DaytonaState::Archived => SandboxState::Archived, DaytonaState::Destroying => SandboxState::Deleting, DaytonaState::Destroyed => SandboxState::Deleted, DaytonaState::Error | DaytonaState::BuildFailed => SandboxState::Error, - DaytonaState::Unknown => SandboxState::Unknown, + DaytonaState::Unknown | DaytonaState::UnknownDefaultOpenApi => SandboxState::Unknown, } } @@ -755,6 +759,22 @@ pub(crate) mod daytona { ); } + #[test] + fn pause_states_normalize_to_fabro_states() { + assert_eq!( + normalize_daytona_state(DaytonaState::Pausing), + SandboxState::Stopping + ); + assert_eq!( + normalize_daytona_state(DaytonaState::Paused), + SandboxState::Paused + ); + assert_eq!( + normalize_daytona_state(DaytonaState::Resuming), + SandboxState::Starting + ); + } + #[test] fn gibibytes_to_bytes_converts_positive_values() { assert_eq!(gibibytes_to_bytes(2.0), Some(2 * 1024 * 1024 * 1024)); diff --git a/lib/components/fabro-sandbox/src/docker.rs b/lib/components/fabro-sandbox/src/docker.rs index c3842ef9a..4bc512d04 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 GIT_CLONE_DEPTH: usize = 10; +const DEFAULT_GIT_CLONE_DEPTH: usize = 10; const GIT_CLONE_TIMEOUT: Duration = Duration::from_mins(5); #[cfg(test)] const EXEC_STOP_POLL_SLEEP_SECONDS: &str = "0.005"; @@ -121,6 +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. + pub clone_depth: usize, /// Create an empty workspace instead of cloning even when an origin exists. pub skip_clone: bool, } @@ -134,6 +136,7 @@ impl Default for DockerSandboxOptions { cpu_quota: None, auto_pull: true, env_vars: Vec::new(), + clone_depth: DEFAULT_GIT_CLONE_DEPTH, skip_clone: false, } } @@ -954,7 +957,7 @@ impl DockerSandbox { &layout.primary_repo_path, "origin", expected_sha, - GIT_CLONE_DEPTH, + self.config.clone_depth, ); if let Err(failure) = self .retry_git_transfer( @@ -992,8 +995,12 @@ impl DockerSandbox { return Err(self.report_clone_failure(&origin_url, error)); } } else { - let command = - git_clone_command(clone_url, branch.as_deref(), &layout.primary_repo_path); + let command = git_clone_command( + clone_url, + branch.as_deref(), + &layout.primary_repo_path, + self.config.clone_depth, + ); if let Err(failure) = self .retry_git_transfer( &command, @@ -1529,7 +1536,12 @@ async fn cache_docker_stdio_completion( } } -fn git_clone_command(clone_url: &str, branch: Option<&str>, checkout_path: &str) -> String { +fn git_clone_command( + clone_url: &str, + branch: Option<&str>, + checkout_path: &str, + depth: usize, +) -> String { let mut command = format!("{} clone", sandbox::GIT); if let Some(branch) = branch { command.push_str(" --branch "); @@ -1537,7 +1549,7 @@ fn git_clone_command(clone_url: &str, branch: Option<&str>, checkout_path: &str) command.push_str(" --single-branch"); } command.push_str(" --depth "); - command.push_str(&GIT_CLONE_DEPTH.to_string()); + command.push_str(&depth.to_string()); command.push_str(" --no-tags"); command.push_str(" -- "); command.push_str(&shell_quote(clone_url)); @@ -2556,19 +2568,21 @@ mod tests { let options = DockerSandboxOptions::default(); assert_eq!(options.image, "buildpack-deps:noble"); assert_eq!(options.network_mode.as_deref(), Some("bridge")); + assert_eq!(options.clone_depth, DEFAULT_GIT_CLONE_DEPTH); assert!(!options.skip_clone); } #[test] - fn clone_command_uses_depth_ten_without_tags_for_branch_clone() { + fn clone_command_uses_configured_depth_without_tags_for_branch_clone() { let command = git_clone_command( "https://github.com/fabro-sh/fabro", Some("main"), "/repos/fabro-sh/fabro", + 1, ); assert_eq!( command, - "git -c maintenance.auto=0 -c gc.auto=0 clone --branch main --single-branch --depth 10 --no-tags -- https://github.com/fabro-sh/fabro /repos/fabro-sh/fabro" + "git -c maintenance.auto=0 -c gc.auto=0 clone --branch main --single-branch --depth 1 --no-tags -- https://github.com/fabro-sh/fabro /repos/fabro-sh/fabro" ); } @@ -2578,6 +2592,7 @@ mod tests { "https://github.com/fabro-sh/fabro", None, "/repos/fabro-sh/fabro", + DEFAULT_GIT_CLONE_DEPTH, ); assert_eq!( command, diff --git a/lib/components/fabro-sandbox/src/from_environment.rs b/lib/components/fabro-sandbox/src/from_environment.rs index 5399927ea..c73023354 100644 --- a/lib/components/fabro-sandbox/src/from_environment.rs +++ b/lib/components/fabro-sandbox/src/from_environment.rs @@ -8,7 +8,9 @@ use std::path::{Path, PathBuf}; use fabro_types::settings::ResolveError; #[cfg(feature = "daytona")] use fabro_types::settings::run::DockerfileSource as ResolvedDockerfileSource; -use fabro_types::settings::run::{EnvironmentNetworkMode, RunEnvironmentSettings}; +use fabro_types::settings::run::{ + EnvironmentNetworkMode, RunCloneSettings, RunEnvironmentSettings, +}; #[cfg(feature = "daytona")] use crate::config::{ @@ -23,19 +25,16 @@ use crate::docker::DockerSandboxOptions; #[must_use] pub fn daytona_config_from_environment( settings: &RunEnvironmentSettings, - skip_clone: bool, + clone: &RunCloneSettings, ) -> DaytonaConfig { DaytonaConfig { auto_stop_interval: settings .lifecycle .auto_stop .map(|duration| duration_to_minutes_i32(duration.as_std())), - labels: (!settings.labels.is_empty()).then(|| settings.labels.clone()), - snapshot: settings - .image - .dockerfile - .as_ref() - .map(|dockerfile| DaytonaSnapshotSettings { + labels: (!settings.labels.is_empty()).then(|| settings.labels.clone()), + snapshot: settings.image.dockerfile.as_ref().map(|dockerfile| { + DaytonaSnapshotSettings { cpu: settings.resources.cpu, memory: settings .resources @@ -53,15 +52,17 @@ pub fn daytona_config_from_environment( SandboxDockerfileSource::Path { path: path.clone() } } }), - }), - network: Some(match settings.network.mode { + } + }), + network: Some(match settings.network.mode { EnvironmentNetworkMode::Block => DaytonaNetwork::Block, EnvironmentNetworkMode::AllowAll => DaytonaNetwork::AllowAll, EnvironmentNetworkMode::CidrAllowList => { DaytonaNetwork::AllowList(settings.network.allow.clone()) } }), - skip_clone, + clone_depth: clone.depth, + skip_clone: !clone.enabled, } } @@ -69,7 +70,7 @@ pub fn daytona_config_from_environment( #[must_use] pub fn docker_config_from_environment( settings: &RunEnvironmentSettings, - skip_clone: bool, + clone: &RunCloneSettings, ) -> DockerSandboxOptions { // No vault is available on this path (server preflight / manifest), so a // `{{ secrets.* }}` value keeps its source form. Nothing else is left to @@ -84,25 +85,23 @@ pub fn docker_config_from_environment( .iter() .map(|(key, value)| (key.clone(), value.as_source())) .collect(); - docker_config_from_environment_env(settings, skip_clone, env) + docker_config_from_environment_env(settings, clone, env) } #[cfg(feature = "docker")] pub fn docker_config_from_environment_with_secrets( settings: &RunEnvironmentSettings, - skip_clone: bool, + clone: &RunCloneSettings, secrets_lookup: impl FnMut(&str) -> Option, ) -> Result { let env = settings.resolve_env(secrets_lookup)?; - Ok(docker_config_from_environment_env( - settings, skip_clone, env, - )) + Ok(docker_config_from_environment_env(settings, clone, env)) } #[cfg(feature = "docker")] fn docker_config_from_environment_env( settings: &RunEnvironmentSettings, - skip_clone: bool, + clone: &RunCloneSettings, env: std::collections::HashMap, ) -> DockerSandboxOptions { let mut env_vars = env @@ -133,7 +132,12 @@ fn docker_config_from_environment_env( .cpu .map(|cpu| i64::from(cpu).saturating_mul(100_000)), env_vars, - skip_clone, + 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 3ba470151..54ace936c 100644 --- a/lib/components/fabro-workflow/src/operations/start.rs +++ b/lib/components/fabro-workflow/src/operations/start.rs @@ -591,7 +591,7 @@ fn resolve_sandbox_provider(settings: &ResolvedRunSettings) -> SandboxProviderKi } fn resolve_daytona_config(settings: &ResolvedRunSettings) -> DaytonaConfig { - daytona_config_from_environment(&settings.environment, !settings.clone.enabled) + daytona_config_from_environment(&settings.environment, &settings.clone) } fn resolve_docker_config( @@ -600,7 +600,7 @@ fn resolve_docker_config( ) -> Result { docker_config_from_environment_with_secrets( &settings.environment, - !settings.clone.enabled, + &settings.clone, secrets_lookup, ) .map_err(|err| Error::engine_with_source("failed to resolve Docker environment config", err)) @@ -1285,6 +1285,7 @@ reasoning = false let settings = settings_from_run_layer(RunLayer { clone: Some(RunCloneLayer { enabled: Some(false), + depth: Some(1), }), ..RunLayer::default() }); @@ -1295,6 +1296,13 @@ reasoning = false .skip_clone ); assert!(resolve_daytona_config(&settings.run).skip_clone); + assert_eq!(resolve_daytona_config(&settings.run).clone_depth, Some(1)); + assert_eq!( + resolve_docker_config(&settings.run, |_| None) + .unwrap() + .clone_depth, + 1 + ); } #[test] diff --git a/lib/foundation/fabro-config/src/layers/run.rs b/lib/foundation/fabro-config/src/layers/run.rs index 8b020666f..b8c265b8d 100644 --- a/lib/foundation/fabro-config/src/layers/run.rs +++ b/lib/foundation/fabro-config/src/layers/run.rs @@ -303,6 +303,8 @@ pub struct RunCheckpointLayer { pub struct RunCloneLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub depth: Option, } /// `[run.run_branch]` — Fabro-managed checkpoint branch policy. diff --git a/lib/foundation/fabro-config/src/resolve/run.rs b/lib/foundation/fabro-config/src/resolve/run.rs index 9010f25f8..b078c8dad 100644 --- a/lib/foundation/fabro-config/src/resolve/run.rs +++ b/lib/foundation/fabro-config/src/resolve/run.rs @@ -28,7 +28,7 @@ pub fn resolve_run( mcp_server_catalog: &HashMap, errors: &mut Vec, ) -> RunNamespace { - let clone = resolve_clone(layer.clone.as_ref()); + let clone = resolve_clone(layer.clone.as_ref(), errors); let run_branch = resolve_run_branch(layer.run_branch.as_ref()); let mut meta_branch = resolve_meta_branch(layer.meta_branch.as_ref()); if !run_branch.enabled { @@ -244,9 +244,25 @@ fn resolve_checkpoint(checkpoint: Option<&RunCheckpointLayer>) -> RunCheckpointS } } -fn resolve_clone(clone: Option<&RunCloneLayer>) -> RunCloneSettings { +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(), + }); + None + } else { + Some(depth) + } + }); + RunCloneSettings { enabled: clone.and_then(|clone| clone.enabled).unwrap_or(true), + depth, } } diff --git a/lib/foundation/fabro-config/src/tests/resolve_run.rs b/lib/foundation/fabro-config/src/tests/resolve_run.rs index d416eb651..890dd2028 100644 --- a/lib/foundation/fabro-config/src/tests/resolve_run.rs +++ b/lib/foundation/fabro-config/src/tests/resolve_run.rs @@ -126,6 +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!(settings.run_branch.enabled); assert!(settings.run_branch.push); assert!(settings.meta_branch.enabled); @@ -296,6 +297,7 @@ _version = 1 [run.clone] enabled = false +depth = 1 [run.run_branch] enabled = true @@ -310,12 +312,36 @@ push = false .run; assert!(!settings.clone.enabled); + assert_eq!(settings.clone.depth, Some(1)); assert!(settings.run_branch.enabled); assert!(!settings.run_branch.push); assert!(settings.meta_branch.enabled); assert!(!settings.meta_branch.push); } +#[test] +fn rejects_non_positive_clone_depth() { + let error = super::workflow_settings_from_toml( + r" +_version = 1 + +[run.clone] +depth = 0 +", + ) + .expect_err("zero clone depth should not resolve"); + + let message = error.to_string(); + assert!( + message.contains("run.clone.depth"), + "unexpected error: {message}" + ); + assert!( + message.contains("at least 1"), + "unexpected error: {message}" + ); +} + #[test] fn disabling_run_branch_forces_meta_branch_off() { let settings = super::workflow_settings_from_toml( diff --git a/lib/foundation/fabro-types/src/settings/run.rs b/lib/foundation/fabro-types/src/settings/run.rs index 8e589a57a..5d96e9ce2 100644 --- a/lib/foundation/fabro-types/src/settings/run.rs +++ b/lib/foundation/fabro-types/src/settings/run.rs @@ -961,11 +961,16 @@ impl Default for RunCheckpointSettings { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunCloneSettings { pub enabled: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub depth: Option, } impl Default for RunCloneSettings { fn default() -> Self { - Self { enabled: true } + Self { + enabled: true, + depth: None, + } } } 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 7258d9d8d..48c5959df 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,4 +16,5 @@ export interface RunCloneSettings { 'enabled': boolean; + 'depth'?: number; } From 6179470eb2c69ae9cd9f33c9070bf365afeec52d Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 21 Aug 2026 17:25:58 -0400 Subject: [PATCH 2/5] 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; } From 1669791956882590636089c244b84fdb3e61ceff Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 21 Aug 2026 17:47:02 -0400 Subject: [PATCH 3/5] test: update clone depth snapshots --- lib/apps/fabro-cli/tests/it/cmd/attach.rs | 1 + lib/apps/fabro-cli/tests/it/cmd/inspect.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/apps/fabro-cli/tests/it/cmd/attach.rs b/lib/apps/fabro-cli/tests/it/cmd/attach.rs index 9f388c14b..93cd58c83 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/attach.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/attach.rs @@ -924,6 +924,7 @@ fn attach_json_errors_without_prompting_for_human_input() { "skip_git_hooks": false }, "clone": { + "depth": 100, "enabled": true }, "environment": { diff --git a/lib/apps/fabro-cli/tests/it/cmd/inspect.rs b/lib/apps/fabro-cli/tests/it/cmd/inspect.rs index d01e477e1..56821fe45 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/inspect.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/inspect.rs @@ -152,7 +152,8 @@ fn inspect_resolves_selector_via_server_endpoint() { "commit_timeout_ms": 30000 }, "clone": { - "enabled": true + "enabled": true, + "depth": 100 }, "run_branch": { "enabled": true, From fe1d9dc69138079fdcb4fbc04bcdb80b924ae7db Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 21 Aug 2026 17:57:22 -0400 Subject: [PATCH 4/5] test: isolate SQLite checkpoint restoration --- .../fabro-store/src/legacy_blob_import.rs | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/components/fabro-store/src/legacy_blob_import.rs b/lib/components/fabro-store/src/legacy_blob_import.rs index cf7f4390b..06199edc2 100644 --- a/lib/components/fabro-store/src/legacy_blob_import.rs +++ b/lib/components/fabro-store/src/legacy_blob_import.rs @@ -790,10 +790,10 @@ mod tests { type TestResult = std::result::Result>; struct TestContext { - _dir: tempfile::TempDir, + dir: tempfile::TempDir, source: Database, source_db: slatedb::Db, - sqlite: fabro_db::Database, + sqlite: sqlx::SqlitePool, target: BlobStore, } @@ -809,9 +809,10 @@ mod tests { let dir = tempfile::tempdir()?; let sqlite = fabro_db::Database::connect(dir.path().join("fabro.sqlite3")).await?; sqlite.migrate().await?; - let target = BlobStore::new(sqlite.clone_pool()); + let sqlite = sqlite.clone_pool(); + let target = BlobStore::new(sqlite.clone()); Ok(Self { - _dir: dir, + dir, source, source_db, sqlite, @@ -819,6 +820,22 @@ mod tests { }) } + async fn new_with_single_sqlite_connection() -> TestResult { + let mut context = Self::new().await?; + context.sqlite.close().await; + let options = SqliteConnectOptions::new() + .filename(context.dir.path().join("fabro.sqlite3")) + .foreign_keys(true) + .journal_mode(SqliteJournalMode::Wal); + let sqlite = SqlitePoolOptions::new() + .max_connections(1) + .connect_with(options) + .await?; + context.target = BlobStore::new(sqlite.clone()); + context.sqlite = sqlite; + Ok(context) + } + async fn put_blob(&self, bytes: &[u8]) -> TestResult { let hash = BlobHash::new(bytes); let key = SlateKey::new("blobs").with("sha256").with(hash); @@ -842,7 +859,7 @@ mod tests { async fn destination_rows(&self) -> TestResult { Ok(sqlx::query_scalar("SELECT COUNT(*) FROM blobs") - .fetch_one(self.sqlite.pool()) + .fetch_one(&self.sqlite) .await?) } @@ -850,7 +867,7 @@ mod tests { sqlx::query("INSERT INTO blobs (hash, data) VALUES (?, ?)") .bind(hash.to_string()) .bind(bytes) - .execute(self.sqlite.pool()) + .execute(&self.sqlite) .await?; Ok(()) } @@ -858,20 +875,20 @@ mod tests { async fn delete_destination(&self, hash: BlobHash) -> TestResult<()> { sqlx::query("DELETE FROM blobs WHERE hash = ?") .bind(hash.to_string()) - .execute(self.sqlite.pool()) + .execute(&self.sqlite) .await?; Ok(()) } async fn set_automatic_checkpoint(&self, pages: i64) -> TestResult<()> { - let mut connection = self.sqlite.pool().acquire().await?; + let mut connection = self.sqlite.acquire().await?; let statement = sqlx::AssertSqlSafe(format!("PRAGMA wal_autocheckpoint = {pages}")); sqlx::query(statement).execute(&mut *connection).await?; Ok(()) } async fn automatic_checkpoint(&self) -> TestResult { - let mut connection = self.sqlite.pool().acquire().await?; + let mut connection = self.sqlite.acquire().await?; Ok(sqlx::query_scalar("PRAGMA wal_autocheckpoint") .fetch_one(&mut *connection) .await?) @@ -1341,13 +1358,13 @@ mod tests { #[tokio::test] async fn automatic_checkpoint_setting_is_restored_after_success_and_failure() -> TestResult<()> { - let success = TestContext::new().await?; + let success = TestContext::new_with_single_sqlite_connection().await?; success.set_automatic_checkpoint(37).await?; success.put_blob(b"success").await?; success.import().await?; assert_eq!(success.automatic_checkpoint().await?, 37); - let failure = TestContext::new().await?; + let failure = TestContext::new_with_single_sqlite_connection().await?; failure.set_automatic_checkpoint(41).await?; let mut invalid_key = legacy_prefix(); invalid_key.extend_from_slice(&[b'z'; 64]); From 09f5bb0f84f11a3174edd7ba8eca436d676e3e5a Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 21 Aug 2026 18:37:39 -0400 Subject: [PATCH 5/5] Simplify clone depth plumbing Make RunCloneSettings::DEFAULT_DEPTH the single owner of the default depth, and interpret the "0 = full history" sentinel in one place via RunCloneSettings::depth_limit(). Docker's clone_depth becomes Option to match Daytona's encoding, with a shared depth_argument() helper for both git command builders. Drop the unreachable Option on the resolved depth field, the hand-written DaytonaSettings::Default, and the pure-forwarding daytona_git_clone_options helper. The blob-import test helper reuses the pool's own connect options instead of rebuilding a partial copy. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019bgXj5J218RXfiT72qhbLV --- .../fabro-sandbox/src/clone_source.rs | 20 +++---- lib/components/fabro-sandbox/src/config.rs | 17 ++---- .../fabro-sandbox/src/daytona/mod.rs | 54 ++++--------------- lib/components/fabro-sandbox/src/docker.rs | 24 ++++----- .../fabro-sandbox/src/from_environment.rs | 7 ++- .../fabro-store/src/legacy_blob_import.rs | 9 ++-- .../fabro-workflow/src/operations/start.rs | 6 +-- .../fabro-config/src/resolve/run.rs | 26 ++++----- .../fabro-config/src/tests/resolve_run.rs | 6 +-- .../fabro-types/src/settings/run.rs | 22 ++++---- 10 files changed, 67 insertions(+), 124 deletions(-) diff --git a/lib/components/fabro-sandbox/src/clone_source.rs b/lib/components/fabro-sandbox/src/clone_source.rs index 964084eb2..9f03a93e3 100644 --- a/lib/components/fabro-sandbox/src/clone_source.rs +++ b/lib/components/fabro-sandbox/src/clone_source.rs @@ -91,13 +91,9 @@ pub(crate) fn exact_fetch_command( checkout_path: &str, fetch_source: &str, commit_sha: &str, - depth: usize, + depth: Option, ) -> String { - let depth_arg = if depth == 0 { - String::new() - } else { - format!(" --depth {depth}") - }; + let depth_arg = depth_argument(depth); format!( "{git} -C {} fetch{depth_arg} --no-tags {} -- {}", sandbox::shell_quote(checkout_path), @@ -107,6 +103,12 @@ pub(crate) fn exact_fetch_command( ) } +/// Leading-space ` --depth N` fragment for a Git command, or empty when +/// `depth` is `None` to fetch full history. +pub(crate) fn depth_argument(depth: Option) -> String { + depth.map_or_else(String::new, |depth| format!(" --depth {depth}")) +} + /// Point the admitted branch at `revision` and attach HEAD to it. /// /// The checkout attaches to a real branch instead of detaching so callers that @@ -479,7 +481,7 @@ mod tests { "/repos/acme's widgets", "https://token@example.com/acme/widgets.git?x=a b", sha, - 10, + Some(10), ); let checkout = exact_checkout_verify_command("/repos/acme's widgets", "feature/a b", "FETCH_HEAD"); @@ -505,7 +507,7 @@ mod tests { "/repos/acme/widgets", "origin", "0123456789abcdef0123456789abcdef01234567", - 0, + None, ), "git -c maintenance.auto=0 -c gc.auto=0 -C /repos/acme/widgets fetch --no-tags origin -- 0123456789abcdef0123456789abcdef01234567" ); @@ -572,7 +574,7 @@ mod tests { ); run_shell( temp.path(), - &exact_fetch_command(checkout_path, remote_path, &admitted_sha, 10), + &exact_fetch_command(checkout_path, remote_path, &admitted_sha, Some(10)), ); let checked_out_sha = run_shell( temp.path(), diff --git a/lib/components/fabro-sandbox/src/config.rs b/lib/components/fabro-sandbox/src/config.rs index bddc7c369..208278d18 100644 --- a/lib/components/fabro-sandbox/src/config.rs +++ b/lib/components/fabro-sandbox/src/config.rs @@ -12,30 +12,19 @@ use std::collections::HashMap; use serde::de::{self, MapAccess, Visitor}; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct DaytonaSettings { pub auto_stop_interval: Option, pub labels: Option>, pub snapshot: Option, pub network: Option, + /// Git history depth for the repository clone; `None` clones full + /// history. pub clone_depth: Option, #[serde(default)] 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 1d1d8da23..0081a05ce 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -101,23 +101,6 @@ const DAYTONA_STATE_CHANGE_POLL_INTERVAL: Duration = Duration::from_secs(1); /// leaked by a dead worker. An explicit `0` disables auto-stop entirely. const DEFAULT_AUTO_STOP_INTERVAL_MINUTES: i32 = 120; -fn daytona_git_clone_options( - branch: Option, - commit_id: Option, - username: Option, - password: Option, - depth: Option, -) -> GitCloneOptions { - GitCloneOptions { - branch, - commit_id, - username, - password, - depth, - ..GitCloneOptions::default() - } -} - pub(crate) fn daytona_not_found(err: &DaytonaError) -> bool { matches!(err, DaytonaError::NotFound { .. }) || err.status_code() == Some(404) } @@ -1623,13 +1606,14 @@ impl Sandbox for DaytonaSandbox { let git_svc = &git_svc; let origin = origin_url.as_str(); let target = layout.primary_repo_path.as_str(); - let options = daytona_git_clone_options( - branch.clone(), - commit_sha.clone(), - username.clone(), - password.clone(), - self.config.clone_depth, - ); + let options = GitCloneOptions { + branch: branch.clone(), + commit_id: commit_sha.clone(), + username: username.clone(), + password: password.clone(), + depth: self.config.clone_depth, + ..GitCloneOptions::default() + }; async move { git_svc.clone(origin, target, options).await } }, |err: &DaytonaError| classify_clone_failure(err, clone_credential_context), @@ -3142,26 +3126,6 @@ mod tests { assert!(!error.to_string().contains("Daytona client")); } - #[test] - fn exact_checkout_uses_daytona_branch_and_commit_options() { - let options = daytona_git_clone_options( - Some("feature/work".to_string()), - Some("0123456789abcdef0123456789abcdef01234567".to_string()), - Some("x-access-token".to_string()), - Some("secret".to_string()), - Some(1), - ); - - assert_eq!(options.branch.as_deref(), Some("feature/work")); - assert_eq!( - options.commit_id.as_deref(), - Some("0123456789abcdef0123456789abcdef01234567") - ); - assert_eq!(options.username.as_deref(), Some("x-access-token")); - assert_eq!(options.password.as_deref(), Some("secret")); - assert_eq!(options.depth, Some(1)); - } - fn mock_sandbox_body(sandbox_id: &str) -> serde_json::Value { serde_json::json!({ "id": sandbox_id, @@ -3528,7 +3492,7 @@ mod tests { assert!(config.snapshot.is_none()); assert!(config.auto_stop_interval.is_none()); assert!(config.labels.is_none()); - assert_eq!(config.clone_depth, Some(100)); + assert!(config.clone_depth.is_none()); } #[test] diff --git a/lib/components/fabro-sandbox/src/docker.rs b/lib/components/fabro-sandbox/src/docker.rs index 97e965ed8..0585ecbdf 100644 --- a/lib/components/fabro-sandbox/src/docker.rs +++ b/lib/components/fabro-sandbox/src/docker.rs @@ -18,6 +18,7 @@ use bollard::image::CreateImageOptions; use bollard::models::{ContainerInspectResponse, HostConfig}; use fabro_github::GitHubCredentials; use fabro_github::token_source::InstallationTokenSource; +use fabro_types::settings::run::RunCloneSettings; use fabro_types::{CommandOutputStream, CommandTermination, RunId, SandboxProviderKind}; use fabro_util::time::elapsed_ms; use futures::StreamExt; @@ -48,7 +49,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 = 100; +const DEFAULT_GIT_CLONE_DEPTH: usize = RunCloneSettings::DEFAULT_DEPTH.unsigned_abs() as usize; const GIT_CLONE_TIMEOUT: Duration = Duration::from_mins(5); #[cfg(test)] const EXEC_STOP_POLL_SLEEP_SECONDS: &str = "0.005"; @@ -121,9 +122,9 @@ 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. Zero fetches full + /// Maximum Git history depth fetched during clone; `None` fetches full /// history. - pub clone_depth: usize, + pub clone_depth: Option, /// Create an empty workspace instead of cloning even when an origin exists. pub skip_clone: bool, } @@ -137,7 +138,7 @@ impl Default for DockerSandboxOptions { cpu_quota: None, auto_pull: true, env_vars: Vec::new(), - clone_depth: DEFAULT_GIT_CLONE_DEPTH, + clone_depth: Some(DEFAULT_GIT_CLONE_DEPTH), skip_clone: false, } } @@ -1541,7 +1542,7 @@ fn git_clone_command( clone_url: &str, branch: Option<&str>, checkout_path: &str, - depth: usize, + depth: Option, ) -> String { let mut command = format!("{} clone", sandbox::GIT); if let Some(branch) = branch { @@ -1549,10 +1550,7 @@ fn git_clone_command( command.push_str(&shell_quote(branch)); command.push_str(" --single-branch"); } - if depth > 0 { - command.push_str(" --depth "); - command.push_str(&depth.to_string()); - } + command.push_str(&clone_source::depth_argument(depth)); command.push_str(" --no-tags"); command.push_str(" -- "); command.push_str(&shell_quote(clone_url)); @@ -2571,7 +2569,7 @@ mod tests { let options = DockerSandboxOptions::default(); assert_eq!(options.image, "buildpack-deps:noble"); assert_eq!(options.network_mode.as_deref(), Some("bridge")); - assert_eq!(options.clone_depth, DEFAULT_GIT_CLONE_DEPTH); + assert_eq!(options.clone_depth, Some(DEFAULT_GIT_CLONE_DEPTH)); assert!(!options.skip_clone); } @@ -2581,7 +2579,7 @@ mod tests { "https://github.com/fabro-sh/fabro", Some("main"), "/repos/fabro-sh/fabro", - 1, + Some(1), ); assert_eq!( command, @@ -2595,7 +2593,7 @@ mod tests { "https://github.com/fabro-sh/fabro", None, "/repos/fabro-sh/fabro", - DEFAULT_GIT_CLONE_DEPTH, + Some(DEFAULT_GIT_CLONE_DEPTH), ); assert_eq!( command, @@ -2609,7 +2607,7 @@ mod tests { "https://github.com/fabro-sh/fabro", Some("main"), "/repos/fabro-sh/fabro", - 0, + None, ); assert_eq!( command, diff --git a/lib/components/fabro-sandbox/src/from_environment.rs b/lib/components/fabro-sandbox/src/from_environment.rs index 4bf76696e..657b4c179 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.filter(|depth| *depth > 0), + clone_depth: clone.depth_limit(), skip_clone: !clone.enabled, } } @@ -133,9 +133,8 @@ fn docker_config_from_environment_env( .map(|cpu| i64::from(cpu).saturating_mul(100_000)), env_vars, clone_depth: clone - .depth - .and_then(|depth| usize::try_from(depth).ok()) - .unwrap_or(default_options.clone_depth), + .depth_limit() + .and_then(|depth| usize::try_from(depth).ok()), skip_clone: !clone.enabled, ..DockerSandboxOptions::default() } diff --git a/lib/components/fabro-store/src/legacy_blob_import.rs b/lib/components/fabro-store/src/legacy_blob_import.rs index 06199edc2..d9ea4ba93 100644 --- a/lib/components/fabro-store/src/legacy_blob_import.rs +++ b/lib/components/fabro-store/src/legacy_blob_import.rs @@ -790,7 +790,7 @@ mod tests { type TestResult = std::result::Result>; struct TestContext { - dir: tempfile::TempDir, + _dir: tempfile::TempDir, source: Database, source_db: slatedb::Db, sqlite: sqlx::SqlitePool, @@ -812,7 +812,7 @@ mod tests { let sqlite = sqlite.clone_pool(); let target = BlobStore::new(sqlite.clone()); Ok(Self { - dir, + _dir: dir, source, source_db, sqlite, @@ -822,11 +822,8 @@ mod tests { async fn new_with_single_sqlite_connection() -> TestResult { let mut context = Self::new().await?; + let options = context.sqlite.connect_options().as_ref().clone(); context.sqlite.close().await; - let options = SqliteConnectOptions::new() - .filename(context.dir.path().join("fabro.sqlite3")) - .foreign_keys(true) - .journal_mode(SqliteJournalMode::Wal); let sqlite = SqlitePoolOptions::new() .max_connections(1) .connect_with(options) diff --git a/lib/components/fabro-workflow/src/operations/start.rs b/lib/components/fabro-workflow/src/operations/start.rs index 9955f1a8d..053548123 100644 --- a/lib/components/fabro-workflow/src/operations/start.rs +++ b/lib/components/fabro-workflow/src/operations/start.rs @@ -1301,7 +1301,7 @@ reasoning = false resolve_docker_config(&settings.run, |_| None) .unwrap() .clone_depth, - 1 + Some(1) ); } @@ -1320,7 +1320,7 @@ reasoning = false resolve_docker_config(&settings.run, |_| None) .unwrap() .clone_depth, - 0 + None ); } @@ -1333,7 +1333,7 @@ reasoning = false resolve_docker_config(&settings.run, |_| None) .unwrap() .clone_depth, - 100 + Some(100) ); } diff --git a/lib/foundation/fabro-config/src/resolve/run.rs b/lib/foundation/fabro-config/src/resolve/run.rs index ba64fa67d..a60bb9b9f 100644 --- a/lib/foundation/fabro-config/src/resolve/run.rs +++ b/lib/foundation/fabro-config/src/resolve/run.rs @@ -248,24 +248,20 @@ fn resolve_clone( clone: Option<&RunCloneLayer>, errors: &mut Vec, ) -> RunCloneSettings { - 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 - } - }); + let mut depth = clone + .and_then(|clone| clone.depth) + .unwrap_or(RunCloneSettings::DEFAULT_DEPTH); + if depth < 0 { + errors.push(ResolveError::Invalid { + path: "run.clone.depth".to_string(), + reason: "depth must be at least 0".to_string(), + }); + depth = RunCloneSettings::DEFAULT_DEPTH; + } RunCloneSettings { enabled: clone.and_then(|clone| clone.enabled).unwrap_or(true), - depth: Some(depth), + depth, } } diff --git a/lib/foundation/fabro-config/src/tests/resolve_run.rs b/lib/foundation/fabro-config/src/tests/resolve_run.rs index d66b075ea..c738869e9 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, Some(100)); + assert_eq!(settings.clone.depth, 100); assert!(settings.run_branch.enabled); assert!(settings.run_branch.push); assert!(settings.meta_branch.enabled); @@ -312,7 +312,7 @@ push = false .run; assert!(!settings.clone.enabled); - assert_eq!(settings.clone.depth, Some(1)); + assert_eq!(settings.clone.depth, 1); assert!(settings.run_branch.enabled); assert!(!settings.run_branch.push); assert!(settings.meta_branch.enabled); @@ -332,7 +332,7 @@ depth = 0 .expect("zero clone depth should resolve") .run; - assert_eq!(settings.clone.depth, Some(0)); + assert_eq!(settings.clone.depth, 0); } #[test] diff --git a/lib/foundation/fabro-types/src/settings/run.rs b/lib/foundation/fabro-types/src/settings/run.rs index a47cb48fb..7854ee6d3 100644 --- a/lib/foundation/fabro-types/src/settings/run.rs +++ b/lib/foundation/fabro-types/src/settings/run.rs @@ -961,32 +961,30 @@ impl Default for RunCheckpointSettings { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunCloneSettings { pub enabled: bool, - #[serde( - default = "default_clone_depth", - skip_serializing_if = "Option::is_none" - )] - pub depth: Option, + #[serde(default = "default_clone_depth")] + pub depth: i32, } impl RunCloneSettings { pub const DEFAULT_DEPTH: i32 = 100; + + /// Git history depth to fetch, or `None` to fetch full history. + pub fn depth_limit(&self) -> Option { + (self.depth > 0).then_some(self.depth) + } } impl Default for RunCloneSettings { fn default() -> Self { Self { enabled: true, - depth: Some(Self::DEFAULT_DEPTH), + depth: 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) +fn default_clone_depth() -> i32 { + RunCloneSettings::DEFAULT_DEPTH } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]