Acquire the remote workflow before observing a local Git target

For --workflow-git selections, target resolution ran before the remote
workflow ref was verified to exist. On Docker and Daytona environments a
path target that is a GitHub checkout is observed via
observe_git_run_target, which may silently push the attached branch, so
a typo in --workflow-ref produced a remote side effect with no run
created. Resolve the remote workflow after parent and environment
validation but before target observation, restoring the pre-existing
workflow-then-target order, and cover it with a caller checkout whose
unpushed branch must stay unpublished.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Scott Werner 2026-09-11 09:42:10 -06:00
parent 38c819b5c9
commit f2a92b243e
2 changed files with 30 additions and 9 deletions

View file

@ -44,7 +44,7 @@ pub(crate) async fn create_run(
)
};
// Preserve local lookup diagnostics before contacting the server. Remote
// acquisition waits until parent, environment, and target are validated.
// acquisition waits until the parent and environment are validated.
let local_package = match &workflow_selection {
WorkflowSelection::Local(_) => Some(resolve_workflow().await?),
WorkflowSelection::Git { .. } => None,
@ -84,6 +84,12 @@ pub(crate) async fn create_run(
},
resolve_run_environment(client.as_ref(), args.environment.as_deref()),
)?;
// Observing a local Git target may push its branch. Acquire the remote
// workflow first so a bad --workflow-ref never causes that side effect.
let package = match local_package {
Some(package) => package,
None => resolve_workflow().await?,
};
let (target, dirty_worktree) = resolution::target(
&target_selection,
environment.settings.provider,
@ -97,10 +103,6 @@ pub(crate) async fn create_run(
styles.yellow.apply_to("Warning:"),
);
}
let package = match local_package {
Some(package) => package,
None => resolve_workflow().await?,
};
let workflow_version_id = package.closure().root_id();
client
.register_workflow_versions(

View file

@ -1889,7 +1889,17 @@ fn remote_workflow_explicit_acquisition_failure_has_no_fallback_or_server_mutati
let source = root.path().join("source");
write_workflow(&source, ".fabro/workflows/other", "Other");
init_remote_fixture(&source, "trunk");
write_workflow(root.path(), ".fabro/workflows/review", "Caller");
// The caller is a GitHub-origin checkout with an unpushed branch, which
// Docker target observation would publish if it ran first.
let caller = root.path().join("caller");
let origin = root.path().join("origin.git");
git2::Repository::init_bare(&origin).unwrap();
write_workflow(&caller, ".fabro/workflows/review", "Caller");
init_remote_fixture(&caller, "main");
git2::Repository::open(&caller)
.unwrap()
.remote("origin", "https://github.com/acme/app")
.unwrap();
write_workflow(
&context.home_dir.join(".fabro/workflows"),
"review",
@ -1899,8 +1909,9 @@ fn remote_workflow_explicit_acquisition_failure_has_no_fallback_or_server_mutati
std::fs::write(
&config,
format!(
"[url \"file://{}\"]\n insteadOf = https://github.com/acme/workflows\n",
source.display()
"[url \"file://{}\"]\n insteadOf = https://github.com/acme/workflows\n[url \"file://{}\"]\n insteadOf = https://github.com/acme/app\n",
source.display(),
origin.display()
),
)
.unwrap();
@ -1911,7 +1922,7 @@ fn remote_workflow_explicit_acquisition_failure_has_no_fallback_or_server_mutati
] {
let output = context
.create_cmd()
.current_dir(root.path())
.current_dir(&caller)
.env("GIT_CONFIG_GLOBAL", &config)
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_COUNT", "0")
@ -1932,4 +1943,12 @@ fn remote_workflow_explicit_acquisition_failure_has_no_fallback_or_server_mutati
environment.assert_calls(3);
version.assert_calls(0);
create.assert_calls(0);
// Acquisition failed before target observation, so nothing was pushed.
assert!(
git2::Repository::open_bare(&origin)
.unwrap()
.find_reference("refs/heads/main")
.is_err(),
"a failed remote workflow acquisition must not publish the target branch"
);
}