1use kotoba_core::types::{Result, Value};
7use kotoba_core::prelude::KotobaError;
8use kotoba_deploy_core::*;
9use serde::Deserialize;
10use std::collections::HashMap;
11use std::sync::{Arc, RwLock};
12use std::time::SystemTime;
13use chrono::{DateTime, Utc};
14
15#[derive(Debug)]
17pub struct GitIntegration {
18 github_config: GitHubConfig,
20 webhook_handler: WebhookHandler,
22 auto_deploy_manager: AutoDeployManager,
24 deployment_history: Arc<RwLock<Vec<DeploymentRecord>>>,
26}
27
28#[derive(Debug, Clone)]
30pub struct GitHubConfig {
31 pub owner: String,
33 pub repo: String,
35 pub access_token: String,
37 pub webhook_secret: Option<String>,
39 pub branches: Vec<String>,
41 pub auto_deploy_enabled: bool,
43}
44
45#[derive(Debug)]
47pub struct WebhookHandler {
48 pub active_webhooks: Arc<RwLock<HashMap<String, WebhookInfo>>>,
50 pub event_queue: Arc<RwLock<Vec<GitHubEvent>>>,
52}
53
54#[derive(Debug, Clone)]
56pub struct AutoDeployManager {
57 pub deploy_scripts: HashMap<String, DeployScript>,
59 pub build_configs: HashMap<String, BuildConfig>,
61 pub deploy_conditions: Vec<DeployCondition>,
63}
64
65#[derive(Debug, Clone)]
67pub struct WebhookInfo {
68 pub id: String,
70 pub url: String,
72 pub events: Vec<String>,
74 pub active: bool,
76 pub created_at: SystemTime,
78}
79
80#[derive(Debug, Clone)]
82pub struct GitHubEvent {
83 pub event_type: String,
85 pub payload: Value,
87 pub received_at: SystemTime,
89 pub signature: Option<String>,
91}
92
93#[derive(Debug, Clone, Deserialize)]
95pub struct PushEventPayload {
96 #[serde(rename = "ref")]
98 pub ref_field: String,
99 pub commits: Vec<CommitInfo>,
101 pub sender: UserInfo,
103 pub repository: RepositoryInfo,
105}
106
107#[derive(Debug, Clone, Deserialize)]
109pub struct PullRequestEventPayload {
110 pub action: String,
112 pub number: u32,
114 pub pull_request: PullRequestInfo,
116 pub repository: RepositoryInfo,
118}
119
120#[derive(Debug, Clone, Deserialize)]
122pub struct CommitInfo {
123 pub id: String,
125 pub message: String,
127 pub timestamp: String,
129 pub author: UserInfo,
131}
132
133#[derive(Debug, Clone, Deserialize)]
135pub struct UserInfo {
136 pub id: u32,
138 pub login: String,
140 pub name: Option<String>,
142}
143
144#[derive(Debug, Clone, Deserialize)]
146pub struct RepositoryInfo {
147 pub id: u32,
149 pub full_name: String,
151 pub name: String,
153 pub owner: UserInfo,
155}
156
157#[derive(Debug, Clone, Deserialize)]
159pub struct PullRequestInfo {
160 pub id: u32,
162 pub number: u32,
164 pub title: String,
166 pub body: Option<String>,
168 pub state: String,
170 pub merged: bool,
172 pub merge_commit_sha: Option<String>,
174 pub head: BranchInfo,
176}
177
178#[derive(Debug, Clone, Deserialize)]
180pub struct BranchInfo {
181 #[serde(rename = "ref")]
183 pub ref_field: String,
184 pub sha: String,
186}
187
188#[derive(Debug, Clone)]
190pub struct DeployScript {
191 pub name: String,
193 pub content: String,
195 pub triggers: Vec<ScriptTrigger>,
197}
198
199#[derive(Debug, Clone)]
201pub struct BuildConfig {
202 pub name: String,
204 pub build_command: String,
206 pub output_dir: String,
208 pub environment: HashMap<String, String>,
210}
211
212#[derive(Debug, Clone)]
214pub struct DeployCondition {
215 pub name: String,
217 pub condition_type: ConditionType,
219 pub value: String,
221}
222
223#[derive(Debug, Clone)]
225pub enum ConditionType {
226 Branch,
228 Tag,
230 FileChanged,
232 Custom,
234}
235
236#[derive(Debug, Clone)]
238pub enum ScriptTrigger {
239 Push,
241 PullRequest,
243 Tag,
245 Manual,
247}
248
249#[derive(Debug, Clone)]
251pub struct DeploymentRecord {
252 pub id: String,
254 pub deployment_name: String,
256 pub commit_id: String,
258 pub branch: String,
260 pub trigger_event: String,
262 pub status: DeploymentStatus,
264 pub started_at: DateTime<Utc>,
266 pub completed_at: Option<DateTime<Utc>>,
268 pub logs: Vec<String>,
270}
271
272impl GitIntegration {
273 pub fn new(github_config: GitHubConfig) -> Self {
275 Self {
276 webhook_handler: WebhookHandler {
277 active_webhooks: Arc::new(RwLock::new(HashMap::new())),
278 event_queue: Arc::new(RwLock::new(Vec::new())),
279 },
280 auto_deploy_manager: AutoDeployManager {
281 deploy_scripts: HashMap::new(),
282 build_configs: HashMap::new(),
283 deploy_conditions: Vec::new(),
284 },
285 deployment_history: Arc::new(RwLock::new(Vec::new())),
286 github_config,
287 }
288 }
289
290 pub fn github_config(&self) -> &GitHubConfig {
292 &self.github_config
293 }
294
295 pub fn webhook_handler(&self) -> &WebhookHandler {
297 &self.webhook_handler
298 }
299
300 pub fn auto_deploy_manager(&self) -> &AutoDeployManager {
302 &self.auto_deploy_manager
303 }
304
305 pub fn deployment_history(&self) -> Arc<RwLock<Vec<DeploymentRecord>>> {
307 Arc::clone(&self.deployment_history)
308 }
309
310 pub async fn process_webhook(&self, event: GitHubEvent) -> Result<()> {
312 {
314 let mut queue = self.webhook_handler.event_queue.write().unwrap();
315 queue.push(event.clone());
316 }
317
318 if self.github_config.auto_deploy_enabled {
320 self.process_auto_deploy(event).await?;
321 }
322
323 Ok(())
324 }
325
326 async fn process_auto_deploy(&self, event: GitHubEvent) -> Result<()> {
328 match event.event_type.as_str() {
329 "push" => {
330 if let Ok(json_value) = serde_json::to_value(&event.payload) {
332 if let Ok(payload) = serde_json::from_value::<PushEventPayload>(json_value) {
333 self.handle_push_event(payload).await?;
334 }
335 }
336 }
337 "pull_request" => {
338 if let Ok(json_value) = serde_json::to_value(&event.payload) {
340 if let Ok(payload) = serde_json::from_value::<PullRequestEventPayload>(json_value) {
341 self.handle_pull_request_event(payload).await?;
342 }
343 }
344 }
345 _ => {
346 }
348 }
349
350 Ok(())
351 }
352
353 async fn handle_push_event(&self, payload: PushEventPayload) -> Result<()> {
355 let branch = payload.ref_field.strip_prefix("refs/heads/").unwrap_or(&payload.ref_field);
357 if !self.github_config.branches.contains(&branch.to_string()) {
358 return Ok(());
359 }
360
361 if !self.check_deploy_conditions(&payload) {
363 return Ok(());
364 }
365
366 let record = DeploymentRecord {
368 id: format!("deploy-{}", Utc::now().timestamp()),
369 deployment_name: format!("{}-{}", self.github_config.repo, branch),
370 commit_id: payload.commits.last().map(|c| c.id.clone()).unwrap_or_default(),
371 branch: branch.to_string(),
372 trigger_event: "push".to_string(),
373 status: DeploymentStatus::Creating,
374 started_at: Utc::now(),
375 completed_at: None,
376 logs: vec!["Starting deployment from push event".to_string()],
377 };
378
379 {
381 let mut history = self.deployment_history.write().unwrap();
382 history.push(record);
383 }
384
385 Ok(())
386 }
387
388 async fn handle_pull_request_event(&self, payload: PullRequestEventPayload) -> Result<()> {
390 if payload.action == "closed" && payload.pull_request.merged {
392 let branch = payload.pull_request.head.ref_field.clone();
394
395 if !self.github_config.branches.contains(&branch) {
397 return Ok(());
398 }
399
400 let record = DeploymentRecord {
402 id: format!("deploy-pr-{}", payload.number),
403 deployment_name: format!("{}-pr-{}", self.github_config.repo, payload.number),
404 commit_id: payload.pull_request.merge_commit_sha.unwrap_or_default(),
405 branch,
406 trigger_event: "pull_request_merged".to_string(),
407 status: DeploymentStatus::Creating,
408 started_at: Utc::now(),
409 completed_at: None,
410 logs: vec!["Starting deployment from merged pull request".to_string()],
411 };
412
413 {
415 let mut history = self.deployment_history.write().unwrap();
416 history.push(record);
417 }
418 }
419
420 Ok(())
421 }
422
423 fn check_deploy_conditions(&self, payload: &PushEventPayload) -> bool {
425 for condition in &self.auto_deploy_manager.deploy_conditions {
426 match condition.condition_type {
427 ConditionType::Branch => {
428 let branch = payload.ref_field.strip_prefix("refs/heads/").unwrap_or(&payload.ref_field);
429 if branch != condition.value {
430 return false;
431 }
432 }
433 ConditionType::FileChanged => {
434 if !payload.commits.iter().any(|commit| {
436 commit.message.contains(&condition.value)
437 }) {
438 return false;
439 }
440 }
441 _ => {
442 }
444 }
445 }
446 true
447 }
448
449 pub fn verify_webhook_signature(&self, payload: &str, signature: &str) -> Result<bool> {
451 if let Some(secret) = &self.github_config.webhook_secret {
452 use hmac::{Hmac, Mac};
453 use sha2::Sha256;
454
455 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
456 .map_err(|_| KotobaError::InvalidArgument("Invalid secret key length".to_string()))?;
457
458 mac.update(payload.as_bytes());
459
460 let expected_signature = format!("sha256={}", hex::encode(mac.finalize().into_bytes()));
461
462 Ok(signature == expected_signature)
464 } else {
465 Ok(false)
466 }
467 }
468}
469
470impl Default for GitHubConfig {
471 fn default() -> Self {
472 Self {
473 owner: "default".to_string(),
474 repo: "default".to_string(),
475 access_token: "".to_string(),
476 webhook_secret: None,
477 branches: vec!["main".to_string()],
478 auto_deploy_enabled: false,
479 }
480 }
481}
482
483impl WebhookHandler {
484 pub fn new() -> Self {
486 Self {
487 active_webhooks: Arc::new(RwLock::new(HashMap::new())),
488 event_queue: Arc::new(RwLock::new(Vec::new())),
489 }
490 }
491
492 pub async fn register_webhook(&self, webhook_info: WebhookInfo) -> Result<()> {
494 let mut webhooks = self.active_webhooks.write().unwrap();
495 webhooks.insert(webhook_info.id.clone(), webhook_info);
496 Ok(())
497 }
498
499 pub async fn unregister_webhook(&self, webhook_id: &str) -> Result<()> {
501 let mut webhooks = self.active_webhooks.write().unwrap();
502 webhooks.remove(webhook_id);
503 Ok(())
504 }
505}
506
507impl Default for WebhookHandler {
508 fn default() -> Self {
509 Self::new()
510 }
511}
512
513impl AutoDeployManager {
514 pub fn new() -> Self {
516 Self {
517 deploy_scripts: HashMap::new(),
518 build_configs: HashMap::new(),
519 deploy_conditions: Vec::new(),
520 }
521 }
522
523 pub fn add_deploy_script(&mut self, script: DeployScript) {
525 self.deploy_scripts.insert(script.name.clone(), script);
526 }
527
528 pub fn add_build_config(&mut self, config: BuildConfig) {
530 self.build_configs.insert(config.name.clone(), config);
531 }
532
533 pub fn add_deploy_condition(&mut self, condition: DeployCondition) {
535 self.deploy_conditions.push(condition);
536 }
537}
538
539impl Default for AutoDeployManager {
540 fn default() -> Self {
541 Self::new()
542 }
543}
544
545pub use GitIntegration as GitManager;
547pub use GitHubConfig as GitConfig;
548pub use WebhookHandler as WebhookManager;
549pub use AutoDeployManager as AutoDeploy;
550pub use DeploymentRecord as DeployRecord;