mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
fix: handle non-git local sandbox runs
Skip worktree checkpoint setup when a local sandbox is not backed by a git repository, and keep the API contract aligned with RunSpec serialization for omitted labels.
This commit is contained in:
parent
ab9b28875b
commit
b24c67acef
8 changed files with 48 additions and 62 deletions
|
|
@ -4523,7 +4523,6 @@ components:
|
|||
- run_id
|
||||
- settings
|
||||
- graph
|
||||
- labels
|
||||
- checkpoints_disabled
|
||||
properties:
|
||||
run_id:
|
||||
|
|
|
|||
|
|
@ -159,7 +159,6 @@ fn attach_replays_completed_detached_run() {
|
|||
exit_code: 0
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree]
|
||||
Sandbox: local (ready in [TIME])
|
||||
✓ Start [TIME]
|
||||
✓ Run Tests [TIME]
|
||||
|
|
@ -266,7 +265,6 @@ fn attach_before_completion_streams_to_finished_state() {
|
|||
exit_code: 0
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree]
|
||||
Sandbox: local (ready in [TIME])
|
||||
✓ start [DURATION]
|
||||
✓ wait [DURATION]
|
||||
|
|
@ -690,22 +688,6 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"run_id": "[ULID]",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"display": "system:worker",
|
||||
"id": "worker",
|
||||
"kind": "system"
|
||||
},
|
||||
"event": "run.notice",
|
||||
"id": "[EVENT_ID]",
|
||||
"properties": {
|
||||
"code": "dirty_worktree",
|
||||
"level": "warn",
|
||||
"message": "Uncommitted changes will not be included in the worktree."
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"display": "system:worker",
|
||||
|
|
|
|||
|
|
@ -270,9 +270,9 @@ fn dump_exports_completed_run_snapshot() {
|
|||
");
|
||||
|
||||
assert_snapshot!(dump_file_summary(&output_dir), @"
|
||||
checkpoints/0014.json
|
||||
checkpoints/0018.json
|
||||
checkpoints/0022.json
|
||||
checkpoints/0013.json
|
||||
checkpoints/0017.json
|
||||
checkpoints/0021.json
|
||||
events.jsonl
|
||||
graph.fabro
|
||||
run.json
|
||||
|
|
|
|||
|
|
@ -188,7 +188,6 @@ fn logs_pretty_formats_small_run() {
|
|||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[CLOCK] Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree]
|
||||
[CLOCK] Sandbox: local [DURATION]
|
||||
[CLOCK] ▶ Simple [ULID]
|
||||
Run tests and report results
|
||||
|
|
|
|||
|
|
@ -674,7 +674,6 @@ fn dry_run_simple() {
|
|||
Goal: Run tests and report results
|
||||
|
||||
Run: [ULID]
|
||||
Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree]
|
||||
Sandbox: local (ready in [TIME])
|
||||
✓ Start [TIME]
|
||||
✓ Run Tests [TIME]
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ fn dry_run_branching() {
|
|||
|
||||
warning [node: implement]: Node 'implement' has goal_gate=true but no retry_target or fallback_retry_target (goal_gate_has_retry)
|
||||
Run: [ULID]
|
||||
Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree]
|
||||
Sandbox: local (ready in [TIME])
|
||||
✓ Start [TIME]
|
||||
✓ Plan [TIME]
|
||||
|
|
@ -56,7 +55,6 @@ fn dry_run_conditions() {
|
|||
Goal: Test condition evaluation with OR and parentheses
|
||||
|
||||
Run: [ULID]
|
||||
Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree]
|
||||
Sandbox: local (ready in [TIME])
|
||||
✓ start [TIME]
|
||||
✓ Decide [TIME]
|
||||
|
|
@ -90,7 +88,6 @@ fn dry_run_parallel() {
|
|||
Goal: Test parallel and fan-in execution
|
||||
|
||||
Run: [ULID]
|
||||
Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree]
|
||||
Sandbox: local (ready in [TIME])
|
||||
✓ start [TIME]
|
||||
✓ Fork Work [TIME]
|
||||
|
|
@ -125,7 +122,6 @@ fn dry_run_styled() {
|
|||
Goal: Build a styled pipeline
|
||||
|
||||
Run: [ULID]
|
||||
Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree]
|
||||
Sandbox: local (ready in [TIME])
|
||||
✓ start [TIME]
|
||||
✓ Plan [TIME]
|
||||
|
|
@ -160,7 +156,6 @@ fn dry_run_legacy_tool() {
|
|||
Goal: Verify backwards compatibility with old tool naming
|
||||
|
||||
Run: [ULID]
|
||||
Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree]
|
||||
Sandbox: local (ready in [TIME])
|
||||
✓ Start [TIME]
|
||||
✓ Echo [TIME]
|
||||
|
|
|
|||
|
|
@ -45,9 +45,9 @@ struct WorktreePlan {
|
|||
async fn resolve_worktree_base_sha(
|
||||
sandbox: &dyn Sandbox,
|
||||
plan: &WorktreePlan,
|
||||
) -> Result<String, Error> {
|
||||
) -> Result<Option<String>, Error> {
|
||||
if let Some(base_sha) = plan.base_sha.as_ref() {
|
||||
return Ok(base_sha.clone());
|
||||
return Ok(Some(base_sha.clone()));
|
||||
}
|
||||
|
||||
let result = sandbox
|
||||
|
|
@ -67,6 +67,9 @@ async fn resolve_worktree_base_sha(
|
|||
} else {
|
||||
output
|
||||
};
|
||||
if is_not_git_repository(output) {
|
||||
return Ok(None);
|
||||
}
|
||||
return Err(Error::engine(format!(
|
||||
"git rev-parse HEAD failed (exit {}): {}",
|
||||
result.exit_code, output
|
||||
|
|
@ -77,7 +80,11 @@ async fn resolve_worktree_base_sha(
|
|||
if base_sha.is_empty() {
|
||||
return Err(Error::engine("git rev-parse HEAD returned no commit sha"));
|
||||
}
|
||||
Ok(base_sha.to_string())
|
||||
Ok(Some(base_sha.to_string()))
|
||||
}
|
||||
|
||||
fn is_not_git_repository(output: &str) -> bool {
|
||||
output.contains("not a git repository") || output.contains("ambiguous argument 'HEAD'")
|
||||
}
|
||||
|
||||
async fn run_hooks(
|
||||
|
|
@ -131,17 +138,19 @@ fn resolve_worktree_plan(options: &mut InitOptions) -> Option<WorktreePlan> {
|
|||
}
|
||||
}
|
||||
|
||||
let git_is_clean = options
|
||||
let local_dirty = options
|
||||
.run_options
|
||||
.pre_run_git
|
||||
.as_ref()
|
||||
.is_some_and(|git| matches!(git.local_dirty, fabro_types::DirtyStatus::Clean));
|
||||
.map(|git| git.local_dirty);
|
||||
let git_is_clean =
|
||||
local_dirty.is_some_and(|status| matches!(status, fabro_types::DirtyStatus::Clean));
|
||||
let strategy =
|
||||
options
|
||||
.sandbox
|
||||
.workdir_strategy(worktree_mode, git_is_clean, options.checkpoint.is_some());
|
||||
|
||||
if !git_is_clean {
|
||||
if matches!(local_dirty, Some(fabro_types::DirtyStatus::Dirty)) {
|
||||
let env_name = match strategy {
|
||||
WorkdirStrategy::LocalWorktree => Some("worktree"),
|
||||
WorkdirStrategy::Cloud => Some("remote sandbox"),
|
||||
|
|
@ -491,33 +500,36 @@ pub async fn initialize(
|
|||
.build(Some(Arc::clone(&sandbox_event_callback)))
|
||||
.await
|
||||
.map_err(|e| Error::engine(e.to_string()))?;
|
||||
metadata_runtime
|
||||
.ensure_git_available(&*inner)
|
||||
.await
|
||||
.map_err(|err| Error::engine(format!("sandbox git unavailable: {err}")))?;
|
||||
let base_sha = resolve_worktree_base_sha(&*inner, plan).await?;
|
||||
options.run_options.display_base_sha = Some(base_sha.clone());
|
||||
options.run_options.git = Some(GitCheckpointOptions {
|
||||
base_sha: Some(base_sha.clone()),
|
||||
run_branch: Some(plan.branch_name.clone()),
|
||||
meta_branch: Some(metadata_branch_name(&options.run_id.to_string())),
|
||||
});
|
||||
let mut worktree = WorktreeSandbox::new(inner, WorktreeOptions {
|
||||
branch_name: plan.branch_name.clone(),
|
||||
base_sha,
|
||||
worktree_path: plan.worktree_path.to_string_lossy().into_owned(),
|
||||
skip_branch_creation: plan.skip_branch_creation,
|
||||
setup_intent: Some(git_setup_intent(&options.run_options)),
|
||||
});
|
||||
worktree.set_event_callback(Arc::clone(&options.emitter).worktree_callback());
|
||||
match worktree.initialize().await {
|
||||
Ok(()) => {
|
||||
worktree_created = true;
|
||||
Arc::new(ReadBeforeWriteSandbox::new(Arc::new(worktree)))
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(Error::engine(format!("Git worktree setup failed: {e}")));
|
||||
if let Some(base_sha) = resolve_worktree_base_sha(&*inner, plan).await? {
|
||||
metadata_runtime
|
||||
.ensure_git_available(&*inner)
|
||||
.await
|
||||
.map_err(|err| Error::engine(format!("sandbox git unavailable: {err}")))?;
|
||||
options.run_options.display_base_sha = Some(base_sha.clone());
|
||||
options.run_options.git = Some(GitCheckpointOptions {
|
||||
base_sha: Some(base_sha.clone()),
|
||||
run_branch: Some(plan.branch_name.clone()),
|
||||
meta_branch: Some(metadata_branch_name(&options.run_id.to_string())),
|
||||
});
|
||||
let mut worktree = WorktreeSandbox::new(inner, WorktreeOptions {
|
||||
branch_name: plan.branch_name.clone(),
|
||||
base_sha,
|
||||
worktree_path: plan.worktree_path.to_string_lossy().into_owned(),
|
||||
skip_branch_creation: plan.skip_branch_creation,
|
||||
setup_intent: Some(git_setup_intent(&options.run_options)),
|
||||
});
|
||||
worktree.set_event_callback(Arc::clone(&options.emitter).worktree_callback());
|
||||
match worktree.initialize().await {
|
||||
Ok(()) => {
|
||||
worktree_created = true;
|
||||
Arc::new(ReadBeforeWriteSandbox::new(Arc::new(worktree)))
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(Error::engine(format!("Git worktree setup failed: {e}")));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Arc::new(ReadBeforeWriteSandbox::new(inner))
|
||||
}
|
||||
} else {
|
||||
Arc::new(ReadBeforeWriteSandbox::new(
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export interface RunSpec {
|
|||
'source_directory'?: string | null;
|
||||
'repo_origin_url'?: string | null;
|
||||
'base_branch'?: string | null;
|
||||
'labels': { [key: string]: string; };
|
||||
'labels'?: { [key: string]: string; };
|
||||
'provenance'?: { [key: string]: any; } | null;
|
||||
'manifest_blob'?: string | null;
|
||||
'definition_blob'?: string | null;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue