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