chore(format): Format code

Signed-off-by: Till Wegmueller <toasterson@gmail.com>
This commit is contained in:
Till Wegmueller 2026-01-25 23:16:36 +01:00
parent c0a7f7e3f2
commit d3841462cf
No known key found for this signature in database
4 changed files with 124 additions and 103 deletions

View file

@ -133,7 +133,10 @@ async fn main() -> Result<()> {
let base_url = resolve_logs_base_url(logs_base_url)?; let base_url = resolve_logs_base_url(logs_base_url)?;
cmd_logs(&base_url, &job_id, category.as_deref()).await?; cmd_logs(&base_url, &job_id, category.as_deref()).await?;
} }
Commands::Tui { logs_base_url, repo } => { Commands::Tui {
logs_base_url,
repo,
} => {
let base_url = resolve_logs_base_url(logs_base_url)?; let base_url = resolve_logs_base_url(logs_base_url)?;
let repo = resolve_repo_url(repo); let repo = resolve_repo_url(repo);
run_tui(&base_url, repo).await?; run_tui(&base_url, repo).await?;
@ -170,11 +173,7 @@ fn detect_git_remote() -> Option<String> {
return None; return None;
} }
let s = String::from_utf8_lossy(&output.stdout).trim().to_string(); let s = String::from_utf8_lossy(&output.stdout).trim().to_string();
if s.is_empty() { if s.is_empty() { None } else { Some(s) }
None
} else {
Some(s)
}
} }
fn config_path() -> Result<PathBuf> { fn config_path() -> Result<PathBuf> {
@ -185,7 +184,9 @@ fn config_path() -> Result<PathBuf> {
} else if let Ok(home) = std::env::var("USERPROFILE") { } else if let Ok(home) = std::env::var("USERPROFILE") {
PathBuf::from(home).join(".config") PathBuf::from(home).join(".config")
} else { } else {
return Err(miette::miette!("Unable to determine home directory for config storage")); return Err(miette::miette!(
"Unable to determine home directory for config storage"
));
}; };
Ok(base.join("solstice").join("ciadm.conf")) Ok(base.join("solstice").join("ciadm.conf"))
} }
@ -212,7 +213,11 @@ fn load_logs_base_url() -> Result<Option<String>> {
let Some(node) = doc.get("logs_base_url") else { let Some(node) = doc.get("logs_base_url") else {
return Ok(None); return Ok(None);
}; };
let Some(value) = node.entries().first().and_then(|entry| entry.value().as_string()) else { let Some(value) = node
.entries()
.first()
.and_then(|entry| entry.value().as_string())
else {
return Ok(None); return Ok(None);
}; };
if value.trim().is_empty() { if value.trim().is_empty() {
@ -222,8 +227,7 @@ fn load_logs_base_url() -> Result<Option<String>> {
} }
async fn cmd_jobs(base_url: &str, repo: Option<&str>) -> Result<()> { async fn cmd_jobs(base_url: &str, repo: Option<&str>) -> Result<()> {
let client = LogsClient::new(base_url) let client = LogsClient::new(base_url).map_err(|err| miette::Report::msg(err.to_string()))?;
.map_err(|err| miette::Report::msg(err.to_string()))?;
let groups = client let groups = client
.list_jobs() .list_jobs()
.await .await
@ -254,8 +258,7 @@ async fn cmd_jobs(base_url: &str, repo: Option<&str>) -> Result<()> {
} }
async fn cmd_logs(base_url: &str, job_id: &str, category: Option<&str>) -> Result<()> { async fn cmd_logs(base_url: &str, job_id: &str, category: Option<&str>) -> Result<()> {
let client = LogsClient::new(base_url) let client = LogsClient::new(base_url).map_err(|err| miette::Report::msg(err.to_string()))?;
.map_err(|err| miette::Report::msg(err.to_string()))?;
let request_id = uuid::Uuid::parse_str(job_id).into_diagnostic()?; let request_id = uuid::Uuid::parse_str(job_id).into_diagnostic()?;
let text = if let Some(cat) = category { let text = if let Some(cat) = category {
client client
@ -324,7 +327,9 @@ fn setup_terminal() -> Result<Terminal<ratatui::backend::CrosstermBackend<Stdout
Terminal::new(backend).into_diagnostic() Terminal::new(backend).into_diagnostic()
} }
fn restore_terminal(mut terminal: Terminal<ratatui::backend::CrosstermBackend<Stdout>>) -> Result<()> { fn restore_terminal(
mut terminal: Terminal<ratatui::backend::CrosstermBackend<Stdout>>,
) -> Result<()> {
terminal::disable_raw_mode().into_diagnostic()?; terminal::disable_raw_mode().into_diagnostic()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen).into_diagnostic()?; execute!(terminal.backend_mut(), LeaveAlternateScreen).into_diagnostic()?;
terminal.show_cursor().into_diagnostic()?; terminal.show_cursor().into_diagnostic()?;
@ -333,10 +338,7 @@ fn restore_terminal(mut terminal: Terminal<ratatui::backend::CrosstermBackend<St
fn filter_groups_by_repo(groups: Vec<JobGroup>, repo: Option<&str>) -> Vec<JobGroup> { fn filter_groups_by_repo(groups: Vec<JobGroup>, repo: Option<&str>) -> Vec<JobGroup> {
if let Some(repo) = repo { if let Some(repo) = repo {
groups groups.into_iter().filter(|g| g.repo_url == repo).collect()
.into_iter()
.filter(|g| g.repo_url == repo)
.collect()
} else { } else {
groups groups
} }
@ -434,20 +436,14 @@ impl TuiApp {
.map_err(|err| miette::Report::msg(err.to_string()))?; .map_err(|err| miette::Report::msg(err.to_string()))?;
let mut repos_map: BTreeMap<String, Vec<JobEntry>> = BTreeMap::new(); let mut repos_map: BTreeMap<String, Vec<JobEntry>> = BTreeMap::new();
for group in groups { for group in groups {
let entries = group let entries = group.jobs.into_iter().map(|job| JobEntry {
.jobs request_id: job.request_id,
.into_iter() commit_sha: group.commit_sha.clone(),
.map(|job| JobEntry { state: job.state,
request_id: job.request_id, runs_on: job.runs_on,
commit_sha: group.commit_sha.clone(), updated_at: job.updated_at,
state: job.state, });
runs_on: job.runs_on, repos_map.entry(group.repo_url).or_default().extend(entries);
updated_at: job.updated_at,
});
repos_map
.entry(group.repo_url)
.or_default()
.extend(entries);
} }
let mut repos: Vec<String> = repos_map.keys().cloned().collect(); let mut repos: Vec<String> = repos_map.keys().cloned().collect();
repos.sort(); repos.sort();
@ -488,20 +484,13 @@ impl TuiApp {
}; };
match self.client.list_log_categories(job.request_id).await { match self.client.list_log_categories(job.request_id).await {
Ok(categories) => { Ok(categories) => {
self.logs_categories = categories self.logs_categories = categories.into_iter().map(|c| c.category).collect();
.into_iter()
.map(|c| c.category)
.collect();
if self.logs_categories.is_empty() { if self.logs_categories.is_empty() {
self.logs_text = "No logs available.".to_string(); self.logs_text = "No logs available.".to_string();
self.logs_category = None; self.logs_category = None;
self.selected_category = 0; self.selected_category = 0;
} else { } else {
if let Some(idx) = self if let Some(idx) = self.logs_categories.iter().position(|c| c == "default") {
.logs_categories
.iter()
.position(|c| c == "default")
{
self.selected_category = idx; self.selected_category = idx;
} else if self.selected_category >= self.logs_categories.len() { } else if self.selected_category >= self.logs_categories.len() {
self.selected_category = 0; self.selected_category = 0;
@ -624,7 +613,11 @@ impl TuiApp {
let size = frame.area(); let size = frame.area();
let layout = Layout::default() let layout = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(5), Constraint::Length(2)]) .constraints([
Constraint::Length(3),
Constraint::Min(5),
Constraint::Length(2),
])
.split(size); .split(size);
self.draw_header(frame, layout[0]); self.draw_header(frame, layout[0]);
self.draw_body(frame, layout[1]); self.draw_body(frame, layout[1]);
@ -796,9 +789,7 @@ fn parse_sgr_params(bytes: &[u8]) -> Vec<u16> {
return Vec::new(); return Vec::new();
} }
let s = String::from_utf8_lossy(bytes); let s = String::from_utf8_lossy(bytes);
s.split(';') s.split(';').filter_map(|p| p.parse::<u16>().ok()).collect()
.filter_map(|p| p.parse::<u16>().ok())
.collect()
} }
fn apply_sgr(style: &mut Style, params: &[u16]) { fn apply_sgr(style: &mut Style, params: &[u16]) {

View file

@ -59,7 +59,11 @@ struct Opts {
hookdeck_signing_secret: Option<String>, hookdeck_signing_secret: Option<String>,
/// GitHub API base (e.g., https://api.github.com) /// GitHub API base (e.g., https://api.github.com)
#[arg(long, env = "GITHUB_API_BASE", default_value = "https://api.github.com")] #[arg(
long,
env = "GITHUB_API_BASE",
default_value = "https://api.github.com"
)]
github_api_base: String, github_api_base: String,
/// GitHub App ID /// GitHub App ID
@ -186,7 +190,9 @@ async fn main() -> Result<()> {
.hookdeck_signing_secret .hookdeck_signing_secret
.or_else(|| std::env::var("HOOKDECK_SECRET").ok()); .or_else(|| std::env::var("HOOKDECK_SECRET").ok());
if webhook_secret.is_none() && hookdeck_signing_secret.is_none() { if webhook_secret.is_none() && hookdeck_signing_secret.is_none() {
warn!("GITHUB_WEBHOOK_SECRET and HOOKDECK_SIGNING_SECRET are not set — accepting webhooks without signature validation (dev mode)"); warn!(
"GITHUB_WEBHOOK_SECRET and HOOKDECK_SIGNING_SECRET are not set — accepting webhooks without signature validation (dev mode)"
);
} }
let app_key_pem = match (&opts.app_key_pem, &opts.app_key_path) { let app_key_pem = match (&opts.app_key_pem, &opts.app_key_path) {
@ -296,7 +302,9 @@ async fn get_installation_token(state: &AppState, installation_id: u64) -> Resul
return Ok(None); return Ok(None);
} }
let v: serde_json::Value = resp.json().await.into_diagnostic()?; let v: serde_json::Value = resp.json().await.into_diagnostic()?;
Ok(v.get("token").and_then(|t| t.as_str()).map(|s| s.to_string())) Ok(v.get("token")
.and_then(|t| t.as_str())
.map(|s| s.to_string()))
} }
async fn get_installation_id_for_repo( async fn get_installation_id_for_repo(
@ -487,15 +495,13 @@ async fn handle_webhook(
Ok(extract) => handle_pull_request(state, extract.payload).await, Ok(extract) => handle_pull_request(state, extract.payload).await,
Err(err) => map_webhook_error(err), Err(err) => map_webhook_error(err),
}, },
"ping" => match webhook::verify_signatures( "ping" => {
&headers, match webhook::verify_signatures(&headers, body.as_ref(), &checks, SignaturePolicy::Any)
body.as_ref(), {
&checks, Ok(_) => StatusCode::OK,
SignaturePolicy::Any, Err(err) => map_webhook_error(WebhookError::Signature(err)),
) { }
Ok(_) => StatusCode::OK, }
Err(err) => map_webhook_error(WebhookError::Signature(err)),
},
_ => StatusCode::NO_CONTENT, _ => StatusCode::NO_CONTENT,
} }
} }
@ -1290,16 +1296,19 @@ async fn handle_job_result(state: &AppState, jobres: &common::messages::JobResul
} }
} }
let (owner, repo) = match ( let (owner, repo) = match (jobres.repo_owner.as_ref(), jobres.repo_name.as_ref()) {
jobres.repo_owner.as_ref(),
jobres.repo_name.as_ref(),
) {
(Some(o), Some(r)) => (Some(o.clone()), Some(r.clone())), (Some(o), Some(r)) => (Some(o.clone()), Some(r.clone())),
_ => parse_owner_repo(&jobres.repo_url).map(|(o, r)| (Some(o), Some(r))).unwrap_or((None, None)), _ => parse_owner_repo(&jobres.repo_url)
.map(|(o, r)| (Some(o), Some(r)))
.unwrap_or((None, None)),
}; };
let Some(owner) = owner else { return Ok(()); }; let Some(owner) = owner else {
let Some(repo) = repo else { return Ok(()); }; return Ok(());
};
let Some(repo) = repo else {
return Ok(());
};
let installation_id = match get_installation_id_for_repo(state, &owner, &repo).await? { let installation_id = match get_installation_id_for_repo(state, &owner, &repo).await? {
Some(id) => id, Some(id) => id,
@ -1310,14 +1319,25 @@ async fn handle_job_result(state: &AppState, jobres: &common::messages::JobResul
}; };
let conclusion = if jobres.success { "success" } else { "failure" }; let conclusion = if jobres.success { "success" } else { "failure" };
let summary = jobres let summary = jobres.summary.clone().unwrap_or_else(|| {
.summary if jobres.success {
.clone() "Job succeeded"
.unwrap_or_else(|| if jobres.success { "Job succeeded" } else { "Job failed" }.to_string()); } else {
"Job failed"
}
.to_string()
});
let external_id = jobres.request_id.to_string(); let external_id = jobres.request_id.to_string();
if let Some(check_run_id) = if let Some(check_run_id) = find_check_run_id(
find_check_run_id(state, &token, &owner, &repo, &jobres.commit_sha, &external_id).await? state,
&token,
&owner,
&repo,
&jobres.commit_sha,
&external_id,
)
.await?
{ {
let _ = update_check_run( let _ = update_check_run(
state, state,

View file

@ -96,7 +96,10 @@ impl LogsClient {
} }
pub async fn list_jobs(&self) -> Result<Vec<JobGroup>> { pub async fn list_jobs(&self) -> Result<Vec<JobGroup>> {
let url = self.base_url.join("jobs").map_err(LogsClientError::InvalidBaseUrl)?; let url = self
.base_url
.join("jobs")
.map_err(LogsClientError::InvalidBaseUrl)?;
self.get_json(url).await self.get_json(url).await
} }
@ -105,11 +108,7 @@ impl LogsClient {
self.get_json(url).await self.get_json(url).await
} }
pub async fn get_logs_by_category( pub async fn get_logs_by_category(&self, request_id: Uuid, category: &str) -> Result<String> {
&self,
request_id: Uuid,
category: &str,
) -> Result<String> {
let url = self.job_logs_url(request_id, Some(category))?; let url = self.job_logs_url(request_id, Some(category))?;
self.get_text(url).await self.get_text(url).await
} }
@ -129,15 +128,15 @@ impl LogsClient {
} }
async fn get_json<T: DeserializeOwned>(&self, url: Url) -> Result<T> { async fn get_json<T: DeserializeOwned>(&self, url: Url) -> Result<T> {
let resp = self let resp =
.client self.client
.get(url.clone()) .get(url.clone())
.send() .send()
.await .await
.map_err(|e| LogsClientError::Request { .map_err(|e| LogsClientError::Request {
url: url.to_string(), url: url.to_string(),
source: e, source: e,
})?; })?;
let status = resp.status(); let status = resp.status();
let bytes = resp.bytes().await.map_err(|e| LogsClientError::Request { let bytes = resp.bytes().await.map_err(|e| LogsClientError::Request {
url: url.to_string(), url: url.to_string(),
@ -158,15 +157,15 @@ impl LogsClient {
} }
async fn get_text(&self, url: Url) -> Result<String> { async fn get_text(&self, url: Url) -> Result<String> {
let resp = self let resp =
.client self.client
.get(url.clone()) .get(url.clone())
.send() .send()
.await .await
.map_err(|e| LogsClientError::Request { .map_err(|e| LogsClientError::Request {
url: url.to_string(), url: url.to_string(),
source: e, source: e,
})?; })?;
let status = resp.status(); let status = resp.status();
let bytes = resp.bytes().await.map_err(|e| LogsClientError::Request { let bytes = resp.bytes().await.map_err(|e| LogsClientError::Request {
url: url.to_string(), url: url.to_string(),
@ -186,9 +185,9 @@ impl LogsClient {
fn job_logs_url(&self, request_id: Uuid, category: Option<&str>) -> Result<Url> { fn job_logs_url(&self, request_id: Uuid, category: Option<&str>) -> Result<Url> {
let mut url = self.base_url.clone(); let mut url = self.base_url.clone();
{ {
let mut segments = url let mut segments = url.path_segments_mut().map_err(|_| {
.path_segments_mut() LogsClientError::InvalidBaseUrl(url::ParseError::RelativeUrlWithoutBase)
.map_err(|_| LogsClientError::InvalidBaseUrl(url::ParseError::RelativeUrlWithoutBase))?; })?;
segments.clear(); segments.clear();
segments.extend(&["jobs", &request_id.to_string(), "logs"]); segments.extend(&["jobs", &request_id.to_string(), "logs"]);
if let Some(cat) = category { if let Some(cat) = category {

View file

@ -1,10 +1,10 @@
use std::net::IpAddr;
use base64::Engine; use base64::Engine;
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use http::HeaderMap; use http::HeaderMap;
use miette::Diagnostic;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use sha2::Sha256; use sha2::Sha256;
use miette::Diagnostic; use std::net::IpAddr;
use thiserror::Error; use thiserror::Error;
type HmacSha256 = Hmac<Sha256>; type HmacSha256 = Hmac<Sha256>;
@ -102,13 +102,17 @@ pub enum SignatureError {
#[error("missing signature headers for {0:?}")] #[error("missing signature headers for {0:?}")]
#[diagnostic( #[diagnostic(
code("webhook.signature.missing"), code("webhook.signature.missing"),
help("Ensure the webhook sender or proxy includes the expected signature header(s) for one of the enabled signature sources.") help(
"Ensure the webhook sender or proxy includes the expected signature header(s) for one of the enabled signature sources."
)
)] )]
Missing(Vec<SignatureSource>), Missing(Vec<SignatureSource>),
#[error("invalid signature for {0:?}")] #[error("invalid signature for {0:?}")]
#[diagnostic( #[diagnostic(
code("webhook.signature.invalid"), code("webhook.signature.invalid"),
help("Check that the signing secret matches the sender/proxy configuration and that the request body is unmodified.") help(
"Check that the signing secret matches the sender/proxy configuration and that the request body is unmodified."
)
)] )]
Invalid(Vec<SignatureSource>), Invalid(Vec<SignatureSource>),
} }
@ -124,7 +128,9 @@ enum SignatureCheckError {
#[error("signature verification failed")] #[error("signature verification failed")]
#[diagnostic( #[diagnostic(
code("webhook.signature.verify_failed"), code("webhook.signature.verify_failed"),
help("Confirm the signing secret and ensure the request body is not modified by intermediaries.") help(
"Confirm the signing secret and ensure the request body is not modified by intermediaries."
)
)] )]
Invalid, Invalid,
} }
@ -214,7 +220,10 @@ impl WebhookInfo {
delivery: header_string(headers, "X-GitHub-Delivery"), delivery: header_string(headers, "X-GitHub-Delivery"),
hook_id: header_u64(headers, "X-GitHub-Hook-Id"), hook_id: header_u64(headers, "X-GitHub-Hook-Id"),
installation_target_id: header_u64(headers, "X-GitHub-Hook-Installation-Target-Id"), installation_target_id: header_u64(headers, "X-GitHub-Hook-Installation-Target-Id"),
installation_target_type: header_string(headers, "X-GitHub-Hook-Installation-Target-Type"), installation_target_type: header_string(
headers,
"X-GitHub-Hook-Installation-Target-Type",
),
user_agent: header_string(headers, "User-Agent"), user_agent: header_string(headers, "User-Agent"),
}; };
@ -255,7 +264,9 @@ pub enum WebhookError {
#[error("signature verification failed")] #[error("signature verification failed")]
#[diagnostic( #[diagnostic(
code("webhook.signature.failed"), code("webhook.signature.failed"),
help("Provide a valid signature header or disable verification only in trusted development environments.") help(
"Provide a valid signature header or disable verification only in trusted development environments."
)
)] )]
Signature(#[from] SignatureError), Signature(#[from] SignatureError),
#[error("failed to parse json payload")] #[error("failed to parse json payload")]