Add goal_file and labels to FabroConfig

Allow workflow authors to set default goal files and labels in
workflow.toml/fabro.toml, reducing repetitive CLI flags. CLI flags
override config values; labels are deep-merged with CLI winning on
key collision.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-27 12:42:20 -04:00
parent b1cab600a8
commit cc52be7016
4 changed files with 32 additions and 4 deletions

View file

@ -529,6 +529,7 @@ async fn start_run(
}),
..Default::default()
};
let run_labels = config.labels.clone();
let persisted = match operations::create(
&req.dot_source,
RunCreateOptions {
@ -536,7 +537,7 @@ async fn start_run(
run_dir: Some(run_dir.clone()),
run_id: Some(run_id.clone()),
workflow_slug: None,
labels: std::collections::HashMap::new(),
labels: run_labels,
base_branch: None,
working_directory: Some(
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),

View file

@ -76,7 +76,11 @@ pub async fn create_run(
run_dir: Some(run_dir.clone()),
run_id: Some(run_id.clone()),
workflow_slug: source_input.workflow_slug.clone(),
labels: parse_labels(&args.label),
labels: {
let mut labels = source_input.config.labels.clone();
labels.extend(parse_labels(&args.label));
labels
},
base_branch,
working_directory: Some(working_directory.clone()),
host_repo_path: Some(working_directory.to_string_lossy().to_string()),

View file

@ -528,7 +528,12 @@ pub(crate) fn load_workflow_source_input(
let raw_source = read_workflow_file(&dot_path)?;
let cli_goal = resolve_cli_goal(goal, goal_file)?;
let goal_override = cli_goal.or_else(|| config.goal.clone());
let goal_override = cli_goal.or_else(|| config.goal.clone()).or_else(|| {
config
.goal_file
.as_ref()
.and_then(|path| resolve_cli_goal(None, Some(path)).ok().flatten())
});
let workflow_toml_path = if resolved_workflow_path
.extension()
@ -847,7 +852,11 @@ async fn run_command_impl(
run_dir: Some(run_dir.clone()),
run_id: Some(run_id.clone()),
workflow_slug: source_input.workflow_slug.clone(),
labels: parse_labels(&label_vec),
labels: {
let mut labels = source_input.config.labels.clone();
labels.extend(parse_labels(&label_vec));
labels
},
base_branch: detected_base_branch.clone(),
working_directory: Some(original_cwd.clone()),
host_repo_path: Some(original_cwd.to_string_lossy().to_string()),

View file

@ -31,9 +31,15 @@ pub struct FabroConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub goal: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub goal_file: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub graph: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub labels: HashMap<String, String>,
// --- Run defaults fields (inlined) ---
#[serde(default, alias = "directory", skip_serializing_if = "Option::is_none")]
pub work_dir: Option<String>,
@ -178,9 +184,17 @@ impl FabroConfig {
if overlay.goal.is_some() {
self.goal = overlay.goal;
}
if overlay.goal_file.is_some() {
self.goal_file = overlay.goal_file;
}
if overlay.graph.is_some() {
self.graph = overlay.graph;
}
if !overlay.labels.is_empty() {
let mut merged = std::mem::take(&mut self.labels);
merged.extend(overlay.labels);
self.labels = merged;
}
// --- Run defaults fields ---
if overlay.work_dir.is_some() {