Add notifications for target maintainers (#10)

This adds a feature where maintainers can add themselves to an array in the source to receive notifications whenever the target fails.

The way this works is via GitHub, I created a new repository https://github.com/Noratrieb/does-it-build-notifications where an app will create issues that ping the respective users. This is the easiest option for me and also fits nicely into people's workflows on GitHub.

Whenever a target fails, an issue is created for it. This issue is kept open (with a new comment every day that it continues to fail), and then closed when the target builds again.
This commit is contained in:
nora 2025-11-10 20:52:15 +01:00 committed by GitHub
parent 112420d224
commit e4c69f17be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 784 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

@ -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 - `DB_PATH`: Path to SQlite DB to store the results
- `DOES_IT_BUILD_PARALLEL_JOBS`: Parallel build jobs, defaults to cores/2. - `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. Build configuration: `DOES_IT_BUILD_OVERRIDE_VERSION` to override the git commit.
## Deployment ## Deployment
deployed at <https://does-it-build.noratrieb.dev/> deployed at <https://does-it-build.noratrieb.dev/>
## Notification
does-it-build supports sending target maintainer notifications on breakage.
It does this by creating an issue <https://github.com/Noratrieb/does-it-build-notifications> 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.

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>