diff options
-rw-r--r-- | ops/gerrit-autosubmit/.gitignore | 1 | ||||
-rw-r--r-- | ops/gerrit-autosubmit/Cargo.lock | 302 | ||||
-rw-r--r-- | ops/gerrit-autosubmit/Cargo.toml | 12 | ||||
-rw-r--r-- | ops/gerrit-autosubmit/default.nix | 7 | ||||
-rw-r--r-- | ops/gerrit-autosubmit/src/main.rs | 193 |
5 files changed, 515 insertions, 0 deletions
diff --git a/ops/gerrit-autosubmit/.gitignore b/ops/gerrit-autosubmit/.gitignore new file mode 100644 index 000000000000..2f7896d1d136 --- /dev/null +++ b/ops/gerrit-autosubmit/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/ops/gerrit-autosubmit/Cargo.lock b/ops/gerrit-autosubmit/Cargo.lock new file mode 100644 index 000000000000..7516c7403455 --- /dev/null +++ b/ops/gerrit-autosubmit/Cargo.lock @@ -0,0 +1,302 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "crimp" +version = "4087.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ead2c83f7d1f9b8e5a6f7a25985d0d1759ccd2cd72abb1eee2db65d05e12b39" +dependencies = [ + "curl", + "serde", + "serde_json", +] + +[[package]] +name = "curl" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2", + "winapi", +] + +[[package]] +name = "curl-sys" +version = "0.4.68+curl-8.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a0d18d88360e374b16b2273c832b5e57258ffc1d4aa4f96b108e0738d5752f" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "windows-sys", +] + +[[package]] +name = "gerrit-autosubmit" +version = "0.1.0" +dependencies = [ + "anyhow", + "crimp", + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "libz-sys" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/ops/gerrit-autosubmit/Cargo.toml b/ops/gerrit-autosubmit/Cargo.toml new file mode 100644 index 000000000000..fa51614a0869 --- /dev/null +++ b/ops/gerrit-autosubmit/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "gerrit-autosubmit" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.75" +crimp = "4087.0.0" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" diff --git a/ops/gerrit-autosubmit/default.nix b/ops/gerrit-autosubmit/default.nix new file mode 100644 index 000000000000..f69a9248e351 --- /dev/null +++ b/ops/gerrit-autosubmit/default.nix @@ -0,0 +1,7 @@ +{ depot, pkgs, ... }: + +depot.third_party.naersk.buildPackage { + src = ./.; + nativeBuildInputs = [ pkgs.pkg-config ]; + buildInputs = [ pkgs.openssl ]; +} diff --git a/ops/gerrit-autosubmit/src/main.rs b/ops/gerrit-autosubmit/src/main.rs new file mode 100644 index 000000000000..9d7087d67deb --- /dev/null +++ b/ops/gerrit-autosubmit/src/main.rs @@ -0,0 +1,193 @@ +//! 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}; + +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: &'static 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<()> { + let response = 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); + + Ok(true) + } else { + println!("nothing ready for autosubmit, waiting ..."); + Ok(false) + } +} + +fn main() -> Result<()> { + let cfg = gerrit::Config::from_env()?; + + loop { + if !autosubmit(&cfg)? { + std::thread::sleep_ms(30000); + } + } +} |