about summary refs log tree commit diff
path: root/ops/gerrit-autosubmit
diff options
context:
space:
mode:
Diffstat (limited to 'ops/gerrit-autosubmit')
-rw-r--r--ops/gerrit-autosubmit/.gitignore1
-rw-r--r--ops/gerrit-autosubmit/Cargo.lock302
-rw-r--r--ops/gerrit-autosubmit/Cargo.toml12
-rw-r--r--ops/gerrit-autosubmit/default.nix7
-rw-r--r--ops/gerrit-autosubmit/src/main.rs194
5 files changed, 516 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..85d8a6af61bb
--- /dev/null
+++ b/ops/gerrit-autosubmit/src/main.rs
@@ -0,0 +1,194 @@
+//! 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));
+        }
+    }
+}