migrate to rootcause

This commit is contained in:
nora 2025-11-23 11:54:57 +01:00
parent 25179d488d
commit 11ccd14830
8 changed files with 176 additions and 99 deletions

53
Cargo.lock generated
View file

@ -454,6 +454,7 @@ dependencies = [
"jsonwebtoken", "jsonwebtoken",
"octocrab", "octocrab",
"reqwest", "reqwest",
"rootcause",
"serde", "serde",
"sqlx", "sqlx",
"tempfile", "tempfile",
@ -545,7 +546,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"spin", "spin 0.9.8",
] ]
[[package]] [[package]]
@ -1157,7 +1158,7 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [ dependencies = [
"spin", "spin 0.9.8",
] ]
[[package]] [[package]]
@ -1791,6 +1792,30 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rootcause"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e661be64332ee23d8849d658db01d9754ca6be964cce16d69ea8496d327f949"
dependencies = [
"hashbrown 0.16.0",
"indexmap",
"rootcause-internals",
"rustc-hash",
"spin 0.10.0",
"triomphe",
"unsize",
]
[[package]]
name = "rootcause-internals"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4814c3046a3c6408352680d691f0ab2f36c0cce6d2c0c1acd05da72eb30aa7ee"
dependencies = [
"triomphe",
]
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.8" version = "0.9.8"
@ -2132,6 +2157,12 @@ dependencies = [
"lock_api", "lock_api",
] ]
[[package]]
name = "spin"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
[[package]] [[package]]
name = "spki" name = "spki"
version = "0.7.3" version = "0.7.3"
@ -2664,6 +2695,15 @@ dependencies = [
"tracing-log", "tracing-log",
] ]
[[package]]
name = "triomphe"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39"
dependencies = [
"unsize",
]
[[package]] [[package]]
name = "try-lock" name = "try-lock"
version = "0.2.5" version = "0.2.5"
@ -2703,6 +2743,15 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unsize"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fa7a7a734c1a5664a662ddcea0b6c9472a21da8888c957c7f1eaa09dba7a939"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"

View file

@ -14,7 +14,6 @@ axum = { version = "0.8.6", default-features = false, features = [
"tracing", "tracing",
"macros", "macros",
] } ] }
color-eyre = "0.6.3"
futures = "0.3.30" futures = "0.3.30"
jiff = "0.2.16" jiff = "0.2.16"
jsonwebtoken = { version = "9.3.1" } jsonwebtoken = { version = "9.3.1" }
@ -23,6 +22,7 @@ reqwest = { version = "0.12.7", features = [
"rustls-tls", "rustls-tls",
"http2", "http2",
], default-features = false } ], default-features = false }
rootcause = "0.9.0"
serde = { version = "1.0.210", features = ["derive"] } serde = { version = "1.0.210", features = ["derive"] }
sqlx = { version = "0.8.2", features = ["runtime-tokio", "sqlite"] } sqlx = { version = "0.8.2", features = ["runtime-tokio", "sqlite"] }
tempfile = "3.12.0" tempfile = "3.12.0"

View file

@ -6,11 +6,8 @@ use std::{
time::{Duration, Instant, SystemTime}, time::{Duration, Instant, SystemTime},
}; };
use color_eyre::{
eyre::{bail, Context},
Result,
};
use futures::StreamExt; use futures::StreamExt;
use rootcause::{prelude::ResultExt, report};
use tokio::process::Command; use tokio::process::Command;
use tracing::{debug, error, info}; use tracing::{debug, error, info};
@ -18,6 +15,7 @@ use crate::{
db::{BuildMode, Db, FullBuildInfo, Status}, db::{BuildMode, Db, FullBuildInfo, Status},
nightlies::Nightlies, nightlies::Nightlies,
notification::GitHubClient, notification::GitHubClient,
Result,
}; };
struct CustomBuildFlags { struct CustomBuildFlags {
@ -73,11 +71,11 @@ pub async fn background_builder(db: Db, github_client: GitHubClient) -> Result<(
} }
async fn background_builder_inner(db: &Db, github_client: &GitHubClient) -> 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.context("fetching nightlies")?;
let already_finished = db let already_finished = db
.finished_nightlies() .finished_nightlies()
.await .await
.wrap_err("fetching finished nightlies")?; .context("fetching finished nightlies")?;
let next = nightlies.select_latest_to_build(&already_finished); let next = nightlies.select_latest_to_build(&already_finished);
match next { match next {
@ -85,12 +83,12 @@ async fn background_builder_inner(db: &Db, github_client: &GitHubClient) -> Resu
info!(%nightly, %mode, "Building next nightly"); 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 .await
.wrap_err_with(|| format!("building targets for toolchain {nightly}")); .context_with(|| format!("building targets for toolchain {nightly}"));
if let Err(err) = result { if let Err(err) = result {
error!(%nightly, %mode, ?err, "Failed to build nightly"); error!(%nightly, %mode, ?err, "Failed to build nightly");
db.finish_nightly_as_broken(&nightly, mode, &format!("{err:?}")) db.finish_nightly_as_broken(&nightly, mode, &format!("{err:?}"))
.await .await
.wrap_err("marking nightly as broken")?; .context("marking nightly as broken")?;
} }
} }
None => { None => {
@ -108,16 +106,16 @@ async fn targets_for_toolchain(toolchain: &Toolchain) -> Result<Vec<String>> {
.arg("target-list") .arg("target-list")
.output() .output()
.await .await
.wrap_err("failed to spawn rustc")?; .context("failed to spawn rustc")?;
if !output.status.success() { if !output.status.success() {
bail!( report!(
"failed to get target-list from rustc: {:?}", "failed to get target-list from rustc: {:?}",
String::from_utf8(output.stderr) String::from_utf8(output.stderr)
); );
} }
Ok(String::from_utf8(output.stdout) Ok(String::from_utf8(output.stdout)
.wrap_err("rustc target-list is invalid UTF-8")? .context("rustc target-list is invalid UTF-8")?
.split_whitespace() .split_whitespace()
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
.collect()) .collect())
@ -135,9 +133,9 @@ async fn install_toolchain(toolchain: &Toolchain, mode: BuildMode) -> Result<()>
.arg("minimal") .arg("minimal")
.output() .output()
.await .await
.wrap_err("failed to spawn rustup")?; .context("failed to spawn rustup")?;
if !result.status.success() { if !result.status.success() {
bail!("rustup failed: {:?}", String::from_utf8(result.stderr)); report!("rustup failed: {:?}", String::from_utf8(result.stderr));
} }
let result = Command::new("rustup") let result = Command::new("rustup")
.arg("component") .arg("component")
@ -147,9 +145,9 @@ async fn install_toolchain(toolchain: &Toolchain, mode: BuildMode) -> Result<()>
.arg(&toolchain.0) .arg(&toolchain.0)
.output() .output()
.await .await
.wrap_err("failed to spawn rustup")?; .context("failed to spawn rustup")?;
if !result.status.success() { if !result.status.success() {
bail!("rustup failed: {:?}", String::from_utf8(result.stderr)); report!("rustup failed: {:?}", String::from_utf8(result.stderr));
} }
Ok(()) Ok(())
@ -165,9 +163,9 @@ async fn uninstall_toolchain(toolchain: &Toolchain) -> Result<()> {
.arg(&toolchain.0) .arg(&toolchain.0)
.output() .output()
.await .await
.wrap_err("failed to spawn rustup")?; .context("failed to spawn rustup")?;
if !result.status.success() { if !result.status.success() {
bail!( report!(
"rustup toolchain remove failed: {:?}", "rustup toolchain remove failed: {:?}",
String::from_utf8(result.stderr) String::from_utf8(result.stderr)
); );
@ -191,7 +189,7 @@ pub async fn build_every_target_for_toolchain(
let targets = targets_for_toolchain(&toolchain) let targets = targets_for_toolchain(&toolchain)
.await .await
.wrap_err("failed to get targets")?; .context("failed to get targets")?;
let results = futures::stream::iter( let results = futures::stream::iter(
targets targets
@ -208,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, github_client) build_single_target(db, nightly, &target, mode, github_client)
.await .await
.wrap_err_with(|| format!("building target {target} for toolchain {toolchain}"))?; .context_with(|| format!("building target {target} for toolchain {toolchain}"))?;
} }
// Mark it as finished, so we never have to build it again. // Mark it as finished, so we never have to build it again.
@ -230,7 +228,7 @@ async fn build_single_target(
let existing = db let existing = db
.build_status_full(nightly, target, mode) .build_status_full(nightly, target, mode)
.await .await
.wrap_err("getting existing build")?; .context("getting existing build")?;
if existing.is_some() { if existing.is_some() {
debug!("Build already exists"); debug!("Build already exists");
return Ok(()); return Ok(());
@ -238,13 +236,13 @@ async fn build_single_target(
info!("Building target"); info!("Building target");
let tmpdir = tempfile::tempdir().wrap_err("creating temporary directory")?; let tmpdir = tempfile::tempdir().context("creating temporary directory")?;
let start_time = Instant::now(); let start_time = Instant::now();
let result = build_target(tmpdir.path(), &Toolchain::from_nightly(nightly), target) let result = build_target(tmpdir.path(), &Toolchain::from_nightly(nightly), target)
.await .await
.wrap_err("running build")?; .context("running build")?;
let full_build_info = FullBuildInfo { let full_build_info = FullBuildInfo {
nightly: nightly.into(), nightly: nightly.into(),
@ -298,14 +296,14 @@ async fn build_target(tmpdir: &Path, toolchain: &Toolchain, target: &str) -> Res
.current_dir(tmpdir) .current_dir(tmpdir)
.output() .output()
.await .await
.wrap_err("spawning cargo init")?; .context("spawning cargo init")?;
if !init.status.success() { if !init.status.success() {
bail!("init failed: {}", String::from_utf8(init.stderr)?); report!("init failed: {}", String::from_utf8(init.stderr)?);
} }
let librs = tmpdir.join("src").join("lib.rs"); let librs = tmpdir.join("src").join("lib.rs");
std::fs::write(&librs, "#![no_std]\n") std::fs::write(&librs, "#![no_std]\n")
.wrap_err_with(|| format!("writing to {}", librs.display()))?; .context_with(|| format!("writing to {}", librs.display()))?;
async fn run( async fn run(
toolchain: &Toolchain, toolchain: &Toolchain,
@ -330,14 +328,16 @@ async fn build_target(tmpdir: &Path, toolchain: &Toolchain, target: &str) -> Res
*rustflags = Some(flags); *rustflags = Some(flags);
} }
cmd.current_dir(tmpdir) let output = cmd
.current_dir(tmpdir)
.output() .output()
.await .await
.wrap_err("spawning cargo build") .context("spawning cargo build")?;
Ok(output)
} }
let mut output = run(toolchain, target, &mut rustflags, tmpdir, "-Zbuild-std").await?; let mut output = run(toolchain, target, &mut rustflags, tmpdir, "-Zbuild-std").await?;
let mut stderr = String::from_utf8(output.stderr).wrap_err("cargo stderr utf8")?; let mut stderr = String::from_utf8(output.stderr).context("cargo stderr utf8")?;
let status = if output.status.success() { let status = if output.status.success() {
Status::Pass Status::Pass
@ -352,7 +352,7 @@ async fn build_target(tmpdir: &Path, toolchain: &Toolchain, target: &str) -> Res
"-Zbuild-std=core", "-Zbuild-std=core",
) )
.await?; .await?;
stderr = String::from_utf8(output.stderr).wrap_err("cargo stderr utf8")?; stderr = String::from_utf8(output.stderr).context("cargo stderr utf8")?;
if output.status.success() { if output.status.success() {
Status::Pass Status::Pass

View file

@ -1,12 +1,11 @@
use std::{fmt::Display, str::FromStr}; use std::{fmt::Display, str::FromStr};
use color_eyre::{ use rootcause::{prelude::ResultExt, report};
eyre::{bail, Context},
Result,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, Pool, Sqlite}; use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, Pool, Sqlite};
use crate::Result;
#[derive(Clone)] #[derive(Clone)]
pub struct Db { pub struct Db {
pub conn: Pool<Sqlite>, pub conn: Pool<Sqlite>,
@ -112,12 +111,13 @@ pub struct NotificationIssue {
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)
.wrap_err("parsing database URL")? .context("parsing database URL")
.attach(format!("url: {path}"))?
.create_if_missing(true); .create_if_missing(true);
let conn = Pool::connect_with(db_opts) let conn = Pool::connect_with(db_opts)
.await .await
.wrap_err_with(|| format!("opening db from `{path}`"))?; .context_with(|| format!("opening db from `{path}`"))?;
Ok(Self { conn }) Ok(Self { conn })
} }
@ -137,38 +137,44 @@ impl Db {
.bind(info.build_duration_ms) .bind(info.build_duration_ms)
.execute(&self.conn) .execute(&self.conn)
.await .await
.wrap_err("inserting build info into database")?; .context("inserting build info into database")?;
Ok(()) Ok(())
} }
pub async fn history_for_target(&self, target: &str) -> Result<Vec<BuildInfo>> { pub async fn history_for_target(&self, target: &str) -> Result<Vec<BuildInfo>> {
sqlx::query_as::<_, BuildInfo>( let history = sqlx::query_as::<_, BuildInfo>(
"SELECT nightly, target, status, mode FROM build_info WHERE target = ?", "SELECT nightly, target, status, mode FROM build_info WHERE target = ?",
) )
.bind(target) .bind(target)
.fetch_all(&self.conn) .fetch_all(&self.conn)
.await .await
.wrap_err("getting history for single target") .context("getting history for single target")
.attach(format!("target: {target}"))?;
Ok(history)
} }
pub async fn history_for_nightly(&self, nightly: &str) -> Result<Vec<BuildInfo>> { pub async fn history_for_nightly(&self, nightly: &str) -> Result<Vec<BuildInfo>> {
sqlx::query_as::<_, BuildInfo>( let history = sqlx::query_as::<_, BuildInfo>(
"SELECT nightly, target, status, mode FROM build_info WHERE nightly = ?", "SELECT nightly, target, status, mode FROM build_info WHERE nightly = ?",
) )
.bind(nightly) .bind(nightly)
.fetch_all(&self.conn) .fetch_all(&self.conn)
.await .await
.wrap_err("getting history for single nightly") .context("getting history for single nightly")
.attach(format!("nightly: {nightly}"))?;
Ok(history)
} }
pub async fn nightly_info(&self, nightly: &str) -> Result<Vec<FinishedNightlyWithBroken>> { pub async fn nightly_info(&self, nightly: &str) -> Result<Vec<FinishedNightlyWithBroken>> {
sqlx::query_as::<_, FinishedNightlyWithBroken>( let info = sqlx::query_as::<_, FinishedNightlyWithBroken>(
"SELECT nightly, mode, is_broken, broken_error FROM finished_nightly WHERE nightly = ?", "SELECT nightly, mode, is_broken, broken_error FROM finished_nightly WHERE nightly = ?",
) )
.bind(nightly) .bind(nightly)
.fetch_all(&self.conn) .fetch_all(&self.conn)
.await .await
.wrap_err("getting finished_nightly for single nightly") .context("getting finished_nightly for single nightly")
.attach(format!("nightly: {nightly}"))?;
Ok(info)
} }
pub async fn target_list(&self) -> Result<Vec<String>> { pub async fn target_list(&self) -> Result<Vec<String>> {
@ -177,11 +183,14 @@ impl Db {
target: String, target: String,
} }
sqlx::query_as::<_, TargetName>("SELECT DISTINCT target FROM build_info ORDER BY target") let list = sqlx::query_as::<_, TargetName>(
.fetch_all(&self.conn) "SELECT DISTINCT target FROM build_info ORDER BY target",
.await )
.wrap_err("getting list of all targets") .fetch_all(&self.conn)
.map(|elems| elems.into_iter().map(|elem| elem.target).collect()) .await
.context("getting list of all targets")?;
Ok(list.into_iter().map(|elem| elem.target).collect())
} }
pub async fn nightly_list(&self) -> Result<Vec<String>> { pub async fn nightly_list(&self) -> Result<Vec<String>> {
@ -190,13 +199,14 @@ impl Db {
nightly: String, nightly: String,
} }
sqlx::query_as::<_, NightlyName>( let list = sqlx::query_as::<_, NightlyName>(
"SELECT DISTINCT nightly FROM build_info ORDER BY nightly DESC", "SELECT DISTINCT nightly FROM build_info ORDER BY nightly DESC",
) )
.fetch_all(&self.conn) .fetch_all(&self.conn)
.await .await
.wrap_err("getting list of all targets") .context("getting list of all nightlies")?;
.map(|elems| elems.into_iter().map(|elem| elem.nightly).collect())
Ok(list.into_iter().map(|elem| elem.nightly).collect())
} }
pub async fn build_count(&self) -> Result<BuildStats> { pub async fn build_count(&self) -> Result<BuildStats> {
@ -211,7 +221,7 @@ impl Db {
) )
.fetch_all(&self.conn) .fetch_all(&self.conn)
.await .await
.wrap_err("getting list of all targets")?; .context("getting total count of builds")?;
let count = |status| { let count = |status| {
results results
@ -242,7 +252,8 @@ impl Db {
.bind(mode) .bind(mode)
.fetch_all(&self.conn) .fetch_all(&self.conn)
.await .await
.wrap_err("getting build status from DB")?; .context("getting build status from DB")
.attach(format!("nightly: {nightly}, target: {target}"))?;
Ok(result.first().cloned()) Ok(result.first().cloned())
} }
@ -251,7 +262,7 @@ impl Db {
sqlx::query_as::<_, FinishedNightly>("SELECT nightly, mode from finished_nightly") sqlx::query_as::<_, FinishedNightly>("SELECT nightly, mode from finished_nightly")
.fetch_all(&self.conn) .fetch_all(&self.conn)
.await .await
.wrap_err("fetching finished nightlies")?; .context("getting finished nightlies")?;
Ok(result) Ok(result)
} }
@ -264,10 +275,10 @@ impl Db {
.bind(mode) .bind(mode)
.fetch_all(&self.conn) .fetch_all(&self.conn)
.await .await
.wrap_err("checking whether a nightly is finished")?; .context("checking whether a nightly is finished")?;
if result.len() > 1 { if result.len() > 1 {
bail!("found more than one result for {nightly} {mode}"); report!("found more than one result for {nightly} {mode}");
} }
Ok(result.len() == 1) Ok(result.len() == 1)
@ -279,7 +290,7 @@ impl Db {
.bind(mode) .bind(mode)
.execute(&self.conn) .execute(&self.conn)
.await .await
.wrap_err("inserting finished nightly")?; .context("inserting finished nightly")?;
Ok(()) Ok(())
} }
@ -295,7 +306,7 @@ impl Db {
.bind(error) .bind(error)
.execute(&self.conn) .execute(&self.conn)
.await .await
.wrap_err("inserting finished broken nightly")?; .context("inserting finished broken nightly")?;
Ok(()) Ok(())
} }
@ -303,13 +314,14 @@ impl Db {
&self, &self,
target: &str, target: &str,
) -> Result<Option<NotificationIssue>> { ) -> Result<Option<NotificationIssue>> {
sqlx::query_as::<_, NotificationIssue>( let notification = sqlx::query_as::<_, NotificationIssue>(
"SELECT * FROM notification_issues WHERE status = 'open' AND target = ?", "SELECT * FROM notification_issues WHERE status = 'open' AND target = ?",
) )
.bind(target) .bind(target)
.fetch_optional(&self.conn) .fetch_optional(&self.conn)
.await .await
.wrap_err("finding existing notification") .context("finding existing notification")?;
Ok(notification)
} }
pub async fn insert_notification(&self, notification: NotificationIssue) -> Result<()> { pub async fn insert_notification(&self, notification: NotificationIssue) -> Result<()> {
@ -325,7 +337,7 @@ impl Db {
.bind(notification.last_update_date) .bind(notification.last_update_date)
.execute(&self.conn) .execute(&self.conn)
.await .await
.wrap_err("inserting new notification")?; .context("inserting new notification")?;
Ok(()) Ok(())
} }
@ -335,7 +347,7 @@ impl Db {
.bind(issue_number) .bind(issue_number)
.execute(&self.conn) .execute(&self.conn)
.await .await
.wrap_err("marking notification as closed")?; .context("marking notification as closed")?;
Ok(()) Ok(())
} }
@ -349,7 +361,7 @@ impl Db {
.bind(issue_number) .bind(issue_number)
.execute(&self.conn) .execute(&self.conn)
.await .await
.wrap_err("marking notification as closed")?; .context("marking notification as closed")?;
Ok(()) Ok(())
} }
} }

View file

@ -4,13 +4,15 @@ mod nightlies;
mod notification; mod notification;
mod web; mod web;
use color_eyre::{eyre::WrapErr, Result};
use db::Db; use db::Db;
use rootcause::{prelude::ResultExt, Report};
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
const VERSION: &str = env!("GIT_COMMIT"); const VERSION: &str = env!("GIT_COMMIT");
const VERSION_SHORT: &str = env!("GIT_COMMIT_SHORT"); const VERSION_SHORT: &str = env!("GIT_COMMIT_SHORT");
type Result<T, E = Report> = std::result::Result<T, E>;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
main_inner().await main_inner().await
@ -25,25 +27,25 @@ async fn main_inner() -> Result<()> {
db::MIGRATOR db::MIGRATOR
.run(&db.conn) .run(&db.conn)
.await .await
.wrap_err("running migrations")?; .context("running migrations")?;
let send_pings = std::env::var("GITHUB_SEND_PINGS") let send_pings = std::env::var("GITHUB_SEND_PINGS")
.map(|_| true) .map(|_| true)
.unwrap_or(false); .unwrap_or(false);
let github_owner = std::env::var("GITHUB_OWNER").wrap_err("missing GITHUB_OWNER env var")?; let github_owner = std::env::var("GITHUB_OWNER").context("missing GITHUB_OWNER env var")?;
let github_repo = std::env::var("GITHUB_REPO").wrap_err("missing GITHUB_REPO env var")?; let github_repo = std::env::var("GITHUB_REPO").context("missing GITHUB_REPO env var")?;
let app_id = std::env::var("GITHUB_APP_ID") let app_id = std::env::var("GITHUB_APP_ID")
.wrap_err("missing GITHUB_APP_ID env var")? .context("missing GITHUB_APP_ID env var")?
.parse::<u64>() .parse::<u64>()
.wrap_err("invalid GITHUB_APP_ID")?; .context("invalid GITHUB_APP_ID")?;
let key = std::env::var("GITHUB_APP_PRIVATE_KEY") let key = std::env::var("GITHUB_APP_PRIVATE_KEY")
.wrap_err("missing GITHUB_APP_PRIVATE_KEY env var")?; .context("missing GITHUB_APP_PRIVATE_KEY env var")?;
let key = jsonwebtoken::EncodingKey::from_rsa_pem(key.as_bytes()).unwrap(); let key = jsonwebtoken::EncodingKey::from_rsa_pem(key.as_bytes()).unwrap();
let github_client = octocrab::Octocrab::builder() let github_client = octocrab::Octocrab::builder()
.app(app_id.into(), key) .app(app_id.into(), key)
.build() .build()
.wrap_err("failed to create client")?; .context("failed to create client")?;
let github_client = notification::GitHubClient::new( let github_client = notification::GitHubClient::new(
send_pings, send_pings,

View file

@ -1,11 +1,11 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::hash::RandomState; use std::hash::RandomState;
use color_eyre::eyre::Context; use rootcause::prelude::ResultExt;
use color_eyre::Result;
use tracing::debug; use tracing::debug;
use crate::db::{BuildMode, FinishedNightly}; use crate::db::{BuildMode, FinishedNightly};
use crate::Result;
const EARLIEST_CUTOFF_DATE: &str = "2023-01-01"; const EARLIEST_CUTOFF_DATE: &str = "2023-01-01";
@ -15,13 +15,21 @@ pub struct Nightlies {
} }
impl Nightlies { impl Nightlies {
pub async fn fetch() -> Result<Nightlies> { async fn get_body(url: &str) -> Result<String, rootcause::Report> {
let manifests = reqwest::get("https://static.rust-lang.org/manifests.txt") Ok(reqwest::get(url)
.await .await
.wrap_err("fetching https://static.rust-lang.org/manifests.txt")? .context("executing GET request")?
.text() .text()
.await .await
.wrap_err("fetching body of https://static.rust-lang.org/manifests.txt")?; .context("fetching body")?)
}
pub async fn fetch() -> Result<Nightlies> {
let url = "https://static.rust-lang.org/manifests.txt";
let manifests = Nightlies::get_body(url)
.await
.context(format!("Fetching manifests.txt"))
.attach(format!("url: {url}"))?;
let mut all = nightlies_from_manifest(&manifests) let mut all = nightlies_from_manifest(&manifests)
.into_iter() .into_iter()
.filter(|date| date.as_str() > EARLIEST_CUTOFF_DATE) .filter(|date| date.as_str() > EARLIEST_CUTOFF_DATE)

View file

@ -1,9 +1,10 @@
use color_eyre::eyre::{Context, Result};
use octocrab::models::issues::IssueStateReason; use octocrab::models::issues::IssueStateReason;
use octocrab::models::IssueState; use octocrab::models::IssueState;
use rootcause::prelude::ResultExt;
use tracing::info; use tracing::info;
use crate::db::{Db, FullBuildInfo, NotificationIssue, NotificationStatus, Status}; use crate::db::{Db, FullBuildInfo, NotificationIssue, NotificationStatus, Status};
use crate::Result;
pub const TABLE_FILE: &str = file!(); pub const TABLE_FILE: &str = file!();
pub const TABLE_LINE: u32 = line!() + 1; pub const TABLE_LINE: u32 = line!() + 1;
@ -55,11 +56,11 @@ impl GitHubClient {
.apps() .apps()
.get_repository_installation(&owner, &repo) .get_repository_installation(&owner, &repo)
.await .await
.wrap_err_with(|| format!("getting installation for {owner}/{repo}"))?; .context_with(|| format!("getting installation for {owner}/{repo}"))?;
let client = client let client = client
.installation(installation.id) .installation(installation.id)
.wrap_err("getting client for installation")?; .context("getting client for installation")?;
Ok(Self { Ok(Self {
send_pings, send_pings,
@ -143,11 +144,11 @@ This update is sent after a month of inactivity.
), ),
) )
.await .await
.wrap_err("creating update comment")?; .context("creating update comment")?;
db.set_notification_last_update(issue.issue_number, jiff::Timestamp::now()) db.set_notification_last_update(issue.issue_number, jiff::Timestamp::now())
.await .await
.wrap_err("updating last_update_date in DB")?; .context("updating last_update_date in DB")?;
} else { } else {
info!("Not sending update for {target}, since not enough time has elapsed since the last one"); info!("Not sending update for {target}, since not enough time has elapsed since the last one");
} }
@ -166,9 +167,13 @@ This update is sent after a month of inactivity.
.issues() .issues()
.create_label(target, "d73a4a", format!("Target: {target}")) .create_label(target, "d73a4a", format!("Target: {target}"))
.await .await
.wrap_err("creating label")?; .context("creating label")?;
}
Err(err) => {
return Err(err)
.context("failed to fetch label label")
.map_err(Into::into)
} }
Err(err) => return Err(err).wrap_err("failed to fetch label label"),
} }
let pings = notify_usernames let pings = notify_usernames
@ -207,7 +212,7 @@ This issue will be closed automatically when this target works again!"
)) ))
.send() .send()
.await .await
.wrap_err("failed to create issue")?; .context("failed to create issue")?;
db.insert_notification(NotificationIssue { db.insert_notification(NotificationIssue {
first_failed_nightly: nightly.into(), first_failed_nightly: nightly.into(),
@ -217,7 +222,7 @@ This issue will be closed automatically when this target works again!"
last_update_date: Some(jiff::Timestamp::now().as_millisecond()), last_update_date: Some(jiff::Timestamp::now().as_millisecond()),
}) })
.await .await
.wrap_err("inserting issue into DB")?; .context("inserting issue into DB")?;
Ok(()) Ok(())
} }
@ -253,7 +258,7 @@ pub async fn notify_build_pass(
thanks for playing this round of Tier 3 rustc target breakage fixing! See y'all next time :3!\n\n<{url}>"), thanks for playing this round of Tier 3 rustc target breakage fixing! See y'all next time :3!\n\n<{url}>"),
) )
.await .await
.wrap_err("creating update comment")?; .context("creating update comment")?;
github_client github_client
.issues() .issues()
@ -262,7 +267,7 @@ pub async fn notify_build_pass(
.state_reason(IssueStateReason::Completed) .state_reason(IssueStateReason::Completed)
.send() .send()
.await .await
.wrap_err("closing issue")?; .context("closing issue")?;
db.finish_notification(issue.issue_number).await?; db.finish_notification(issue.issue_number).await?;
} }

View file

@ -7,13 +7,13 @@ use axum::{
routing::get, routing::get,
Router, Router,
}; };
use color_eyre::{eyre::Context, Result}; use rootcause::{prelude::ResultExt, Report};
use serde::Deserialize; use serde::Deserialize;
use tracing::{error, info}; use tracing::{error, info};
use crate::{ use crate::{
db::{BuildInfo, BuildMode, BuildStats, Db, Status}, db::{BuildInfo, BuildMode, BuildStats, Db, Status},
notification, notification, Result,
}; };
#[derive(Clone)] #[derive(Clone)]
@ -38,7 +38,10 @@ pub async fn webserver(db: Db, notification_repo: String) -> Result<()> {
info!("Serving website on port 3000 (commit {})", crate::VERSION); info!("Serving website on port 3000 (commit {})", crate::VERSION);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.wrap_err("failed to serve") axum::serve(listener, app)
.await
.context("failed to serve")?;
Ok(())
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -325,12 +328,10 @@ async fn web_root(State(state): State<AppState>) -> impl IntoResponse {
Ok(Html(page.render().unwrap()).into_response()) Ok(Html(page.render().unwrap()).into_response())
} }
render(state) render(state).await.unwrap_or_else(|err: Report| {
.await error!(?err, "Error loading data for root page");
.unwrap_or_else(|err: color_eyre::eyre::Error| { StatusCode::INTERNAL_SERVER_ERROR.into_response()
error!(?err, "Error loading data for root page"); })
StatusCode::INTERNAL_SERVER_ERROR.into_response()
})
} }
async fn index_css() -> impl IntoResponse { async fn index_css() -> impl IntoResponse {