mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Merge pull request #778 from fabro-sh/feat/daytona-clone-depth
Support configurable sandbox clone depth
This commit is contained in:
commit
868d8857bf
22 changed files with 375 additions and 122 deletions
76
Cargo.lock
generated
76
Cargo.lock
generated
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -14633,6 +14633,12 @@ components:
|
|||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
depth:
|
||||
type: integer
|
||||
format: int32
|
||||
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. 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,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 = 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.
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `enabled` | When `false`, Fabro skips the repository clone. Defaults to `true`. |
|
||||
| `depth` | Git history depth for Docker and Daytona. Defaults to `100`. Set it to `0` to clone full history. |
|
||||
|
||||
### `[run.run_branch]`
|
||||
|
||||
Configure Fabro's managed `fabro/run/<id>` checkpoint branch.
|
||||
|
|
|
|||
|
|
@ -141,6 +141,15 @@ provider = "daytona"
|
|||
enabled = false
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Set `depth = 0` to clone the full repository history.
|
||||
|
||||
If the clone fails without GitHub access configured, Fabro suggests running the setup flow:
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -924,6 +924,7 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"skip_git_hooks": false
|
||||
},
|
||||
"clone": {
|
||||
"depth": 100,
|
||||
"enabled": true
|
||||
},
|
||||
"environment": {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -91,10 +91,11 @@ pub(crate) fn exact_fetch_command(
|
|||
checkout_path: &str,
|
||||
fetch_source: &str,
|
||||
commit_sha: &str,
|
||||
depth: usize,
|
||||
depth: Option<usize>,
|
||||
) -> String {
|
||||
let depth_arg = depth_argument(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),
|
||||
|
|
@ -102,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<usize>) -> 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
|
||||
|
|
@ -474,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");
|
||||
|
|
@ -493,6 +500,19 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_fetch_omits_depth_for_full_history() {
|
||||
assert_eq!(
|
||||
exact_fetch_command(
|
||||
"/repos/acme/widgets",
|
||||
"origin",
|
||||
"0123456789abcdef0123456789abcdef01234567",
|
||||
None,
|
||||
),
|
||||
"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";
|
||||
|
|
@ -554,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(),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ pub struct DaytonaSettings {
|
|||
pub labels: Option<HashMap<String, String>>,
|
||||
pub snapshot: Option<DaytonaSnapshotSettings>,
|
||||
pub network: Option<DaytonaNetwork>,
|
||||
/// Git history depth for the repository clone; `None` clones full
|
||||
/// history.
|
||||
pub clone_depth: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub skip_clone: bool,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,30 +101,16 @@ 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<String>,
|
||||
commit_id: Option<String>,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
) -> GitCloneOptions {
|
||||
GitCloneOptions {
|
||||
branch,
|
||||
commit_id,
|
||||
username,
|
||||
password,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn daytona_not_found(err: &DaytonaError) -> bool {
|
||||
matches!(err, DaytonaError::NotFound { .. }) || err.status_code() == Some(404)
|
||||
}
|
||||
|
||||
/// 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 +244,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",
|
||||
}
|
||||
}
|
||||
|
|
@ -1620,12 +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(),
|
||||
);
|
||||
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),
|
||||
|
|
@ -3138,24 +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()),
|
||||
);
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
fn mock_sandbox_body(sandbox_id: &str) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"id": sandbox_id,
|
||||
|
|
@ -3171,6 +3141,7 @@ mod tests {
|
|||
"gpu": 0.0,
|
||||
"memory": 4.0,
|
||||
"disk": 20.0,
|
||||
"toolboxProxyUrl": "https://proxy.example.com/toolbox",
|
||||
"state": "started"
|
||||
})
|
||||
}
|
||||
|
|
@ -3488,6 +3459,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 +3481,7 @@ mod tests {
|
|||
"gpu": 0.0,
|
||||
"memory": 4.0,
|
||||
"disk": 20.0,
|
||||
"toolboxProxyUrl": "https://proxy.example.com/toolbox",
|
||||
"state": state.to_string()
|
||||
})
|
||||
}
|
||||
|
|
@ -3519,6 +3492,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 +4169,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 +4308,7 @@ mod tests {
|
|||
"gpu": 0.0,
|
||||
"memory": 4.0,
|
||||
"disk": 20.0,
|
||||
"toolboxProxyUrl": "https://proxy.example.com/toolbox",
|
||||
"state": "started"
|
||||
}));
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 GIT_CLONE_DEPTH: usize = 10;
|
||||
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,6 +122,9 @@ 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; `None` fetches full
|
||||
/// history.
|
||||
pub clone_depth: Option<usize>,
|
||||
/// Create an empty workspace instead of cloning even when an origin exists.
|
||||
pub skip_clone: bool,
|
||||
}
|
||||
|
|
@ -134,6 +138,7 @@ impl Default for DockerSandboxOptions {
|
|||
cpu_quota: None,
|
||||
auto_pull: true,
|
||||
env_vars: Vec::new(),
|
||||
clone_depth: Some(DEFAULT_GIT_CLONE_DEPTH),
|
||||
skip_clone: false,
|
||||
}
|
||||
}
|
||||
|
|
@ -954,7 +959,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 +997,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,15 +1538,19 @@ 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: Option<usize>,
|
||||
) -> String {
|
||||
let mut command = format!("{} clone", sandbox::GIT);
|
||||
if let Some(branch) = branch {
|
||||
command.push_str(" --branch ");
|
||||
command.push_str(&shell_quote(branch));
|
||||
command.push_str(" --single-branch");
|
||||
}
|
||||
command.push_str(" --depth ");
|
||||
command.push_str(&GIT_CLONE_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));
|
||||
|
|
@ -2556,19 +2569,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, Some(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",
|
||||
Some(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,10 +2593,25 @@ mod tests {
|
|||
"https://github.com/fabro-sh/fabro",
|
||||
None,
|
||||
"/repos/fabro-sh/fabro",
|
||||
Some(DEFAULT_GIT_CLONE_DEPTH),
|
||||
);
|
||||
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",
|
||||
None,
|
||||
);
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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_limit(),
|
||||
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<String>,
|
||||
) -> Result<DockerSandboxOptions, ResolveError> {
|
||||
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<String, String>,
|
||||
) -> DockerSandboxOptions {
|
||||
let mut env_vars = env
|
||||
|
|
@ -133,7 +132,10 @@ fn docker_config_from_environment_env(
|
|||
.cpu
|
||||
.map(|cpu| i64::from(cpu).saturating_mul(100_000)),
|
||||
env_vars,
|
||||
skip_clone,
|
||||
clone_depth: clone
|
||||
.depth_limit()
|
||||
.and_then(|depth| usize::try_from(depth).ok()),
|
||||
skip_clone: !clone.enabled,
|
||||
..DockerSandboxOptions::default()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -793,7 +793,7 @@ mod tests {
|
|||
_dir: tempfile::TempDir,
|
||||
source: Database,
|
||||
source_db: slatedb::Db,
|
||||
sqlite: fabro_db::Database,
|
||||
sqlite: sqlx::SqlitePool,
|
||||
target: BlobStore,
|
||||
}
|
||||
|
||||
|
|
@ -809,7 +809,8 @@ 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,
|
||||
source,
|
||||
|
|
@ -819,6 +820,19 @@ mod tests {
|
|||
})
|
||||
}
|
||||
|
||||
async fn new_with_single_sqlite_connection() -> TestResult<Self> {
|
||||
let mut context = Self::new().await?;
|
||||
let options = context.sqlite.connect_options().as_ref().clone();
|
||||
context.sqlite.close().await;
|
||||
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<BlobHash> {
|
||||
let hash = BlobHash::new(bytes);
|
||||
let key = SlateKey::new("blobs").with("sha256").with(hash);
|
||||
|
|
@ -842,7 +856,7 @@ mod tests {
|
|||
|
||||
async fn destination_rows(&self) -> TestResult<i64> {
|
||||
Ok(sqlx::query_scalar("SELECT COUNT(*) FROM blobs")
|
||||
.fetch_one(self.sqlite.pool())
|
||||
.fetch_one(&self.sqlite)
|
||||
.await?)
|
||||
}
|
||||
|
||||
|
|
@ -850,7 +864,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 +872,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<i64> {
|
||||
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 +1355,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]);
|
||||
|
|
|
|||
|
|
@ -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<DockerSandboxOptions, Error> {
|
||||
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,45 @@ 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,
|
||||
Some(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[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,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[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,
|
||||
Some(100)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -303,6 +303,8 @@ pub struct RunCheckpointLayer {
|
|||
pub struct RunCloneLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub depth: Option<i32>,
|
||||
}
|
||||
|
||||
/// `[run.run_branch]` — Fabro-managed checkpoint branch policy.
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ pub fn resolve_run(
|
|||
mcp_server_catalog: &HashMap<String, McpServerSettings>,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) -> 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,24 @@ fn resolve_checkpoint(checkpoint: Option<&RunCheckpointLayer>) -> RunCheckpointS
|
|||
}
|
||||
}
|
||||
|
||||
fn resolve_clone(clone: Option<&RunCloneLayer>) -> RunCloneSettings {
|
||||
fn resolve_clone(
|
||||
clone: Option<&RunCloneLayer>,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) -> RunCloneSettings {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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, 100);
|
||||
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,52 @@ push = false
|
|||
.run;
|
||||
|
||||
assert!(!settings.clone.enabled);
|
||||
assert_eq!(settings.clone.depth, 1);
|
||||
assert!(settings.run_branch.enabled);
|
||||
assert!(!settings.run_branch.push);
|
||||
assert!(settings.meta_branch.enabled);
|
||||
assert!(!settings.meta_branch.push);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_clone_depth_requests_full_history() {
|
||||
let settings = super::workflow_settings_from_toml(
|
||||
r"
|
||||
_version = 1
|
||||
|
||||
[run.clone]
|
||||
depth = 0
|
||||
",
|
||||
)
|
||||
.expect("zero clone depth should resolve")
|
||||
.run;
|
||||
|
||||
assert_eq!(settings.clone.depth, 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!(
|
||||
message.contains("run.clone.depth"),
|
||||
"unexpected error: {message}"
|
||||
);
|
||||
assert!(
|
||||
message.contains("at least 0"),
|
||||
"unexpected error: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabling_run_branch_forces_meta_branch_off() {
|
||||
let settings = super::workflow_settings_from_toml(
|
||||
|
|
|
|||
|
|
@ -961,14 +961,32 @@ impl Default for RunCheckpointSettings {
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunCloneSettings {
|
||||
pub enabled: bool,
|
||||
#[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<i32> {
|
||||
(self.depth > 0).then_some(self.depth)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RunCloneSettings {
|
||||
fn default() -> Self {
|
||||
Self { enabled: true }
|
||||
Self {
|
||||
enabled: true,
|
||||
depth: Self::DEFAULT_DEPTH,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_clone_depth() -> i32 {
|
||||
RunCloneSettings::DEFAULT_DEPTH
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunBranchSettings {
|
||||
pub enabled: bool,
|
||||
|
|
|
|||
|
|
@ -16,4 +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