use deadpool_redis::cluster::Pool as RedisPool; use parsefile::{Pipeline, TriggerEvent}; use crate::{ bare::GitBare, errors::GitError, sync::{HookTask, TaskType}, }; const PIPELINE_FILE: &str = "pipeline.yaml"; #[derive(Debug)] pub enum CiCheckOutcome { Enqueued, NoPipelineFile, NotTriggered, } fn ci_queue_keys(repo_id: uuid::Uuid) -> (String, String) { let hash_tag = format!("{{ci:{}}}", repo_id); ( format!("{}:pending", hash_tag), format!("{}:processing", hash_tag), ) } pub async fn check_and_enqueue( bare: &GitBare, repo_id: uuid::Uuid, event: &TriggerEvent, redis_pool: &RedisPool, ) -> Result { let output = bare.git_command_trusted_stdout(vec![ "show".to_string(), format!("HEAD:{}", PIPELINE_FILE), ]); let content = match output { Ok(c) => c, Err(_) => return Ok(CiCheckOutcome::NoPipelineFile), }; let pipeline = parsefile::parse_from_str(&content).map_err(|e| { GitError::Internal(format!("failed to parse {}: {}", PIPELINE_FILE, e)) })?; if !pipeline.should_run(event) { return Ok(CiCheckOutcome::NotTriggered); } enqueue_ci_task(repo_id, event, &pipeline, redis_pool) .await .map_err(|e| { GitError::Internal(format!("failed to enqueue CI task: {}", e)) })?; Ok(CiCheckOutcome::Enqueued) } async fn enqueue_ci_task( repo_id: uuid::Uuid, event: &TriggerEvent, pipeline: &Pipeline, redis_pool: &RedisPool, ) -> Result<(), String> { let hook_task = HookTask { id: uuid::Uuid::new_v4().to_string(), repo_id: repo_id.to_string(), task_type: TaskType::Sync, payload: serde_json::json!({ "ci": true, "pipeline_name": pipeline.name, "trigger": event_variant_name(event), }), created_at: chrono::Utc::now(), retry_count: 0, }; let task_json = serde_json::to_string(&hook_task) .map_err(|e| format!("serialize error: {}", e))?; let (pending_key, _) = ci_queue_keys(repo_id); let redis = redis_pool .get() .await .map_err(|e| format!("redis pool: {}", e))?; let mut conn: deadpool_redis::cluster::Connection = redis; redis::cmd("LPUSH") .arg(&pending_key) .arg(&task_json) .query_async::<()>(&mut conn) .await .map_err(|e| format!("LPUSH error: {}", e))?; tracing::info!( repo_id = %repo_id, pipeline = %pipeline.name, trigger = %event_variant_name(event), "CI task enqueued" ); Ok(()) } fn event_variant_name(event: &TriggerEvent) -> &'static str { match event { TriggerEvent::PushBranch(_) => "push_branch", TriggerEvent::PushTag(_) => "push_tag", TriggerEvent::PullRequest { .. } => "pull_request", } } pub async fn poll_ci_task_for_repo( redis_pool: &RedisPool, repo_id: uuid::Uuid, block_timeout_secs: usize, ) -> Option { let (pending_key, processing_key) = ci_queue_keys(repo_id); let redis = redis_pool.get().await.ok()?; let mut conn: deadpool_redis::cluster::Connection = redis; redis::cmd("BLMOVE") .arg(&pending_key) .arg(&processing_key) .arg("RIGHT") .arg("LEFT") .arg(block_timeout_secs) .query_async::>(&mut conn) .await .ok() .flatten() } pub async fn ack_ci_task( redis_pool: &RedisPool, repo_id: uuid::Uuid, task_json: &str, ) { let (_, processing_key) = ci_queue_keys(repo_id); let redis = match redis_pool.get().await { Ok(c) => c, Err(e) => { tracing::warn!(error = %e, "CI ack: failed to get redis connection"); return; } }; let mut conn: deadpool_redis::cluster::Connection = redis; if let Err(e) = redis::cmd("LREM") .arg(&processing_key) .arg(1) .arg(task_json) .query_async::<()>(&mut conn) .await { tracing::warn!(error = %e, "CI ack: LREM failed"); } }