Compare commits
7 Commits
7883cfa908
...
b0f66f9463
Author | SHA1 | Date | |
---|---|---|---|
b0f66f9463 | |||
91fe491751 | |||
e9e80abad0 | |||
1fda414e05 | |||
28a1d09974 | |||
dca625af31 | |||
bd927840d6 |
2
.env
2
.env
@ -3,3 +3,5 @@ FOREST_S3_BUCKET=forest
|
||||
FOREST_S3_REGION=eu-west-1
|
||||
FOREST_S3_USER=forestadmin
|
||||
FOREST_S3_PASSWORD=forestadmin
|
||||
|
||||
FOREST_LOG_LEVEL=forest=trace
|
||||
|
20
Cargo.lock
generated
20
Cargo.lock
generated
@ -138,9 +138,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.31"
|
||||
version = "4.5.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
|
||||
checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@ -148,9 +148,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.31"
|
||||
version = "4.5.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
|
||||
checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@ -160,9 +160,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.28"
|
||||
version = "4.5.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
|
||||
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@ -1071,9 +1071,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.43.0"
|
||||
version = "1.44.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
|
||||
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@ -1209,9 +1209,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.15.1"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
|
||||
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
@ -1,12 +1,13 @@
|
||||
use std::{net::SocketAddr, path::PathBuf};
|
||||
|
||||
use anyhow::Context as AnyContext;
|
||||
use clap::{FromArgMatches, Parser, Subcommand, crate_authors, crate_description, crate_version};
|
||||
use colored_json::ToColoredJson;
|
||||
use kdl::KdlDocument;
|
||||
use rusty_s3::{Bucket, Credentials, S3Action};
|
||||
|
||||
use crate::{
|
||||
model::{Context, Plan, Project},
|
||||
model::{Context, ForestFile, Plan, Project, WorkspaceProject},
|
||||
plan_reconciler::PlanReconciler,
|
||||
state::SharedState,
|
||||
};
|
||||
@ -47,6 +48,7 @@ fn get_root(include_run: bool) -> clap::Command {
|
||||
.author(crate_authors!())
|
||||
.version(crate_version!())
|
||||
.about(crate_description!())
|
||||
.ignore_errors(include_run)
|
||||
.arg(
|
||||
clap::Arg::new("project_path")
|
||||
.long("project-path")
|
||||
@ -55,9 +57,8 @@ fn get_root(include_run: bool) -> clap::Command {
|
||||
);
|
||||
|
||||
if include_run {
|
||||
root_cmd = root_cmd.subcommand(clap::Command::new("run").allow_external_subcommands(true));
|
||||
root_cmd = root_cmd.subcommand(clap::Command::new("run").allow_external_subcommands(true))
|
||||
}
|
||||
|
||||
Commands::augment_subcommands(root_cmd)
|
||||
}
|
||||
|
||||
@ -78,13 +79,189 @@ pub async fn execute() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
let project_file = tokio::fs::read_to_string(&project_file_path).await?;
|
||||
let project_doc: KdlDocument = project_file.parse()?;
|
||||
let doc: KdlDocument = project_file.parse()?;
|
||||
let project: ForestFile = doc.try_into()?;
|
||||
|
||||
let project: Project = project_doc.try_into()?;
|
||||
match project {
|
||||
ForestFile::Workspace(workspace) => {
|
||||
tracing::trace!("running as workspace");
|
||||
|
||||
// 1. For each member load the project
|
||||
|
||||
let mut workspace_members = Vec::new();
|
||||
|
||||
for member in workspace.members {
|
||||
let workspace_member_path = project_path.join(&member.path);
|
||||
|
||||
let project_file_path = workspace_member_path.join("forest.kdl");
|
||||
if !project_file_path.exists() {
|
||||
anyhow::bail!(
|
||||
"no 'forest.kdl' file was found at: {}",
|
||||
workspace_member_path.display().to_string()
|
||||
);
|
||||
}
|
||||
|
||||
let project_file = tokio::fs::read_to_string(&project_file_path).await?;
|
||||
let doc: KdlDocument = project_file.parse()?;
|
||||
let project: WorkspaceProject = doc.try_into().context(format!(
|
||||
"workspace member: {} failed to parse",
|
||||
&member.path
|
||||
))?;
|
||||
|
||||
workspace_members.push((workspace_member_path, project));
|
||||
}
|
||||
|
||||
// TODO: 1a (optional). Resolve dependencies
|
||||
// 2. Reconcile plans
|
||||
|
||||
let mut member_contexts = Vec::new();
|
||||
|
||||
for (member_path, member) in &workspace_members {
|
||||
match member {
|
||||
WorkspaceProject::Plan(_plan) => {
|
||||
tracing::warn!("skipping reconcile for plans for now")
|
||||
}
|
||||
WorkspaceProject::Project(project) => {
|
||||
let plan = if let Some(plan_file_path) = PlanReconciler::new()
|
||||
.reconcile(
|
||||
project,
|
||||
member_path,
|
||||
Some(workspace_members.as_ref()),
|
||||
Some(&project_path),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
let plan_file = tokio::fs::read_to_string(&plan_file_path)
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to read file at: {}",
|
||||
project_path.to_string_lossy()
|
||||
))?;
|
||||
let plan_doc: KdlDocument = plan_file.parse()?;
|
||||
|
||||
let plan: Plan = plan_doc.try_into()?;
|
||||
tracing::trace!("found a plan name: {}", project.name);
|
||||
|
||||
Some(plan)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let context = Context {
|
||||
project: project.clone(),
|
||||
plan,
|
||||
};
|
||||
member_contexts.push((member_path, context));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!("run is called, building extra commands, rerunning the parser");
|
||||
let mut run_cmd = clap::Command::new("run").subcommand_required(true);
|
||||
|
||||
// 3. Provide context and aggregated commands for projects
|
||||
for (_, context) in &member_contexts {
|
||||
let commands = run::Run::augment_workspace_command(context, &context.project.name);
|
||||
run_cmd = run_cmd.subcommands(commands);
|
||||
}
|
||||
|
||||
run_cmd =
|
||||
run_cmd.subcommand(clap::Command::new("all").allow_external_subcommands(true));
|
||||
|
||||
let mut root = get_root(false).subcommand(run_cmd);
|
||||
let matches = root.get_matches_mut();
|
||||
|
||||
if matches.subcommand().is_none() {
|
||||
root.print_help()?;
|
||||
anyhow::bail!("failed to find command");
|
||||
}
|
||||
|
||||
match matches
|
||||
.subcommand()
|
||||
.expect("forest requires a command to be passed")
|
||||
{
|
||||
("run", args) => {
|
||||
let (run_args, args) = args.subcommand().expect("run must have subcommands");
|
||||
|
||||
match run_args {
|
||||
"all" => {
|
||||
let (all_cmd, _args) = args
|
||||
.subcommand()
|
||||
.expect("to be able to get a subcommand (todo: might not work)");
|
||||
|
||||
for (member_path, context) in member_contexts {
|
||||
run::Run::execute_command_if_exists(all_cmd, member_path, &context)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let (project_name, command) = run_args
|
||||
.split_once("::")
|
||||
.expect("commands to always be pairs for workspaces");
|
||||
|
||||
let mut found_context = false;
|
||||
for (member_path, context) in &member_contexts {
|
||||
if project_name == context.project.name {
|
||||
run::Run::execute_command(command, member_path, context)
|
||||
.await?;
|
||||
|
||||
found_context = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !found_context {
|
||||
anyhow::bail!("no matching context was found")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => match Commands::from_arg_matches(&matches).unwrap() {
|
||||
Commands::Init {} => {
|
||||
tracing::info!("initializing project");
|
||||
}
|
||||
Commands::Info {} => {
|
||||
let output = serde_json::to_string_pretty(&member_contexts)?;
|
||||
println!("{}", output.to_colored_json_auto().unwrap_or(output));
|
||||
}
|
||||
Commands::Template(template) => {
|
||||
//template.execute(&project_path, &context).await?;
|
||||
}
|
||||
Commands::Serve {
|
||||
s3_endpoint,
|
||||
s3_bucket,
|
||||
s3_region,
|
||||
s3_user,
|
||||
s3_password,
|
||||
..
|
||||
} => {
|
||||
tracing::info!("Starting server");
|
||||
let creds = Credentials::new(s3_user, s3_password);
|
||||
let bucket = Bucket::new(
|
||||
url::Url::parse(&s3_endpoint)?,
|
||||
rusty_s3::UrlStyle::Path,
|
||||
s3_bucket,
|
||||
s3_region,
|
||||
)?;
|
||||
let put_object = bucket.put_object(Some(&creds), "some-object");
|
||||
let _url = put_object.sign(std::time::Duration::from_secs(30));
|
||||
let _state = SharedState::new().await?;
|
||||
}
|
||||
Commands::Clean {} => {
|
||||
todo!();
|
||||
// let forest_path = project_path.join(".forest");
|
||||
// if forest_path.exists() {
|
||||
// tokio::fs::remove_dir_all(forest_path).await?;
|
||||
// tracing::info!("removed .forest");
|
||||
// }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
ForestFile::Project(project) => {
|
||||
tracing::trace!("found a project name: {}", project.name);
|
||||
|
||||
let plan = if let Some(plan_file_path) = PlanReconciler::new()
|
||||
.reconcile(&project, &project_path)
|
||||
.reconcile(&project, &project_path, None, None)
|
||||
.await?
|
||||
{
|
||||
let plan_file = tokio::fs::read_to_string(&plan_file_path).await?;
|
||||
@ -104,9 +281,8 @@ pub async fn execute() -> anyhow::Result<()> {
|
||||
tracing::debug!("run is called, building extra commands, rerunning the parser");
|
||||
let root = get_root(false);
|
||||
|
||||
let root = run::Run::augment_command(root, &context);
|
||||
|
||||
root.get_matches()
|
||||
let run_cmd = run::Run::augment_command(&context);
|
||||
root.subcommand(run_cmd).get_matches()
|
||||
} else {
|
||||
matches
|
||||
};
|
||||
@ -159,6 +335,8 @@ pub async fn execute() -> anyhow::Result<()> {
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::{collections::BTreeMap, path::Path};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{model::Context, script::ScriptExecutor};
|
||||
|
||||
@ -7,7 +7,7 @@ use crate::{model::Context, script::ScriptExecutor};
|
||||
// create a new sub command that encapsulates all the run complexities
|
||||
pub struct Run {}
|
||||
impl Run {
|
||||
pub fn augment_command(root: clap::Command, ctx: &Context) -> clap::Command {
|
||||
pub fn augment_command(ctx: &Context) -> clap::Command {
|
||||
let mut run_cmd = clap::Command::new("run")
|
||||
.subcommand_required(true)
|
||||
.about("runs any kind of script from either the project or plan");
|
||||
@ -37,7 +37,37 @@ impl Run {
|
||||
}
|
||||
}
|
||||
|
||||
root.subcommand(run_cmd)
|
||||
run_cmd
|
||||
}
|
||||
|
||||
pub fn augment_workspace_command(ctx: &Context, prefix: &str) -> Vec<clap::Command> {
|
||||
let mut commands = Vec::new();
|
||||
if let Some(scripts) = &ctx.project.scripts {
|
||||
for name in scripts.items.keys() {
|
||||
let cmd = clap::Command::new(format!("{prefix}::{name}"));
|
||||
commands.push(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(plan) = &ctx.plan {
|
||||
if let Some(scripts) = &plan.scripts {
|
||||
let existing_cmds = commands
|
||||
.iter()
|
||||
.map(|s| format!("{prefix}::{}", s.get_name()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for name in scripts.items.keys() {
|
||||
if existing_cmds.contains(name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cmd = clap::Command::new(format!("{prefix}::{name}"));
|
||||
commands.push(cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commands
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
@ -73,4 +103,64 @@ impl Run {
|
||||
|
||||
anyhow::bail!("no scripts were found for command: {}", name)
|
||||
}
|
||||
|
||||
pub async fn execute_command(
|
||||
command: &str,
|
||||
project_path: &Path,
|
||||
ctx: &Context,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(scripts_ctx) = &ctx.project.scripts {
|
||||
if let Some(script_ctx) = scripts_ctx.items.get(command) {
|
||||
ScriptExecutor::new(project_path.into(), ctx.clone())
|
||||
.run(script_ctx, command)
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(plan) = &ctx.plan {
|
||||
if let Some(scripts_ctx) = &plan.scripts {
|
||||
if let Some(script_ctx) = scripts_ctx.items.get(command) {
|
||||
ScriptExecutor::new(project_path.into(), ctx.clone())
|
||||
.run(script_ctx, command)
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!("no scripts were found for command: {}", command)
|
||||
}
|
||||
|
||||
pub async fn execute_command_if_exists(
|
||||
command: &str,
|
||||
project_path: &Path,
|
||||
ctx: &Context,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(scripts_ctx) = &ctx.project.scripts {
|
||||
if let Some(script_ctx) = scripts_ctx.items.get(command) {
|
||||
ScriptExecutor::new(project_path.into(), ctx.clone())
|
||||
.run(script_ctx, command)
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(plan) = &ctx.plan {
|
||||
if let Some(scripts_ctx) = &plan.scripts {
|
||||
if let Some(script_ctx) = scripts_ctx.items.get(command) {
|
||||
ScriptExecutor::new(project_path.into(), ctx.clone())
|
||||
.run(script_ctx, command)
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
use std::{collections::BTreeMap, fmt::Debug, path::PathBuf};
|
||||
|
||||
use colored_json::Paint;
|
||||
use kdl::{KdlDocument, KdlNode, KdlValue};
|
||||
use serde::Serialize;
|
||||
|
||||
@ -13,7 +12,9 @@ pub struct Context {
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Plan {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub templates: Option<Templates>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub scripts: Option<Scripts>,
|
||||
}
|
||||
|
||||
@ -55,6 +56,7 @@ impl TryFrom<KdlDocument> for Plan {
|
||||
pub enum ProjectPlan {
|
||||
Local { path: PathBuf },
|
||||
Git { url: String, path: Option<PathBuf> },
|
||||
Workspace { name: String },
|
||||
NoPlan,
|
||||
}
|
||||
|
||||
@ -94,6 +96,17 @@ impl TryFrom<&KdlNode> for ProjectPlan {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(workspace) = children.get_arg("workspace") {
|
||||
return Ok(Self::Workspace {
|
||||
name: workspace
|
||||
.as_string()
|
||||
.map(|w| w.to_string())
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"workspace requires a project name in the same project"
|
||||
))?,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self::NoPlan)
|
||||
}
|
||||
}
|
||||
@ -164,6 +177,12 @@ pub struct Global {
|
||||
items: BTreeMap<String, GlobalVariable>,
|
||||
}
|
||||
|
||||
impl Global {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.items.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Global> for minijinja::Value {
|
||||
fn from(value: &Global) -> Self {
|
||||
Self::from_serialize(&value.items)
|
||||
@ -312,10 +331,16 @@ impl TryFrom<&KdlNode> for Scripts {
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Project {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub plan: Option<ProjectPlan>,
|
||||
|
||||
#[serde(skip_serializing_if = "Global::is_empty")]
|
||||
pub global: Global,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub templates: Option<Templates>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub scripts: Option<Scripts>,
|
||||
}
|
||||
|
||||
@ -372,3 +397,111 @@ impl TryFrom<KdlDocument> for Project {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct WorkspaceMember {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl TryFrom<&kdl::KdlNode> for WorkspaceMember {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &kdl::KdlNode) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
path: value
|
||||
.entries()
|
||||
.first()
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"is supposed to have a path `member ./some-path`"
|
||||
))?
|
||||
.value()
|
||||
.as_string()
|
||||
.ok_or(anyhow::anyhow!("value is required to be a string"))?
|
||||
.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Workspace {
|
||||
pub members: Vec<WorkspaceMember>,
|
||||
}
|
||||
|
||||
impl TryFrom<KdlDocument> for Workspace {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: KdlDocument) -> Result<Self, Self::Error> {
|
||||
let workspace = value
|
||||
.get("workspace")
|
||||
.expect("to have a workspace at this point")
|
||||
.children()
|
||||
.ok_or(anyhow::anyhow!("workspace to be a section"))?;
|
||||
|
||||
Ok(Self {
|
||||
members: workspace
|
||||
.get("members")
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"a members section is required for a workspace"
|
||||
))?
|
||||
.children()
|
||||
.ok_or(anyhow::anyhow!("a members is required to have children"))?
|
||||
.nodes()
|
||||
.iter()
|
||||
.map(|m| m.try_into())
|
||||
.collect::<anyhow::Result<Vec<_>>>()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub enum ForestFile {
|
||||
Workspace(Workspace),
|
||||
Project(Project),
|
||||
}
|
||||
|
||||
impl TryFrom<KdlDocument> for ForestFile {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: KdlDocument) -> Result<Self, Self::Error> {
|
||||
if value.get("workspace").is_some() && value.get("project").is_some() {
|
||||
anyhow::bail!("a forest.kdl file cannot contain both a workspace and project")
|
||||
}
|
||||
|
||||
if value.get("project").is_some() {
|
||||
return Ok(Self::Project(value.try_into()?));
|
||||
}
|
||||
|
||||
if value.get("workspace").is_some() {
|
||||
return Ok(Self::Workspace(value.try_into()?));
|
||||
}
|
||||
|
||||
anyhow::bail!("a forest.kdl file must be either a project, workspace or plan")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum WorkspaceProject {
|
||||
Plan(Plan),
|
||||
Project(Project),
|
||||
}
|
||||
|
||||
impl TryFrom<KdlDocument> for WorkspaceProject {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: KdlDocument) -> Result<Self, Self::Error> {
|
||||
if value.get("plan").is_some() && value.get("project").is_some() {
|
||||
anyhow::bail!("a forest.kdl file cannot contain both a plan and project")
|
||||
}
|
||||
|
||||
if value.get("project").is_some() {
|
||||
return Ok(Self::Project(value.try_into()?));
|
||||
}
|
||||
|
||||
if value.get("plan").is_some() {
|
||||
return Ok(Self::Plan(value.try_into()?));
|
||||
}
|
||||
|
||||
anyhow::bail!("a forest.kdl file must be either a project, workspace or plan")
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::model::Project;
|
||||
use crate::model::{Project, WorkspaceProject};
|
||||
|
||||
pub mod git;
|
||||
pub mod local;
|
||||
@ -21,6 +21,8 @@ impl PlanReconciler {
|
||||
&self,
|
||||
project: &Project,
|
||||
destination: &Path,
|
||||
workspace_members: Option<&Vec<(PathBuf, WorkspaceProject)>>,
|
||||
workspace_root: Option<&Path>,
|
||||
) -> anyhow::Result<Option<PathBuf>> {
|
||||
tracing::info!("reconciling project");
|
||||
if project.plan.is_none() {
|
||||
@ -55,6 +57,21 @@ impl PlanReconciler {
|
||||
crate::model::ProjectPlan::Git { url, path } => {
|
||||
git::reconcile(url, path, &plan_dir).await?;
|
||||
}
|
||||
crate::model::ProjectPlan::Workspace { name } => {
|
||||
let workspace_root = workspace_root.expect("to have workspace root available");
|
||||
|
||||
if let Some(workspace_members) = workspace_members {
|
||||
for (member_path, member) in workspace_members {
|
||||
if let WorkspaceProject::Plan(plan) = member {
|
||||
if &plan.name == name {
|
||||
tracing::debug!("found workspace project: {}", name);
|
||||
local::reconcile(&workspace_root.join(member_path), &plan_dir)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::model::ProjectPlan::NoPlan => {
|
||||
tracing::debug!("no plan, returning");
|
||||
return Ok(None);
|
||||
|
@ -29,7 +29,7 @@ impl ShellExecutor {
|
||||
}
|
||||
|
||||
let mut cmd = tokio::process::Command::new(&script_path);
|
||||
let cmd = cmd.current_dir(path);
|
||||
let cmd = cmd.current_dir(&self.root.project_path);
|
||||
cmd.stdin(Stdio::inherit());
|
||||
cmd.stdout(Stdio::inherit());
|
||||
cmd.stderr(Stdio::inherit());
|
||||
@ -53,6 +53,7 @@ impl ShellExecutor {
|
||||
|
||||
fn get_path(&self) -> PathBuf {
|
||||
match self.ty {
|
||||
//ShellType::Plan => self.root.project_path.join(".forest").join("plan"),
|
||||
ShellType::Plan => self.root.project_path.join(".forest").join("plan"),
|
||||
ShellType::Project => self.root.project_path.clone(),
|
||||
}
|
||||
|
0
examples/workspace/components/flux/forest.kdl
Normal file
0
examples/workspace/components/flux/forest.kdl
Normal file
9
examples/workspace/forest.kdl
Normal file
9
examples/workspace/forest.kdl
Normal file
@ -0,0 +1,9 @@
|
||||
workspace {
|
||||
members {
|
||||
member "projects/a"
|
||||
member "projects/b"
|
||||
member "plan/a"
|
||||
member "plan/b"
|
||||
// member "components/*"
|
||||
}
|
||||
}
|
7
examples/workspace/plan/a/forest.kdl
Normal file
7
examples/workspace/plan/a/forest.kdl
Normal file
@ -0,0 +1,7 @@
|
||||
plan {
|
||||
name a
|
||||
|
||||
scripts {
|
||||
hello_plan type=shell {}
|
||||
}
|
||||
}
|
7
examples/workspace/plan/a/scripts/hello_plan.sh
Executable file
7
examples/workspace/plan/a/scripts/hello_plan.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
set -e
|
||||
|
||||
echo "hello from plan"
|
||||
|
||||
echo "i am here: $PWD"
|
3
examples/workspace/plan/b/forest.kdl
Normal file
3
examples/workspace/plan/b/forest.kdl
Normal file
@ -0,0 +1,3 @@
|
||||
plan {
|
||||
name b
|
||||
}
|
11
examples/workspace/projects/a/forest.kdl
Normal file
11
examples/workspace/projects/a/forest.kdl
Normal file
@ -0,0 +1,11 @@
|
||||
project {
|
||||
name a
|
||||
|
||||
plan {
|
||||
workspace a
|
||||
}
|
||||
|
||||
scripts {
|
||||
hello type=shell {}
|
||||
}
|
||||
}
|
7
examples/workspace/projects/a/scripts/hello.sh
Executable file
7
examples/workspace/projects/a/scripts/hello.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
set -e
|
||||
|
||||
echo "hello from a"
|
||||
|
||||
echo "i am here: $PWD"
|
7
examples/workspace/projects/b/forest.kdl
Normal file
7
examples/workspace/projects/b/forest.kdl
Normal file
@ -0,0 +1,7 @@
|
||||
project {
|
||||
name b
|
||||
|
||||
scripts {
|
||||
hello type=shell {}
|
||||
}
|
||||
}
|
5
examples/workspace/projects/b/scripts/hello.sh
Executable file
5
examples/workspace/projects/b/scripts/hello.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
set -e
|
||||
|
||||
echo "hello from b"
|
Loading…
x
Reference in New Issue
Block a user