diff --git a/.gitignore b/.gitignore index 54e7505..8c42e23 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /targets /results /db.sqlite* +/.envrc diff --git a/Cargo.lock b/Cargo.lock index b99fb9a..d8fd4ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,21 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "askama" version = "0.14.0" @@ -74,6 +89,17 @@ dependencies = [ "winnow", ] +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atoi" version = "2.0.0" @@ -253,6 +279,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "color-eyre" version = "0.6.5" @@ -295,6 +335,22 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -395,6 +451,8 @@ dependencies = [ "axum", "color-eyre", "futures", + "jsonwebtoken", + "octocrab", "reqwest", "serde", "sqlx", @@ -797,7 +855,9 @@ dependencies = [ "http", "hyper", "hyper-util", + "log", "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -805,6 +865,19 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.17" @@ -829,6 +902,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -979,6 +1076,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1121,6 +1233,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.5" @@ -1182,12 +1304,58 @@ dependencies = [ "memchr", ] +[[package]] +name = "octocrab" +version = "0.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f50b2657b7e31c849c612c4ca71527861631fe3c392f931fb28990b045f972" +dependencies = [ + "arc-swap", + "async-trait", + "base64", + "bytes", + "cfg-if", + "chrono", + "either", + "futures", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jsonwebtoken", + "once_cell", + "percent-encoding", + "pin-project", + "secrecy", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "snafu", + "tokio", + "tower", + "tower-http", + "tracing", + "url", + "web-time", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "owo-colors" version = "4.2.3" @@ -1223,6 +1391,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1238,6 +1416,26 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1568,6 +1766,7 @@ version = "0.23.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -1576,6 +1775,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.13.0" @@ -1609,12 +1820,53 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.228" @@ -1737,6 +1989,18 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.11" @@ -1752,6 +2016,27 @@ dependencies = [ "serde", ] +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "socket2" version = "0.6.1" @@ -2170,6 +2455,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.5.2" @@ -2181,6 +2479,7 @@ dependencies = [ "pin-project-lite", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -2202,6 +2501,7 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -2474,6 +2774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", + "serde", "wasm-bindgen", ] @@ -2496,12 +2797,65 @@ dependencies = [ "wasite", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index da744c8..4227ae4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,8 @@ askama = "0.14.0" axum = { version = "0.8.6", features = ["macros"] } color-eyre = "0.6.3" futures = "0.3.30" +jsonwebtoken = { version = "9.3.1", features = [] } +octocrab = "0.47.1" reqwest = { version = "0.12.7", features = [ "rustls-tls", ], default-features = false } diff --git a/README.md b/README.md index 9752465..72db0bc 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,21 @@ It does this in parallel, using half of the available threads (or `DOES_IT_BUILD - `DB_PATH`: Path to SQlite DB to store the results - `DOES_IT_BUILD_PARALLEL_JOBS`: Parallel build jobs, defaults to cores/2. +- `GITHUB_SEND_PINGS`: If this is set, actual pings will be sent for notification issues +- `GITHUB_OWNER`: The owner of the notification repo +- `GITHUB_REPO`: The repo name of the notification repo +- `GITHUB_APP_ID`: The app ID of the notification GitHub app +- `GITHUB_APP_PRIVATE_KEY`: The RSA private key for the notification GitHub app Build configuration: `DOES_IT_BUILD_OVERRIDE_VERSION` to override the git commit. ## Deployment deployed at + +## Notification + +does-it-build supports sending target maintainer notifications on breakage. + +It does this by creating an issue that pings the registered maintainers. +There is an array in the source code (linked to on the website target page) where people can add or remove themselves. diff --git a/migrations/20251110170025_notifications.sql b/migrations/20251110170025_notifications.sql new file mode 100644 index 0000000..ac3e4ae --- /dev/null +++ b/migrations/20251110170025_notifications.sql @@ -0,0 +1,10 @@ +-- Add migration script here + +CREATE TABLE notification_issues( + "issue_number" INTEGER PRIMARY KEY, + "status" TEXT NOT NULL, -- open/closed + "first_failed_nightly" TEXT NOT NULL, + "target" TEXT NOT NULL +) STRICT; + +CREATE INDEX notification_issues_target on notification_issues("target", "status"); diff --git a/src/build.rs b/src/build.rs index 6bded12..48adaf1 100644 --- a/src/build.rs +++ b/src/build.rs @@ -17,6 +17,7 @@ use tracing::{debug, error, info}; use crate::{ db::{BuildMode, Db, FullBuildInfo, Status}, nightlies::Nightlies, + notification::GitHubClient, }; struct CustomBuildFlags { @@ -52,7 +53,7 @@ impl Display for Toolchain { } } -pub async fn background_builder(db: Db) -> Result<()> { +pub async fn background_builder(db: Db, github_client: GitHubClient) -> Result<()> { if concurrent_jobs() == 0 { info!("Suspending background thread since DOES_IT_BUILD_PARALLEL_JOBS=0"); loop { @@ -61,7 +62,7 @@ pub async fn background_builder(db: Db) -> Result<()> { } loop { - if let Err(err) = background_builder_inner(&db).await { + if let Err(err) = background_builder_inner(&db, &github_client).await { error!( ?err, "error in background builder, waiting for an hour before retrying: {err}" @@ -71,7 +72,7 @@ pub async fn background_builder(db: Db) -> Result<()> { } } -async fn background_builder_inner(db: &Db) -> Result<()> { +async fn background_builder_inner(db: &Db, github_client: &GitHubClient) -> Result<()> { let nightlies = Nightlies::fetch().await.wrap_err("fetching nightlies")?; let already_finished = db .finished_nightlies() @@ -82,7 +83,7 @@ async fn background_builder_inner(db: &Db) -> Result<()> { match next { Some((nightly, mode)) => { info!(%nightly, %mode, "Building next nightly"); - let result = build_every_target_for_toolchain(db, &nightly, mode) + let result = build_every_target_for_toolchain(db, &nightly, mode, &github_client) .await .wrap_err_with(|| format!("building targets for toolchain {nightly}")); if let Err(err) = result { @@ -178,6 +179,7 @@ pub async fn build_every_target_for_toolchain( db: &Db, nightly: &str, mode: BuildMode, + github_client: &GitHubClient, ) -> Result<()> { if db.is_nightly_finished(nightly, mode).await? { debug!("Nightly is already finished, not trying again"); @@ -194,7 +196,7 @@ pub async fn build_every_target_for_toolchain( let results = futures::stream::iter( targets .iter() - .map(|target| build_single_target(db, nightly, target, mode)), + .map(|target| build_single_target(db, nightly, target, mode, github_client)), ) .buffer_unordered(concurrent_jobs()) .collect::>>() @@ -204,7 +206,7 @@ pub async fn build_every_target_for_toolchain( } for target in targets { - build_single_target(db, nightly, &target, mode) + build_single_target(db, nightly, &target, mode, github_client) .await .wrap_err_with(|| format!("building target {target} for toolchain {toolchain}"))?; } @@ -217,8 +219,14 @@ pub async fn build_every_target_for_toolchain( Ok(()) } -#[tracing::instrument(skip(db))] -async fn build_single_target(db: &Db, nightly: &str, target: &str, mode: BuildMode) -> Result<()> { +#[tracing::instrument(skip(db, github_client))] +async fn build_single_target( + db: &Db, + nightly: &str, + target: &str, + mode: BuildMode, + github_client: &GitHubClient, +) -> Result<()> { let existing = db .build_status_full(nightly, target, mode) .await @@ -238,7 +246,7 @@ async fn build_single_target(db: &Db, nightly: &str, target: &str, mode: BuildMo .await .wrap_err("running build")?; - db.insert(FullBuildInfo { + let full_build_info = FullBuildInfo { nightly: nightly.into(), target: target.into(), status: result.status, @@ -255,8 +263,14 @@ async fn build_single_target(db: &Db, nightly: &str, target: &str, mode: BuildMo ), does_it_build_version: Some(crate::VERSION_SHORT.into()), build_duration_ms: Some(start_time.elapsed().as_millis().try_into().unwrap()), - }) - .await?; + }; + + let result = crate::notification::notify_build(github_client, db, &full_build_info).await; + if let Err(err) = result { + error!(?err, "Failed to send build notification"); + } + + db.insert(full_build_info).await?; Ok(()) } diff --git a/src/db.rs b/src/db.rs index 7083dd0..b25518a 100644 --- a/src/db.rs +++ b/src/db.rs @@ -92,6 +92,22 @@ impl BuildStats { } } +#[derive(Debug, PartialEq, Clone, Copy, sqlx::Type, Serialize, Deserialize)] +#[sqlx(rename_all = "snake_case")] +#[serde(rename_all = "lowercase")] +pub enum NotificationStatus { + Open, + Closed, +} + +#[derive(Debug, sqlx::FromRow)] +pub struct NotificationIssue { + pub issue_number: i64, + pub status: NotificationStatus, + pub first_failed_nightly: String, + pub target: String, +} + impl Db { pub async fn open(path: &str) -> Result { let db_opts = SqliteConnectOptions::from_str(path) @@ -281,4 +297,43 @@ impl Db { .wrap_err("inserting finished broken nightly")?; Ok(()) } + + pub async fn find_existing_notification( + &self, + target: &str, + ) -> Result> { + sqlx::query_as::<_, NotificationIssue>( + "SELECT * FROM notification_issues WHERE status = 'open' AND target = ?", + ) + .bind(target) + .fetch_optional(&self.conn) + .await + .wrap_err("finding existing notification") + } + + pub async fn insert_notification(&self, notification: NotificationIssue) -> Result<()> { + sqlx::query( + "INSERT INTO notification_issues\ + (issue_number, status, first_failed_nightly, target)\ + VALUES (?, ?, ?, ?)", + ) + .bind(notification.issue_number) + .bind(notification.status) + .bind(notification.first_failed_nightly) + .bind(notification.target) + .execute(&self.conn) + .await + .wrap_err("inserting new notification")?; + Ok(()) + } + + pub async fn finish_notification(&self, issue_number: i64) -> Result<()> { + sqlx::query("UPDATE notification_issues SET status = ? WHERE issue_number = ?") + .bind(NotificationStatus::Closed) + .bind(issue_number) + .execute(&self.conn) + .await + .wrap_err("marking notification as closed")?; + Ok(()) + } } diff --git a/src/github.rs b/src/github.rs new file mode 100644 index 0000000..954915b --- /dev/null +++ b/src/github.rs @@ -0,0 +1,39 @@ +use color_eyre::{eyre::Context, Result}; +use octocrab::issues; + +pub struct GitHubClient { + pub send_pings: bool, + owner: String, + repo: String, + pub client: octocrab::Octocrab, +} + +impl GitHubClient { + pub async fn new( + send_pings: bool, + client: octocrab::Octocrab, + owner: String, + repo: String, + ) -> Result { + let installation = client + .apps() + .get_repository_installation(&owner, &repo) + .await + .wrap_err_with(|| format!("getting installation for {owner}/{repo}"))?; + + let client = client + .installation(installation.id) + .wrap_err("getting client for installation")?; + + Ok(Self { + send_pings, + owner, + repo, + client, + }) + } + + pub fn issues(&self) -> issues::IssueHandler<'_> { + self.client.issues(&self.owner, &self.repo) + } +} diff --git a/src/main.rs b/src/main.rs index 0619c77..3a512a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod build; mod db; mod nightlies; +mod notification; mod web; use color_eyre::{eyre::WrapErr, Result}; @@ -12,6 +13,10 @@ const VERSION_SHORT: &str = env!("GIT_COMMIT_SHORT"); #[tokio::main] async fn main() -> Result<()> { + main_inner().await +} + +async fn main_inner() -> Result<()> { tracing_subscriber::fmt() .with_env_filter(EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("info"))) .init(); @@ -22,7 +27,33 @@ async fn main() -> Result<()> { .await .wrap_err("running migrations")?; - let builder = build::background_builder(db.clone()); + let send_pings = std::env::var("GITHUB_SEND_PINGS") + .map(|_| true) + .unwrap_or(false); + let github_owner = std::env::var("GITHUB_OWNER").wrap_err("missing GITHUB_OWNER env var")?; + let github_repo = std::env::var("GITHUB_REPO").wrap_err("missing GITHUB_REPO env var")?; + let app_id = std::env::var("GITHUB_APP_ID") + .wrap_err("missing GITHUB_APP_ID env var")? + .parse::() + .wrap_err("invalid GITHUB_APP_ID")?; + let key = std::env::var("GITHUB_APP_PRIVATE_KEY") + .wrap_err("missing GITHUB_APP_PRIVATE_KEY env var")?; + let key = jsonwebtoken::EncodingKey::from_rsa_pem(key.as_bytes()).unwrap(); + + let github_client = octocrab::Octocrab::builder() + .app(app_id.into(), key) + .build() + .wrap_err("failed to create client")?; + + let github_client = notification::GitHubClient::new( + send_pings, + github_client, + github_owner.clone(), + github_repo.clone(), + ) + .await?; + + let builder = build::background_builder(db.clone(), github_client); let server = web::webserver(db); tokio::select! { diff --git a/src/notification.rs b/src/notification.rs new file mode 100644 index 0000000..4d8c87f --- /dev/null +++ b/src/notification.rs @@ -0,0 +1,232 @@ +use color_eyre::eyre::{Context, Result}; +use octocrab::models::issues::IssueStateReason; +use octocrab::models::IssueState; +use tracing::info; + +use crate::db::{Db, FullBuildInfo, NotificationIssue, NotificationStatus, Status}; + +pub const TABLE_FILE: &str = file!(); +pub const TABLE_LINE: u32 = line!() + 1; +const TARGET_NOTIFICATIONS: &[(&str, &[&str])] = &[("armv7-sony-vita-newlibeabihf", &["pheki"])]; + +pub fn notification_pr_url() -> String { + format!("https://github.com/Noratrieb/does-it-build/blob/main/{TABLE_FILE}#L{TABLE_LINE}") +} + +pub fn maintainers_for_target(target: &str) -> Option<&'static [&'static str]> { + TARGET_NOTIFICATIONS + .iter() + .find(|(target_name, _)| *target_name == target) + .map(|(_, maintainers)| *maintainers) +} + +pub struct GitHubClient { + pub send_pings: bool, + owner: String, + repo: String, + pub client: octocrab::Octocrab, +} + +impl GitHubClient { + pub async fn new( + send_pings: bool, + client: octocrab::Octocrab, + owner: String, + repo: String, + ) -> Result { + let installation = client + .apps() + .get_repository_installation(&owner, &repo) + .await + .wrap_err_with(|| format!("getting installation for {owner}/{repo}"))?; + + let client = client + .installation(installation.id) + .wrap_err("getting client for installation")?; + + Ok(Self { + send_pings, + owner, + repo, + client, + }) + } + + pub fn issues(&self) -> octocrab::issues::IssueHandler<'_> { + self.client.issues(&self.owner, &self.repo) + } +} + +pub async fn notify_build( + github_client: &GitHubClient, + db: &Db, + build_info: &FullBuildInfo, +) -> Result<()> { + match build_info.status { + Status::Error => notify_build_failure(github_client, db, build_info).await, + Status::Pass => notify_build_pass(github_client, db, build_info).await, + } +} + +pub async fn notify_build_failure( + github_client: &GitHubClient, + db: &Db, + build_info: &FullBuildInfo, +) -> Result<()> { + let FullBuildInfo { + target, + nightly, + stderr, + .. + } = build_info; + + let Some(notify_usernames) = maintainers_for_target(target) else { + return Ok(()); + }; + + info!("Creating issue for target {target}, notifying {notify_usernames:?}"); + + let issue = db.find_existing_notification(target).await?; + + let url = format!( + "https://does-it-build.noratrieb.dev/build?nightly={nightly}&target={target}&mode=std" + ); + + if let Some(issue) = issue { + // An existing issue, send a comment. + + github_client + .issues() + .create_comment( + issue.issue_number as u64, + format!( + "💥 The target {target} still fails to build on the nightly {nightly}! + +<{url}> + +
full logs + +``` +{stderr} +``` + +
+" + ), + ) + .await + .wrap_err("creating update comment")?; + return Ok(()); + } + + // Ensure the labels exist. + let label = github_client.issues().get_label(target).await; + match label { + Ok(_) => {} + Err(octocrab::Error::GitHub { source, .. }) if source.status_code.as_u16() == 404 => { + github_client + .issues() + .create_label(target, "d73a4a", format!("Target: {target}")) + .await + .wrap_err("creating label")?; + } + Err(err) => return Err(err).wrap_err("failed to fetch label label"), + } + + let pings = notify_usernames + .iter() + .map(|name| { + if github_client.send_pings { + format!("@{name}") + } else { + format!("@\\{name}") + } + }) + .collect::>() + .join(" "); + + let issue = github_client + .issues() + .create(format!("{target} fails to build on {nightly}")) + .labels(Some(vec![target.to_owned()])) + .body(format!( + "💥 The target {target} fails to build on the nightly {nightly}! + +<{url}> + +
+full logs + +``` +{stderr} +``` + +
+ +{pings} + +This issue will be closed automatically when this target works again!" + )) + .send() + .await + .wrap_err("failed to create issue")?; + + db.insert_notification(NotificationIssue { + first_failed_nightly: nightly.into(), + issue_number: issue.number as i64, + status: NotificationStatus::Open, + target: target.into(), + }) + .await + .wrap_err("inserting issue into DB")?; + + Ok(()) +} + +pub async fn notify_build_pass( + github_client: &GitHubClient, + db: &Db, + build_info: &FullBuildInfo, +) -> Result<()> { + let FullBuildInfo { + target, nightly, .. + } = build_info; + + let issue = db.find_existing_notification(target).await?; + + if let Some(issue) = issue { + info!( + "Closing issue {} for {target}, since {nightly} builds again", + issue.issue_number + ); + + let url = format!( + "https://does-it-build.noratrieb.dev/build?nightly={nightly}&target={target}&mode=std" + ); + + // An existing issue, send a comment. + + github_client + .issues() + .create_comment( + issue.issue_number as u64, + format!("✅ The target {target} successfully builds on nightly {nightly}, \ + thanks for playing this round of Tier 3 rustc target breakage fixing! See y'all next time :3!\n\n<{url}>"), + ) + .await + .wrap_err("creating update comment")?; + + github_client + .issues() + .update(issue.issue_number as u64) + .state(IssueState::Closed) + .state_reason(IssueStateReason::Completed) + .send() + .await + .wrap_err("closing issue")?; + + db.finish_notification(issue.issue_number as i64).await?; + } + + Ok(()) +} diff --git a/src/web.rs b/src/web.rs index b39c788..90a6408 100644 --- a/src/web.rs +++ b/src/web.rs @@ -11,7 +11,10 @@ use color_eyre::{eyre::Context, Result}; use serde::Deserialize; use tracing::{error, info}; -use crate::db::{BuildInfo, BuildMode, BuildStats, Db, Status}; +use crate::{ + db::{BuildInfo, BuildMode, BuildStats, Db, Status}, + notification, +}; #[derive(Clone)] pub struct AppState { @@ -138,6 +141,8 @@ async fn web_target(State(state): State, Query(query): Query, Option)>, showing_failures: bool, + notification_pr_url: String, + maintainers: Option<&'static [&'static str]>, } let filter_failures = query.failures.unwrap_or(false); @@ -173,12 +178,16 @@ async fn web_target(State(state): State, Query(query): Query>(); builds.sort_by_cached_key(|build| Reverse(build.0.clone())); + let maintainers = notification::maintainers_for_target(&query.target); + let page = TargetPage { status, target: query.target, version: crate::VERSION, builds, showing_failures: filter_failures, + notification_pr_url: notification::notification_pr_url(), + maintainers, }; Html(page.render().unwrap()).into_response() diff --git a/templates/target.html b/templates/target.html index b55660f..5e5a9f5 100644 --- a/templates/target.html +++ b/templates/target.html @@ -31,6 +31,18 @@ older targets (older than November 2024) this does not always work reliably, so some failed std builds there are simply no-std targets.

+

+ 🔔 does-it-build supports sending notifications to target maintainers via GitHub issues. + You can add yourself with a PR. +

+ {% if let Some(maintainers) = maintainers %} +

+ Maintainers that will receive pings for this target: + {% for maintainer in maintainers %} + {{maintainer}} + {% endfor %} +

+ {% endif %} {% if showing_failures %}

show all