Extract mirror push from ci into its own module
Mirror push and CI share only the trigger event; make them
sibling modules and let server.rs orchestrate.
Assisted-by: Claude Opus 4.7 (1M context) via Claude Code
diff --git a/src/ci.rs b/src/ci.rs
index 93618a0..463410b 100644
--- a/src/ci.rs
+++ b/src/ci.rs
@@ -462,8 +462,9 @@ 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) {
+/// Trigger CI for a push event: scan each updated ref for `.quire/ci.fnl`,
+/// create a run, and evaluate + validate it.
+pub fn trigger(quire: &crate::Quire, event: &PushEvent) {
let repo = match quire.repo(&event.repo) {
Ok(r) if r.exists() => r,
Ok(_) => {
@@ -476,19 +477,13 @@ pub async fn dispatch_push(quire: &crate::Quire, event: &PushEvent) {
}
};
- 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: &Repo, event: &PushEvent) {
for push_ref in event.updated_refs() {
- if let Err(e) = dispatch_ci_ref(repo, &event.pushed_at, push_ref) {
+ if let Err(e) = trigger_ref(&repo, &event.pushed_at, push_ref) {
tracing::error!(
repo = %event.repo,
sha = %push_ref.new_sha,
%e,
- "CI dispatch failed"
+ "CI trigger failed"
);
}
}
@@ -497,8 +492,8 @@ fn dispatch_ci(repo: &Repo, event: &PushEvent) {
/// 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: &Repo, pushed_at: &str, push_ref: &PushRef) -> Result<()> {
+/// or failed), or `Err` if the trigger itself failed.
+fn trigger_ref(repo: &Repo, pushed_at: &str, push_ref: &PushRef) -> Result<()> {
if !repo.has_ci_fnl(&push_ref.new_sha) {
return Ok(());
}
@@ -549,64 +544,6 @@ fn eval_and_validate(repo: &Repo, sha: &str) -> Result<()> {
Ok(())
}
-/// Push updated refs to the configured mirror.
-async fn dispatch_mirror(quire: &crate::Quire, repo: 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"),
- }
-}
-
/// Write a serializable value to a YAML file atomically (temp file + rename).
fn write_yaml<T: serde::Serialize>(path: &Path, value: &T) -> Result<()> {
let tmp_path = path.with_extension("yml.tmp");
diff --git a/src/lib.rs b/src/lib.rs
index d96db51..2c5bb55 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,6 +2,7 @@ pub mod ci;
mod error;
pub mod event;
pub mod fennel;
+pub mod mirror;
pub mod quire;
pub mod secret;
pub mod server;
diff --git a/src/mirror.rs b/src/mirror.rs
new file mode 100644
index 0000000..f52b7a2
--- /dev/null
+++ b/src/mirror.rs
@@ -0,0 +1,77 @@
+//! Mirror push: replicate ref updates to a configured remote.
+
+use crate::Quire;
+use crate::event::PushEvent;
+
+/// Push updated refs to the configured mirror, if one is set.
+///
+/// Loads repo and global config, resolves the GitHub token, and runs
+/// the libgit2 push on a blocking task. Errors are logged; the function
+/// itself is infallible from the caller's perspective.
+pub async fn push(quire: &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;
+ }
+ };
+
+ 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;
+ }
+ };
+
+ 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"),
+ }
+}
diff --git a/src/server.rs b/src/server.rs
index 3bab935..d307f0a 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -8,6 +8,7 @@ use miette::{Context, IntoDiagnostic, Result};
use crate::Quire;
use crate::ci;
use crate::event::PushEvent;
+use crate::mirror;
async fn health() -> &'static str {
"ok"
@@ -110,5 +111,6 @@ async fn handle_event_connection(mut stream: tokio::net::UnixStream, quire: Quir
return;
}
- ci::dispatch_push(&quire, &event).await;
+ ci::trigger(&quire, &event);
+ mirror::push(&quire, &event).await;
}