use crate::orchestrator::Orchestrator;
use async_trait::async_trait;
use repolith_core::action::Action;
use repolith_core::cache::Cache;
use repolith_core::manifest::Manifest;
use repolith_core::paths::contain_within;
use repolith_core::types::{ActionId, BuildError, BuildEvent, BuildOutput, Ctx, ExecMode, Sha256};
use sha2::{Digest, Sha256 as ShaHasher};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::process::Command;
use tokio::sync::Semaphore;
pub const MAX_FEDERATION_DEPTH: usize = 8;
pub trait FederationHooks: Send + Sync {
fn build_actions(
&self,
manifest: &Manifest,
stack_dir: &Path,
chain: &[PathBuf],
) -> Result<Vec<Box<dyn Action>>, BuildError>;
fn open_cache(&self, stack_dir: &Path) -> Result<Box<dyn Cache>, BuildError>;
}
pub struct RepolithSync {
pub id: ActionId,
pub node_path: PathBuf,
pub manifest_rel: Option<PathBuf>,
pub chain: Vec<PathBuf>,
pub mode: ExecMode,
pub shared_sem: Arc<Semaphore>,
pub hooks: Arc<dyn FederationHooks>,
pub deps: Vec<ActionId>,
}
impl RepolithSync {
fn resolved_manifest(&self) -> Result<PathBuf, BuildError> {
let rel = self
.manifest_rel
.as_deref()
.unwrap_or(Path::new("repolith.toml"));
let manifest = contain_within(&self.node_path, rel)?;
if let Some(pos) = self.chain.iter().position(|p| p == &manifest) {
let mut cycle: Vec<String> = self.chain[pos..]
.iter()
.map(|p| p.display().to_string())
.collect();
cycle.push(manifest.display().to_string());
return Err(BuildError::Io(format!(
"federation cycle: {}",
cycle.join(" -> ")
)));
}
if self.chain.len() >= MAX_FEDERATION_DEPTH {
return Err(BuildError::Io(format!(
"federation depth {} exceeds the maximum of {MAX_FEDERATION_DEPTH} (chain: {})",
self.chain.len() + 1,
self.chain
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(" -> ")
)));
}
Ok(manifest)
}
}
#[async_trait]
impl Action for RepolithSync {
fn id(&self) -> ActionId {
self.id.clone()
}
fn deps(&self) -> Vec<ActionId> {
self.deps.clone()
}
fn is_coordinator(&self) -> bool {
true
}
async fn input_hash(&self, ctx: &Ctx) -> Result<Sha256, BuildError> {
let mut h = ShaHasher::new();
h.update(b"repolith:manifest:");
match self.resolved_manifest() {
Ok(manifest) => {
let bytes = tokio::fs::read(&manifest).await.map_err(|e| {
BuildError::Io(format!("read manifest {}: {e}", manifest.display()))
})?;
h.update(&bytes);
let mut head = Command::new("git");
head.args(["-C"])
.arg(&self.node_path)
.args(["rev-parse", "HEAD"]);
let out = tokio::select! {
r = head.output() => {
r.map_err(|e| BuildError::Io(format!("spawn git rev-parse: {e}")))?
}
() = ctx.cancel.cancelled() => return Err(BuildError::Cancelled),
};
h.update(b":checkout:");
if out.status.success() {
h.update(&out.stdout);
} else {
h.update(b"no-git");
}
}
Err(_) if !self.node_path.exists() => {
h.update(b"pre-clone");
}
Err(e) => return Err(e),
}
Ok(Sha256(h.finalize().into()))
}
async fn execute(&self, ctx: &Ctx) -> Result<BuildOutput, BuildError> {
let manifest_path = self.resolved_manifest()?;
let stack_dir = manifest_path
.parent()
.ok_or_else(|| {
BuildError::Io(format!(
"manifest {} has no parent",
manifest_path.display()
))
})?
.to_path_buf();
let src = tokio::fs::read_to_string(&manifest_path)
.await
.map_err(|e| {
BuildError::Io(format!("read manifest {}: {e}", manifest_path.display()))
})?;
let manifest = Manifest::from_toml(&src).map_err(|e| {
BuildError::Io(format!("child manifest {}: {e}", manifest_path.display()))
})?;
let mut chain = self.chain.clone();
chain.push(manifest_path.clone());
let actions = self.hooks.build_actions(&manifest, &stack_dir, &chain)?;
let cache = self.hooks.open_cache(&stack_dir)?;
let child_ctx = Ctx {
cancel: ctx.cancel.child_token(),
workdir: stack_dir.clone(),
env: ctx.env.clone(),
};
let mut builder = Orchestrator::builder()
.manifest(manifest)
.base_ctx(child_ctx)
.shared_semaphore(Arc::clone(&self.shared_sem));
for a in actions {
builder = builder.register_boxed(a);
}
let mut orch = builder
.cache_boxed(cache)
.build()
.map_err(|e| BuildError::Io(format!("child orchestrator: {e}")))?;
let plan = orch.compute_plan().await.map_err(|e| {
BuildError::Io(format!("child plan ({}): {e}", manifest_path.display()))
})?;
let events = orch.execute_plan(&plan, self.mode).await.map_err(|e| {
BuildError::Io(format!("child sync ({}): {e}", manifest_path.display()))
})?;
let mut pairs: Vec<(String, [u8; 32])> = events
.iter()
.filter_map(|e| match e {
BuildEvent::Success { id, output, .. } => Some((id.0.clone(), output.0)),
BuildEvent::Failed { .. } => None,
})
.collect();
pairs.sort();
let mut h = ShaHasher::new();
h.update(b"repolith:events:");
for (id, out) in &pairs {
h.update(id.as_bytes());
h.update(b"=");
h.update(out);
}
Ok(BuildOutput {
output_hash: Sha256(h.finalize().into()),
stdout: format!(
"child stack {} synced — {} action(s)",
stack_dir.display(),
pairs.len()
),
})
}
}