tvl-depot/ops/gerrit-autosubmit/src/main.rs
Vincent Ambo 15309c7deb chore(ops/gerrit-autosubmit): clean up warnings & clippy lints
Change-Id: I0e914d093b8fb45fa85bb19fb68c4f84f5ba6336
Reviewed-on: https://cl.tvl.fyi/c/depot/+/10531
Tested-by: BuildkiteCI
Autosubmit: tazjin <tazjin@tvl.su>
Reviewed-by: flokli <flokli@flokli.de>
2024-01-03 20:28:56 +00:00

194 lines
5.7 KiB
Rust

//! gerrit-autosubmit connects to a Gerrit instance and submits the
//! longest chain of changes in which all ancestors are ready and
//! marked for autosubmit.
//!
//! It works like this:
//!
//! * it fetches all changes the Gerrit query API considers
//! submittable (i.e. all requirements fulfilled), and that have the
//! `Autosubmit` label set
//!
//! * it filters these changes down to those that are _actually_
//! submittable (in Gerrit API terms: that have an active Submit button)
//!
//! * it filters out those that would submit ancestors that are *not*
//! marked with the `Autosubmit` label
//!
//! * it submits the longest chain
//!
//! After that it just loops.
use anyhow::{Context, Result};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::{thread, time};
mod gerrit {
use anyhow::{anyhow, Context, Result};
use serde::Deserialize;
use serde_json::Value;
use std::collections::HashMap;
use std::env;
pub struct Config {
gerrit_url: String,
username: String,
password: String,
}
impl Config {
pub fn from_env() -> Result<Self> {
Ok(Config {
gerrit_url: env::var("GERRIT_URL")
.context("Gerrit base URL (no trailing slash) must be set in GERRIT_URL")?,
username: env::var("GERRIT_USERNAME")
.context("Gerrit username must be set in GERRIT_USERNAME")?,
password: env::var("GERRIT_PASSWORD")
.context("Gerrit password must be set in GERRIT_PASSWORD")?,
})
}
}
#[derive(Deserialize)]
pub struct ChangeInfo {
pub id: String,
pub revisions: HashMap<String, Value>,
}
#[derive(Deserialize)]
pub struct Action {
#[serde(default)]
pub enabled: bool,
}
const GERRIT_RESPONSE_PREFIX: &str = ")]}'";
pub fn get<T: serde::de::DeserializeOwned>(cfg: &Config, endpoint: &str) -> Result<T> {
let response = crimp::Request::get(&format!("{}/a{}", cfg.gerrit_url, endpoint))
.user_agent("gerrit-autosubmit")?
.basic_auth(&cfg.username, &cfg.password)?
.send()?
.error_for_status(|r| anyhow!("request failed with status {}", r.status))?;
let result: T = serde_json::from_slice(&response.body[GERRIT_RESPONSE_PREFIX.len()..])?;
Ok(result)
}
pub fn submit(cfg: &Config, change_id: &str) -> Result<()> {
crimp::Request::post(&format!(
"{}/a/changes/{}/submit",
cfg.gerrit_url, change_id
))
.user_agent("gerrit-autosubmit")?
.basic_auth(&cfg.username, &cfg.password)?
.send()?
.error_for_status(|r| anyhow!("submit failed with status {}", r.status))?;
Ok(())
}
}
#[derive(Debug)]
struct SubmittableChange {
id: String,
revision: String,
}
fn list_submittable(cfg: &gerrit::Config) -> Result<Vec<SubmittableChange>> {
let mut out = Vec::new();
let changes: Vec<gerrit::ChangeInfo> = gerrit::get(
&cfg,
"/changes/?q=is:submittable+label:Autosubmit+-is:wip+is:open&o=SKIP_DIFFSTAT&o=CURRENT_REVISION",
)
.context("failed to list submittable changes")?;
for change in changes.into_iter() {
out.push(SubmittableChange {
id: change.id,
revision: change
.revisions
.into_keys()
.next()
.context("change had no current revision")?,
});
}
Ok(out)
}
fn is_submittable(cfg: &gerrit::Config, change: &SubmittableChange) -> Result<bool> {
let response: HashMap<String, gerrit::Action> = gerrit::get(
cfg,
&format!(
"/changes/{}/revisions/{}/actions",
change.id, change.revision
),
)
.context("failed to fetch actions for change")?;
match response.get("submit") {
None => Ok(false),
Some(action) => Ok(action.enabled),
}
}
fn submitted_with(cfg: &gerrit::Config, change_id: &str) -> Result<HashSet<String>> {
let response: Vec<gerrit::ChangeInfo> =
gerrit::get(cfg, &format!("/changes/{}/submitted_together", change_id))
.context("failed to fetch related change list")?;
Ok(response.into_iter().map(|c| c.id).collect())
}
fn autosubmit(cfg: &gerrit::Config) -> Result<bool> {
let mut submittable_changes: HashSet<String> = Default::default();
for change in list_submittable(&cfg)? {
if !is_submittable(&cfg, &change)? {
continue;
}
submittable_changes.insert(change.id.clone());
}
let mut chains: BTreeMap<usize, String> = Default::default();
for change_id in &submittable_changes {
let ancestors = submitted_with(&cfg, &change_id)?;
if ancestors.is_subset(&submittable_changes) {
chains.insert(
if ancestors.is_empty() {
1
} else {
ancestors.len()
},
change_id.clone(),
);
}
}
// BTreeMap::last_key_value gives us the value associated with the
// largest key, i.e. with the longest submittable chain of changes.
if let Some((count, change_id)) = chains.last_key_value() {
println!(
"submitting change {} with chain length {}",
change_id, count
);
gerrit::submit(cfg, change_id).context("while submitting")?;
Ok(true)
} else {
println!("nothing ready for autosubmit, waiting ...");
Ok(false)
}
}
fn main() -> Result<()> {
let cfg = gerrit::Config::from_env()?;
loop {
if !autosubmit(&cfg)? {
thread::sleep(time::Duration::from_secs(30));
}
}
}