summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/dbctx.rs116
-rw-r--r--src/main.rs145
-rw-r--r--src/sql.rs7
3 files changed, 252 insertions, 16 deletions
diff --git a/src/dbctx.rs b/src/dbctx.rs
index 67740e8..476c3fc 100644
--- a/src/dbctx.rs
+++ b/src/dbctx.rs
@@ -20,6 +20,41 @@ pub struct DbCtx {
}
#[derive(Debug, Clone)]
+pub struct Repo {
+ pub id: u64,
+ pub name: String,
+}
+
+#[derive(Debug)]
+pub struct Remote {
+ pub id: u64,
+ pub repo_id: u64,
+ pub remote_path: String,
+ pub remote_api: String,
+ pub remote_url: String,
+ pub remote_git_url: String,
+ pub notifier_config_path: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct Job {
+ pub id: u64,
+ pub artifacts_path: Option<String>,
+ pub state: sql::JobState,
+ pub run_host: String,
+ pub remote_id: u64,
+ pub commit_id: u64,
+ pub created_time: u64,
+ pub start_time: Option<u64>,
+ pub complete_time: Option<u64>,
+ pub build_token: Option<String>,
+ pub job_timeout: Option<u64>,
+ pub source: Option<String>,
+ pub build_result: Option<u8>,
+ pub final_text: Option<String>,
+}
+
+#[derive(Debug, Clone)]
pub struct PendingJob {
pub id: u64,
pub artifacts: Option<String>,
@@ -131,6 +166,17 @@ impl DbCtx {
ArtifactDescriptor::new(job_id, artifact_id).await
}
+ pub fn commit_sha(&self, commit_id: u64) -> Result<String, String> {
+ self.conn.lock()
+ .unwrap()
+ .query_row(
+ "select sha from commits where id=?1",
+ [commit_id],
+ |row| { row.get(0) }
+ )
+ .map_err(|e| e.to_string())
+ }
+
pub fn job_for_commit(&self, sha: &str) -> Result<Option<u64>, String> {
self.conn.lock()
.unwrap()
@@ -268,6 +314,54 @@ impl DbCtx {
Ok(artifacts)
}
+ pub fn get_repos(&self) -> Result<Vec<Repo>, String> {
+ let conn = self.conn.lock().unwrap();
+
+ let mut repos_query = conn.prepare(sql::ALL_REPOS).unwrap();
+ let mut repos = repos_query.query([]).unwrap();
+ let mut result = Vec::new();
+
+ while let Some(row) = repos.next().unwrap() {
+ let (id, repo_name) = row.try_into().unwrap();
+ result.push(Repo {
+ id,
+ name: repo_name,
+ });
+ }
+
+ Ok(result)
+ }
+
+ pub fn last_job_from_remote(&self, id: u64) -> Result<Option<Job>, String> {
+ let conn = self.conn.lock().unwrap();
+
+ let mut job_query = conn.prepare(sql::LAST_JOB_FROM_REMOTE).unwrap();
+ let mut result = job_query.query([id]).unwrap();
+
+ let job = result.next().expect("can get next row, which may be None").map(|row| {
+ let (id, artifacts_path, state, run_host, remote_id, commit_id, created_time, start_time, complete_time, build_token, job_timeout, source, build_result, final_text) = row.try_into().unwrap();
+ let state: u8 = state;
+ Job {
+ id,
+ artifacts_path,
+ state: state.try_into().unwrap(),
+ run_host,
+ remote_id,
+ commit_id,
+ created_time,
+ start_time,
+ complete_time,
+ build_token,
+ job_timeout,
+ source,
+ build_result,
+ final_text,
+ }
+ });
+
+ Ok(job)
+ }
+
pub fn get_pending_jobs(&self) -> Result<Vec<PendingJob>, String> {
let conn = self.conn.lock().unwrap();
@@ -289,17 +383,7 @@ impl DbCtx {
Ok(pending)
}
- pub fn notifiers_by_repo(&self, repo_id: u64) -> Result<Vec<RemoteNotifier>, String> {
- #[derive(Debug)]
- #[allow(dead_code)]
- struct Remote {
- id: u64,
- repo_id: u64,
- remote_path: String,
- remote_api: String,
- notifier_config_path: String,
- }
-
+ pub fn remotes_by_repo(&self, repo_id: u64) -> Result<Vec<Remote>, String> {
let mut remotes: Vec<Remote> = Vec::new();
let conn = self.conn.lock().unwrap();
@@ -308,11 +392,15 @@ impl DbCtx {
while let Some(row) = remote_results.next().unwrap() {
let (id, repo_id, remote_path, remote_api, remote_url, remote_git_url, notifier_config_path) = row.try_into().unwrap();
- let _: String = remote_url;
- let _: String = remote_git_url;
- remotes.push(Remote { id, repo_id, remote_path, remote_api, notifier_config_path });
+ remotes.push(Remote { id, repo_id, remote_path, remote_api, remote_url, remote_git_url, notifier_config_path });
}
+ Ok(remotes)
+ }
+
+ pub fn notifiers_by_repo(&self, repo_id: u64) -> Result<Vec<RemoteNotifier>, String> {
+ let remotes = self.remotes_by_repo(repo_id)?;
+
let mut notifiers: Vec<RemoteNotifier> = Vec::new();
for remote in remotes.into_iter() {
diff --git a/src/main.rs b/src/main.rs
index 9c16762..7f89ced 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,6 +2,7 @@
#![allow(unused_variables)]
#![allow(unused_imports)]
+use chrono::{Utc, TimeZone};
use lazy_static::lazy_static;
use std::sync::RwLock;
use serde_derive::{Deserialize, Serialize};
@@ -33,7 +34,7 @@ mod dbctx;
use sql::JobState;
-use dbctx::DbCtx;
+use dbctx::{DbCtx, Job};
use rusqlite::OptionalExtension;
@@ -71,6 +72,44 @@ enum GithubEvent {
Other {}
}
+/// return a duration rendered as the largest two non-zero units.
+///
+/// 60000ms -> 1m
+/// 60001ms -> 1m
+/// 61000ms -> 1m1s
+/// 1030ms -> 1.03s
+fn duration_as_human_string(duration_ms: u64) -> String {
+ let duration_sec = duration_ms / 1000;
+ let duration_min = duration_sec / 60;
+ let duration_hours = duration_min / 60;
+
+ let duration_ms = duration_ms % 1000;
+ let duration_sec = duration_sec & 60;
+ let duration_min = duration_hours & 60;
+ // no need to clamp hours, we're gonna just hope that it's a reasonable number of hours
+
+ if duration_hours != 0 {
+ let mut res = format!("{}h", duration_hours);
+ if duration_min != 0 {
+ res.push_str(&format!("{}m", duration_min));
+ }
+ res
+ } else if duration_min != 0 {
+ let mut res = format!("{}m", duration_min);
+ if duration_min != 0 {
+ res.push_str(&format!("{}s", duration_sec));
+ }
+ res
+ } else {
+ let mut res = format!("{}", duration_sec);
+ if duration_ms != 0 {
+ res.push_str(&format!(".{:03}", duration_ms));
+ }
+ res.push('s');
+ res
+ }
+}
+
fn parse_push_event(body: serde_json::Value) -> Result<GithubEvent, GithubHookError> {
let body = body.as_object()
.ok_or(GithubHookError::BodyNotObject)?;
@@ -187,7 +226,109 @@ async fn handle_github_event(ctx: Arc<DbCtx>, owner: String, repo: String, event
}
async fn handle_ci_index(State(ctx): State<Arc<DbCtx>>) -> impl IntoResponse {
- "hello and welcome to my websight"
+ eprintln!("root index");
+ let repos = match ctx.get_repos() {
+ Ok(repos) => repos,
+ Err(e) => {
+ eprintln!("failed to get repos: {:?}", e);
+ return (StatusCode::INTERNAL_SERVER_ERROR, "gonna feel that one tomorrow".to_string());
+ }
+ };
+
+ let mut response = String::new();
+
+ response.push_str("<html>\n");
+ response.push_str("<h1>builds and build accessories</h1>\n");
+
+ match repos.len() {
+ 0 => { response.push_str(&format!("<p>no repos configured, so there are no builds</p>\n")); },
+ 1 => { response.push_str("<p>1 repo configured</p>\n"); },
+ other => { response.push_str(&format!("<p>{} repos configured</p>\n", other)); },
+ }
+
+ response.push_str("<table>\n");
+ response.push_str("<tr>\n");
+ let headings = ["repo", "last build", "build commit", "duration", "status", "result"];
+ for heading in headings {
+ response.push_str(&format!("<th>{}</th>", heading));
+ }
+ response.push_str("</tr>\n");
+
+ for repo in repos {
+ let mut most_recent_job: Option<Job> = None;
+
+ for remote in ctx.remotes_by_repo(repo.id).expect("remotes by repo works") {
+ let last_job = ctx.last_job_from_remote(remote.id).expect("job by remote works");
+ if let Some(last_job) = last_job {
+ if most_recent_job.as_ref().map(|job| job.created_time < last_job.created_time).unwrap_or(true) {
+ most_recent_job = Some(last_job);
+ }
+ }
+ }
+
+ let row_html: String = match most_recent_job {
+ Some(job) => {
+ let job_commit = ctx.commit_sha(job.commit_id).expect("job has a commit");
+
+ let last_build_time = Utc.timestamp_millis_opt(job.created_time as i64).unwrap().to_rfc2822();
+ let duration = if let Some(start_time) = job.start_time {
+ if let Some(complete_time) = job.complete_time {
+ let duration_ms = complete_time - start_time;
+ let duration = duration_as_human_string(duration_ms);
+ duration
+ } else {
+ let now_ms = SystemTime::now().duration_since(UNIX_EPOCH).expect("now is after then").as_millis() as u64;
+ let mut duration = duration_as_human_string(now_ms - start_time);
+ duration.push_str(" (ongoing)");
+ duration
+ }
+ } else {
+ "not yet run".to_owned()
+ };
+
+ let status = format!("{:?}", job.state).to_lowercase();
+
+ let result = match job.build_result {
+ Some(0) => "<span style='color:green;'>pass</span>",
+ Some(_) => "<span style='color:red;'>fail</span>",
+ None => match job.state {
+ JobState::Pending => { "unstarted" },
+ JobState::Started => { "<span style='color:yellow;'>in progress</span>" },
+ _ => { "<span style='color:red;'>unreported</span>" }
+ }
+ };
+
+ let entries = [repo.name.as_str(), last_build_time.as_str(), job_commit.as_str(), &duration, &status, &result];
+ let entries = entries.iter().chain(std::iter::repeat(&"")).take(headings.len());
+
+ let mut row_html = String::new();
+ row_html.push_str("<tr>");
+ for entry in entries {
+ row_html.push_str(&format!("<td>{}</td>", entry));
+ }
+ row_html.push_str("</tr>");
+ row_html
+ }
+ None => {
+ let entries = [repo.name.as_str()];
+ let entries = entries.iter().chain(std::iter::repeat(&"")).take(headings.len());
+
+ let mut row_html = String::new();
+ row_html.push_str("<tr>");
+ for entry in entries {
+ row_html.push_str(&format!("<td>{}</td>", entry));
+ }
+ row_html.push_str("</tr>");
+ row_html
+ }
+ };
+
+ response.push_str(&row_html);
+ }
+
+ response.push_str("</html>");
+
+ (StatusCode::OK, response)
}
async fn handle_commit_status(Path(path): Path<(String, String, String)>, State(ctx): State<Arc<DbCtx>>) -> impl IntoResponse {
diff --git a/src/sql.rs b/src/sql.rs
index 7bd33fd..b262e9a 100644
--- a/src/sql.rs
+++ b/src/sql.rs
@@ -106,3 +106,10 @@ pub const COMMIT_TO_ID: &'static str = "\
pub const REMOTES_FOR_REPO: &'static str = "\
select * from remotes where repo_id=?1;";
+
+pub const ALL_REPOS: &'static str = "\
+ select * from repos;";
+
+pub const LAST_JOB_FROM_REMOTE: &'static str = "\
+ select * from jobs where remote_id=?1 order by created_time desc limit 1;";
+