anvilsign in

collin/anvil

1//! Post-push CI trigger: detect which branches changed on a push and enqueue a
2//! CI run for any whose new commit defines a pipeline (`.anvil/ci.toml`).
3//!
4//! Used by the receive-pack paths in `anvil-web` and `anvil-ssh`: snapshot the
5//! branch tips *before* the push, then call [`enqueue_ci_for_push`] after it
6//! succeeds.
7
8use std::{
9 collections::HashMap,
10 path::Path,
11};
12
13/// Local branch short-name → tip commit id (hex).
14pub type BranchTips = HashMap<String, String>;
15
16/// Snapshot current local branch tips. Best-effort — returns empty on error.
17pub fn snapshot_branches(repo_path: &Path) -> BranchTips {
18 let mut tips = HashMap::new();
19 let Ok(repo) = gix::open(repo_path) else {
20 return tips;
21 };
22 let Ok(platform) = repo.references() else {
23 return tips;
24 };
25 let Ok(iter) = platform.local_branches() else {
26 return tips;
27 };
28 for mut branch in iter.flatten() {
29 let name = branch.name().shorten().to_string();
30 if let Ok(id) = branch.peel_to_id() {
31 tips.insert(name, id.detach().to_string());
32 }
33 }
34 tips
35}
36
37/// After a push, enqueue a CI run for each branch whose tip changed (or is new)
38/// and whose new commit contains a pipeline definition. Returns the enqueued
39/// run ids (so the caller can notify the runner).
40pub async fn enqueue_ci_for_push(
41 db: &toasty::Db,
42 repo_id: i64,
43 repo_path: &Path,
44 before: &BranchTips,
45) -> Vec<i64> {
46 let after = snapshot_branches(repo_path);
47 let mut enqueued = Vec::new();
48 for (branch, new_tip) in &after {
49 if before.get(branch).map(String::as_str) == Some(new_tip.as_str()) {
50 continue; // unchanged branch
51 }
52 let defines = |path| {
53 matches!(
54 crate::browse::read_blob(repo_path, new_tip, path),
55 Ok(Some(_))
56 )
57 };
58 // The legacy path counts as "defines a pipeline" on purpose: it gets a
59 // run that fails telling you to rename the file (see `load_pipeline`
60 // in anvil-ci), which a repository whose CI just stopped needs to see.
61 if !defines(anvil_core::ci::PIPELINE_PATH) && !defines(anvil_core::ci::LEGACY_PIPELINE_PATH)
62 {
63 continue;
64 }
65 match anvil_core::ci::enqueue(db, repo_id, new_tip, branch).await {
66 Ok(run) => {
67 tracing::info!("enqueued CI run {} for {branch}@{new_tip}", run.id);
68 enqueued.push(run.id);
69 }
70 Err(e) => tracing::error!("failed to enqueue CI run: {e}"),
71 }
72 }
73 enqueued
74}