Skip to content

Commit 029e066

Browse files
feat: implement build trigger system (#1880)
* feat: implement build trigger system Signed-off-by: allure <1550220889@qq.com> * feat: refactor build trigger system and fix error Signed-off-by: allure <1550220889@qq.com> --------- Signed-off-by: allure <1550220889@qq.com>
1 parent 3d9752a commit 029e066

18 files changed

Lines changed: 1667 additions & 70 deletions
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
use std::{
2+
path::{Path, PathBuf},
3+
sync::Arc,
4+
};
5+
6+
use common::errors::MegaError;
7+
use git_internal::hash::ObjectHash;
8+
use jupiter::storage::Storage;
9+
10+
use crate::{
11+
api_service::{cache::GitObjectCache, mono_api_service::MonoApiService},
12+
build_trigger::{SerializableBuildInfo, SerializableStatus, TriggerContext},
13+
model::change_list::{ClDiffFile, ClFilesRes},
14+
};
15+
16+
pub struct ChangesCalculator {
17+
storage: Storage,
18+
git_object_cache: Arc<GitObjectCache>,
19+
}
20+
21+
impl ChangesCalculator {
22+
pub fn new(storage: Storage, git_object_cache: Arc<GitObjectCache>) -> Self {
23+
Self {
24+
storage,
25+
git_object_cache,
26+
}
27+
}
28+
29+
pub async fn get_builds_for_commit(
30+
&self,
31+
context: &TriggerContext,
32+
) -> Result<Vec<SerializableBuildInfo>, MegaError> {
33+
let old_files = self.get_commit_blobs(&context.from_hash).await?;
34+
let new_files = self.get_commit_blobs(&context.commit_hash).await?;
35+
let diff_files = self.cl_files_list(old_files, new_files).await?;
36+
37+
let changes = self.build_changes(&context.repo_path, diff_files)?;
38+
39+
Ok(vec![SerializableBuildInfo { changes }])
40+
}
41+
42+
fn build_changes(
43+
&self,
44+
cl_path: &str,
45+
cl_diff_files: Vec<ClDiffFile>,
46+
) -> Result<Vec<SerializableStatus>, MegaError> {
47+
let cl_base = PathBuf::from(cl_path);
48+
let path_str = cl_base.to_str().ok_or_else(|| {
49+
MegaError::Other(format!("CL base path is not valid UTF-8: {:?}", cl_base))
50+
})?;
51+
52+
let changes = cl_diff_files
53+
.into_iter()
54+
.map(|m| {
55+
let mut item: ClFilesRes = m.into();
56+
item.path = cl_base.join(item.path).to_string_lossy().to_string();
57+
item
58+
})
59+
.collect::<Vec<_>>();
60+
61+
let counter_changes = changes
62+
.iter()
63+
.filter(|&s| PathBuf::from(&s.path).starts_with(&cl_base))
64+
.map(|s| {
65+
let rel = Path::new(&s.path)
66+
.strip_prefix(path_str)
67+
.map_err(|_| {
68+
MegaError::Other(format!("Invalid project-relative path: {}", s.path))
69+
})?
70+
.to_string_lossy()
71+
.replace('\\', "/")
72+
.trim_start_matches('/')
73+
.to_string();
74+
75+
let status = if s.action == "new" {
76+
SerializableStatus::Added(rel)
77+
} else if s.action == "deleted" {
78+
SerializableStatus::Removed(rel)
79+
} else if s.action == "modified" {
80+
SerializableStatus::Modified(rel)
81+
} else {
82+
return Err(MegaError::Other(format!(
83+
"Unsupported change action: {}",
84+
s.action
85+
)));
86+
};
87+
Ok(status)
88+
})
89+
.collect::<Result<Vec<_>, MegaError>>()?;
90+
91+
Ok(counter_changes)
92+
}
93+
94+
async fn get_commit_blobs(
95+
&self,
96+
commit_hash: &str,
97+
) -> Result<Vec<(PathBuf, ObjectHash)>, MegaError> {
98+
let api_service = MonoApiService {
99+
storage: self.storage.clone(),
100+
git_object_cache: self.git_object_cache.clone(),
101+
};
102+
api_service.get_commit_blobs(commit_hash).await
103+
}
104+
105+
async fn cl_files_list(
106+
&self,
107+
old_files: Vec<(PathBuf, ObjectHash)>,
108+
new_files: Vec<(PathBuf, ObjectHash)>,
109+
) -> Result<Vec<ClDiffFile>, MegaError> {
110+
let api_service = MonoApiService {
111+
storage: self.storage.clone(),
112+
git_object_cache: self.git_object_cache.clone(),
113+
};
114+
api_service.cl_files_list(old_files, new_files).await
115+
}
116+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use std::sync::Arc;
2+
3+
use bellatrix::{Bellatrix, orion_client::OrionBuildRequest};
4+
use common::errors::MegaError;
5+
use jupiter::storage::Storage;
6+
7+
use crate::build_trigger::{BuildTrigger, BuildTriggerPayload, SerializableBuildInfo};
8+
9+
/// Handles dispatching build triggers to the build execution layer (Bellatrix/Orion).
10+
pub struct BuildDispatcher {
11+
storage: Storage,
12+
bellatrix: Arc<Bellatrix>,
13+
}
14+
15+
impl BuildDispatcher {
16+
pub fn new(storage: Storage, bellatrix: Arc<Bellatrix>) -> Self {
17+
Self { storage, bellatrix }
18+
}
19+
20+
/// Dispatch a build trigger.
21+
///
22+
/// This method:
23+
/// 1. Persists the trigger to the database
24+
/// 2. Sends the build request to Bellatrix/Orion asynchronously
25+
///
26+
/// Returns the ID of the created trigger record.
27+
pub async fn dispatch(&self, trigger: BuildTrigger) -> Result<i64, MegaError> {
28+
let trigger_payload = serde_json::to_value(&trigger.payload).map_err(|e| {
29+
tracing::error!("Failed to serialize payload: {}", e);
30+
MegaError::Other(format!("Failed to serialize payload: {}", e))
31+
})?;
32+
33+
let db_record = self
34+
.storage
35+
.build_trigger_storage()
36+
.insert(
37+
trigger.trigger_type.to_string(),
38+
trigger.trigger_source.to_string(),
39+
trigger_payload,
40+
)
41+
.await?;
42+
43+
// Persist to database
44+
tracing::info!("BuildDispatcher: Persisted trigger ID: {}", db_record.id);
45+
46+
if !self.bellatrix.enable_build() {
47+
tracing::info!("BuildDispatcher: Completed (build system disabled)");
48+
return Ok(db_record.id);
49+
}
50+
51+
// Extract data from payload using pattern matching
52+
let (cl_link, repo, builds_json, cl_id) = match &trigger.payload {
53+
BuildTriggerPayload::GitPush(p) => (&p.cl_link, &p.repo, &p.builds, p.cl_id),
54+
BuildTriggerPayload::Manual(p) => (&p.cl_link, &p.repo, &p.builds, p.cl_id),
55+
BuildTriggerPayload::Retry(p) => (&p.cl_link, &p.repo, &p.builds, p.cl_id),
56+
BuildTriggerPayload::Webhook(p) => (&p.cl_link, &p.repo, &p.builds, p.cl_id),
57+
BuildTriggerPayload::Schedule(p) => (&p.cl_link, &p.repo, &p.builds, p.cl_id),
58+
};
59+
60+
let builds: Vec<SerializableBuildInfo> = serde_json::from_value(builds_json.clone())
61+
.map_err(|e| {
62+
tracing::error!("Failed to deserialize builds from payload: {}", e);
63+
MegaError::Other(format!("Failed to deserialize builds from payload: {}", e))
64+
})?;
65+
66+
let bellatrix_builds: Vec<bellatrix::orion_client::BuildInfo> = builds
67+
.into_iter()
68+
.enumerate()
69+
.map(|(idx, info)| {
70+
tracing::debug!(" Build [{}]: {} change(s)", idx + 1, info.changes.len());
71+
bellatrix::orion_client::BuildInfo {
72+
changes: info.changes.into_iter().map(|s| s.into()).collect(),
73+
}
74+
})
75+
.collect();
76+
77+
let req = OrionBuildRequest {
78+
cl_link: cl_link.to_string(),
79+
mount_path: repo.to_string(),
80+
cl: cl_id.unwrap_or(0),
81+
builds: bellatrix_builds,
82+
};
83+
84+
// Dispatch asynchronously
85+
let bellatrix = self.bellatrix.clone();
86+
let trigger_id = db_record.id;
87+
tokio::spawn(async move {
88+
match bellatrix.on_post_receive(req).await {
89+
Ok(_) => {
90+
tracing::info!(
91+
"BuildDispatcher: Build request sent to Bellatrix (Trigger ID: {})",
92+
trigger_id
93+
);
94+
}
95+
Err(err) => {
96+
tracing::error!(
97+
"BuildDispatcher: Failed to dispatch build (Trigger ID: {}): {}",
98+
trigger_id,
99+
err
100+
);
101+
}
102+
}
103+
});
104+
105+
Ok(db_record.id)
106+
}
107+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use std::sync::Arc;
2+
3+
use async_trait::async_trait;
4+
use chrono::Utc;
5+
use common::errors::MegaError;
6+
use jupiter::storage::Storage;
7+
8+
use super::changes_calculator::ChangesCalculator;
9+
use crate::{
10+
api_service::cache::GitObjectCache,
11+
build_trigger::{
12+
BuildTrigger, BuildTriggerPayload, BuildTriggerType, GitPushPayload, TriggerContext,
13+
TriggerHandler,
14+
},
15+
};
16+
17+
/// Handler for Git push triggers.
18+
pub struct GitPushHandler {
19+
changes_calculator: ChangesCalculator,
20+
}
21+
22+
impl GitPushHandler {
23+
pub fn new(storage: Storage, git_object_cache: Arc<GitObjectCache>) -> Self {
24+
Self {
25+
changes_calculator: ChangesCalculator::new(storage, git_object_cache),
26+
}
27+
}
28+
}
29+
30+
#[async_trait]
31+
impl TriggerHandler for GitPushHandler {
32+
async fn handle(&self, context: &TriggerContext) -> Result<BuildTrigger, MegaError> {
33+
let builds = self
34+
.changes_calculator
35+
.get_builds_for_commit(context)
36+
.await?;
37+
38+
let cl_link = context.cl_link.clone().unwrap_or_else(|| {
39+
format!(
40+
"push-{}-{}",
41+
Utc::now().timestamp_millis(),
42+
&context.commit_hash[..8.min(context.commit_hash.len())]
43+
)
44+
});
45+
46+
Ok(BuildTrigger {
47+
trigger_type: context.trigger_type,
48+
trigger_source: context.trigger_source,
49+
trigger_time: Utc::now(),
50+
payload: BuildTriggerPayload::GitPush(GitPushPayload {
51+
repo: context.repo_path.clone(),
52+
from_hash: context.from_hash.clone(),
53+
commit_hash: context.commit_hash.clone(),
54+
cl_link,
55+
cl_id: context.cl_id,
56+
builds: serde_json::to_value(&builds)
57+
.map_err(|e| MegaError::Other(format!("Failed to serialize builds: {}", e)))?,
58+
triggered_by: context.triggered_by.clone(),
59+
}),
60+
})
61+
}
62+
63+
fn trigger_type(&self) -> BuildTriggerType {
64+
BuildTriggerType::GitPush
65+
}
66+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use std::sync::Arc;
2+
3+
use async_trait::async_trait;
4+
use chrono::Utc;
5+
use common::errors::MegaError;
6+
use jupiter::storage::Storage;
7+
8+
use super::changes_calculator::ChangesCalculator;
9+
use crate::{
10+
api_service::cache::GitObjectCache,
11+
build_trigger::{
12+
BuildTrigger, BuildTriggerPayload, BuildTriggerType, ManualPayload, TriggerContext,
13+
TriggerHandler,
14+
},
15+
};
16+
17+
/// Handler for manual build triggers.
18+
pub struct ManualHandler {
19+
storage: Storage,
20+
changes_calculator: ChangesCalculator,
21+
}
22+
23+
impl ManualHandler {
24+
pub fn new(storage: Storage, git_object_cache: Arc<GitObjectCache>) -> Self {
25+
Self {
26+
storage: storage.clone(),
27+
changes_calculator: ChangesCalculator::new(storage, git_object_cache),
28+
}
29+
}
30+
31+
/// Get the parent commit hash for a given commit.
32+
/// Returns the first parent if available, otherwise returns the same commit hash.
33+
async fn get_parent_commit(&self, commit_hash: &str) -> Result<String, MegaError> {
34+
let commit = self
35+
.storage
36+
.mono_storage()
37+
.get_commit_by_hash(commit_hash)
38+
.await?
39+
.ok_or_else(|| {
40+
MegaError::Other(format!("[code:404] Commit not found: {}", commit_hash))
41+
})?;
42+
43+
// Parse parents_id JSON array
44+
let parent_ids: Vec<String> =
45+
serde_json::from_value(commit.parents_id.clone()).unwrap_or_default();
46+
47+
// Use first parent if available, otherwise return same hash (initial commit case)
48+
Ok(parent_ids
49+
.first()
50+
.cloned()
51+
.unwrap_or_else(|| commit_hash.to_string()))
52+
}
53+
}
54+
55+
#[async_trait]
56+
impl TriggerHandler for ManualHandler {
57+
async fn handle(&self, context: &TriggerContext) -> Result<BuildTrigger, MegaError> {
58+
let from_hash = if context.from_hash == context.commit_hash {
59+
self.get_parent_commit(&context.commit_hash).await?
60+
} else {
61+
context.from_hash.clone()
62+
};
63+
64+
let adjusted_context = TriggerContext {
65+
from_hash: from_hash.clone(),
66+
..context.clone()
67+
};
68+
69+
let builds = self
70+
.changes_calculator
71+
.get_builds_for_commit(&adjusted_context)
72+
.await?;
73+
74+
let cl_link = context.cl_link.clone().unwrap_or_else(|| {
75+
format!(
76+
"manual-{}-{}",
77+
Utc::now().timestamp_millis(),
78+
&context.commit_hash[..8.min(context.commit_hash.len())]
79+
)
80+
});
81+
82+
Ok(BuildTrigger {
83+
trigger_type: BuildTriggerType::Manual,
84+
trigger_source: context.trigger_source,
85+
trigger_time: Utc::now(),
86+
payload: BuildTriggerPayload::Manual(ManualPayload {
87+
repo: context.repo_path.clone(),
88+
commit_hash: context.commit_hash.clone(),
89+
triggered_by: context
90+
.triggered_by
91+
.clone()
92+
.unwrap_or_else(|| "unknown".to_string()),
93+
builds: serde_json::to_value(&builds)
94+
.map_err(|e| MegaError::Other(format!("Failed to serialize builds: {}", e)))?,
95+
params: context.params.clone(),
96+
cl_link: cl_link.clone(),
97+
cl_id: context.cl_id,
98+
ref_name: context.ref_name.clone(),
99+
ref_type: context.ref_type.clone(),
100+
}),
101+
})
102+
}
103+
104+
fn trigger_type(&self) -> BuildTriggerType {
105+
BuildTriggerType::Manual
106+
}
107+
}

0 commit comments

Comments
 (0)