From 7d050ed0f60c567f58086c6e545d1ebad1b55fef Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Fri, 11 Sep 2026 09:35:41 -0600 Subject: [PATCH] Reject unusable remote default branches with a --target-branch hint Target resolution accepted any remote default HEAD that passed the ref selector grammar, then failed inside GitRunTarget::validate with a generic branch-grammar error when the default branch was something like heads/main or tags/release. Validate the default branch as a working branch name up front and point the user at --target-branch, since they passed no branch at all. Co-Authored-By: Claude Fable 5.1 --- .../src/commands/run/remote_workflow.rs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/apps/fabro-cli/src/commands/run/remote_workflow.rs b/lib/apps/fabro-cli/src/commands/run/remote_workflow.rs index 2e1dc417d..b0117f14f 100644 --- a/lib/apps/fabro-cli/src/commands/run/remote_workflow.rs +++ b/lib/apps/fabro-cli/src/commands/run/remote_workflow.rs @@ -195,7 +195,7 @@ impl NativeGit { let records = self .records(root, &repository, &["HEAD".into()], cancel) .await?; - default_head(&records)? + default_target_branch(&records)? }; let target = GitRunTarget { repo: repository.to_string(), @@ -402,6 +402,20 @@ fn default_head(records: &str) -> anyhow::Result<(String, String)> { )) } +/// The remote default branch as a run target. `GitRunTarget` requires a bare +/// working branch name, which is stricter than the ref grammar `default_head` +/// accepts for workflow acquisition; report the mismatch with the flag that +/// resolves it instead of a generic branch-grammar error. +fn default_target_branch(records: &str) -> anyhow::Result<(String, String)> { + let (branch, sha) = default_head(records)?; + if !repository::is_valid_git_branch_name(&branch) { + bail!( + "remote default branch `{branch}` cannot name a run target branch; pass --target-branch to select a working branch" + ); + } + Ok((branch, sha)) +} + /// The fully qualified refs a validated `--workflow-ref` may name. A bare name /// may be a branch or a tag; a `refs/heads/` or `refs/tags/` name is exactly /// one. @@ -953,6 +967,26 @@ mod tests { ); } + #[test] + fn remote_workflow_default_target_branch_requires_a_working_branch_name() { + let sha = "1234567890123456789012345678901234567890"; + for (head, valid) in [ + ("trunk", true), + ("topic/slash", true), + ("heads/main", false), + ("tags/release", false), + (sha, false), + ] { + let records = format!("ref: refs/heads/{head}\tHEAD\n{sha}\tHEAD\n"); + assert_eq!(default_head(&records).unwrap().0, head); + let target = default_target_branch(&records); + assert_eq!(target.is_ok(), valid, "{head}"); + if !valid { + assert!(target.unwrap_err().to_string().contains("--target-branch")); + } + } + } + #[test] fn remote_workflow_matches_records_exactly_and_rejects_host_symlinks() { let sha = "1234567890123456789012345678901234567890";