Move dispatch logic from event to ci module
event owned both the push event data types and the dispatch
reactions (CI gating, mirror push). The reactions are CI concerns,
so they belong in ci. event now only defines the event shapes.

Assisted-by: GLM-5.1 via pi
change zquxutwxuntxrtltokkxvtqllntvnnvy
commit 1c95dee5d097c5f203e7bf21a18414ae046b1e59
author Alpha Chen <alpha@kejadlen.dev>
date
parent yqunuuwq
diff --git a/src/ci.rs b/src/ci.rs
index cf81103..ac95be7 100644
--- a/src/ci.rs
+++ b/src/ci.rs
@@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
 use mlua::Lua;
 
 use crate::Result;
+use crate::event::PushEvent;
 
 /// The state of a CI run.
 #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -503,6 +504,155 @@ pub fn validate(jobs: &[JobDef]) -> std::result::Result<(), Vec<ValidationError>
     }
 }
 
+/// Dispatch a push event: CI gating and mirror push.
+pub async fn dispatch_push(quire: &crate::Quire, event: &PushEvent) {
+    let repo = match quire.repo(&event.repo) {
+        Ok(r) if r.exists() => r,
+        Ok(_) => {
+            tracing::error!(repo = %event.repo, "repo not found on disk");
+            return;
+        }
+        Err(e) => {
+            tracing::error!(repo = %event.repo, %e, "invalid repo name in event");
+            return;
+        }
+    };
+
+    dispatch_ci(&repo, event);
+    dispatch_mirror(quire, repo, event).await;
+}
+
+/// Check each updated ref for .quire/ci.fnl, create runs, and eval + validate.
+fn dispatch_ci(repo: &crate::quire::Repo, event: &PushEvent) {
+    for push_ref in event.updated_refs() {
+        if let Err(e) = dispatch_ci_ref(repo, &event.pushed_at, push_ref) {
+            tracing::error!(
+                repo = %event.repo,
+                sha = %push_ref.new_sha,
+                %e,
+                "CI dispatch failed"
+            );
+        }
+    }
+}
+
+/// Create and run CI for a single updated ref.
+///
+/// Returns `Ok(())` if CI ran (regardless of whether the run succeeded
+/// or failed), or `Err` if dispatch itself failed.
+fn dispatch_ci_ref(
+    repo: &crate::quire::Repo,
+    pushed_at: &str,
+    push_ref: &crate::event::PushRef,
+) -> crate::Result<()> {
+    if !repo.has_ci_fnl(&push_ref.new_sha) {
+        return Ok(());
+    }
+
+    let meta = RunMeta {
+        sha: push_ref.new_sha.clone(),
+        r#ref: push_ref.r#ref.clone(),
+        pushed_at: pushed_at.to_string(),
+    };
+
+    let mut run = repo.runs().create(&meta)?;
+
+    tracing::info!(
+        run_id = %run.id(),
+        sha = %push_ref.new_sha,
+        r#ref = %push_ref.r#ref,
+        "created CI run"
+    );
+
+    run.transition(RunState::Active)?;
+
+    let result = eval_and_validate(repo, &push_ref.new_sha);
+    match result {
+        Ok(()) => {
+            run.transition(RunState::Complete)?;
+        }
+        Err(e) => {
+            run.transition(RunState::Failed)?;
+            run.write_state(&RunStateFile {
+                status: RunState::Failed,
+                started_at: None,
+                finished_at: Some(jiff::Zoned::now().to_string()),
+            })?;
+            // Return the eval/validation error as the dispatch error.
+            Err(e)?;
+        }
+    }
+
+    Ok(())
+}
+
+/// Evaluate ci.fnl at a given SHA and validate the job graph.
+fn eval_and_validate(repo: &crate::quire::Repo, sha: &str) -> crate::Result<()> {
+    let source = repo.ci_fnl_source(sha)?;
+    let fennel = crate::fennel::Fennel::new()?;
+    let eval_result = eval_ci(&fennel, &source, &format!("{sha}:.quire/ci.fnl"))?;
+    validate(&eval_result.jobs)?;
+    Ok(())
+}
+
+/// Push updated refs to the configured mirror.
+async fn dispatch_mirror(quire: &crate::Quire, repo: crate::quire::Repo, event: &PushEvent) {
+    let config = match repo.config() {
+        Ok(c) => c,
+        Err(e) => {
+            tracing::error!(repo = %event.repo, %e, "failed to load repo config");
+            return;
+        }
+    };
+
+    let Some(mirror) = config.mirror else {
+        tracing::debug!(repo = %event.repo, "no mirror configured, skipping");
+        return;
+    };
+
+    let global_config = match quire.global_config() {
+        Ok(c) => c,
+        Err(e) => {
+            tracing::error!(%e, "failed to load global config for mirror push");
+            return;
+        }
+    };
+
+    let token = match global_config.github.token.reveal() {
+        Ok(t) => t.to_string(),
+        Err(e) => {
+            tracing::error!(%e, "failed to resolve GitHub token");
+            return;
+        }
+    };
+
+    // Only push refs that were actually updated (non-zero new sha).
+    let refs: Vec<String> = event
+        .updated_refs()
+        .iter()
+        .map(|r| r.r#ref.clone())
+        .collect();
+
+    if refs.is_empty() {
+        return;
+    }
+
+    let mirror_url = mirror.url.clone();
+    tracing::info!(url = %mirror.url, refs = ?refs, "pushing to mirror");
+
+    let result = tokio::task::spawn_blocking(move || {
+        let ref_slices: Vec<&str> = refs.iter().map(|s| s.as_str()).collect();
+        repo.push_to_mirror(&mirror, &token, &ref_slices)
+    })
+    .await;
+
+    match result {
+        Ok(Ok(())) => tracing::info!(url = %mirror_url, "mirror push complete"),
+        Ok(Err(e)) => tracing::error!(url = %mirror_url, %e, "mirror push failed"),
+        Err(e) => tracing::error!(url = %mirror_url, %e, "mirror push task panicked"),
+    }
+}
+
 fn write_yaml<T: serde::Serialize>(path: &Path, value: &T) -> Result<()> {
     let tmp_path = path.with_extension("yml.tmp");
     let f = fs_err::File::create(&tmp_path)?;
diff --git a/src/event.rs b/src/event.rs
index ddd7db6..91b82e5 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -1,5 +1,3 @@
-use crate::ci::{RunMeta, RunState, RunStateFile};
-
 /// A single ref update from a push.
 #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
 pub struct PushRef {
@@ -17,155 +15,6 @@ pub struct PushEvent {
     pub refs: Vec<PushRef>,
 }
 
-/// Dispatch a push event: CI gating and mirror push.
-pub async fn dispatch_push(quire: &crate::Quire, event: &PushEvent) {
-    let repo = match quire.repo(&event.repo) {
-        Ok(r) if r.exists() => r,
-        Ok(_) => {
-            tracing::error!(repo = %event.repo, "repo not found on disk");
-            return;
-        }
-        Err(e) => {
-            tracing::error!(repo = %event.repo, %e, "invalid repo name in event");
-            return;
-        }
-    };
-
-    dispatch_ci(&repo, event);
-    dispatch_mirror(quire, repo, event).await;
-}
-
-/// Check each updated ref for .quire/ci.fnl, create runs, and eval + validate.
-fn dispatch_ci(repo: &crate::quire::Repo, event: &PushEvent) {
-    for push_ref in event.updated_refs() {
-        if let Err(e) = dispatch_ci_ref(repo, &event.pushed_at, push_ref) {
-            tracing::error!(
-                repo = %event.repo,
-                sha = %push_ref.new_sha,
-                %e,
-                "CI dispatch failed"
-            );
-        }
-    }
-}
-
-/// Create and run CI for a single updated ref.
-///
-/// Returns `Ok(())` if CI ran (regardless of whether the run succeeded
-/// or failed), or `Err` if dispatch itself failed.
-fn dispatch_ci_ref(
-    repo: &crate::quire::Repo,
-    pushed_at: &str,
-    push_ref: &PushRef,
-) -> crate::Result<()> {
-    if !repo.has_ci_fnl(&push_ref.new_sha) {
-        return Ok(());
-    }
-
-    let meta = RunMeta {
-        sha: push_ref.new_sha.clone(),
-        r#ref: push_ref.r#ref.clone(),
-        pushed_at: pushed_at.to_string(),
-    };
-
-    let mut run = repo.runs().create(&meta)?;
-
-    tracing::info!(
-        run_id = %run.id(),
-        sha = %push_ref.new_sha,
-        r#ref = %push_ref.r#ref,
-        "created CI run"
-    );
-
-    run.transition(RunState::Active)?;
-
-    let result = eval_and_validate(repo, &push_ref.new_sha);
-    match result {
-        Ok(()) => {
-            run.transition(RunState::Complete)?;
-        }
-        Err(e) => {
-            run.transition(RunState::Failed)?;
-            run.write_state(&RunStateFile {
-                status: RunState::Failed,
-                started_at: None,
-                finished_at: Some(jiff::Zoned::now().to_string()),
-            })?;
-            // Return the eval/validation error as the dispatch error.
-            Err(e)?;
-        }
-    }
-
-    Ok(())
-}
-
-/// Evaluate ci.fnl at a given SHA and validate the job graph.
-fn eval_and_validate(repo: &crate::quire::Repo, sha: &str) -> crate::Result<()> {
-    let source = repo.ci_fnl_source(sha)?;
-    let fennel = crate::fennel::Fennel::new()?;
-    let eval_result = crate::ci::eval_ci(&fennel, &source, &format!("{sha}:.quire/ci.fnl"))?;
-    crate::ci::validate(&eval_result.jobs)?;
-    Ok(())
-}
-
-/// Push updated refs to the configured mirror.
-async fn dispatch_mirror(quire: &crate::Quire, repo: crate::quire::Repo, event: &PushEvent) {
-    let config = match repo.config() {
-        Ok(c) => c,
-        Err(e) => {
-            tracing::error!(repo = %event.repo, %e, "failed to load repo config");
-            return;
-        }
-    };
-
-    let Some(mirror) = config.mirror else {
-        tracing::debug!(repo = %event.repo, "no mirror configured, skipping");
-        return;
-    };
-
-    let global_config = match quire.global_config() {
-        Ok(c) => c,
-        Err(e) => {
-            tracing::error!(%e, "failed to load global config for mirror push");
-            return;
-        }
-    };
-
-    let token = match global_config.github.token.reveal() {
-        Ok(t) => t.to_string(),
-        Err(e) => {
-            tracing::error!(%e, "failed to resolve GitHub token");
-            return;
-        }
-    };
-
-    // Only push refs that were actually updated (non-zero new sha).
-    let refs: Vec<String> = event
-        .updated_refs()
-        .iter()
-        .map(|r| r.r#ref.clone())
-        .collect();
-
-    if refs.is_empty() {
-        return;
-    }
-
-    let mirror_url = mirror.url.clone();
-    tracing::info!(url = %mirror.url, refs = ?refs, "pushing to mirror");
-
-    let result = tokio::task::spawn_blocking(move || {
-        let ref_slices: Vec<&str> = refs.iter().map(|s| s.as_str()).collect();
-        repo.push_to_mirror(&mirror, &token, &ref_slices)
-    })
-    .await;
-
-    match result {
-        Ok(Ok(())) => tracing::info!(url = %mirror_url, "mirror push complete"),
-        Ok(Err(e)) => tracing::error!(url = %mirror_url, %e, "mirror push failed"),
-        Err(e) => tracing::error!(url = %mirror_url, %e, "mirror push task panicked"),
-    }
-}
-
 impl PushEvent {
     /// Build a push event from the repo name and updated refs.
     ///
diff --git a/src/server.rs b/src/server.rs
index 3c716b3..a2a0487 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -108,5 +108,5 @@ async fn handle_event_connection(mut stream: tokio::net::UnixStream, quire: Quir
         return;
     }
 
-    crate::event::dispatch_push(&quire, &event).await;
+    crate::ci::dispatch_push(&quire, &event).await;
 }