From 0cd88456b21d5c444ac4257b426c03d2872e6afd Mon Sep 17 00:00:00 2001 From: allure <1550220889@qq.com> Date: Mon, 2 Feb 2026 10:51:56 +0800 Subject: [PATCH 1/2] feat: implement build trigger system Signed-off-by: allure <1550220889@qq.com> --- ceres/src/build_trigger/dispatcher.rs | 107 +++++ ceres/src/build_trigger/git_push_handler.rs | 151 ++++++ ceres/src/build_trigger/manual_handler.rs | 68 +++ ceres/src/build_trigger/mod.rs | 114 +++++ ceres/src/build_trigger/model.rs | 505 ++++++++++++++++++++ ceres/src/build_trigger/ref_resolver.rs | 159 ++++++ ceres/src/build_trigger/retry_handler.rs | 72 +++ ceres/src/build_trigger/service.rs | 201 ++++++++ ceres/src/lib.rs | 1 + ceres/src/pack/monorepo.rs | 72 +-- mono/Cargo.toml | 1 + mono/src/api/api_router.rs | 8 +- mono/src/api/error.rs | 34 +- mono/src/api/mod.rs | 11 + mono/src/api/router/build_trigger_router.rs | 148 ++++++ mono/src/api/router/mod.rs | 1 + mono/src/server/http_server.rs | 1 + 17 files changed, 1586 insertions(+), 68 deletions(-) create mode 100644 ceres/src/build_trigger/dispatcher.rs create mode 100644 ceres/src/build_trigger/git_push_handler.rs create mode 100644 ceres/src/build_trigger/manual_handler.rs create mode 100644 ceres/src/build_trigger/mod.rs create mode 100644 ceres/src/build_trigger/model.rs create mode 100644 ceres/src/build_trigger/ref_resolver.rs create mode 100644 ceres/src/build_trigger/retry_handler.rs create mode 100644 ceres/src/build_trigger/service.rs create mode 100644 mono/src/api/router/build_trigger_router.rs diff --git a/ceres/src/build_trigger/dispatcher.rs b/ceres/src/build_trigger/dispatcher.rs new file mode 100644 index 000000000..e31cdba62 --- /dev/null +++ b/ceres/src/build_trigger/dispatcher.rs @@ -0,0 +1,107 @@ +use std::sync::Arc; + +use bellatrix::{Bellatrix, orion_client::OrionBuildRequest}; +use common::errors::MegaError; +use jupiter::storage::Storage; + +use crate::build_trigger::{BuildTrigger, BuildTriggerPayload, SerializableBuildInfo}; + +/// Handles dispatching build triggers to the build execution layer (Bellatrix/Orion). +pub struct BuildDispatcher { + storage: Storage, + bellatrix: Arc, +} + +impl BuildDispatcher { + pub fn new(storage: Storage, bellatrix: Arc) -> Self { + Self { storage, bellatrix } + } + + /// Dispatch a build trigger. + /// + /// This method: + /// 1. Persists the trigger to the database + /// 2. Sends the build request to Bellatrix/Orion asynchronously + /// + /// Returns the ID of the created trigger record. + pub async fn dispatch(&self, trigger: BuildTrigger) -> Result { + let trigger_payload = serde_json::to_value(&trigger.payload).map_err(|e| { + tracing::error!("Failed to serialize payload: {}", e); + MegaError::Other(format!("Failed to serialize payload: {}", e)) + })?; + + let db_record = self + .storage + .build_trigger_storage() + .insert( + trigger.trigger_type.to_string(), + trigger.trigger_source.to_string(), + trigger_payload, + ) + .await?; + + // Persist to database + tracing::info!("BuildDispatcher: Persisted trigger ID: {}", db_record.id); + + if !self.bellatrix.enable_build() { + tracing::info!("BuildDispatcher: Completed (build system disabled)"); + return Ok(db_record.id); + } + + // Extract data from payload using pattern matching + let (cl_link, repo, builds_json, cl_id) = match &trigger.payload { + BuildTriggerPayload::GitPush(p) => (&p.cl_link, &p.repo, &p.builds, p.cl_id), + BuildTriggerPayload::Manual(p) => (&p.cl_link, &p.repo, &p.builds, p.cl_id), + BuildTriggerPayload::Retry(p) => (&p.cl_link, &p.repo, &p.builds, p.cl_id), + BuildTriggerPayload::Webhook(p) => (&p.cl_link, &p.repo, &p.builds, p.cl_id), + BuildTriggerPayload::Schedule(p) => (&p.cl_link, &p.repo, &p.builds, p.cl_id), + }; + + let builds: Vec = serde_json::from_value(builds_json.clone()) + .map_err(|e| { + tracing::error!("Failed to deserialize builds from payload: {}", e); + MegaError::Other(format!("Failed to deserialize builds from payload: {}", e)) + })?; + + let bellatrix_builds: Vec = builds + .into_iter() + .enumerate() + .map(|(idx, info)| { + tracing::debug!(" Build [{}]: {} change(s)", idx + 1, info.changes.len()); + bellatrix::orion_client::BuildInfo { + changes: info.changes.into_iter().map(|s| s.into()).collect(), + } + }) + .collect(); + + let req = OrionBuildRequest { + cl_link: cl_link.to_string(), + mount_path: repo.to_string(), + cl: cl_id.unwrap_or(0), + builds: bellatrix_builds, + }; + + // Dispatch asynchronously + let bellatrix = self.bellatrix.clone(); + let trigger_id = db_record.id; + tokio::spawn(async move { + match bellatrix.on_post_receive(req).await { + Ok(_) => { + tracing::info!( + "BuildDispatcher: Build request sent to Bellatrix (Trigger ID: {})", + trigger_id + ); + } + Err(err) => { + tracing::error!( + "BuildDispatcher: Failed to dispatch build (Trigger ID: {}): {}", + trigger_id, + err + ); + } + } + }); + + Ok(db_record.id) + } +} diff --git a/ceres/src/build_trigger/git_push_handler.rs b/ceres/src/build_trigger/git_push_handler.rs new file mode 100644 index 000000000..1f1adf513 --- /dev/null +++ b/ceres/src/build_trigger/git_push_handler.rs @@ -0,0 +1,151 @@ +use std::{path::PathBuf, sync::Arc}; + +use async_trait::async_trait; +use bellatrix::orion_client::{BuildInfo, ProjectRelativePath, Status}; +use chrono::Utc; +use common::errors::MegaError; +use git_internal::hash::ObjectHash; +use jupiter::storage::Storage; + +use crate::{ + api_service::{cache::GitObjectCache, mono_api_service::MonoApiService}, + build_trigger::{ + BuildTrigger, BuildTriggerPayload, BuildTriggerType, TriggerContext, TriggerHandler, + }, + model::change_list::ClFilesRes, +}; + +/// Handler for Git push triggers. +pub struct GitPushHandler { + storage: Storage, + git_object_cache: Arc, +} + +impl GitPushHandler { + pub fn new(storage: Storage, git_object_cache: Arc) -> Self { + Self { + storage, + git_object_cache, + } + } + + pub async fn get_builds_for_commit( + &self, + context: &TriggerContext, + ) -> Result, MegaError> { + let old_files = self.get_commit_blobs(&context.from_hash).await?; + let new_files = self.get_commit_blobs(&context.commit_hash).await?; + let diff_files = self.cl_files_list(old_files, new_files).await?; + + let changes = self.build_changes(&context.repo_path, diff_files)?; + + let builds = vec![BuildInfo { + changes: changes.clone(), + }]; + + Ok(builds) + } + + fn build_changes( + &self, + cl_path: &str, + cl_diff_files: Vec, + ) -> Result>, MegaError> { + let cl_base = PathBuf::from(cl_path); + let path_str = cl_base.to_str().ok_or_else(|| { + MegaError::Other(format!("CL base path is not valid UTF-8: {:?}", cl_base)) + })?; + + let changes = cl_diff_files + .into_iter() + .map(|m| { + let mut item: ClFilesRes = m.into(); + item.path = cl_base.join(item.path).to_string_lossy().to_string(); + item + }) + .collect::>(); + + let counter_changes = changes + .iter() + .filter(|&s| PathBuf::from(&s.path).starts_with(&cl_base)) + .map(|s| { + let path = ProjectRelativePath::from_abs(&s.path, path_str).ok_or_else(|| { + MegaError::Other(format!("Invalid project-relative path: {}", s.path)) + })?; + let status = if s.action == "new" { + Status::Added(path) + } else if s.action == "deleted" { + Status::Removed(path) + } else if s.action == "modified" { + Status::Modified(path) + } else { + return Err(MegaError::Other(format!( + "Unsupported change action: {}", + s.action + ))); + }; + Ok(status) + }) + .collect::, MegaError>>()?; + + Ok(counter_changes) + } + + async fn get_commit_blobs( + &self, + commit_hash: &str, + ) -> Result, MegaError> { + let api_service = MonoApiService { + storage: self.storage.clone(), + git_object_cache: self.git_object_cache.clone(), + }; + api_service.get_commit_blobs(commit_hash).await + } + + async fn cl_files_list( + &self, + old_files: Vec<(PathBuf, ObjectHash)>, + new_files: Vec<(PathBuf, ObjectHash)>, + ) -> Result, MegaError> { + let api_service = MonoApiService { + storage: self.storage.clone(), + git_object_cache: self.git_object_cache.clone(), + }; + api_service.cl_files_list(old_files, new_files).await + } +} + +#[async_trait] +impl TriggerHandler for GitPushHandler { + async fn handle(&self, context: &TriggerContext) -> Result { + let builds = self.get_builds_for_commit(context).await?; + + let cl_link = context.cl_link.clone().unwrap_or_else(|| { + format!( + "push-{}-{}", + Utc::now().timestamp_millis(), + &context.commit_hash[..8.min(context.commit_hash.len())] + ) + }); + + Ok(BuildTrigger { + trigger_type: context.trigger_type, + trigger_source: context.trigger_source, + trigger_time: Utc::now(), + payload: BuildTriggerPayload::GitPush(crate::build_trigger::GitPushPayload { + repo: context.repo_path.clone(), + from_hash: context.from_hash.clone(), + commit_hash: context.commit_hash.clone(), + cl_link, + cl_id: context.cl_id, + builds: serde_json::to_value(&builds) + .map_err(|e| MegaError::Other(format!("Failed to serialize builds: {}", e)))?, + triggered_by: context.triggered_by.clone(), + }), + }) + } + + fn trigger_type(&self) -> BuildTriggerType { + BuildTriggerType::GitPush + } +} diff --git a/ceres/src/build_trigger/manual_handler.rs b/ceres/src/build_trigger/manual_handler.rs new file mode 100644 index 000000000..fa592347f --- /dev/null +++ b/ceres/src/build_trigger/manual_handler.rs @@ -0,0 +1,68 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use chrono::Utc; +use common::errors::MegaError; +use jupiter::storage::Storage; + +use super::git_push_handler::GitPushHandler; +use crate::{ + api_service::cache::GitObjectCache, + build_trigger::{ + BuildTrigger, BuildTriggerPayload, BuildTriggerType, TriggerContext, TriggerHandler, + }, +}; + +/// Handler for manual build triggers. +pub struct ManualHandler { + git_push_handler: GitPushHandler, +} + +impl ManualHandler { + pub fn new(storage: Storage, git_object_cache: Arc) -> Self { + Self { + git_push_handler: GitPushHandler::new(storage, git_object_cache), + } + } +} + +#[async_trait] +impl TriggerHandler for ManualHandler { + async fn handle(&self, context: &TriggerContext) -> Result { + // Reuse GitPushHandler logic for getting builds + let builds = self.git_push_handler.get_builds_for_commit(context).await?; + + let cl_link = context.cl_link.clone().unwrap_or_else(|| { + format!( + "manual-{}-{}", + Utc::now().timestamp_millis(), + &context.commit_hash[..8.min(context.commit_hash.len())] + ) + }); + + Ok(BuildTrigger { + trigger_type: BuildTriggerType::Manual, + trigger_source: context.trigger_source, + trigger_time: Utc::now(), + payload: BuildTriggerPayload::Manual(crate::build_trigger::ManualPayload { + repo: context.repo_path.clone(), + commit_hash: context.commit_hash.clone(), + triggered_by: context + .triggered_by + .clone() + .unwrap_or_else(|| "unknown".to_string()), + builds: serde_json::to_value(&builds) + .map_err(|e| MegaError::Other(format!("Failed to serialize builds: {}", e)))?, + params: context.params.clone(), + cl_link: cl_link.clone(), + cl_id: context.cl_id, + ref_name: context.ref_name.clone(), + ref_type: context.ref_type.clone(), + }), + }) + } + + fn trigger_type(&self) -> BuildTriggerType { + BuildTriggerType::Manual + } +} diff --git a/ceres/src/build_trigger/mod.rs b/ceres/src/build_trigger/mod.rs new file mode 100644 index 000000000..1c69c7d76 --- /dev/null +++ b/ceres/src/build_trigger/mod.rs @@ -0,0 +1,114 @@ +use std::{collections::HashMap, sync::Arc}; + +use async_trait::async_trait; +use bellatrix::Bellatrix; +use common::errors::MegaError; +use jupiter::storage::Storage; + +use crate::api_service::cache::GitObjectCache; + +mod dispatcher; +mod git_push_handler; +mod manual_handler; +mod model; +mod ref_resolver; +mod retry_handler; + +// Export all models from the single model file +pub use model::*; +pub use ref_resolver::{RefResolver, RefType, ResolvedRef}; +pub mod service; +use dispatcher::BuildDispatcher; +use git_push_handler::GitPushHandler; +use manual_handler::ManualHandler; +use retry_handler::RetryHandler; +pub use service::BuildTriggerService; + +/// Trait for handling different types of build triggers. +#[async_trait] +pub trait TriggerHandler: Send + Sync { + /// Handle the trigger and return a BuildTrigger. + async fn handle(&self, context: &TriggerContext) -> Result; + + /// Get the trigger type this handler supports. + fn trigger_type(&self) -> BuildTriggerType; +} + +/// Registry for managing and dispatching build triggers. +pub struct TriggerRegistry { + handlers: HashMap>, + dispatcher: Arc, +} + +impl TriggerRegistry { + /// Create a new TriggerRegistry with all handlers registered. + pub fn new( + storage: Storage, + git_object_cache: Arc, + bellatrix: Arc, + ) -> Self { + let mut registry = Self { + handlers: HashMap::new(), + dispatcher: Arc::new(BuildDispatcher::new(storage.clone(), bellatrix)), + }; + + // Register core handlers (Git Push, Manual, Retry) + registry.register(Box::new(GitPushHandler::new( + storage.clone(), + git_object_cache.clone(), + ))); + registry.register(Box::new(ManualHandler::new( + storage.clone(), + git_object_cache.clone(), + ))); + registry.register(Box::new(RetryHandler::new( + storage.clone(), + git_object_cache.clone(), + ))); + + // Note: Webhook and Schedule handlers are reserved for future implementation + // but not registered yet as they are not part of the current requirements + + registry + } + + /// Register a trigger handler. + fn register(&mut self, handler: Box) { + self.handlers.insert(handler.trigger_type(), handler); + } + + /// Trigger a build using the unified interface. + /// + /// This is the single entry point for all build triggers. + /// + /// Returns the ID of the created trigger record. + pub async fn trigger_build(&self, context: TriggerContext) -> Result { + tracing::info!( + "TriggerRegistry: Received {:?} build trigger for {} (commit: {})", + context.trigger_type, + context.repo_path, + &context.commit_hash[..8.min(context.commit_hash.len())] + ); + + // Find the appropriate handler + let handler = self.handlers.get(&context.trigger_type).ok_or_else(|| { + MegaError::Other(format!( + "No handler for trigger type: {:?}", + context.trigger_type + )) + })?; + + // Handle the trigger + let trigger = handler.handle(&context).await?; + + // Dispatch the build and return the trigger ID + let trigger_id = self.dispatcher.dispatch(trigger).await?; + + tracing::info!( + "TriggerRegistry: Build trigger completed (ID: {})", + trigger_id + ); + + Ok(trigger_id) + } +} diff --git a/ceres/src/build_trigger/model.rs b/ceres/src/build_trigger/model.rs new file mode 100644 index 000000000..c0f14accc --- /dev/null +++ b/ceres/src/build_trigger/model.rs @@ -0,0 +1,505 @@ +use std::{collections::HashMap, fmt}; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, ToSchema, PartialEq, Eq, Hash)] +#[serde(rename_all = "snake_case")] +pub enum BuildTriggerType { + GitPush, + Manual, + Retry, + Webhook, + Schedule, +} + +impl fmt::Display for BuildTriggerType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + BuildTriggerType::GitPush => "git_push", + BuildTriggerType::Manual => "manual", + BuildTriggerType::Retry => "retry", + BuildTriggerType::Webhook => "webhook", + BuildTriggerType::Schedule => "schedule", + }; + write!(f, "{}", s) + } +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum TriggerSource { + User, + System, + Service, +} + +impl fmt::Display for TriggerSource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + TriggerSource::User => "user", + TriggerSource::System => "system", + TriggerSource::Service => "service", + }; + write!(f, "{}", s) + } +} + +/// Optional build parameters +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct BuildParams { + /// Specific Buck build target (e.g., "//path/to:target") + #[serde(skip_serializing_if = "Option::is_none")] + pub build_target: Option, + + /// Additional build parameters + #[serde(flatten)] + #[schema(additional_properties)] + pub extra: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GitPushPayload { + pub repo: String, + pub from_hash: String, + pub commit_hash: String, + pub cl_link: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub cl_id: Option, + pub builds: serde_json::Value, + #[serde(skip_serializing_if = "Option::is_none")] + pub triggered_by: Option, +} + +/// Pure Git Event DTO for decoupling pack layer from build trigger system +#[derive(Debug, Clone)] +pub struct GitPushEvent { + pub repo_path: String, + pub from_hash: String, + pub commit_hash: String, + pub cl_link: String, + pub cl_id: Option, + pub triggered_by: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ManualPayload { + pub repo: String, + pub commit_hash: String, + pub triggered_by: String, + pub builds: serde_json::Value, + #[serde(skip_serializing_if = "Option::is_none")] + pub params: Option, + pub cl_link: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub cl_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ref_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ref_type: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RetryPayload { + pub repo: String, + pub from_hash: String, + pub commit_hash: String, + pub triggered_by: String, + pub builds: serde_json::Value, + pub original_trigger_id: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub original_cl_link: Option, + pub cl_link: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub cl_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ref_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ref_type: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebhookPayload { + pub repo: String, + pub commit_hash: String, + pub builds: serde_json::Value, + pub webhook_source: String, + pub cl_link: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub cl_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub raw_payload: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SchedulePayload { + pub repo: String, + pub commit_hash: String, + pub builds: serde_json::Value, + pub schedule_name: String, + pub cron_expression: String, + pub cl_link: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub cl_id: Option, +} + +/// Trigger payload - stores context specific to each trigger type +/// This enum is serialized to JSON and stored in database's trigger_payload column +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum BuildTriggerPayload { + GitPush(GitPushPayload), + Manual(ManualPayload), + Retry(RetryPayload), + Webhook(WebhookPayload), + Schedule(SchedulePayload), +} + +impl BuildTriggerPayload { + pub fn repo_path(&self) -> &str { + match self { + BuildTriggerPayload::GitPush(p) => &p.repo, + BuildTriggerPayload::Manual(p) => &p.repo, + BuildTriggerPayload::Retry(p) => &p.repo, + BuildTriggerPayload::Webhook(p) => &p.repo, + BuildTriggerPayload::Schedule(p) => &p.repo, + } + } + + pub fn commit_hash(&self) -> &str { + match self { + BuildTriggerPayload::GitPush(p) => &p.commit_hash, + BuildTriggerPayload::Manual(p) => &p.commit_hash, + BuildTriggerPayload::Retry(p) => &p.commit_hash, + BuildTriggerPayload::Webhook(p) => &p.commit_hash, + BuildTriggerPayload::Schedule(p) => &p.commit_hash, + } + } + + pub fn cl_link(&self) -> &str { + match self { + BuildTriggerPayload::GitPush(p) => &p.cl_link, + BuildTriggerPayload::Manual(p) => &p.cl_link, + BuildTriggerPayload::Retry(p) => &p.cl_link, + BuildTriggerPayload::Webhook(p) => &p.cl_link, + BuildTriggerPayload::Schedule(p) => &p.cl_link, + } + } + + pub fn cl_id(&self) -> Option { + match self { + BuildTriggerPayload::GitPush(p) => p.cl_id, + BuildTriggerPayload::Manual(p) => p.cl_id, + BuildTriggerPayload::Retry(p) => p.cl_id, + BuildTriggerPayload::Webhook(p) => p.cl_id, + BuildTriggerPayload::Schedule(p) => p.cl_id, + } + } + + pub fn triggered_by(&self) -> Option<&str> { + match self { + BuildTriggerPayload::GitPush(p) => p.triggered_by.as_deref(), + BuildTriggerPayload::Manual(p) => Some(&p.triggered_by), + BuildTriggerPayload::Retry(p) => Some(&p.triggered_by), + BuildTriggerPayload::Webhook(_) => None, + BuildTriggerPayload::Schedule(_) => None, + } + } + + pub fn from_hash(&self) -> &str { + match self { + BuildTriggerPayload::GitPush(p) => &p.from_hash, + BuildTriggerPayload::Manual(p) => &p.commit_hash, + BuildTriggerPayload::Retry(p) => &p.from_hash, + BuildTriggerPayload::Webhook(_) => "", + BuildTriggerPayload::Schedule(_) => "", + } + } +} + +/// Internal build trigger object (in-memory, used by handlers) +#[derive(Debug, Serialize)] +pub struct BuildTrigger { + pub trigger_type: BuildTriggerType, + pub trigger_source: TriggerSource, + pub trigger_time: DateTime, + pub payload: BuildTriggerPayload, +} + +/// Trigger record for history queries (maps to database table) +#[derive(Debug, Serialize, ToSchema)] +pub struct TriggerRecord { + pub id: i64, + pub trigger_type: String, + pub trigger_source: String, + pub trigger_time: DateTime, + pub trigger_payload: serde_json::Value, + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(value_type = Option)] + pub task_id: Option, +} + +impl TriggerRecord { + pub fn from_db_model(model: callisto::build_triggers::Model) -> Self { + Self { + id: model.id, + trigger_type: model.trigger_type, + trigger_source: model.trigger_source, + trigger_time: DateTime::from_naive_utc_and_offset(model.trigger_time, Utc), + trigger_payload: model.trigger_payload.clone(), + task_id: model.task_id, + } + } + + /// Parse the JSON payload into strongly-typed BuildTriggerPayload + pub fn parse_payload(&self) -> Result { + serde_json::from_value(self.trigger_payload.clone()) + } +} + +/// Unified context for triggering builds from any source +#[derive(Debug, Clone)] +pub struct TriggerContext { + pub trigger_type: BuildTriggerType, + pub trigger_source: TriggerSource, + pub triggered_by: Option, + pub repo_path: String, + pub from_hash: String, + pub commit_hash: String, + pub cl_link: Option, + pub cl_id: Option, + pub params: Option, + pub original_trigger_id: Option, + pub ref_name: Option, + pub ref_type: Option, +} + +impl TriggerContext { + pub fn from_git_push( + repo_path: String, + from_hash: String, + commit_hash: String, + cl_link: String, + cl_id: Option, + triggered_by: Option, + ) -> Self { + Self { + trigger_type: BuildTriggerType::GitPush, + trigger_source: if triggered_by.is_some() { + TriggerSource::User + } else { + TriggerSource::System + }, + triggered_by, + repo_path, + from_hash, + commit_hash, + cl_link: Some(cl_link), + cl_id, + params: None, + original_trigger_id: None, + ref_name: None, + ref_type: None, + } + } + + pub fn from_manual( + repo_path: String, + commit_hash: String, + triggered_by: String, + params: Option, + ) -> Self { + Self { + trigger_type: BuildTriggerType::Manual, + trigger_source: TriggerSource::User, + triggered_by: Some(triggered_by), + repo_path, + from_hash: commit_hash.clone(), + commit_hash, + cl_link: None, + cl_id: None, + params, + original_trigger_id: None, + ref_name: None, + ref_type: None, + } + } + + pub fn from_retry( + repo_path: String, + from_hash: String, + commit_hash: String, + cl_link: Option, + cl_id: Option, + triggered_by: Option, + original_trigger_id: i64, + ) -> Self { + Self { + trigger_type: BuildTriggerType::Retry, + trigger_source: TriggerSource::User, + triggered_by, + repo_path, + from_hash, + commit_hash, + cl_link, + cl_id, + params: None, + original_trigger_id: Some(original_trigger_id), + ref_name: None, + ref_type: None, + } + } +} + +/// Create trigger request (new RESTful API) +#[derive(Debug, Deserialize, ToSchema)] +pub struct CreateTriggerRequest { + pub repo_path: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub ref_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub params: Option, +} + +/// Trigger detail response (new RESTful API) +#[derive(Debug, Serialize, ToSchema)] +pub struct TriggerResponse { + pub id: i64, + pub trigger_type: String, + pub trigger_source: String, + pub triggered_by: Option, + pub triggered_at: DateTime, + pub repo_path: String, + pub commit_hash: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub ref_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ref_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cl_link: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cl_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub params: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(value_type = Option)] + pub task_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub original_trigger_id: Option, +} + +impl TriggerResponse { + /// Create TriggerResponse from database model + pub fn from_trigger_record(record: &TriggerRecord) -> Result { + let payload = record.parse_payload()?; + + // Extract common fields using helper methods + let repo_path = payload.repo_path().to_string(); + let commit_hash = payload.commit_hash().to_string(); + let cl_link = Some(payload.cl_link().to_string()); + let cl_id = payload.cl_id(); + let triggered_by = payload.triggered_by().map(|s| s.to_string()); + + // Extract type-specific fields + let (params, original_trigger_id, ref_name, ref_type) = match &payload { + BuildTriggerPayload::Manual(p) => ( + p.params.as_ref().and_then(|p| serde_json::to_value(p).ok()), + None, + p.ref_name.clone(), + p.ref_type.clone(), + ), + BuildTriggerPayload::Retry(p) => ( + None, + Some(p.original_trigger_id), + p.ref_name.clone(), + p.ref_type.clone(), + ), + BuildTriggerPayload::Webhook(p) => (p.raw_payload.clone(), None, None, None), + _ => (None, None, None, None), + }; + + Ok(Self { + id: record.id, + trigger_type: record.trigger_type.clone(), + trigger_source: record.trigger_source.clone(), + triggered_by, + triggered_at: record.trigger_time, + repo_path, + commit_hash, + ref_name, + ref_type, + cl_link, + cl_id, + params, + task_id: record.task_id, + original_trigger_id, + }) + } +} + +/// Query parameters for listing triggers (Google-style API) +#[derive(Debug, Deserialize, ToSchema, Default)] +pub struct ListTriggersParams { + /// Filter by repository path + #[serde(skip_serializing_if = "Option::is_none")] + pub repo_path: Option, + /// Filter by trigger type (git_push, manual, retry, webhook, schedule) + #[serde(skip_serializing_if = "Option::is_none")] + pub trigger_type: Option, + /// Filter by trigger source (user, system, service) + #[serde(skip_serializing_if = "Option::is_none")] + pub trigger_source: Option, + /// Filter by who triggered (username) + #[serde(skip_serializing_if = "Option::is_none")] + pub triggered_by: Option, + /// Filter by time range start (ISO 8601 format) + #[serde(skip_serializing_if = "Option::is_none")] + pub start_time: Option>, + /// Filter by time range end (ISO 8601 format) + #[serde(skip_serializing_if = "Option::is_none")] + pub end_time: Option>, +} + +impl From for jupiter::storage::build_trigger_storage::ListTriggersFilter { + fn from(params: ListTriggersParams) -> Self { + Self { + repo_path: params.repo_path, + trigger_type: params.trigger_type, + trigger_source: params.trigger_source, + triggered_by: params.triggered_by, + start_time: params.start_time, + end_time: params.end_time, + } + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct SerializableBuildInfo { + pub changes: Vec, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub enum SerializableStatus { + Modified(String), + Added(String), + Removed(String), +} + +impl From + for bellatrix::orion_client::Status +{ + fn from(status: SerializableStatus) -> Self { + match status { + SerializableStatus::Modified(path) => bellatrix::orion_client::Status::Modified( + bellatrix::orion_client::ProjectRelativePath::new(&path), + ), + SerializableStatus::Added(path) => bellatrix::orion_client::Status::Added( + bellatrix::orion_client::ProjectRelativePath::new(&path), + ), + SerializableStatus::Removed(path) => bellatrix::orion_client::Status::Removed( + bellatrix::orion_client::ProjectRelativePath::new(&path), + ), + } + } +} diff --git a/ceres/src/build_trigger/ref_resolver.rs b/ceres/src/build_trigger/ref_resolver.rs new file mode 100644 index 000000000..c64928ee2 --- /dev/null +++ b/ceres/src/build_trigger/ref_resolver.rs @@ -0,0 +1,159 @@ +//! Git Reference Resolution Service +//! +//! Provides unified resolution of git references (branches, tags, commits) to commit hashes. +//! This service handles: +//! - Branch name → latest commit on branch +//! - Tag name → tagged commit +//! - Commit hash → validated commit hash +//! - Default resolution to "main" branch when no ref specified +//! - Ambiguous ref handling (branch and tag with same name - prefer branch) + +use common::errors::MegaError; +use jupiter::storage::Storage; + +/// Result of ref resolution containing the commit hash and metadata +#[derive(Debug, Clone)] +pub struct ResolvedRef { + /// The resolved commit hash + pub commit_hash: String, + /// The original ref name (branch/tag name, or commit hash) + pub ref_name: String, + /// The type of ref: "branch", "tag", or "commit" + pub ref_type: RefType, +} + +/// Type of git reference +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RefType { + Branch, + Tag, + Commit, +} + +impl RefType { + pub fn as_str(&self) -> &'static str { + match self { + RefType::Branch => "branch", + RefType::Tag => "tag", + RefType::Commit => "commit", + } + } +} + +/// Service for resolving git references to commit hashes +pub struct RefResolver { + storage: Storage, +} + +impl RefResolver { + pub fn new(storage: Storage) -> Self { + Self { storage } + } + + /// Resolve a git reference to a commit hash with metadata. + /// + /// Resolution order: + /// 1. If ref is None, resolve to "main" branch + /// 2. Try as branch name (refs/heads/{ref}) + /// 3. Try as tag name (refs/tags/{ref}) + /// 4. Try as commit hash (validate it exists) + /// 5. Return error if not found + /// + /// For ambiguous refs (both branch and tag exist), prefer branch. + pub async fn resolve(&self, ref_name: Option<&str>) -> Result { + // Handle default case: no ref specified → use "main" + let ref_name = ref_name.unwrap_or("main"); + tracing::debug!("Resolving ref: {}", ref_name); + + // Try to resolve as branch first + if let Some(resolved) = self.try_resolve_as_branch(ref_name).await? { + return Ok(resolved); + } + + // Try to resolve as tag + if let Some(resolved) = self.try_resolve_as_tag(ref_name).await? { + return Ok(resolved); + } + + // Try to resolve as commit hash + if let Some(resolved) = self.try_resolve_as_commit(ref_name).await? { + return Ok(resolved); + } + + // Not found + Err(MegaError::Other(format!( + "[code:404] Reference not found: '{}' (not a branch, tag, or commit)", + ref_name + ))) + } + + /// Try to resolve ref as a branch name + async fn try_resolve_as_branch( + &self, + ref_name: &str, + ) -> Result, MegaError> { + let full_ref_name = format!("refs/heads/{}", ref_name); + + if let Some(branch_ref) = self + .storage + .mono_storage() + .get_ref_by_name(&full_ref_name) + .await? + { + return Ok(Some(ResolvedRef { + commit_hash: branch_ref.ref_commit_hash, + ref_name: ref_name.to_string(), + ref_type: RefType::Branch, + })); + } + + Ok(None) + } + + /// Try to resolve ref as a tag name + async fn try_resolve_as_tag(&self, ref_name: &str) -> Result, MegaError> { + let full_ref_name = format!("refs/tags/{}", ref_name); + + if let Some(tag_ref) = self + .storage + .mono_storage() + .get_ref_by_name(&full_ref_name) + .await? + { + return Ok(Some(ResolvedRef { + commit_hash: tag_ref.ref_commit_hash, + ref_name: ref_name.to_string(), + ref_type: RefType::Tag, + })); + } + + Ok(None) + } + + /// Try to resolve ref as a commit hash (validate it exists) + async fn try_resolve_as_commit( + &self, + ref_name: &str, + ) -> Result, MegaError> { + // Check if it looks like a commit hash (hex string, typically 40 chars but can be shorter) + if !ref_name.chars().all(|c| c.is_ascii_hexdigit()) { + return Ok(None); + } + + // Validate the commit exists in the database + if let Some(_commit) = self + .storage + .mono_storage() + .get_commit_by_hash(ref_name) + .await? + { + return Ok(Some(ResolvedRef { + commit_hash: ref_name.to_string(), + ref_name: ref_name.to_string(), + ref_type: RefType::Commit, + })); + } + + Ok(None) + } +} diff --git a/ceres/src/build_trigger/retry_handler.rs b/ceres/src/build_trigger/retry_handler.rs new file mode 100644 index 000000000..58020bf97 --- /dev/null +++ b/ceres/src/build_trigger/retry_handler.rs @@ -0,0 +1,72 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use chrono::Utc; +use common::errors::MegaError; +use jupiter::storage::Storage; + +use super::git_push_handler::GitPushHandler; +use crate::{ + api_service::cache::GitObjectCache, + build_trigger::{ + BuildTrigger, BuildTriggerPayload, BuildTriggerType, TriggerContext, TriggerHandler, + }, +}; + +/// Handler for retry build triggers. +pub struct RetryHandler { + git_push_handler: GitPushHandler, +} + +impl RetryHandler { + pub fn new(storage: Storage, git_object_cache: Arc) -> Self { + Self { + git_push_handler: GitPushHandler::new(storage, git_object_cache), + } + } +} + +#[async_trait] +impl TriggerHandler for RetryHandler { + async fn handle(&self, context: &TriggerContext) -> Result { + // Reuse GitPushHandler logic for getting builds + let builds = self.git_push_handler.get_builds_for_commit(context).await?; + + let cl_link = context.cl_link.clone().unwrap_or_else(|| { + format!( + "retry-{}-{}", + Utc::now().timestamp_millis(), + &context.commit_hash[..8.min(context.commit_hash.len())] + ) + }); + + Ok(BuildTrigger { + trigger_type: BuildTriggerType::Retry, + trigger_source: context.trigger_source, + trigger_time: Utc::now(), + payload: BuildTriggerPayload::Retry(crate::build_trigger::RetryPayload { + repo: context.repo_path.clone(), + from_hash: context.from_hash.clone(), + commit_hash: context.commit_hash.clone(), + triggered_by: context + .triggered_by + .clone() + .unwrap_or_else(|| "unknown".to_string()), + builds: serde_json::to_value(&builds) + .map_err(|e| MegaError::Other(format!("Failed to serialize builds: {}", e)))?, + original_trigger_id: context.original_trigger_id.ok_or_else(|| { + MegaError::Other("Missing original_trigger_id for retry".to_string()) + })?, + original_cl_link: context.cl_link.clone(), + cl_link, + cl_id: context.cl_id, + ref_name: context.ref_name.clone(), + ref_type: context.ref_type.clone(), + }), + }) + } + + fn trigger_type(&self) -> BuildTriggerType { + BuildTriggerType::Retry + } +} diff --git a/ceres/src/build_trigger/service.rs b/ceres/src/build_trigger/service.rs new file mode 100644 index 000000000..4b1bce829 --- /dev/null +++ b/ceres/src/build_trigger/service.rs @@ -0,0 +1,201 @@ +use std::sync::Arc; + +use api_model::common::Pagination; +use bellatrix::Bellatrix; +use common::errors::MegaError; +use jupiter::storage::Storage; + +use crate::{ + api_service::cache::GitObjectCache, + build_trigger::{ + RefResolver, TriggerRegistry, + model::{BuildParams, ListTriggersParams, TriggerContext, TriggerRecord, TriggerResponse}, + }, +}; + +/// Service for handling build trigger operations +pub struct BuildTriggerService { + storage: Storage, + registry: TriggerRegistry, + bellatrix: Arc, +} + +impl BuildTriggerService { + pub fn new( + storage: Storage, + git_object_cache: Arc, + bellatrix: Arc, + ) -> Self { + let registry = TriggerRegistry::new(storage.clone(), git_object_cache, bellatrix.clone()); + Self { + storage, + registry, + bellatrix, + } + } + + fn check_build_enabled(&self) -> Result<(), MegaError> { + if !self.bellatrix.enable_build() { + return Err(MegaError::Other( + "[code:503] Build system is not enabled".to_string(), + )); + } + Ok(()) + } + + /// Static helper to handle git push events from the pack layer + /// This acts as a boundary to decouple monorepo logic from build trigger internals + pub async fn handle_git_push_event( + storage: Storage, + git_cache: Arc, + bellatrix: Arc, + event: crate::build_trigger::model::GitPushEvent, + ) -> Result, MegaError> { + if !bellatrix.enable_build() { + return Ok(None); + } + let registry = TriggerRegistry::new(storage, git_cache, bellatrix); + + let context = TriggerContext::from_git_push( + event.repo_path, + event.from_hash, + event.commit_hash, + event.cl_link, + event.cl_id, + event.triggered_by, + ); + + let id = registry.trigger_build(context).await?; + Ok(Some(id)) + } + + /// Create a manual trigger (complete workflow) + pub async fn create_manual_trigger( + &self, + repo_path: String, + ref_name: Option, + params: Option, + triggered_by: String, + ) -> Result { + self.check_build_enabled()?; + + // 1. Resolve reference + let ref_resolver = RefResolver::new(self.storage.clone()); + let resolved = ref_resolver + .resolve(ref_name.as_deref()) + .await + .map_err(|_| { + let ref_str = ref_name.unwrap_or_else(|| "main".to_string()); + MegaError::Other(format!("[code:400] Reference not found: {}", ref_str)) + })?; + + // 2. Build context + let mut context = TriggerContext::from_manual( + repo_path, + resolved.commit_hash.clone(), + triggered_by, + params, + ); + context.ref_name = Some(resolved.ref_name.clone()); + context.ref_type = Some(resolved.ref_type.as_str().to_string()); + + // 3. Create trigger + let trigger_id = self.registry.trigger_build(context).await?; + + // 4. Get and return response + self.get_trigger_response(trigger_id).await + } + + /// Get trigger details + pub async fn get_trigger(&self, trigger_id: i64) -> Result { + self.check_build_enabled()?; + self.get_trigger_response(trigger_id).await + } + + /// List triggers with filters and pagination + pub async fn list_triggers( + &self, + params: ListTriggersParams, + pagination: Pagination, + ) -> Result<(Vec, i64), MegaError> { + self.check_build_enabled()?; + + let (triggers, total) = self + .storage + .build_trigger_storage() + .get_trigger_list(params, pagination) + .await?; + + let responses = triggers + .into_iter() + .map(|t| { + let record = TriggerRecord::from_db_model(t); + TriggerResponse::from_trigger_record(&record) + .map_err(|e| MegaError::Other(format!("Failed to parse trigger: {}", e))) + }) + .collect::, _>>()?; + + Ok((responses, total as i64)) + } + + /// Retry a previous build trigger + pub async fn retry_trigger( + &self, + original_trigger_id: i64, + triggered_by: String, + ) -> Result { + self.check_build_enabled()?; + + // 1. Get original trigger + let original_trigger = self + .storage + .build_trigger_storage() + .get_by_id(original_trigger_id) + .await? + .ok_or_else(|| { + MegaError::Other(format!( + "[code:404] Trigger not found: {}", + original_trigger_id + )) + })?; + + // 2. Parse payload + let trigger_record = TriggerRecord::from_db_model(original_trigger); + let payload = trigger_record + .parse_payload() + .map_err(|e| MegaError::Other(format!("Failed to parse payload: {}", e)))?; + + // 3. Construct retry context + let context = TriggerContext::from_retry( + payload.repo_path().to_string(), + payload.from_hash().to_string(), + payload.commit_hash().to_string(), + Some(payload.cl_link().to_string()), + payload.cl_id(), + Some(triggered_by), + original_trigger_id, + ); + + // 4. Create new trigger + let new_trigger_id = self.registry.trigger_build(context).await?; + + // 5. Get and return response + self.get_trigger_response(new_trigger_id).await + } + + /// Internal helper method: get trigger response + async fn get_trigger_response(&self, trigger_id: i64) -> Result { + let model = self + .storage + .build_trigger_storage() + .get_by_id(trigger_id) + .await? + .ok_or_else(|| { + MegaError::Other(format!("[code:404] Trigger not found: {}", trigger_id)) + })?; + + let record = TriggerRecord::from_db_model(model); + TriggerResponse::from_trigger_record(&record) + .map_err(|e| MegaError::Other(format!("Failed to parse trigger: {}", e))) + } +} diff --git a/ceres/src/lib.rs b/ceres/src/lib.rs index cc7116ccb..a06803f53 100644 --- a/ceres/src/lib.rs +++ b/ceres/src/lib.rs @@ -1,4 +1,5 @@ pub mod api_service; +pub mod build_trigger; pub mod lfs; pub mod merge_checker; pub mod model; diff --git a/ceres/src/pack/monorepo.rs b/ceres/src/pack/monorepo.rs index 12a8a3d21..ef78b2470 100644 --- a/ceres/src/pack/monorepo.rs +++ b/ceres/src/pack/monorepo.rs @@ -12,10 +12,7 @@ use std::{ use api_model::common::Pagination; use async_recursion::async_recursion; use async_trait::async_trait; -use bellatrix::{ - Bellatrix, - orion_client::{BuildInfo, OrionBuildRequest, ProjectRelativePath, Status}, -}; +use bellatrix::Bellatrix; use callisto::{ entity_ext::generate_link, mega_cl, mega_code_review_anchor, mega_commit, mega_refs, @@ -1047,59 +1044,22 @@ impl MonoRepo { .ok_or_else(|| MegaError::Other(format!("CL not found for link: {}", link)))?; if self.bellatrix.enable_build() { - let old_files = self.get_commit_blobs(&cl_info.from_hash).await?; - let new_files = self.get_commit_blobs(&cl_info.to_hash).await?; - let cl_diff_files = self.cl_files_list(old_files, new_files.clone()).await?; - - let cl_base = PathBuf::from(&cl_info.path); - let changes = cl_diff_files - .into_iter() - .map(|m| { - let mut item: crate::model::change_list::ClFilesRes = m.into(); - item.path = cl_base.join(item.path).to_string_lossy().to_string(); - item - }) - .collect::>(); - - let path_str = cl_base.to_str().ok_or_else(|| { - MegaError::Other(format!("CL base path is not valid UTF-8: {:?}", cl_base)) - })?; - let counter_changes: Vec<_> = changes - .iter() - .filter(|&s| PathBuf::from(&s.path).starts_with(&cl_base)) - .map(|s| { - let path = ProjectRelativePath::from_abs(&s.path, path_str).unwrap(); - if s.action == "new" { - Status::Added(path) - } else if s.action == "deleted" { - Status::Removed(path) - } else if s.action == "modified" { - Status::Modified(path) - } else { - unreachable!() - } - }) - .collect(); - - tracing::info!( - "Trigger bellatrix build for cl: {}, changes: {:?}, repo: {}", - cl_info.id, - counter_changes, - path_str - ); - - let req: OrionBuildRequest = OrionBuildRequest { - cl_link: link.clone(), - mount_path: path_str.to_string(), - cl: cl_info.id, - builds: vec![BuildInfo { - changes: counter_changes, - }], + let event = crate::build_trigger::GitPushEvent { + repo_path: cl_info.path.clone(), + from_hash: cl_info.from_hash.clone(), + commit_hash: cl_info.to_hash.clone(), + cl_link: cl_info.link.clone(), + cl_id: Some(cl_info.id), + triggered_by: self.username.clone(), }; - let bellatrix = self.bellatrix.clone(); - tokio::spawn(async move { - let _ = bellatrix.on_post_receive(req).await; - }); + + let _trigger_id = crate::build_trigger::BuildTriggerService::handle_git_push_event( + self.storage.clone(), + self.git_object_cache.clone(), + self.bellatrix.clone(), + event, + ) + .await?; } let check_reg = CheckerRegistry::new(self.storage.clone().into(), self.username()); diff --git a/mono/Cargo.toml b/mono/Cargo.toml index 904a8c8bf..a68305afe 100644 --- a/mono/Cargo.toml +++ b/mono/Cargo.toml @@ -17,6 +17,7 @@ path = "src/main.rs" [dependencies] api-model = { workspace = true } +bellatrix = { workspace = true } common = { workspace = true } callisto = { workspace = true } jupiter = { workspace = true } diff --git a/mono/src/api/api_router.rs b/mono/src/api/api_router.rs index 637dcfc93..b2a2ecf7c 100644 --- a/mono/src/api/api_router.rs +++ b/mono/src/api/api_router.rs @@ -16,9 +16,10 @@ use crate::{ error::ApiError, notes::note_router, router::{ - admin_router, buck_router, cl_router, code_review_router, commit_router, conv_router, - dynamic_sidebar_router, gpg_router, issue_router, label_router, merge_queue_router, - preview_router, repo_router, reviewer_router, tag_router, user_router, + admin_router, buck_router, build_trigger_router, cl_router, code_review_router, + commit_router, conv_router, dynamic_sidebar_router, gpg_router, issue_router, + label_router, merge_queue_router, preview_router, repo_router, reviewer_router, + tag_router, user_router, }, }, server::http_server::SYSTEM_COMMON, @@ -46,6 +47,7 @@ pub fn routers() -> OpenApiRouter { .merge(buck_router::routers()) .merge(admin_router::routers()) .merge(code_review_router::routers()) + .merge(build_trigger_router::routers()) } /// Health Check diff --git a/mono/src/api/error.rs b/mono/src/api/error.rs index 5c0b0746a..80a7c3d7d 100644 --- a/mono/src/api/error.rs +++ b/mono/src/api/error.rs @@ -6,16 +6,32 @@ use http::StatusCode; /// Parse [code:xxx] format from error message. /// Returns (status_code, clean_message) if found, None otherwise. fn parse_error_code(err_str: &str) -> Option<(&str, &str)> { - if err_str.starts_with("[code:") - && let Some(code_end) = err_str.find(']').filter(|&idx| idx >= 6) - { - let code = &err_str[6..code_end]; - // Use safe .get() to avoid potential panic on unicode boundaries - let msg = err_str.get(code_end + 1..)?.trim(); - Some((code, msg)) - } else { - None + // Find [code:xxx] anywhere in the string + let start = err_str.find("[code:")?; + let code_start = start + 6; // Skip "[code:" + + // Find the closing bracket after [code: + let remaining = &err_str[start..]; + let code_end_relative = remaining.find(']')?; + + // Ensure we have at least one character for the code + if code_end_relative <= 6 { + return None; } + + let code_end = start + code_end_relative; + let code = &err_str[code_start..code_end]; + + // Validate that code is not empty and contains only valid characters + if code.is_empty() || !code.chars().all(|c| c.is_ascii_digit()) { + return None; + } + + // Extract message after the closing bracket; allow empty messages for compatibility + let msg_start = code_end + 1; + let msg = err_str.get(msg_start..).unwrap_or("").trim_start(); + + Some((code, msg)) } #[derive(Debug)] diff --git a/mono/src/api/mod.rs b/mono/src/api/mod.rs index ab4a23f61..d8665acab 100644 --- a/mono/src/api/mod.rs +++ b/mono/src/api/mod.rs @@ -4,11 +4,13 @@ use std::{ }; use axum::extract::FromRef; +use bellatrix::Bellatrix; use ceres::{ api_service::{ ApiHandler, cache::GitObjectCache, import_api_service::ImportApiService, mono_api_service::MonoApiService, state::ProtocolApiState, }, + build_trigger::service::BuildTriggerService, protocol::repo::Repo, }; use common::errors::ProtocolError; @@ -146,6 +148,15 @@ impl MonoApiServiceState { self.storage.dynamic_sidebar_storage() } + pub fn build_trigger_service(&self) -> BuildTriggerService { + let bellatrix = Arc::new(Bellatrix::new(self.storage.config().build.clone())); + BuildTriggerService::new( + self.storage.clone(), + self.git_object_cache.clone(), + bellatrix, + ) + } + async fn api_handler(&self, path: &Path) -> Result, ProtocolError> { // Normalize path to ensure it has a root component let path = if path.has_root() { diff --git a/mono/src/api/router/build_trigger_router.rs b/mono/src/api/router/build_trigger_router.rs new file mode 100644 index 000000000..7437c6e1f --- /dev/null +++ b/mono/src/api/router/build_trigger_router.rs @@ -0,0 +1,148 @@ +//! Build Trigger API v1 Router (RESTful) +//! +//! Provides RESTful endpoints for creating and managing build triggers. +//! This is the new API design that follows industry best practices. + +use api_model::common::{CommonPage, CommonResult, PageParams}; +use axum::{ + Json, + extract::{Path, State}, +}; +use ceres::build_trigger::{CreateTriggerRequest, ListTriggersParams, TriggerResponse}; +use utoipa_axum::{router::OpenApiRouter, routes}; + +use crate::{ + api::{MonoApiServiceState, error::ApiError, oauth::model::LoginUser}, + server::http_server::BUILD_TRIGGER_TAG, +}; + +pub fn routers() -> OpenApiRouter { + OpenApiRouter::new().nest( + "/triggers", + OpenApiRouter::new() + .routes(routes!(create_trigger)) + .routes(routes!(list_triggers)) + .routes(routes!(get_trigger)) + .routes(routes!(retry_trigger)), + ) +} + +/// Create a new build trigger +/// +/// Creates a new build trigger with automatic ref resolution. +/// Supports branch names, tag names, commit hashes, or CL links. +/// Defaults to "main" branch if no ref is specified. +#[utoipa::path( + post, + path = "", + request_body = CreateTriggerRequest, + responses( + (status = 200, body = CommonResult, content_type = "application/json"), + (status = 400, description = "Invalid request parameters or ref not found"), + (status = 503, description = "Build system not enabled") + ), + tag = BUILD_TRIGGER_TAG +)] +async fn create_trigger( + user: LoginUser, + state: State, + Json(req): Json, +) -> Result>, ApiError> { + let service = state.build_trigger_service(); + let response = service + .create_manual_trigger(req.repo_path, req.ref_name, req.params, user.username) + .await?; + Ok(Json(CommonResult::success(Some(response)))) +} + +/// List build triggers with filters +/// +/// Returns build triggers with pagination and optional filters. +/// Supports filtering by repository, trigger type, source, user, and time range. +/// +/// This endpoint follows the project's standard Google-style API pattern: +/// - Uses POST method for complex query parameters +/// - Accepts PageParams with pagination and filter parameters +/// - Returns CommonPage with items and total count +#[utoipa::path( + post, + path = "/list", + request_body = PageParams, + responses( + (status = 200, body = CommonResult>, content_type = "application/json"), + (status = 503, description = "Build system not enabled") + ), + tag = BUILD_TRIGGER_TAG +)] +async fn list_triggers( + _user: LoginUser, + state: State, + Json(json): Json>, +) -> Result>>, ApiError> { + let service = state.build_trigger_service(); + let (items, total) = service + .list_triggers(json.additional, json.pagination) + .await?; + Ok(Json(CommonResult::success(Some(CommonPage { + items, + total: total as u64, + })))) +} + +/// Get a specific build trigger by ID +/// +/// Returns complete details about a specific trigger including: +/// - Trigger metadata (type, source, time) +/// - Repository and commit information +/// - Ref information (branch/tag name if applicable) +/// - Build parameters +#[utoipa::path( + get, + path = "/{id}", + params( + ("id" = i64, Path, description = "Trigger ID") + ), + responses( + (status = 200, body = CommonResult, content_type = "application/json"), + (status = 404, description = "Trigger not found"), + (status = 503, description = "Build system not enabled") + ), + tag = BUILD_TRIGGER_TAG +)] +async fn get_trigger( + _user: LoginUser, + state: State, + Path(id): Path, +) -> Result>, ApiError> { + let service = state.build_trigger_service(); + let response = service.get_trigger(id).await?; + Ok(Json(CommonResult::success(Some(response)))) +} + +/// Retry a specific build trigger +/// +/// Creates a new trigger that retries a previous build. +/// The new trigger will use the same repository, commit, and parameters +/// as the original trigger. +#[utoipa::path( + post, + path = "/{id}/retry", + params( + ("id" = i64, Path, description = "Original trigger ID to retry") + ), + responses( + (status = 200, body = CommonResult, content_type = "application/json"), + (status = 404, description = "Original trigger not found"), + (status = 503, description = "Build system not enabled") + ), + tag = BUILD_TRIGGER_TAG +)] +async fn retry_trigger( + user: LoginUser, + state: State, + Path(id): Path, +) -> Result>, ApiError> { + let service = state.build_trigger_service(); + let response = service.retry_trigger(id, user.username).await?; + Ok(Json(CommonResult::success(Some(response)))) +} diff --git a/mono/src/api/router/mod.rs b/mono/src/api/router/mod.rs index 894aa0010..dcd2d6d50 100644 --- a/mono/src/api/router/mod.rs +++ b/mono/src/api/router/mod.rs @@ -1,5 +1,6 @@ pub mod admin_router; pub mod buck_router; +pub mod build_trigger_router; pub mod cl_router; pub mod code_review_router; pub mod commit_router; diff --git a/mono/src/server/http_server.rs b/mono/src/server/http_server.rs index 48f100939..7450d356f 100644 --- a/mono/src/server/http_server.rs +++ b/mono/src/server/http_server.rs @@ -424,6 +424,7 @@ pub const MERGE_QUEUE_TAG: &str = "Merge Queue Management"; pub const BUCK_TAG: &str = "Buck Upload API"; pub const LFS_TAG: &str = "Git LFS"; pub const CODE_REVIEW_TAG: &str = "Code Review"; +pub const BUILD_TRIGGER_TAG: &str = "Build Trigger"; #[derive(OpenApi)] #[openapi()] struct ApiDoc; From 96b0ee196b38b2fa5fc3d704d2f9a59da2deea67 Mon Sep 17 00:00:00 2001 From: allure <1550220889@qq.com> Date: Mon, 2 Feb 2026 13:19:25 +0800 Subject: [PATCH 2/2] feat: refactor build trigger system and fix error Signed-off-by: allure <1550220889@qq.com> --- ceres/src/build_trigger/changes_calculator.rs | 116 ++++++++++++++++++ ceres/src/build_trigger/git_push_handler.rs | 109 ++-------------- ceres/src/build_trigger/manual_handler.rs | 53 ++++++-- ceres/src/build_trigger/mod.rs | 1 + ceres/src/build_trigger/retry_handler.rs | 18 +-- ceres/src/build_trigger/service.rs | 13 +- ceres/src/pack/monorepo.rs | 9 +- mono/src/api/mod.rs | 4 +- mono/src/server/http_server.rs | 2 + 9 files changed, 202 insertions(+), 123 deletions(-) create mode 100644 ceres/src/build_trigger/changes_calculator.rs diff --git a/ceres/src/build_trigger/changes_calculator.rs b/ceres/src/build_trigger/changes_calculator.rs new file mode 100644 index 000000000..63afbb69b --- /dev/null +++ b/ceres/src/build_trigger/changes_calculator.rs @@ -0,0 +1,116 @@ +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use common::errors::MegaError; +use git_internal::hash::ObjectHash; +use jupiter::storage::Storage; + +use crate::{ + api_service::{cache::GitObjectCache, mono_api_service::MonoApiService}, + build_trigger::{SerializableBuildInfo, SerializableStatus, TriggerContext}, + model::change_list::{ClDiffFile, ClFilesRes}, +}; + +pub struct ChangesCalculator { + storage: Storage, + git_object_cache: Arc, +} + +impl ChangesCalculator { + pub fn new(storage: Storage, git_object_cache: Arc) -> Self { + Self { + storage, + git_object_cache, + } + } + + pub async fn get_builds_for_commit( + &self, + context: &TriggerContext, + ) -> Result, MegaError> { + let old_files = self.get_commit_blobs(&context.from_hash).await?; + let new_files = self.get_commit_blobs(&context.commit_hash).await?; + let diff_files = self.cl_files_list(old_files, new_files).await?; + + let changes = self.build_changes(&context.repo_path, diff_files)?; + + Ok(vec![SerializableBuildInfo { changes }]) + } + + fn build_changes( + &self, + cl_path: &str, + cl_diff_files: Vec, + ) -> Result, MegaError> { + let cl_base = PathBuf::from(cl_path); + let path_str = cl_base.to_str().ok_or_else(|| { + MegaError::Other(format!("CL base path is not valid UTF-8: {:?}", cl_base)) + })?; + + let changes = cl_diff_files + .into_iter() + .map(|m| { + let mut item: ClFilesRes = m.into(); + item.path = cl_base.join(item.path).to_string_lossy().to_string(); + item + }) + .collect::>(); + + let counter_changes = changes + .iter() + .filter(|&s| PathBuf::from(&s.path).starts_with(&cl_base)) + .map(|s| { + let rel = Path::new(&s.path) + .strip_prefix(path_str) + .map_err(|_| { + MegaError::Other(format!("Invalid project-relative path: {}", s.path)) + })? + .to_string_lossy() + .replace('\\', "/") + .trim_start_matches('/') + .to_string(); + + let status = if s.action == "new" { + SerializableStatus::Added(rel) + } else if s.action == "deleted" { + SerializableStatus::Removed(rel) + } else if s.action == "modified" { + SerializableStatus::Modified(rel) + } else { + return Err(MegaError::Other(format!( + "Unsupported change action: {}", + s.action + ))); + }; + Ok(status) + }) + .collect::, MegaError>>()?; + + Ok(counter_changes) + } + + async fn get_commit_blobs( + &self, + commit_hash: &str, + ) -> Result, MegaError> { + let api_service = MonoApiService { + storage: self.storage.clone(), + git_object_cache: self.git_object_cache.clone(), + }; + api_service.get_commit_blobs(commit_hash).await + } + + async fn cl_files_list( + &self, + old_files: Vec<(PathBuf, ObjectHash)>, + new_files: Vec<(PathBuf, ObjectHash)>, + ) -> Result, MegaError> { + let api_service = MonoApiService { + storage: self.storage.clone(), + git_object_cache: self.git_object_cache.clone(), + }; + api_service.cl_files_list(old_files, new_files).await + } +} diff --git a/ceres/src/build_trigger/git_push_handler.rs b/ceres/src/build_trigger/git_push_handler.rs index 1f1adf513..d08a426d3 100644 --- a/ceres/src/build_trigger/git_push_handler.rs +++ b/ceres/src/build_trigger/git_push_handler.rs @@ -1,124 +1,39 @@ -use std::{path::PathBuf, sync::Arc}; +use std::sync::Arc; use async_trait::async_trait; -use bellatrix::orion_client::{BuildInfo, ProjectRelativePath, Status}; use chrono::Utc; use common::errors::MegaError; -use git_internal::hash::ObjectHash; use jupiter::storage::Storage; +use super::changes_calculator::ChangesCalculator; use crate::{ - api_service::{cache::GitObjectCache, mono_api_service::MonoApiService}, + api_service::cache::GitObjectCache, build_trigger::{ - BuildTrigger, BuildTriggerPayload, BuildTriggerType, TriggerContext, TriggerHandler, + BuildTrigger, BuildTriggerPayload, BuildTriggerType, GitPushPayload, TriggerContext, + TriggerHandler, }, - model::change_list::ClFilesRes, }; /// Handler for Git push triggers. pub struct GitPushHandler { - storage: Storage, - git_object_cache: Arc, + changes_calculator: ChangesCalculator, } impl GitPushHandler { pub fn new(storage: Storage, git_object_cache: Arc) -> Self { Self { - storage, - git_object_cache, + changes_calculator: ChangesCalculator::new(storage, git_object_cache), } } - - pub async fn get_builds_for_commit( - &self, - context: &TriggerContext, - ) -> Result, MegaError> { - let old_files = self.get_commit_blobs(&context.from_hash).await?; - let new_files = self.get_commit_blobs(&context.commit_hash).await?; - let diff_files = self.cl_files_list(old_files, new_files).await?; - - let changes = self.build_changes(&context.repo_path, diff_files)?; - - let builds = vec![BuildInfo { - changes: changes.clone(), - }]; - - Ok(builds) - } - - fn build_changes( - &self, - cl_path: &str, - cl_diff_files: Vec, - ) -> Result>, MegaError> { - let cl_base = PathBuf::from(cl_path); - let path_str = cl_base.to_str().ok_or_else(|| { - MegaError::Other(format!("CL base path is not valid UTF-8: {:?}", cl_base)) - })?; - - let changes = cl_diff_files - .into_iter() - .map(|m| { - let mut item: ClFilesRes = m.into(); - item.path = cl_base.join(item.path).to_string_lossy().to_string(); - item - }) - .collect::>(); - - let counter_changes = changes - .iter() - .filter(|&s| PathBuf::from(&s.path).starts_with(&cl_base)) - .map(|s| { - let path = ProjectRelativePath::from_abs(&s.path, path_str).ok_or_else(|| { - MegaError::Other(format!("Invalid project-relative path: {}", s.path)) - })?; - let status = if s.action == "new" { - Status::Added(path) - } else if s.action == "deleted" { - Status::Removed(path) - } else if s.action == "modified" { - Status::Modified(path) - } else { - return Err(MegaError::Other(format!( - "Unsupported change action: {}", - s.action - ))); - }; - Ok(status) - }) - .collect::, MegaError>>()?; - - Ok(counter_changes) - } - - async fn get_commit_blobs( - &self, - commit_hash: &str, - ) -> Result, MegaError> { - let api_service = MonoApiService { - storage: self.storage.clone(), - git_object_cache: self.git_object_cache.clone(), - }; - api_service.get_commit_blobs(commit_hash).await - } - - async fn cl_files_list( - &self, - old_files: Vec<(PathBuf, ObjectHash)>, - new_files: Vec<(PathBuf, ObjectHash)>, - ) -> Result, MegaError> { - let api_service = MonoApiService { - storage: self.storage.clone(), - git_object_cache: self.git_object_cache.clone(), - }; - api_service.cl_files_list(old_files, new_files).await - } } #[async_trait] impl TriggerHandler for GitPushHandler { async fn handle(&self, context: &TriggerContext) -> Result { - let builds = self.get_builds_for_commit(context).await?; + let builds = self + .changes_calculator + .get_builds_for_commit(context) + .await?; let cl_link = context.cl_link.clone().unwrap_or_else(|| { format!( @@ -132,7 +47,7 @@ impl TriggerHandler for GitPushHandler { trigger_type: context.trigger_type, trigger_source: context.trigger_source, trigger_time: Utc::now(), - payload: BuildTriggerPayload::GitPush(crate::build_trigger::GitPushPayload { + payload: BuildTriggerPayload::GitPush(GitPushPayload { repo: context.repo_path.clone(), from_hash: context.from_hash.clone(), commit_hash: context.commit_hash.clone(), diff --git a/ceres/src/build_trigger/manual_handler.rs b/ceres/src/build_trigger/manual_handler.rs index fa592347f..e8be35a23 100644 --- a/ceres/src/build_trigger/manual_handler.rs +++ b/ceres/src/build_trigger/manual_handler.rs @@ -5,32 +5,71 @@ use chrono::Utc; use common::errors::MegaError; use jupiter::storage::Storage; -use super::git_push_handler::GitPushHandler; +use super::changes_calculator::ChangesCalculator; use crate::{ api_service::cache::GitObjectCache, build_trigger::{ - BuildTrigger, BuildTriggerPayload, BuildTriggerType, TriggerContext, TriggerHandler, + BuildTrigger, BuildTriggerPayload, BuildTriggerType, ManualPayload, TriggerContext, + TriggerHandler, }, }; /// Handler for manual build triggers. pub struct ManualHandler { - git_push_handler: GitPushHandler, + storage: Storage, + changes_calculator: ChangesCalculator, } impl ManualHandler { pub fn new(storage: Storage, git_object_cache: Arc) -> Self { Self { - git_push_handler: GitPushHandler::new(storage, git_object_cache), + storage: storage.clone(), + changes_calculator: ChangesCalculator::new(storage, git_object_cache), } } + + /// Get the parent commit hash for a given commit. + /// Returns the first parent if available, otherwise returns the same commit hash. + async fn get_parent_commit(&self, commit_hash: &str) -> Result { + let commit = self + .storage + .mono_storage() + .get_commit_by_hash(commit_hash) + .await? + .ok_or_else(|| { + MegaError::Other(format!("[code:404] Commit not found: {}", commit_hash)) + })?; + + // Parse parents_id JSON array + let parent_ids: Vec = + serde_json::from_value(commit.parents_id.clone()).unwrap_or_default(); + + // Use first parent if available, otherwise return same hash (initial commit case) + Ok(parent_ids + .first() + .cloned() + .unwrap_or_else(|| commit_hash.to_string())) + } } #[async_trait] impl TriggerHandler for ManualHandler { async fn handle(&self, context: &TriggerContext) -> Result { - // Reuse GitPushHandler logic for getting builds - let builds = self.git_push_handler.get_builds_for_commit(context).await?; + let from_hash = if context.from_hash == context.commit_hash { + self.get_parent_commit(&context.commit_hash).await? + } else { + context.from_hash.clone() + }; + + let adjusted_context = TriggerContext { + from_hash: from_hash.clone(), + ..context.clone() + }; + + let builds = self + .changes_calculator + .get_builds_for_commit(&adjusted_context) + .await?; let cl_link = context.cl_link.clone().unwrap_or_else(|| { format!( @@ -44,7 +83,7 @@ impl TriggerHandler for ManualHandler { trigger_type: BuildTriggerType::Manual, trigger_source: context.trigger_source, trigger_time: Utc::now(), - payload: BuildTriggerPayload::Manual(crate::build_trigger::ManualPayload { + payload: BuildTriggerPayload::Manual(ManualPayload { repo: context.repo_path.clone(), commit_hash: context.commit_hash.clone(), triggered_by: context diff --git a/ceres/src/build_trigger/mod.rs b/ceres/src/build_trigger/mod.rs index 1c69c7d76..388a72193 100644 --- a/ceres/src/build_trigger/mod.rs +++ b/ceres/src/build_trigger/mod.rs @@ -7,6 +7,7 @@ use jupiter::storage::Storage; use crate::api_service::cache::GitObjectCache; +mod changes_calculator; mod dispatcher; mod git_push_handler; mod manual_handler; diff --git a/ceres/src/build_trigger/retry_handler.rs b/ceres/src/build_trigger/retry_handler.rs index 58020bf97..28ecfbcbd 100644 --- a/ceres/src/build_trigger/retry_handler.rs +++ b/ceres/src/build_trigger/retry_handler.rs @@ -5,23 +5,24 @@ use chrono::Utc; use common::errors::MegaError; use jupiter::storage::Storage; -use super::git_push_handler::GitPushHandler; +use super::changes_calculator::ChangesCalculator; use crate::{ api_service::cache::GitObjectCache, build_trigger::{ - BuildTrigger, BuildTriggerPayload, BuildTriggerType, TriggerContext, TriggerHandler, + BuildTrigger, BuildTriggerPayload, BuildTriggerType, RetryPayload, TriggerContext, + TriggerHandler, }, }; /// Handler for retry build triggers. pub struct RetryHandler { - git_push_handler: GitPushHandler, + changes_calculator: ChangesCalculator, } impl RetryHandler { pub fn new(storage: Storage, git_object_cache: Arc) -> Self { Self { - git_push_handler: GitPushHandler::new(storage, git_object_cache), + changes_calculator: ChangesCalculator::new(storage, git_object_cache), } } } @@ -29,8 +30,11 @@ impl RetryHandler { #[async_trait] impl TriggerHandler for RetryHandler { async fn handle(&self, context: &TriggerContext) -> Result { - // Reuse GitPushHandler logic for getting builds - let builds = self.git_push_handler.get_builds_for_commit(context).await?; + // Compute builds from trigger context + let builds = self + .changes_calculator + .get_builds_for_commit(context) + .await?; let cl_link = context.cl_link.clone().unwrap_or_else(|| { format!( @@ -44,7 +48,7 @@ impl TriggerHandler for RetryHandler { trigger_type: BuildTriggerType::Retry, trigger_source: context.trigger_source, trigger_time: Utc::now(), - payload: BuildTriggerPayload::Retry(crate::build_trigger::RetryPayload { + payload: BuildTriggerPayload::Retry(RetryPayload { repo: context.repo_path.clone(), from_hash: context.from_hash.clone(), commit_hash: context.commit_hash.clone(), diff --git a/ceres/src/build_trigger/service.rs b/ceres/src/build_trigger/service.rs index 4b1bce829..86c558aff 100644 --- a/ceres/src/build_trigger/service.rs +++ b/ceres/src/build_trigger/service.rs @@ -9,7 +9,10 @@ use crate::{ api_service::cache::GitObjectCache, build_trigger::{ RefResolver, TriggerRegistry, - model::{BuildParams, ListTriggersParams, TriggerContext, TriggerRecord, TriggerResponse}, + model::{ + BuildParams, GitPushEvent, ListTriggersParams, TriggerContext, TriggerRecord, + TriggerResponse, + }, }, }; @@ -43,13 +46,11 @@ impl BuildTriggerService { Ok(()) } - /// Static helper to handle git push events from the pack layer - /// This acts as a boundary to decouple monorepo logic from build trigger internals pub async fn handle_git_push_event( storage: Storage, - git_cache: Arc, + git_cache: Arc, bellatrix: Arc, - event: crate::build_trigger::model::GitPushEvent, + event: GitPushEvent, ) -> Result, MegaError> { if !bellatrix.enable_build() { return Ok(None); @@ -86,7 +87,7 @@ impl BuildTriggerService { .await .map_err(|_| { let ref_str = ref_name.unwrap_or_else(|| "main".to_string()); - MegaError::Other(format!("[code:400] Reference not found: {}", ref_str)) + MegaError::Other(format!("[code:404] Reference not found: {}", ref_str)) })?; // 2. Build context diff --git a/ceres/src/pack/monorepo.rs b/ceres/src/pack/monorepo.rs index ef78b2470..43d52273d 100644 --- a/ceres/src/pack/monorepo.rs +++ b/ceres/src/pack/monorepo.rs @@ -43,8 +43,9 @@ use tokio_stream::wrappers::ReceiverStream; use crate::{ api_service::{ApiHandler, cache::GitObjectCache, mono_api_service::MonoApiService, tree_ops}, + build_trigger::{BuildTriggerService, GitPushEvent}, merge_checker::CheckerRegistry, - model::change_list::BuckFile, + model::change_list::{BuckFile, ClDiffFile}, pack::RepoHandler, protocol::import_refs::{RefCommand, Refs}, }; @@ -1044,7 +1045,7 @@ impl MonoRepo { .ok_or_else(|| MegaError::Other(format!("CL not found for link: {}", link)))?; if self.bellatrix.enable_build() { - let event = crate::build_trigger::GitPushEvent { + let event = GitPushEvent { repo_path: cl_info.path.clone(), from_hash: cl_info.from_hash.clone(), commit_hash: cl_info.to_hash.clone(), @@ -1053,7 +1054,7 @@ impl MonoRepo { triggered_by: self.username.clone(), }; - let _trigger_id = crate::build_trigger::BuildTriggerService::handle_git_push_event( + let _trigger_id = BuildTriggerService::handle_git_push_event( self.storage.clone(), self.git_object_cache.clone(), self.bellatrix.clone(), @@ -1079,7 +1080,7 @@ impl MonoRepo { &self, old_files: Vec<(PathBuf, ObjectHash)>, new_files: Vec<(PathBuf, ObjectHash)>, - ) -> Result, MegaError> { + ) -> Result, MegaError> { let api_service: MonoApiService = self.into(); api_service.cl_files_list(old_files, new_files).await } diff --git a/mono/src/api/mod.rs b/mono/src/api/mod.rs index d8665acab..25ca1a526 100644 --- a/mono/src/api/mod.rs +++ b/mono/src/api/mod.rs @@ -65,6 +65,7 @@ pub struct MonoApiServiceState { pub session_store: Option, pub listen_addr: String, pub entity_store: EntityStore, + pub bellatrix: Arc, } impl FromRef for MemoryStore { @@ -149,11 +150,10 @@ impl MonoApiServiceState { } pub fn build_trigger_service(&self) -> BuildTriggerService { - let bellatrix = Arc::new(Bellatrix::new(self.storage.config().build.clone())); BuildTriggerService::new( self.storage.clone(), self.git_object_cache.clone(), - bellatrix, + self.bellatrix.clone(), ) } diff --git a/mono/src/server/http_server.rs b/mono/src/server/http_server.rs index 7450d356f..52dfed8fe 100644 --- a/mono/src/server/http_server.rs +++ b/mono/src/server/http_server.rs @@ -10,6 +10,7 @@ use axum::{ response::Response, routing::any, }; +use bellatrix::Bellatrix; use ceres::{ api_service::{cache::GitObjectCache, state::ProtocolApiState}, protocol::{ServiceType, SmartProtocol, TransportProtocol}, @@ -261,6 +262,7 @@ pub async fn app(ctx: AppContext, host: String, port: u16) -> Router { listen_addr: format!("http://{host}:{port}"), entity_store: EntityStore::new(), git_object_cache, + bellatrix: Arc::new(Bellatrix::new(storage.config().build.clone())), }; let origins: Vec = oauth_config