mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
feat: default sandbox clone depth to 100
This commit is contained in:
parent
438bab29f0
commit
6179470eb2
14 changed files with 152 additions and 34 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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]`
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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<i32>,
|
||||
pub labels: Option<HashMap<String, String>>,
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
/// 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"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -248,21 +248,24 @@ fn resolve_clone(
|
|||
clone: Option<&RunCloneLayer>,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) -> 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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<i32>,
|
||||
}
|
||||
|
||||
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<i32> type"
|
||||
)]
|
||||
fn default_clone_depth() -> Option<i32> {
|
||||
Some(RunCloneSettings::DEFAULT_DEPTH)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunBranchSettings {
|
||||
pub enabled: bool,
|
||||
|
|
|
|||
|
|
@ -16,5 +16,8 @@
|
|||
|
||||
export interface RunCloneSettings {
|
||||
'enabled': boolean;
|
||||
/**
|
||||
* Git history depth. Set to 0 to clone full history.
|
||||
*/
|
||||
'depth'?: number;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue