gitdataai/lib/git/sync/cicheck.rs
2026-05-30 01:38:40 +08:00

158 lines
4.1 KiB
Rust

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<CiCheckOutcome, GitError> {
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<String> {
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::<Option<String>>(&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");
}
}