use dense run settings in manifest and workflow loaders

This commit is contained in:
Bryan Helmkamp 2026-04-23 16:20:14 -04:00
parent 3cc094316b
commit 4ce91cdc64
No known key found for this signature in database
2 changed files with 73 additions and 37 deletions

View file

@ -8,15 +8,15 @@ use std::path::{Component, Path, PathBuf};
use anyhow::{Context, Result, anyhow};
use fabro_api::types;
use fabro_config::parse_settings_layer;
use fabro_config::project::{self, discover_project_config, resolve_workflow_path};
use fabro_config::run::resolve_run_goal;
use fabro_config::run::{resolve_run_goal, resolve_run_goal_from_namespace};
use fabro_config::{WorkflowSettingsBuilder, parse_settings_layer};
use fabro_graphviz::graph::AttrValue;
use fabro_graphviz::parser;
use fabro_sandbox::daytona::detect_repo_info;
use fabro_types::RunId;
use fabro_types::settings::SettingsLayer;
use fabro_types::settings::run::{DaytonaDockerfileLayer, ResolvedGoalSource, ResolvedRunGoal};
use fabro_types::settings::{Combine, SettingsLayer};
use fabro_types::{RunId, WorkflowSettings};
use fabro_workflow::git::{GitSyncStatus, head_sha, sync_status};
use crate::args::{PreflightArgs, RunArgs};
@ -70,21 +70,18 @@ pub(crate) fn build_run_manifest(input: ManifestBuildInput) -> Result<BuiltManif
.parent()
.unwrap_or_else(|| Path::new("."));
let project_config = discover_project_config(workflow_parent)?;
let workflow_layer = root_resolution
.workflow_config
.clone()
.unwrap_or_default()
.combine(
project_config
.as_ref()
.map(|(_, config)| config.clone())
.unwrap_or_default(),
);
let merged_settings = input
.args_layer
.clone()
.combine(workflow_layer)
.combine(input.user_layer);
let workflow_layer = root_resolution.workflow_config.clone().unwrap_or_default();
let project_layer = project_config
.as_ref()
.map(|(_, config)| config.clone())
.unwrap_or_default();
let workflow_settings = WorkflowSettingsBuilder::new()
.args_layer(input.args_layer.clone())
.workflow_layer(workflow_layer.clone())
.project_layer(project_layer)
.user_layer(input.user_layer.clone())
.build()
.map_err(|errors| anyhow!("failed to resolve manifest settings: {errors}"))?;
let target_path = root_resolution.dot_path.clone();
let target_logical_path = to_logical_path(&target_path, &input.cwd)?;
let target_logical_path_string = logical_path_string(&target_logical_path);
@ -124,7 +121,7 @@ pub(crate) fn build_run_manifest(input: ManifestBuildInput) -> Result<BuiltManif
let goal = resolve_manifest_goal(
&input.args_layer,
&merged_settings,
&workflow_settings,
&root_source,
&target_path,
&input.cwd,
@ -405,12 +402,12 @@ fn collect_bundled_file(
fn resolve_manifest_goal(
args_layer: &SettingsLayer,
settings: &SettingsLayer,
settings: &WorkflowSettings,
root_source: &str,
root_dot_path: &Path,
cwd: &Path,
) -> Result<Option<types::ManifestGoal>> {
let working_directory = project::resolve_working_directory(settings, cwd);
let working_directory = project::resolve_working_directory_from_run(&settings.run, cwd);
// Precedence 1: CLI args (`--goal` / `--goal-file`). These are already
// resolved to absolute paths by `overrides::goal_layer_from_args`.
@ -423,7 +420,7 @@ fn resolve_manifest_goal(
// Precedence 2: merged config `run.goal`. Config-sourced `goal.file`
// paths were rewritten to absolute by `load_settings_path` at the
// directory of the config file that declared them.
if let Some(resolved) = resolve_run_goal(settings, &working_directory)
if let Some(resolved) = resolve_run_goal_from_namespace(&settings.run, &working_directory)
.context("failed to resolve run.goal.file contents")?
{
return Ok(Some(resolved_goal_to_manifest(resolved)));

View file

@ -6,10 +6,11 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::Context;
use fabro_config::project as project_config;
use fabro_config::run::resolve_run_goal;
use anyhow::{Context, anyhow};
use fabro_config::run::resolve_run_goal_from_namespace;
use fabro_config::{WorkflowSettingsBuilder, project as project_config};
use fabro_types::settings::SettingsLayer;
use fabro_types::settings::run::RunNamespace;
use crate::file_resolver::{FileResolver, FilesystemFileResolver};
use crate::workflow_bundle::BundledWorkflow;
@ -67,11 +68,10 @@ pub(crate) fn resolve_workflow(request: ResolveWorkflowInput) -> anyhow::Result<
WorkflowInput::Path(workflow_path) => {
let resolution = project_config::resolve_workflow_path(&workflow_path, &request.cwd)?;
let settings = request.settings;
let working_directory =
project_config::resolve_working_directory(&settings, &request.cwd);
let raw_source = std::fs::read_to_string(&resolution.dot_path)
.with_context(|| format!("Failed to read {}", resolution.dot_path.display()))?;
let goal_override = resolve_goal_override(&settings, &working_directory)?;
let (_run_settings, working_directory, goal_override) =
resolve_runtime_run_settings(&settings, &request.cwd)?;
let current_dir = resolution
.dot_path
.parent()
@ -94,9 +94,8 @@ pub(crate) fn resolve_workflow(request: ResolveWorkflowInput) -> anyhow::Result<
}
WorkflowInput::DotSource { source, base_dir } => {
let settings = request.settings;
let working_directory =
project_config::resolve_working_directory(&settings, &request.cwd);
let goal_override = resolve_goal_override(&settings, &working_directory)?;
let (_run_settings, working_directory, goal_override) =
resolve_runtime_run_settings(&settings, &request.cwd)?;
let has_base_dir = base_dir.is_some();
Ok(ResolvedWorkflow {
raw_source: source,
@ -116,9 +115,8 @@ pub(crate) fn resolve_workflow(request: ResolveWorkflowInput) -> anyhow::Result<
}
WorkflowInput::Bundled(workflow) => {
let settings = request.settings;
let working_directory =
project_config::resolve_working_directory(&settings, &request.cwd);
let goal_override = resolve_goal_override(&settings, &working_directory)?;
let (_run_settings, working_directory, goal_override) =
resolve_runtime_run_settings(&settings, &request.cwd)?;
Ok(ResolvedWorkflow {
raw_source: workflow.source.clone(),
@ -135,15 +133,27 @@ pub(crate) fn resolve_workflow(request: ResolveWorkflowInput) -> anyhow::Result<
}
}
fn resolve_runtime_run_settings(
settings: &SettingsLayer,
cwd: &Path,
) -> anyhow::Result<(RunNamespace, PathBuf, Option<String>)> {
let run_settings = WorkflowSettingsBuilder::from_layer(settings)
.map_err(|errors| anyhow!("failed to resolve workflow settings: {errors}"))?
.run;
let working_directory = project_config::resolve_working_directory_from_run(&run_settings, cwd);
let goal_override = resolve_goal_override(&run_settings, &working_directory)?;
Ok((run_settings, working_directory, goal_override))
}
/// Resolve the `run.goal` override for a direct (non-manifest) workflow
/// run. Reads the file from disk if the goal layer is the `file` variant.
/// Relative paths that survived config load (e.g. env-interpolated ones)
/// are anchored at `working_directory`.
fn resolve_goal_override(
settings: &SettingsLayer,
run_settings: &RunNamespace,
working_directory: &Path,
) -> anyhow::Result<Option<String>> {
resolve_run_goal(settings, working_directory)
resolve_run_goal_from_namespace(run_settings, working_directory)
.map(|opt| opt.map(|resolved| resolved.text))
.map_err(anyhow::Error::from)
}
@ -176,4 +186,33 @@ mod tests {
assert_eq!(resolved.working_directory, dir.path().join("workspace"));
}
#[test]
fn resolve_workflow_reads_goal_override_from_dense_run_settings() {
use fabro_types::settings::InterpString;
use fabro_types::settings::run::{RunGoalLayer, RunLayer};
let dir = tempfile::tempdir().unwrap();
let goal_path = dir.path().join("goal.md");
std::fs::write(&goal_path, "dense goal").unwrap();
let resolved = resolve_workflow(ResolveWorkflowInput {
workflow: WorkflowInput::DotSource {
source: "digraph Test { start -> exit }".to_string(),
base_dir: None,
},
settings: SettingsLayer {
run: Some(RunLayer {
goal: Some(RunGoalLayer::File {
file: InterpString::parse(&goal_path.display().to_string()),
}),
..RunLayer::default()
}),
..SettingsLayer::default()
},
cwd: dir.path().to_path_buf(),
})
.unwrap();
assert_eq!(resolved.goal_override.as_deref(), Some("dense goal"));
}
}