| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | |
|
| 7 | |
|
| 8 | |
|
| 9 | | use crate::auth::roles::{self, Access}; |
| 10 | | use crate::db::{self, accounts, proposals as pdb}; |
| 11 | | use crate::repos::RepoName; |
| 12 | | use crate::Shared; |
| 13 | |
|
| 14 | |
|
| 15 | |
|
| 16 | | static TURN: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); |
| 17 | |
|
| 18 | |
|
| 19 | | const FAILED: [&str; 2] = ["failed", "cancelled"]; |
| 20 | |
|
| 21 | |
|
| 22 | |
|
| 23 | 4× | pub async fn set(app: &Shared, row: &db::repos::RepoRow, p: &pdb::ProposalRow, by: &accounts::Account, on: bool) -> sqlx::Result<()> { |
| 24 | 4× | if on { |
| 25 | 3× | pdb::set_auto_land(&app.db, p.id, &by.username).await?; |
| 26 | 3× | pdb::add_event(&app.db, p.id, &by.username, "set it to land when its checks pass", "").await.ok(); |
| 27 | 3× | accounts::audit(&app.db, Some(by), "proposal.auto_land", &format!("{}/{}#{}", row.owner, row.name, p.number), "on").await.ok(); |
| 28 | 3× | let p = pdb::get(&app.db, row.id, p.number).await?.unwrap_or_else(|| p.clone()); |
| 29 | 3× | evaluate(app, row, p).await; |
| 30 | 1× | } else if !p.auto_land_by.is_empty() { |
| 31 | 0× | pdb::set_auto_land(&app.db, p.id, "").await?; |
| 32 | 0× | pdb::add_event(&app.db, p.id, &by.username, "turned off landing when its checks pass", "").await.ok(); |
| 33 | 0× | accounts::audit(&app.db, Some(by), "proposal.auto_land", &format!("{}/{}#{}", row.owner, row.name, p.number), "off").await.ok(); |
| 34 | 1× | } |
| 35 | 4× | Ok(()) |
| 36 | 4× | } |
| 37 | |
|
| 38 | |
|
| 39 | |
|
| 40 | 716× | pub async fn on_check(app: &Shared, row: &db::repos::RepoRow, state: &str) { |
| 41 | 716× | let waiting = match pdb::auto_landing(&app.db, row.id, state).await { |
| 42 | 716× | Ok(w) => w, |
| 43 | 0× | Err(e) => { |
| 44 | 0× | tracing::warn!("auto-land: {e}"); |
| 45 | 0× | return; |
| 46 | | } |
| 47 | | }; |
| 48 | 716× | for p in waiting { |
| 49 | 3× | evaluate(app, row, p).await; |
| 50 | | } |
| 51 | 716× | } |
| 52 | |
|
| 53 | |
|
| 54 | |
|
| 55 | 716× | pub fn on_check_task( |
| 56 | 716× | app: Shared, |
| 57 | 716× | row: db::repos::RepoRow, |
| 58 | 716× | state: String, |
| 59 | 716× | ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> { |
| 60 | 716× | Box::pin(async move { on_check(&app, &row, &state).await }) |
| 61 | 716× | } |
| 62 | |
|
| 63 | 2× | async fn off(app: &Shared, p: &pdb::ProposalRow, why: &str) { |
| 64 | 2× | pdb::set_auto_land(&app.db, p.id, "").await.ok(); |
| 65 | 2× | pdb::add_event(&app.db, p.id, "oxygenhub", "turned off landing when its checks pass", why).await.ok(); |
| 66 | 2× | } |
| 67 | |
|
| 68 | |
|
| 69 | |
|
| 70 | |
|
| 71 | 6× | async fn evaluate(app: &Shared, row: &db::repos::RepoRow, p: pdb::ProposalRow) { |
| 72 | 6× | let _turn = TURN.lock().await; |
| 73 | | |
| 74 | | |
| 75 | 6× | let Ok(_proposal) = crate::leader::exclusive(&app.db, &format!("autoland:{}", p.id)).await else { return }; |
| 76 | | |
| 77 | 6× | let Ok(Some(p)) = pdb::get(&app.db, row.id, p.number).await else { return }; |
| 78 | 6× | if !p.is_open() || p.auto_land_by.is_empty() { |
| 79 | 0× | return; |
| 80 | 6× | } |
| 81 | 6× | let setter = match accounts::account_by_name(&app.db, &p.auto_land_by).await { |
| 82 | 6× | Ok(Some(a)) if !a.blocked() => a, |
| 83 | 0× | _ => return off(app, &p, &format!("{} can no longer land it", p.auto_land_by)).await, |
| 84 | | }; |
| 85 | 6× | let role = roles::role_for(&app.db, Some(&setter), row).await.ok().flatten(); |
| 86 | 6× | if !roles::allows(role, Access::Write) { |
| 87 | 0× | return off(app, &p, &format!("{} can no longer land it", setter.username)).await; |
| 88 | 6× | } |
| 89 | 6× | let Some(repo) = RepoName::new(&row.owner, &row.name) else { return }; |
| 90 | 6× | let number = p.number; |
| 91 | 6× | let Ok(loaded) = crate::web::proposals::load_summary(app, &repo, p.clone()).await else { return }; |
| 92 | 6× | let required = db::hooks::required_checks(&app.db, row.id).await.unwrap_or_default(); |
| 93 | 6× | let checks = db::hooks::checks_for_state(&app.db, row.id, &loaded.state).await.unwrap_or_default(); |
| 94 | 6× | let failed: Vec<&str> = required |
| 95 | 6× | .iter() |
| 96 | 6× | .filter(|n| checks.iter().any(|c| &c.name == *n && FAILED.contains(&c.status.as_str()))) |
| 97 | 6× | .map(|n| n.as_str()) |
| 98 | 6× | .collect(); |
| 99 | 6× | if !failed.is_empty() { |
| 100 | 2× | let why = format!("{} did not pass", failed.join(", ")); |
| 101 | 2× | return off(app, &p, &why).await; |
| 102 | 4× | } |
| 103 | 4× | let Ok(v) = crate::web::proposals::verdict(app, &loaded, row.id).await else { return }; |
| 104 | 4× | if !v.blockers.is_empty() { |
| 105 | 3× | return; |
| 106 | 1× | } |
| 107 | 1× | let via = format!("when its checks passed (set by {})", setter.username); |
| 108 | 1× | match crate::web::proposals::land_as(app, &repo, row, &setter, number, &via, false, None).await { |
| 109 | | Ok(_) => { |
| 110 | 1× | pdb::set_auto_land(&app.db, p.id, "").await.ok(); |
| 111 | | } |
| 112 | 0× | Err(crate::web::proposals::Landing::Refused(why)) => off(app, &p, &why).await, |
| 113 | 0× | Err(crate::web::proposals::Landing::NotFound) => {} |
| 114 | | } |
| 115 | 6× | } |