use super::*;
pub(super) fn load_resolved_config(
opts: &CliOrchestrateOptions,
) -> Result<OrchestrationConfig, String> {
let mut config = read_orchestration_config(&opts.config_path)?;
if let Some(profile_name) = &opts.profile {
apply_profile(&mut config, profile_name)?;
}
for overlay_file in &opts.overlay_files {
apply_overlay_file(&mut config, overlay_file)?;
}
Ok(config)
}
fn read_orchestration_config(path: &PathBuf) -> Result<OrchestrationConfig, String> {
let content = std::fs::read_to_string(path)
.map_err(|err| format!("read orchestrate config {} failed: {err}", path.display()))?;
serde_json::from_str::<OrchestrationConfig>(&content)
.map_err(|err| format!("parse orchestrate config {} failed: {err}", path.display()))
}
fn apply_profile(config: &mut OrchestrationConfig, profile_name: &str) -> Result<(), String> {
let profile = config
.profiles
.get(profile_name)
.cloned()
.ok_or_else(|| format!("profile not found: {profile_name}"))?;
if let Some(max_parallel) = profile.max_parallel {
config.max_parallel = max_parallel;
}
for task in &mut config.tasks {
if !profile.env.is_empty() {
for (key, value) in &profile.env {
task.env.entry(key.clone()).or_insert_with(|| value.clone());
}
}
if let Some(override_item) = profile.tasks.get(&task.name) {
apply_task_override(task, override_item);
}
}
Ok(())
}
fn apply_overlay_file(config: &mut OrchestrationConfig, overlay_file: &Path) -> Result<(), String> {
let content = std::fs::read_to_string(overlay_file)
.map_err(|err| format!("read overlay {} failed: {err}", overlay_file.display()))?;
let overlay = serde_json::from_str::<OrchestrationOverlay>(&content)
.map_err(|err| format!("parse overlay {} failed: {err}", overlay_file.display()))?;
if let Some(project) = overlay.project {
config.project = project;
}
if let Some(max_parallel) = overlay.max_parallel {
config.max_parallel = max_parallel;
}
for task in &mut config.tasks {
for (key, value) in &overlay.env {
task.env.entry(key.clone()).or_insert_with(|| value.clone());
}
if let Some(override_item) = overlay.tasks.get(&task.name) {
apply_task_override(task, override_item);
}
}
for task in overlay.add_tasks {
config.tasks.push(task);
}
Ok(())
}
fn apply_task_override(task: &mut OrchestrationTask, override_item: &TaskOverride) {
if let Some(command) = &override_item.command {
task.command = command.clone();
}
if let Some(depends_on) = &override_item.depends_on {
task.depends_on = depends_on.clone();
}
if let Some(cwd) = &override_item.cwd {
task.cwd = Some(cwd.clone());
}
if !override_item.env.is_empty() {
for (key, value) in &override_item.env {
task.env.insert(key.clone(), value.clone());
}
}
if let Some(continue_on_error) = override_item.continue_on_error {
task.continue_on_error = continue_on_error;
}
if let Some(enabled) = override_item.enabled {
task.enabled = enabled;
}
if let Some(group) = &override_item.group {
task.group = Some(group.clone());
}
if let Some(run_mode) = &override_item.run_mode {
task.run_mode = run_mode.clone();
}
}
pub(super) fn default_orchestration_config() -> OrchestrationConfig {
let mut profiles = BTreeMap::new();
profiles.insert(
String::from("ci"),
OrchestrationProfile {
description: Some(String::from("CI gate profile")),
max_parallel: Some(2),
env: BTreeMap::new(),
tasks: BTreeMap::new(),
},
);
OrchestrationConfig {
api_version: String::from(ORCHESTRATION_CONFIG_API_VERSION),
project: String::from("robotrt"),
max_parallel: 1,
profiles,
tasks: vec![
OrchestrationTask {
name: String::from("gate-fast-validated"),
command: vec![
String::from("bash"),
String::from("scripts/gate-fast-validated.sh"),
],
depends_on: Vec::new(),
cwd: None,
env: BTreeMap::new(),
continue_on_error: false,
enabled: true,
group: Some(String::from("gate")),
run_mode: TaskRunMode::Oneshot,
},
OrchestrationTask {
name: String::from("compat-matrix"),
command: vec![
String::from("bash"),
String::from("scripts/compat-matrix-gate.sh"),
],
depends_on: vec![String::from("gate-fast-validated")],
cwd: None,
env: BTreeMap::new(),
continue_on_error: false,
enabled: true,
group: Some(String::from("gate")),
run_mode: TaskRunMode::Oneshot,
},
OrchestrationTask {
name: String::from("slo-gate"),
command: vec![String::from("bash"), String::from("scripts/slo-gate.sh")],
depends_on: vec![String::from("gate-fast-validated")],
cwd: None,
env: BTreeMap::new(),
continue_on_error: false,
enabled: true,
group: Some(String::from("gate")),
run_mode: TaskRunMode::Oneshot,
},
OrchestrationTask {
name: String::from("obs-export-check"),
command: vec![
String::from("bash"),
String::from("scripts/obs-export-check.sh"),
],
depends_on: vec![String::from("gate-fast-validated")],
cwd: None,
env: BTreeMap::new(),
continue_on_error: false,
enabled: true,
group: Some(String::from("gate")),
run_mode: TaskRunMode::Oneshot,
},
OrchestrationTask {
name: String::from("release-candidate"),
command: vec![
String::from("bash"),
String::from("scripts/release-candidate.sh"),
],
depends_on: vec![
String::from("compat-matrix"),
String::from("slo-gate"),
String::from("obs-export-check"),
],
cwd: None,
env: BTreeMap::new(),
continue_on_error: false,
enabled: true,
group: Some(String::from("release")),
run_mode: TaskRunMode::Oneshot,
},
],
}
}
pub(super) fn validate_config(config: &OrchestrationConfig) -> Vec<String> {
let mut errors = Vec::new();
if config.api_version != ORCHESTRATION_CONFIG_API_VERSION {
errors.push(format!(
"api_version must be {}",
ORCHESTRATION_CONFIG_API_VERSION
));
}
if config.max_parallel == 0 {
errors.push(String::from("max_parallel must be >= 1"));
}
if config.tasks.is_empty() {
errors.push(String::from("tasks must not be empty"));
return errors;
}
let mut names = HashSet::new();
for task in &config.tasks {
if task.name.trim().is_empty() {
errors.push(String::from("task.name must not be empty"));
continue;
}
if !names.insert(task.name.clone()) {
errors.push(format!("duplicate task name: {}", task.name));
}
if task.command.is_empty() || task.command[0].trim().is_empty() {
errors.push(format!(
"task {} must provide a non-empty command array",
task.name
));
}
}
for task in &config.tasks {
for dep in &task.depends_on {
if dep == &task.name {
errors.push(format!("task {} depends on itself", task.name));
} else if !names.contains(dep) {
errors.push(format!(
"task {} references missing dependency {}",
task.name, dep
));
}
}
}
for (profile_name, profile) in &config.profiles {
if profile.max_parallel == Some(0) {
errors.push(format!("profile {} has max_parallel=0", profile_name));
}
for task_name in profile.tasks.keys() {
if !names.contains(task_name) {
errors.push(format!(
"profile {} references missing task {}",
profile_name, task_name
));
}
}
}
if errors.is_empty()
&& let Err(err) = topological_order(config, None)
{
errors.push(err);
}
errors
}