support github pings

This commit is contained in:
nora 2025-11-10 20:25:13 +01:00
parent 112420d224
commit b71f565e5b
11 changed files with 772 additions and 13 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
/targets /targets
/results /results
/db.sqlite* /db.sqlite*
/.envrc

354
Cargo.lock generated
View file

@ -32,6 +32,21 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 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]] [[package]]
name = "askama" name = "askama"
version = "0.14.0" version = "0.14.0"
@ -74,6 +89,17 @@ dependencies = [
"winnow", "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]] [[package]]
name = "atoi" name = "atoi"
version = "2.0.0" version = "2.0.0"
@ -253,6 +279,20 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 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]] [[package]]
name = "color-eyre" name = "color-eyre"
version = "0.6.5" version = "0.6.5"
@ -295,6 +335,22 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 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]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.17" version = "0.2.17"
@ -395,6 +451,8 @@ dependencies = [
"axum", "axum",
"color-eyre", "color-eyre",
"futures", "futures",
"jsonwebtoken",
"octocrab",
"reqwest", "reqwest",
"serde", "serde",
"sqlx", "sqlx",
@ -797,7 +855,9 @@ dependencies = [
"http", "http",
"hyper", "hyper",
"hyper-util", "hyper-util",
"log",
"rustls", "rustls",
"rustls-native-certs",
"rustls-pki-types", "rustls-pki-types",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
@ -805,6 +865,19 @@ dependencies = [
"webpki-roots", "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]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.17" version = "0.1.17"
@ -829,6 +902,30 @@ dependencies = [
"tracing", "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]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.1.1" version = "2.1.1"
@ -979,6 +1076,21 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -1121,6 +1233,16 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "num-bigint-dig" name = "num-bigint-dig"
version = "0.8.5" version = "0.8.5"
@ -1182,12 +1304,58 @@ dependencies = [
"memchr", "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]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]] [[package]]
name = "owo-colors" name = "owo-colors"
version = "4.2.3" version = "4.2.3"
@ -1223,6 +1391,16 @@ dependencies = [
"windows-link", "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]] [[package]]
name = "pem-rfc7468" name = "pem-rfc7468"
version = "0.7.0" version = "0.7.0"
@ -1238,6 +1416,26 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 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]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@ -1568,6 +1766,7 @@ version = "0.23.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
dependencies = [ dependencies = [
"log",
"once_cell", "once_cell",
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
@ -1576,6 +1775,18 @@ dependencies = [
"zeroize", "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]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.13.0" version = "1.13.0"
@ -1609,12 +1820,53 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 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]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 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]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@ -1737,6 +1989,18 @@ dependencies = [
"rand_core 0.6.4", "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]] [[package]]
name = "slab" name = "slab"
version = "0.4.11" version = "0.4.11"
@ -1752,6 +2016,27 @@ dependencies = [
"serde", "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]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.1" version = "0.6.1"
@ -2170,6 +2455,19 @@ dependencies = [
"tokio", "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]] [[package]]
name = "tower" name = "tower"
version = "0.5.2" version = "0.5.2"
@ -2181,6 +2479,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-util",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing", "tracing",
@ -2202,6 +2501,7 @@ dependencies = [
"tower", "tower",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]
@ -2474,6 +2774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"serde",
"wasm-bindgen", "wasm-bindgen",
] ]
@ -2496,12 +2797,65 @@ dependencies = [
"wasite", "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]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"

View file

@ -8,6 +8,8 @@ askama = "0.14.0"
axum = { version = "0.8.6", features = ["macros"] } axum = { version = "0.8.6", features = ["macros"] }
color-eyre = "0.6.3" color-eyre = "0.6.3"
futures = "0.3.30" futures = "0.3.30"
jsonwebtoken = { version = "9.3.1", features = [] }
octocrab = "0.47.1"
reqwest = { version = "0.12.7", features = [ reqwest = { version = "0.12.7", features = [
"rustls-tls", "rustls-tls",
], default-features = false } ], default-features = false }

View file

@ -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");

View file

@ -17,6 +17,7 @@ use tracing::{debug, error, info};
use crate::{ use crate::{
db::{BuildMode, Db, FullBuildInfo, Status}, db::{BuildMode, Db, FullBuildInfo, Status},
nightlies::Nightlies, nightlies::Nightlies,
notification::GitHubClient,
}; };
struct CustomBuildFlags { 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 { if concurrent_jobs() == 0 {
info!("Suspending background thread since DOES_IT_BUILD_PARALLEL_JOBS=0"); info!("Suspending background thread since DOES_IT_BUILD_PARALLEL_JOBS=0");
loop { loop {
@ -61,7 +62,7 @@ pub async fn background_builder(db: Db) -> Result<()> {
} }
loop { loop {
if let Err(err) = background_builder_inner(&db).await { if let Err(err) = background_builder_inner(&db, &github_client).await {
error!( error!(
?err, ?err,
"error in background builder, waiting for an hour before retrying: {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 nightlies = Nightlies::fetch().await.wrap_err("fetching nightlies")?;
let already_finished = db let already_finished = db
.finished_nightlies() .finished_nightlies()
@ -82,7 +83,7 @@ async fn background_builder_inner(db: &Db) -> Result<()> {
match next { match next {
Some((nightly, mode)) => { Some((nightly, mode)) => {
info!(%nightly, %mode, "Building next nightly"); 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 .await
.wrap_err_with(|| format!("building targets for toolchain {nightly}")); .wrap_err_with(|| format!("building targets for toolchain {nightly}"));
if let Err(err) = result { if let Err(err) = result {
@ -178,6 +179,7 @@ pub async fn build_every_target_for_toolchain(
db: &Db, db: &Db,
nightly: &str, nightly: &str,
mode: BuildMode, mode: BuildMode,
github_client: &GitHubClient,
) -> Result<()> { ) -> Result<()> {
if db.is_nightly_finished(nightly, mode).await? { if db.is_nightly_finished(nightly, mode).await? {
debug!("Nightly is already finished, not trying again"); 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( let results = futures::stream::iter(
targets targets
.iter() .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()) .buffer_unordered(concurrent_jobs())
.collect::<Vec<Result<()>>>() .collect::<Vec<Result<()>>>()
@ -204,7 +206,7 @@ pub async fn build_every_target_for_toolchain(
} }
for target in targets { for target in targets {
build_single_target(db, nightly, &target, mode) build_single_target(db, nightly, &target, mode, github_client)
.await .await
.wrap_err_with(|| format!("building target {target} for toolchain {toolchain}"))?; .wrap_err_with(|| format!("building target {target} for toolchain {toolchain}"))?;
} }
@ -217,8 +219,14 @@ pub async fn build_every_target_for_toolchain(
Ok(()) Ok(())
} }
#[tracing::instrument(skip(db))] #[tracing::instrument(skip(db, github_client))]
async fn build_single_target(db: &Db, nightly: &str, target: &str, mode: BuildMode) -> Result<()> { async fn build_single_target(
db: &Db,
nightly: &str,
target: &str,
mode: BuildMode,
github_client: &GitHubClient,
) -> Result<()> {
let existing = db let existing = db
.build_status_full(nightly, target, mode) .build_status_full(nightly, target, mode)
.await .await
@ -238,7 +246,7 @@ async fn build_single_target(db: &Db, nightly: &str, target: &str, mode: BuildMo
.await .await
.wrap_err("running build")?; .wrap_err("running build")?;
db.insert(FullBuildInfo { let full_build_info = FullBuildInfo {
nightly: nightly.into(), nightly: nightly.into(),
target: target.into(), target: target.into(),
status: result.status, 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()), does_it_build_version: Some(crate::VERSION_SHORT.into()),
build_duration_ms: Some(start_time.elapsed().as_millis().try_into().unwrap()), 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(()) Ok(())
} }

View file

@ -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 { impl Db {
pub async fn open(path: &str) -> Result<Self> { pub async fn open(path: &str) -> Result<Self> {
let db_opts = SqliteConnectOptions::from_str(path) let db_opts = SqliteConnectOptions::from_str(path)
@ -281,4 +297,43 @@ impl Db {
.wrap_err("inserting finished broken nightly")?; .wrap_err("inserting finished broken nightly")?;
Ok(()) Ok(())
} }
pub async fn find_existing_notification(
&self,
target: &str,
) -> Result<Option<NotificationIssue>> {
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(())
}
} }

39
src/github.rs Normal file
View file

@ -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<Self> {
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)
}
}

View file

@ -1,6 +1,7 @@
mod build; mod build;
mod db; mod db;
mod nightlies; mod nightlies;
mod notification;
mod web; mod web;
use color_eyre::{eyre::WrapErr, Result}; use color_eyre::{eyre::WrapErr, Result};
@ -12,6 +13,10 @@ const VERSION_SHORT: &str = env!("GIT_COMMIT_SHORT");
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
main_inner().await
}
async fn main_inner() -> Result<()> {
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("info"))) .with_env_filter(EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("info")))
.init(); .init();
@ -22,7 +27,33 @@ async fn main() -> Result<()> {
.await .await
.wrap_err("running migrations")?; .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::<u64>()
.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); let server = web::webserver(db);
tokio::select! { tokio::select! {

232
src/notification.rs Normal file
View file

@ -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<Self> {
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}>
<details><summary>full logs</summary>
```
{stderr}
```
</details>
"
),
)
.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::<Vec<_>>()
.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}>
<details>
<summary>full logs</summary>
```
{stderr}
```
</details>
{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(())
}

View file

@ -11,7 +11,10 @@ use color_eyre::{eyre::Context, Result};
use serde::Deserialize; use serde::Deserialize;
use tracing::{error, info}; use tracing::{error, info};
use crate::db::{BuildInfo, BuildMode, BuildStats, Db, Status}; use crate::{
db::{BuildInfo, BuildMode, BuildStats, Db, Status},
notification,
};
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
@ -138,6 +141,8 @@ async fn web_target(State(state): State<AppState>, Query(query): Query<TargetQue
version: &'static str, version: &'static str,
builds: Vec<(String, Option<BuildInfo>, Option<BuildInfo>)>, builds: Vec<(String, Option<BuildInfo>, Option<BuildInfo>)>,
showing_failures: bool, showing_failures: bool,
notification_pr_url: String,
maintainers: Option<&'static [&'static str]>,
} }
let filter_failures = query.failures.unwrap_or(false); let filter_failures = query.failures.unwrap_or(false);
@ -173,12 +178,16 @@ async fn web_target(State(state): State<AppState>, Query(query): Query<TargetQue
.collect::<Vec<_>>(); .collect::<Vec<_>>();
builds.sort_by_cached_key(|build| Reverse(build.0.clone())); builds.sort_by_cached_key(|build| Reverse(build.0.clone()));
let maintainers = notification::maintainers_for_target(&query.target);
let page = TargetPage { let page = TargetPage {
status, status,
target: query.target, target: query.target,
version: crate::VERSION, version: crate::VERSION,
builds, builds,
showing_failures: filter_failures, showing_failures: filter_failures,
notification_pr_url: notification::notification_pr_url(),
maintainers,
}; };
Html(page.render().unwrap()).into_response() Html(page.render().unwrap()).into_response()

View file

@ -31,6 +31,18 @@
older targets (older than November 2024) this does not always work older targets (older than November 2024) this does not always work
reliably, so some failed std builds there are simply no-std targets. reliably, so some failed std builds there are simply no-std targets.
</p> </p>
<p>
🔔 does-it-build supports sending notifications to target maintainers via GitHub issues.
You can add yourself with <a href="{{notification_pr_url}}">a PR</a>.
</p>
{% if let Some(maintainers) = maintainers %}
<p>
Maintainers that will receive pings for this target:
{% for maintainer in maintainers %}
<a href="https://github.com/{{maintainer}}">{{maintainer}}</a>
{% endfor %}
</p>
{% endif %}
{% if showing_failures %} {% if showing_failures %}
<p> <p>
<a href="/target?target={{target}}">show all</a> <a href="/target?target={{target}}">show all</a>