diff --git a/Cargo.lock b/Cargo.lock index d8fd4ed..683f47a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,7 +144,6 @@ dependencies = [ "percent-encoding", "pin-project-lite", "serde_core", - "serde_json", "serde_path_to_error", "serde_urlencoded", "sync_wrapper", @@ -451,13 +450,13 @@ dependencies = [ "axum", "color-eyre", "futures", + "jiff", "jsonwebtoken", "octocrab", "reqwest", "serde", "sqlx", "tempfile", - "time", "tokio", "tracing", "tracing-subscriber", @@ -713,6 +712,25 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -834,6 +852,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1066,6 +1085,47 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-sys 0.61.2", +] + +[[package]] +name = "jiff-static" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "js-sys" version = "0.3.82" @@ -1475,6 +1535,21 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1672,6 +1747,7 @@ dependencies = [ "base64", "bytes", "futures-core", + "h2", "http", "http-body", "http-body-util", diff --git a/Cargo.toml b/Cargo.toml index 4227ae4..fa7bd18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,25 +5,29 @@ edition = "2021" [dependencies] askama = "0.14.0" -axum = { version = "0.8.6", features = ["macros"] } +axum = { version = "0.8.6", default-features = false, features = [ + "http1", + "matched-path", + "query", + "tokio", + "tower-log", + "tracing", + "macros", +] } color-eyre = "0.6.3" futures = "0.3.30" -jsonwebtoken = { version = "9.3.1", features = [] } +jiff = "0.2.16" +jsonwebtoken = { version = "9.3.1" } octocrab = "0.47.1" reqwest = { version = "0.12.7", features = [ "rustls-tls", + "http2", ], default-features = false } serde = { version = "1.0.210", features = ["derive"] } -sqlx = { version = "0.8.2", features = [ - "macros", - "migrate", - "runtime-tokio", - "sqlite", -] } +sqlx = { version = "0.8.2", features = ["runtime-tokio", "sqlite"] } tempfile = "3.12.0" -time = { version = "0.3.36", features = ["formatting", "macros", "parsing"] } tokio = { version = "1.40.0", features = ["full"] } -tracing = "0.1.40" +tracing = { version = "0.1.40", features = ["attributes"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } [build-dependencies] diff --git a/migrations/20251111165945_notifications_last_update.sql b/migrations/20251111165945_notifications_last_update.sql new file mode 100644 index 0000000..ada4ff7 --- /dev/null +++ b/migrations/20251111165945_notifications_last_update.sql @@ -0,0 +1,4 @@ +-- Add migration script here + +ALTER TABLE notification_issues + ADD COLUMN "last_update_date" INTEGER NULL; diff --git a/src/build.rs b/src/build.rs index 48adaf1..ea74e3b 100644 --- a/src/build.rs +++ b/src/build.rs @@ -83,7 +83,7 @@ async fn background_builder_inner(db: &Db, github_client: &GitHubClient) -> Resu match next { Some((nightly, mode)) => { info!(%nightly, %mode, "Building next nightly"); - let result = build_every_target_for_toolchain(db, &nightly, mode, &github_client) + 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 { diff --git a/src/db.rs b/src/db.rs index b25518a..66f20f2 100644 --- a/src/db.rs +++ b/src/db.rs @@ -106,6 +106,7 @@ pub struct NotificationIssue { pub status: NotificationStatus, pub first_failed_nightly: String, pub target: String, + pub last_update_date: Option, } impl Db { @@ -314,13 +315,14 @@ impl Db { pub async fn insert_notification(&self, notification: NotificationIssue) -> Result<()> { sqlx::query( "INSERT INTO notification_issues\ - (issue_number, status, first_failed_nightly, target)\ - VALUES (?, ?, ?, ?)", + (issue_number, status, first_failed_nightly, target, last_update_date)\ + VALUES (?, ?, ?, ?, ?)", ) .bind(notification.issue_number) .bind(notification.status) .bind(notification.first_failed_nightly) .bind(notification.target) + .bind(notification.last_update_date) .execute(&self.conn) .await .wrap_err("inserting new notification")?; @@ -336,4 +338,18 @@ impl Db { .wrap_err("marking notification as closed")?; Ok(()) } + + pub async fn set_notification_last_update( + &self, + issue_number: i64, + last_update: jiff::Timestamp, + ) -> Result<()> { + sqlx::query("UPDATE notification_issues SET last_update_date = ? WHERE issue_number = ?") + .bind(last_update.as_millisecond()) + .bind(issue_number) + .execute(&self.conn) + .await + .wrap_err("marking notification as closed")?; + Ok(()) + } } diff --git a/src/github.rs b/src/github.rs deleted file mode 100644 index 954915b..0000000 --- a/src/github.rs +++ /dev/null @@ -1,39 +0,0 @@ -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/notification.rs b/src/notification.rs index 4d8c87f..7d4c449 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -84,8 +84,6 @@ pub async fn notify_build_failure( return Ok(()); }; - info!("Creating issue for target {target}, notifying {notify_usernames:?}"); - let issue = db.find_existing_notification(target).await?; let url = format!( @@ -93,14 +91,25 @@ pub async fn notify_build_failure( ); if let Some(issue) = issue { - // An existing issue, send a comment. + // An existing issue, send a comment if it's been a month since the last update. - github_client - .issues() - .create_comment( - issue.issue_number as u64, - format!( - "💥 The target {target} still fails to build on the nightly {nightly}! + if issue.last_update_date.is_none_or(|last_update_date| { + jiff::Timestamp::from_millisecond(last_update_date).is_ok_and(|last_update_date| { + jiff::Timestamp::now() + .since(last_update_date) + .is_ok_and(|diff| diff.get_months() > 0) + }) + }) { + info!( + "Sending update for {target}, since enough time has elapsed since the last update" + ); + + github_client + .issues() + .create_comment( + issue.issue_number as u64, + format!( + "💥 The target {target} still fails to build on the nightly {nightly}! <{url}> @@ -111,14 +120,26 @@ pub async fn notify_build_failure( ``` + +This update is sent after a month of inactivity. " - ), - ) - .await - .wrap_err("creating update comment")?; + ), + ) + .await + .wrap_err("creating update comment")?; + + db.set_notification_last_update(issue.issue_number, jiff::Timestamp::now()) + .await + .wrap_err("updating last_update_date in DB")?; + } else { + info!("Not sending update for {target}, since not enough time has elapsed since the last one"); + } + return Ok(()); } + info!("Creating issue for target {target}, notifying {notify_usernames:?}"); + // Ensure the labels exist. let label = github_client.issues().get_label(target).await; match label { @@ -176,6 +197,7 @@ This issue will be closed automatically when this target works again!" issue_number: issue.number as i64, status: NotificationStatus::Open, target: target.into(), + last_update_date: Some(jiff::Timestamp::now().as_millisecond()), }) .await .wrap_err("inserting issue into DB")?; @@ -225,7 +247,7 @@ pub async fn notify_build_pass( .await .wrap_err("closing issue")?; - db.finish_notification(issue.issue_number as i64).await?; + db.finish_notification(issue.issue_number).await?; } Ok(()) diff --git a/src/web.rs b/src/web.rs index 90a6408..fdb5f37 100644 --- a/src/web.rs +++ b/src/web.rs @@ -101,14 +101,10 @@ async fn web_build(State(state): State, Query(query): Query) -> impl IntoResponse { nightlies: Vec, version: &'static str, build_count: BuildStats, + notification_pr_url: String, } let targets = state.db.target_list().await?; @@ -301,6 +298,7 @@ async fn web_root(State(state): State) -> impl IntoResponse { nightlies, version: crate::VERSION, build_count, + notification_pr_url: notification::notification_pr_url(), }; Ok(Html(page.render().unwrap()).into_response()) diff --git a/static/index.css b/static/index.css index 876e7ed..90ca657 100644 --- a/static/index.css +++ b/static/index.css @@ -1,29 +1,54 @@ +:root { + color-scheme: light dark; +} + html { font-family: sans-serif; } +@media screen and (min-width: 768px) { + html { + margin: 20px; + } +} + +table { + border-spacing: 0; +} + th, td { - border: 1px solid; - margin: 0; + padding: 2px 5px; +} + +tr:not(:last-child) { + th, + td { + border-bottom: 1px solid; + } } .error { - background-color: lightcoral; + background-color: light-dark(lightcoral, darkred); } .pass { - background-color: greenyellow; + background-color: light-dark(greenyellow, darkgreen); } .missing { - background-color: lightgray; + background-color: light-dark(lightgray, rgb(85, 85, 85)); } .target-header { writing-mode: sideways-lr; } +.build-indicator-big { + padding: 10px; + margin-top: 20px; +} + .build-cell { font-size: 2rem; } @@ -36,13 +61,20 @@ td { padding: 5px; } -.target-name-col { - white-space: nowrap; -} - .footer { margin-top: 20px; display: flex; align-items: center; gap: 10px; } + +@media (prefers-color-scheme: dark) { + html { + background-color: #1b191c; + color: #e6dae9; + } + + a { + color: #e5a5c2; + } +} diff --git a/templates/build.html b/templates/build.html index fb90738..8eaa010 100644 --- a/templates/build.html +++ b/templates/build.html @@ -1,4 +1,4 @@ - + @@ -6,18 +6,15 @@ Build {{nightly}} {{target}} - -

Build results for nightly-{{nightly}} target {{target}} {{mode}}

+

+ Build results for + nightly-{{nightly}}, target + {{target}} ({{mode}}) +

Home -
- {{status}} -
+
{{status}}
{% if let Some(rustflags) = rustflags %}

Using rustflags: {{rustflags}} diff --git a/templates/index.html b/templates/index.html index 3b1702a..e6084c0 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,4 +1,4 @@ - + @@ -25,6 +25,12 @@ history.

+

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

+

Nightlies

    {% for nightly in nightlies.iter().take(5) %} diff --git a/templates/target.html b/templates/target.html index 5e5a9f5..c5678ce 100644 --- a/templates/target.html +++ b/templates/target.html @@ -5,11 +5,6 @@ {{target}} build history -

    Target build history for {{target}}

    @@ -33,7 +28,7 @@

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

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