kotoba_deploy_git/
lib.rs

1//! # Kotoba Deploy Git Integration
2//!
3//! Git integration module for the Kotoba deployment system.
4//! Provides GitHub webhook handling, automatic deployment, and CI/CD integration.
5
6use 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/// Git統合マネージャー
16#[derive(Debug)]
17pub struct GitIntegration {
18    /// GitHub設定
19    github_config: GitHubConfig,
20    /// Webhookハンドラー
21    webhook_handler: WebhookHandler,
22    /// 自動デプロイマネージャー
23    auto_deploy_manager: AutoDeployManager,
24    /// デプロイ履歴
25    deployment_history: Arc<RwLock<Vec<DeploymentRecord>>>,
26}
27
28/// GitHub設定
29#[derive(Debug, Clone)]
30pub struct GitHubConfig {
31    /// リポジトリ所有者
32    pub owner: String,
33    /// リポジトリ名
34    pub repo: String,
35    /// アクセストークン
36    pub access_token: String,
37    /// Webhookシークレット
38    pub webhook_secret: Option<String>,
39    /// 監視するブランチ
40    pub branches: Vec<String>,
41    /// 自動デプロイ有効化
42    pub auto_deploy_enabled: bool,
43}
44
45/// Webhookハンドラー
46#[derive(Debug)]
47pub struct WebhookHandler {
48    /// アクティブなWebhook
49    pub active_webhooks: Arc<RwLock<HashMap<String, WebhookInfo>>>,
50    /// イベント処理キュー
51    pub event_queue: Arc<RwLock<Vec<GitHubEvent>>>,
52}
53
54/// 自動デプロイマネージャー
55#[derive(Debug, Clone)]
56pub struct AutoDeployManager {
57    /// デプロイスクリプト
58    pub deploy_scripts: HashMap<String, DeployScript>,
59    /// ビルド設定
60    pub build_configs: HashMap<String, BuildConfig>,
61    /// デプロイ条件
62    pub deploy_conditions: Vec<DeployCondition>,
63}
64
65/// Webhook情報
66#[derive(Debug, Clone)]
67pub struct WebhookInfo {
68    /// Webhook ID
69    pub id: String,
70    /// URL
71    pub url: String,
72    /// イベントタイプ
73    pub events: Vec<String>,
74    /// アクティブ
75    pub active: bool,
76    /// 作成時刻
77    pub created_at: SystemTime,
78}
79
80/// GitHubイベント
81#[derive(Debug, Clone)]
82pub struct GitHubEvent {
83    /// イベントタイプ
84    pub event_type: String,
85    /// ペイロード
86    pub payload: Value,
87    /// 受信時刻
88    pub received_at: SystemTime,
89    /// 署名
90    pub signature: Option<String>,
91}
92
93/// プッシュイベントペイロード
94#[derive(Debug, Clone, Deserialize)]
95pub struct PushEventPayload {
96    /// リファレンス
97    #[serde(rename = "ref")]
98    pub ref_field: String,
99    /// コミット
100    pub commits: Vec<CommitInfo>,
101    /// 送信者
102    pub sender: UserInfo,
103    /// リポジトリ
104    pub repository: RepositoryInfo,
105}
106
107/// プルリクエストイベントペイロード
108#[derive(Debug, Clone, Deserialize)]
109pub struct PullRequestEventPayload {
110    /// アクション
111    pub action: String,
112    /// プルリクエスト番号
113    pub number: u32,
114    /// プルリクエスト
115    pub pull_request: PullRequestInfo,
116    /// リポジトリ
117    pub repository: RepositoryInfo,
118}
119
120/// コミット情報
121#[derive(Debug, Clone, Deserialize)]
122pub struct CommitInfo {
123    /// コミットID
124    pub id: String,
125    /// メッセージ
126    pub message: String,
127    /// タイムスタンプ
128    pub timestamp: String,
129    /// 作者
130    pub author: UserInfo,
131}
132
133/// ユーザー情報
134#[derive(Debug, Clone, Deserialize)]
135pub struct UserInfo {
136    /// ユーザーID
137    pub id: u32,
138    /// ログイン名
139    pub login: String,
140    /// 表示名
141    pub name: Option<String>,
142}
143
144/// リポジトリ情報
145#[derive(Debug, Clone, Deserialize)]
146pub struct RepositoryInfo {
147    /// リポジトリID
148    pub id: u32,
149    /// フルネーム
150    pub full_name: String,
151    /// 名前
152    pub name: String,
153    /// 所有者
154    pub owner: UserInfo,
155}
156
157/// プルリクエスト情報
158#[derive(Debug, Clone, Deserialize)]
159pub struct PullRequestInfo {
160    /// ID
161    pub id: u32,
162    /// 番号
163    pub number: u32,
164    /// タイトル
165    pub title: String,
166    /// 説明
167    pub body: Option<String>,
168    /// 状態
169    pub state: String,
170    /// マージ済み
171    pub merged: bool,
172    /// マージコミットSHA
173    pub merge_commit_sha: Option<String>,
174    /// ヘッドブランチ情報
175    pub head: BranchInfo,
176}
177
178/// ブランチ情報
179#[derive(Debug, Clone, Deserialize)]
180pub struct BranchInfo {
181    /// リファレンス
182    #[serde(rename = "ref")]
183    pub ref_field: String,
184    /// SHA
185    pub sha: String,
186}
187
188/// デプロイスクリプト
189#[derive(Debug, Clone)]
190pub struct DeployScript {
191    /// スクリプト名
192    pub name: String,
193    /// スクリプト内容
194    pub content: String,
195    /// トリガー条件
196    pub triggers: Vec<ScriptTrigger>,
197}
198
199/// ビルド設定
200#[derive(Debug, Clone)]
201pub struct BuildConfig {
202    /// ビルド名
203    pub name: String,
204    /// ビルドコマンド
205    pub build_command: String,
206    /// 出力ディレクトリ
207    pub output_dir: String,
208    /// 環境変数
209    pub environment: HashMap<String, String>,
210}
211
212/// デプロイ条件
213#[derive(Debug, Clone)]
214pub struct DeployCondition {
215    /// 条件名
216    pub name: String,
217    /// 条件タイプ
218    pub condition_type: ConditionType,
219    /// 値
220    pub value: String,
221}
222
223/// 条件タイプ
224#[derive(Debug, Clone)]
225pub enum ConditionType {
226    /// ブランチ名
227    Branch,
228    /// タグ
229    Tag,
230    /// ファイル変更
231    FileChanged,
232    /// カスタム条件
233    Custom,
234}
235
236/// スクリプトトリガー
237#[derive(Debug, Clone)]
238pub enum ScriptTrigger {
239    /// プッシュ
240    Push,
241    /// プルリクエスト
242    PullRequest,
243    /// タグ作成
244    Tag,
245    /// マニュアル
246    Manual,
247}
248
249/// デプロイ履歴レコード
250#[derive(Debug, Clone)]
251pub struct DeploymentRecord {
252    /// レコードID
253    pub id: String,
254    /// デプロイメント名
255    pub deployment_name: String,
256    /// コミットID
257    pub commit_id: String,
258    /// ブランチ名
259    pub branch: String,
260    /// トリガーイベント
261    pub trigger_event: String,
262    /// デプロイステータス
263    pub status: DeploymentStatus,
264    /// 開始時刻
265    pub started_at: DateTime<Utc>,
266    /// 完了時刻
267    pub completed_at: Option<DateTime<Utc>>,
268    /// ログ
269    pub logs: Vec<String>,
270}
271
272impl GitIntegration {
273    /// 新しいGit統合マネージャーを作成
274    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    /// GitHub設定を取得
291    pub fn github_config(&self) -> &GitHubConfig {
292        &self.github_config
293    }
294
295    /// Webhookハンドラーを取得
296    pub fn webhook_handler(&self) -> &WebhookHandler {
297        &self.webhook_handler
298    }
299
300    /// 自動デプロイマネージャーを取得
301    pub fn auto_deploy_manager(&self) -> &AutoDeployManager {
302        &self.auto_deploy_manager
303    }
304
305    /// デプロイ履歴を取得
306    pub fn deployment_history(&self) -> Arc<RwLock<Vec<DeploymentRecord>>> {
307        Arc::clone(&self.deployment_history)
308    }
309
310    /// Webhookイベントを処理
311    pub async fn process_webhook(&self, event: GitHubEvent) -> Result<()> {
312        // イベントをキューに追加
313        {
314            let mut queue = self.webhook_handler.event_queue.write().unwrap();
315            queue.push(event.clone());
316        }
317
318        // 自動デプロイが有効な場合は処理
319        if self.github_config.auto_deploy_enabled {
320            self.process_auto_deploy(event).await?;
321        }
322
323        Ok(())
324    }
325
326    /// 自動デプロイを処理
327    async fn process_auto_deploy(&self, event: GitHubEvent) -> Result<()> {
328        match event.event_type.as_str() {
329            "push" => {
330                // kotoba_core::Value を serde_json::Value に変換
331                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                // kotoba_core::Value を serde_json::Value に変換
339                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                // その他のイベントは無視
347            }
348        }
349
350        Ok(())
351    }
352
353    /// プッシュイベントを処理
354    async fn handle_push_event(&self, payload: PushEventPayload) -> Result<()> {
355        // 監視対象のブランチかチェック
356        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        // デプロイ条件をチェック
362        if !self.check_deploy_conditions(&payload) {
363            return Ok(());
364        }
365
366        // デプロイメントレコードを作成
367        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        // 履歴に追加
380        {
381            let mut history = self.deployment_history.write().unwrap();
382            history.push(record);
383        }
384
385        Ok(())
386    }
387
388    /// プルリクエストイベントを処理
389    async fn handle_pull_request_event(&self, payload: PullRequestEventPayload) -> Result<()> {
390        // マージされたプルリクエストのみ処理
391        if payload.action == "closed" && payload.pull_request.merged {
392            // マージされたブランチを取得
393            let branch = payload.pull_request.head.ref_field.clone();
394
395            // 監視対象のブランチかチェック
396            if !self.github_config.branches.contains(&branch) {
397                return Ok(());
398            }
399
400            // デプロイメントレコードを作成
401            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            // 履歴に追加
414            {
415                let mut history = self.deployment_history.write().unwrap();
416                history.push(record);
417            }
418        }
419
420        Ok(())
421    }
422
423    /// デプロイ条件をチェック
424    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                    // ファイル変更チェック(簡易実装)
435                    if !payload.commits.iter().any(|commit| {
436                        commit.message.contains(&condition.value)
437                    }) {
438                        return false;
439                    }
440                }
441                _ => {
442                    // その他の条件は無視
443                }
444            }
445        }
446        true
447    }
448
449    /// Webhook署名を検証
450    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            // 簡易的な定時間比較
463            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    /// 新しいWebhookハンドラーを作成
485    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    /// Webhookを登録
493    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    /// Webhookを削除
500    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    /// 新しい自動デプロイマネージャーを作成
515    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    /// デプロイスクリプトを追加
524    pub fn add_deploy_script(&mut self, script: DeployScript) {
525        self.deploy_scripts.insert(script.name.clone(), script);
526    }
527
528    /// ビルド設定を追加
529    pub fn add_build_config(&mut self, config: BuildConfig) {
530        self.build_configs.insert(config.name.clone(), config);
531    }
532
533    /// デプロイ条件を追加
534    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
545// Re-export commonly used types
546pub use GitIntegration as GitManager;
547pub use GitHubConfig as GitConfig;
548pub use WebhookHandler as WebhookManager;
549pub use AutoDeployManager as AutoDeploy;
550pub use DeploymentRecord as DeployRecord;