treewide: address all clippy lints
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I5cf55cc4cb558c3f9f764c71224e87176a6a6964
This commit is contained in:
parent
967d51e867
commit
a127f3f62c
63 changed files with 1790 additions and 1089 deletions
|
|
@ -96,7 +96,7 @@ async fn system_status(
|
|||
.await
|
||||
.map_err(|e| ApiError(fc_common::CiError::Database(e)))?;
|
||||
|
||||
let stats = fc_common::repo::builds::get_stats(pool)
|
||||
let build_stats = fc_common::repo::builds::get_stats(pool)
|
||||
.await
|
||||
.map_err(ApiError)?;
|
||||
let builders = fc_common::repo::remote_builders::count(pool)
|
||||
|
|
@ -112,10 +112,10 @@ async fn system_status(
|
|||
projects_count: projects.0,
|
||||
jobsets_count: jobsets.0,
|
||||
evaluations_count: evaluations.0,
|
||||
builds_pending: stats.pending_builds.unwrap_or(0),
|
||||
builds_running: stats.running_builds.unwrap_or(0),
|
||||
builds_completed: stats.completed_builds.unwrap_or(0),
|
||||
builds_failed: stats.failed_builds.unwrap_or(0),
|
||||
builds_pending: build_stats.pending_builds.unwrap_or(0),
|
||||
builds_running: build_stats.running_builds.unwrap_or(0),
|
||||
builds_completed: build_stats.completed_builds.unwrap_or(0),
|
||||
builds_failed: build_stats.failed_builds.unwrap_or(0),
|
||||
remote_builders: builders,
|
||||
channels_count: channels.0,
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -29,11 +29,8 @@ async fn build_badge(
|
|||
.map_err(ApiError)?;
|
||||
|
||||
let jobset = jobsets.iter().find(|j| j.name == jobset_name);
|
||||
let jobset = match jobset {
|
||||
Some(j) => j,
|
||||
None => {
|
||||
return Ok(shield_svg("build", "not found", "#9f9f9f").into_response());
|
||||
},
|
||||
let Some(jobset) = jobset else {
|
||||
return Ok(shield_svg("build", "not found", "#9f9f9f").into_response());
|
||||
};
|
||||
|
||||
// Get latest evaluation
|
||||
|
|
@ -41,13 +38,10 @@ async fn build_badge(
|
|||
.await
|
||||
.map_err(ApiError)?;
|
||||
|
||||
let eval = match eval {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
return Ok(
|
||||
shield_svg("build", "no evaluations", "#9f9f9f").into_response(),
|
||||
);
|
||||
},
|
||||
let Some(eval) = eval else {
|
||||
return Ok(
|
||||
shield_svg("build", "no evaluations", "#9f9f9f").into_response(),
|
||||
);
|
||||
};
|
||||
|
||||
// Find the build for this job
|
||||
|
|
@ -58,31 +52,24 @@ async fn build_badge(
|
|||
|
||||
let build = builds.iter().find(|b| b.job_name == job_name);
|
||||
|
||||
let (label, color) = match build {
|
||||
Some(b) => {
|
||||
match b.status {
|
||||
fc_common::BuildStatus::Succeeded => ("passing", "#4c1"),
|
||||
fc_common::BuildStatus::Failed => ("failing", "#e05d44"),
|
||||
fc_common::BuildStatus::Running => ("building", "#dfb317"),
|
||||
fc_common::BuildStatus::Pending => ("queued", "#dfb317"),
|
||||
fc_common::BuildStatus::Cancelled => ("cancelled", "#9f9f9f"),
|
||||
fc_common::BuildStatus::DependencyFailed => ("dep failed", "#e05d44"),
|
||||
fc_common::BuildStatus::Aborted => ("aborted", "#9f9f9f"),
|
||||
fc_common::BuildStatus::FailedWithOutput => {
|
||||
("failed output", "#e05d44")
|
||||
},
|
||||
fc_common::BuildStatus::Timeout => ("timeout", "#e05d44"),
|
||||
fc_common::BuildStatus::CachedFailure => ("cached fail", "#e05d44"),
|
||||
fc_common::BuildStatus::UnsupportedSystem => ("unsupported", "#9f9f9f"),
|
||||
fc_common::BuildStatus::LogLimitExceeded => ("log limit", "#e05d44"),
|
||||
fc_common::BuildStatus::NarSizeLimitExceeded => {
|
||||
("nar limit", "#e05d44")
|
||||
},
|
||||
fc_common::BuildStatus::NonDeterministic => ("non-det", "#e05d44"),
|
||||
}
|
||||
},
|
||||
None => ("not found", "#9f9f9f"),
|
||||
};
|
||||
let (label, color) = build.map_or(("not found", "#9f9f9f"), |b| {
|
||||
match b.status {
|
||||
fc_common::BuildStatus::Succeeded => ("passing", "#4c1"),
|
||||
fc_common::BuildStatus::Failed => ("failing", "#e05d44"),
|
||||
fc_common::BuildStatus::Running => ("building", "#dfb317"),
|
||||
fc_common::BuildStatus::Pending => ("queued", "#dfb317"),
|
||||
fc_common::BuildStatus::Cancelled => ("cancelled", "#9f9f9f"),
|
||||
fc_common::BuildStatus::DependencyFailed => ("dep failed", "#e05d44"),
|
||||
fc_common::BuildStatus::Aborted => ("aborted", "#9f9f9f"),
|
||||
fc_common::BuildStatus::FailedWithOutput => ("failed output", "#e05d44"),
|
||||
fc_common::BuildStatus::Timeout => ("timeout", "#e05d44"),
|
||||
fc_common::BuildStatus::CachedFailure => ("cached fail", "#e05d44"),
|
||||
fc_common::BuildStatus::UnsupportedSystem => ("unsupported", "#9f9f9f"),
|
||||
fc_common::BuildStatus::LogLimitExceeded => ("log limit", "#e05d44"),
|
||||
fc_common::BuildStatus::NarSizeLimitExceeded => ("nar limit", "#e05d44"),
|
||||
fc_common::BuildStatus::NonDeterministic => ("non-det", "#e05d44"),
|
||||
}
|
||||
});
|
||||
|
||||
Ok(
|
||||
(
|
||||
|
|
@ -117,24 +104,16 @@ async fn latest_build(
|
|||
.map_err(ApiError)?;
|
||||
|
||||
let jobset = jobsets.iter().find(|j| j.name == jobset_name);
|
||||
let jobset = match jobset {
|
||||
Some(j) => j,
|
||||
None => {
|
||||
return Ok((StatusCode::NOT_FOUND, "Jobset not found").into_response());
|
||||
},
|
||||
let Some(jobset) = jobset else {
|
||||
return Ok((StatusCode::NOT_FOUND, "Jobset not found").into_response());
|
||||
};
|
||||
|
||||
let eval = fc_common::repo::evaluations::get_latest(&state.pool, jobset.id)
|
||||
.await
|
||||
.map_err(ApiError)?;
|
||||
|
||||
let eval = match eval {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
return Ok(
|
||||
(StatusCode::NOT_FOUND, "No evaluations found").into_response(),
|
||||
);
|
||||
},
|
||||
let Some(eval) = eval else {
|
||||
return Ok((StatusCode::NOT_FOUND, "No evaluations found").into_response());
|
||||
};
|
||||
|
||||
let builds =
|
||||
|
|
@ -143,10 +122,10 @@ async fn latest_build(
|
|||
.map_err(ApiError)?;
|
||||
|
||||
let build = builds.iter().find(|b| b.job_name == job_name);
|
||||
match build {
|
||||
Some(b) => Ok(axum::Json(b.clone()).into_response()),
|
||||
None => Ok((StatusCode::NOT_FOUND, "Build not found").into_response()),
|
||||
}
|
||||
build.map_or_else(
|
||||
|| Ok((StatusCode::NOT_FOUND, "Build not found").into_response()),
|
||||
|b| Ok(axum::Json(b.clone()).into_response()),
|
||||
)
|
||||
}
|
||||
|
||||
fn shield_svg(subject: &str, status: &str, color: &str) -> String {
|
||||
|
|
|
|||
|
|
@ -133,10 +133,10 @@ async fn list_build_products(
|
|||
async fn build_stats(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<fc_common::BuildStats>, ApiError> {
|
||||
let stats = fc_common::repo::builds::get_stats(&state.pool)
|
||||
let build_stats = fc_common::repo::builds::get_stats(&state.pool)
|
||||
.await
|
||||
.map_err(ApiError)?;
|
||||
Ok(Json(stats))
|
||||
Ok(Json(build_stats))
|
||||
}
|
||||
|
||||
async fn recent_builds(
|
||||
|
|
@ -242,13 +242,10 @@ async fn download_build_product(
|
|||
},
|
||||
};
|
||||
|
||||
let stdout = match child.stdout.take() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
return Err(ApiError(fc_common::CiError::Build(
|
||||
"Failed to capture output".to_string(),
|
||||
)));
|
||||
},
|
||||
let Some(stdout) = child.stdout.take() else {
|
||||
return Err(ApiError(fc_common::CiError::Build(
|
||||
"Failed to capture output".to_string(),
|
||||
)));
|
||||
};
|
||||
|
||||
let stream = tokio_util::io::ReaderStream::new(stdout);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ fn first_path_info_entry(
|
|||
}
|
||||
}
|
||||
|
||||
/// Look up a store path by its nix hash, checking both build_products and
|
||||
/// Look up a store path by its nix hash, checking both `build_products` and
|
||||
/// builds tables.
|
||||
async fn find_store_path(
|
||||
pool: &sqlx::PgPool,
|
||||
|
|
@ -64,6 +64,8 @@ async fn narinfo(
|
|||
State(state): State<AppState>,
|
||||
Path(hash): Path<String>,
|
||||
) -> Result<Response, ApiError> {
|
||||
use std::fmt::Write;
|
||||
|
||||
if !state.config.cache.enabled {
|
||||
return Ok(StatusCode::NOT_FOUND.into_response());
|
||||
}
|
||||
|
|
@ -97,9 +99,8 @@ async fn narinfo(
|
|||
Err(_) => return Ok(StatusCode::NOT_FOUND.into_response()),
|
||||
};
|
||||
|
||||
let (entry, path_from_info) = match first_path_info_entry(&parsed) {
|
||||
Some(e) => e,
|
||||
None => return Ok(StatusCode::NOT_FOUND.into_response()),
|
||||
let Some((entry, path_from_info)) = first_path_info_entry(&parsed) else {
|
||||
return Ok(StatusCode::NOT_FOUND.into_response());
|
||||
};
|
||||
|
||||
let nar_hash = entry.get("narHash").and_then(|v| v.as_str()).unwrap_or("");
|
||||
|
|
@ -132,8 +133,6 @@ async fn narinfo(
|
|||
|
||||
let file_hash = nar_hash;
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
let refs_joined = refs.join(" ");
|
||||
let mut narinfo_text = format!(
|
||||
"StorePath: {store_path}\nURL: nar/{hash}.nar.zst\nCompression: \
|
||||
|
|
@ -142,10 +141,10 @@ async fn narinfo(
|
|||
);
|
||||
|
||||
if let Some(deriver) = deriver {
|
||||
let _ = write!(narinfo_text, "Deriver: {deriver}\n");
|
||||
let _ = writeln!(narinfo_text, "Deriver: {deriver}");
|
||||
}
|
||||
if let Some(ca) = ca {
|
||||
let _ = write!(narinfo_text, "CA: {ca}\n");
|
||||
let _ = writeln!(narinfo_text, "CA: {ca}");
|
||||
}
|
||||
|
||||
// Optionally sign if secret key is configured
|
||||
|
|
@ -177,9 +176,8 @@ async fn sign_narinfo(narinfo: &str, key_file: &std::path::Path) -> String {
|
|||
.find(|l| l.starts_with("StorePath: "))
|
||||
.and_then(|l| l.strip_prefix("StorePath: "));
|
||||
|
||||
let store_path = match store_path {
|
||||
Some(p) => p,
|
||||
None => return narinfo.to_string(),
|
||||
let Some(store_path) = store_path else {
|
||||
return narinfo.to_string();
|
||||
};
|
||||
|
||||
let output = Command::new("nix")
|
||||
|
|
@ -260,9 +258,8 @@ async fn serve_nar_zst(
|
|||
))
|
||||
})?;
|
||||
|
||||
let nix_stdout = match nix_child.stdout.take() {
|
||||
Some(s) => s,
|
||||
None => return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()),
|
||||
let Some(nix_stdout) = nix_child.stdout.take() else {
|
||||
return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response());
|
||||
};
|
||||
|
||||
let mut zstd_child = Command::new("zstd")
|
||||
|
|
@ -278,9 +275,8 @@ async fn serve_nar_zst(
|
|||
))
|
||||
})?;
|
||||
|
||||
let zstd_stdout = match zstd_child.stdout.take() {
|
||||
Some(s) => s,
|
||||
None => return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()),
|
||||
let Some(zstd_stdout) = zstd_child.stdout.take() else {
|
||||
return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response());
|
||||
};
|
||||
|
||||
let stream = tokio_util::io::ReaderStream::new(zstd_stdout);
|
||||
|
|
@ -320,14 +316,12 @@ async fn serve_nar(
|
|||
.kill_on_drop(true)
|
||||
.spawn();
|
||||
|
||||
let mut child = match child {
|
||||
Ok(c) => c,
|
||||
Err(_) => return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()),
|
||||
let Ok(mut child) = child else {
|
||||
return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response());
|
||||
};
|
||||
|
||||
let stdout = match child.stdout.take() {
|
||||
Some(s) => s,
|
||||
None => return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()),
|
||||
let Some(stdout) = child.stdout.take() else {
|
||||
return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response());
|
||||
};
|
||||
|
||||
let stream = tokio_util::io::ReaderStream::new(stdout);
|
||||
|
|
@ -343,7 +337,7 @@ async fn serve_nar(
|
|||
)
|
||||
}
|
||||
|
||||
/// Combined NAR handler — dispatches to zstd or plain based on suffix.
|
||||
/// Dispatches to zstd or plain based on suffix.
|
||||
/// GET /nix-cache/nar/{hash} where hash includes .nar.zst or .nar suffix
|
||||
async fn serve_nar_combined(
|
||||
state: State<AppState>,
|
||||
|
|
|
|||
|
|
@ -63,18 +63,15 @@ async fn create_channel(
|
|||
// Catch-up: if the jobset already has a completed evaluation, promote now
|
||||
if let Ok(Some(eval)) =
|
||||
fc_common::repo::evaluations::get_latest(&state.pool, jobset_id).await
|
||||
&& eval.status == fc_common::models::EvaluationStatus::Completed
|
||||
&& let Err(e) = fc_common::repo::channels::auto_promote_if_complete(
|
||||
&state.pool,
|
||||
jobset_id,
|
||||
eval.id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
if eval.status == fc_common::models::EvaluationStatus::Completed {
|
||||
if let Err(e) = fc_common::repo::channels::auto_promote_if_complete(
|
||||
&state.pool,
|
||||
jobset_id,
|
||||
eval.id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(jobset_id = %jobset_id, "Failed to auto-promote channel: {e}");
|
||||
}
|
||||
}
|
||||
tracing::warn!(jobset_id = %jobset_id, "Failed to auto-promote channel: {e}");
|
||||
}
|
||||
|
||||
// Re-fetch to include any promotion
|
||||
|
|
@ -159,13 +156,12 @@ async fn nixexprs_tarball(
|
|||
let _ = writeln!(nix_src, "in {{");
|
||||
|
||||
for build in &succeeded {
|
||||
let output_path = match &build.build_output_path {
|
||||
Some(p) => p,
|
||||
None => continue,
|
||||
let Some(output_path) = &build.build_output_path else {
|
||||
continue;
|
||||
};
|
||||
let system = build.system.as_deref().unwrap_or("x86_64-linux");
|
||||
// Sanitize job_name for use as a Nix attribute (replace dots/slashes)
|
||||
let attr_name = build.job_name.replace('.', "-").replace('/', "-");
|
||||
let attr_name = build.job_name.replace(['.', '/'], "-");
|
||||
let _ = writeln!(
|
||||
nix_src,
|
||||
" \"{attr_name}\" = mkFakeDerivation {{ name = \"{}\"; system = \
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ struct BuildView {
|
|||
log_url: String,
|
||||
}
|
||||
|
||||
/// Enhanced build view for queue page with elapsed time and builder info
|
||||
/// Queue page build info with elapsed time and builder details
|
||||
struct QueueBuildView {
|
||||
id: Uuid,
|
||||
job_name: String,
|
||||
|
|
@ -379,7 +379,7 @@ struct ChannelsTemplate {
|
|||
channels: Vec<Channel>,
|
||||
}
|
||||
|
||||
/// Enhanced builder view with load and activity info
|
||||
/// Builder info with load and activity metrics
|
||||
struct BuilderView {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
|
|
@ -455,7 +455,7 @@ async fn home(
|
|||
State(state): State<AppState>,
|
||||
extensions: Extensions,
|
||||
) -> Html<String> {
|
||||
let stats = fc_common::repo::builds::get_stats(&state.pool)
|
||||
let build_stats = fc_common::repo::builds::get_stats(&state.pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let builds = fc_common::repo::builds::list_recent(&state.pool, 10)
|
||||
|
|
@ -499,13 +499,13 @@ async fn home(
|
|||
last_eval = Some(e);
|
||||
}
|
||||
}
|
||||
let (status, class, time) = match &last_eval {
|
||||
Some(e) => {
|
||||
let (status, class, time) = last_eval.as_ref().map_or_else(
|
||||
|| ("-".into(), "pending".into(), "-".into()),
|
||||
|e| {
|
||||
let (t, c) = eval_badge(&e.status);
|
||||
(t, c, e.evaluation_time.format("%Y-%m-%d %H:%M").to_string())
|
||||
},
|
||||
None => ("-".into(), "pending".into(), "-".into()),
|
||||
};
|
||||
);
|
||||
project_summaries.push(ProjectSummaryView {
|
||||
id: p.id,
|
||||
name: p.name.clone(),
|
||||
|
|
@ -517,11 +517,11 @@ async fn home(
|
|||
}
|
||||
|
||||
let tmpl = HomeTemplate {
|
||||
total_builds: stats.total_builds.unwrap_or(0),
|
||||
completed_builds: stats.completed_builds.unwrap_or(0),
|
||||
failed_builds: stats.failed_builds.unwrap_or(0),
|
||||
running_builds: stats.running_builds.unwrap_or(0),
|
||||
pending_builds: stats.pending_builds.unwrap_or(0),
|
||||
total_builds: build_stats.total_builds.unwrap_or(0),
|
||||
completed_builds: build_stats.completed_builds.unwrap_or(0),
|
||||
failed_builds: build_stats.failed_builds.unwrap_or(0),
|
||||
running_builds: build_stats.running_builds.unwrap_or(0),
|
||||
pending_builds: build_stats.pending_builds.unwrap_or(0),
|
||||
recent_builds: builds.iter().map(build_view).collect(),
|
||||
recent_evals: evals.iter().map(eval_view).collect(),
|
||||
projects: project_summaries,
|
||||
|
|
@ -581,9 +581,9 @@ async fn project_page(
|
|||
Path(id): Path<Uuid>,
|
||||
extensions: Extensions,
|
||||
) -> Html<String> {
|
||||
let project = match fc_common::repo::projects::get(&state.pool, id).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return Html("Project not found".to_string()),
|
||||
let Ok(project) = fc_common::repo::projects::get(&state.pool, id).await
|
||||
else {
|
||||
return Html("Project not found".to_string());
|
||||
};
|
||||
let jobsets =
|
||||
fc_common::repo::jobsets::list_for_project(&state.pool, id, 100, 0)
|
||||
|
|
@ -604,7 +604,7 @@ async fn project_page(
|
|||
.unwrap_or_default();
|
||||
evals.append(&mut js_evals);
|
||||
}
|
||||
evals.sort_by(|a, b| b.evaluation_time.cmp(&a.evaluation_time));
|
||||
evals.sort_by_key(|e| std::cmp::Reverse(e.evaluation_time));
|
||||
evals.truncate(10);
|
||||
|
||||
let tmpl = ProjectTemplate {
|
||||
|
|
@ -625,18 +625,13 @@ async fn jobset_page(
|
|||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Html<String> {
|
||||
let jobset = match fc_common::repo::jobsets::get(&state.pool, id).await {
|
||||
Ok(j) => j,
|
||||
Err(_) => return Html("Jobset not found".to_string()),
|
||||
let Ok(jobset) = fc_common::repo::jobsets::get(&state.pool, id).await else {
|
||||
return Html("Jobset not found".to_string());
|
||||
};
|
||||
let project = match fc_common::repo::projects::get(
|
||||
&state.pool,
|
||||
jobset.project_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(_) => return Html("Project not found".to_string()),
|
||||
let Ok(project) =
|
||||
fc_common::repo::projects::get(&state.pool, jobset.project_id).await
|
||||
else {
|
||||
return Html("Project not found".to_string());
|
||||
};
|
||||
|
||||
let evals = fc_common::repo::evaluations::list_filtered(
|
||||
|
|
@ -769,24 +764,20 @@ async fn evaluation_page(
|
|||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Html<String> {
|
||||
let eval = match fc_common::repo::evaluations::get(&state.pool, id).await {
|
||||
Ok(e) => e,
|
||||
Err(_) => return Html("Evaluation not found".to_string()),
|
||||
let Ok(eval) = fc_common::repo::evaluations::get(&state.pool, id).await
|
||||
else {
|
||||
return Html("Evaluation not found".to_string());
|
||||
};
|
||||
|
||||
let jobset =
|
||||
match fc_common::repo::jobsets::get(&state.pool, eval.jobset_id).await {
|
||||
Ok(j) => j,
|
||||
Err(_) => return Html("Jobset not found".to_string()),
|
||||
};
|
||||
let project = match fc_common::repo::projects::get(
|
||||
&state.pool,
|
||||
jobset.project_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(_) => return Html("Project not found".to_string()),
|
||||
let Ok(jobset) =
|
||||
fc_common::repo::jobsets::get(&state.pool, eval.jobset_id).await
|
||||
else {
|
||||
return Html("Jobset not found".to_string());
|
||||
};
|
||||
let Ok(project) =
|
||||
fc_common::repo::projects::get(&state.pool, jobset.project_id).await
|
||||
else {
|
||||
return Html("Project not found".to_string());
|
||||
};
|
||||
|
||||
let builds = fc_common::repo::builds::list_filtered(
|
||||
|
|
@ -919,31 +910,24 @@ async fn build_page(
|
|||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Html<String> {
|
||||
let build = match fc_common::repo::builds::get(&state.pool, id).await {
|
||||
Ok(b) => b,
|
||||
Err(_) => return Html("Build not found".to_string()),
|
||||
let Ok(build) = fc_common::repo::builds::get(&state.pool, id).await else {
|
||||
return Html("Build not found".to_string());
|
||||
};
|
||||
|
||||
let eval =
|
||||
match fc_common::repo::evaluations::get(&state.pool, build.evaluation_id)
|
||||
.await
|
||||
{
|
||||
Ok(e) => e,
|
||||
Err(_) => return Html("Evaluation not found".to_string()),
|
||||
};
|
||||
let jobset =
|
||||
match fc_common::repo::jobsets::get(&state.pool, eval.jobset_id).await {
|
||||
Ok(j) => j,
|
||||
Err(_) => return Html("Jobset not found".to_string()),
|
||||
};
|
||||
let project = match fc_common::repo::projects::get(
|
||||
&state.pool,
|
||||
jobset.project_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(_) => return Html("Project not found".to_string()),
|
||||
let Ok(eval) =
|
||||
fc_common::repo::evaluations::get(&state.pool, build.evaluation_id).await
|
||||
else {
|
||||
return Html("Evaluation not found".to_string());
|
||||
};
|
||||
let Ok(jobset) =
|
||||
fc_common::repo::jobsets::get(&state.pool, eval.jobset_id).await
|
||||
else {
|
||||
return Html("Jobset not found".to_string());
|
||||
};
|
||||
let Ok(project) =
|
||||
fc_common::repo::projects::get(&state.pool, jobset.project_id).await
|
||||
else {
|
||||
return Html("Project not found".to_string());
|
||||
};
|
||||
|
||||
let eval_commit_short = if eval.commit_hash.len() > 12 {
|
||||
|
|
@ -1016,12 +1000,10 @@ async fn queue_page(State(state): State<AppState>) -> Html<String> {
|
|||
let running_builds: Vec<QueueBuildView> = running
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let elapsed = if let Some(started) = b.started_at {
|
||||
let elapsed = b.started_at.map_or_else(String::new, |started| {
|
||||
let dur = chrono::Utc::now() - started;
|
||||
format_elapsed(dur.num_seconds())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
});
|
||||
let builder_name =
|
||||
b.builder_id.and_then(|id| builder_map.get(&id).cloned());
|
||||
QueueBuildView {
|
||||
|
|
@ -1114,7 +1096,7 @@ async fn admin_page(
|
|||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap_or((0,));
|
||||
let stats = fc_common::repo::builds::get_stats(pool)
|
||||
let build_stats = fc_common::repo::builds::get_stats(pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let builders_count = fc_common::repo::remote_builders::count(pool)
|
||||
|
|
@ -1129,10 +1111,10 @@ async fn admin_page(
|
|||
projects_count: projects.0,
|
||||
jobsets_count: jobsets.0,
|
||||
evaluations_count: evaluations.0,
|
||||
builds_pending: stats.pending_builds.unwrap_or(0),
|
||||
builds_running: stats.running_builds.unwrap_or(0),
|
||||
builds_completed: stats.completed_builds.unwrap_or(0),
|
||||
builds_failed: stats.failed_builds.unwrap_or(0),
|
||||
builds_pending: build_stats.pending_builds.unwrap_or(0),
|
||||
builds_running: build_stats.running_builds.unwrap_or(0),
|
||||
builds_completed: build_stats.completed_builds.unwrap_or(0),
|
||||
builds_failed: build_stats.failed_builds.unwrap_or(0),
|
||||
remote_builders: builders_count,
|
||||
channels_count: channels.0,
|
||||
};
|
||||
|
|
@ -1381,36 +1363,28 @@ async fn logout_action(
|
|||
.and_then(|v| v.to_str().ok())
|
||||
{
|
||||
// Check for user session
|
||||
if let Some(session_id) = cookie_header
|
||||
.split(';')
|
||||
.filter_map(|pair| {
|
||||
let pair = pair.trim();
|
||||
let (k, v) = pair.split_once('=')?;
|
||||
if k.trim() == "fc_user_session" {
|
||||
Some(v.trim().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.next()
|
||||
{
|
||||
if let Some(session_id) = cookie_header.split(';').find_map(|pair| {
|
||||
let pair = pair.trim();
|
||||
let (k, v) = pair.split_once('=')?;
|
||||
if k.trim() == "fc_user_session" {
|
||||
Some(v.trim().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
state.sessions.remove(&session_id);
|
||||
}
|
||||
|
||||
// Check for legacy API key session
|
||||
if let Some(session_id) = cookie_header
|
||||
.split(';')
|
||||
.filter_map(|pair| {
|
||||
let pair = pair.trim();
|
||||
let (k, v) = pair.split_once('=')?;
|
||||
if k.trim() == "fc_session" {
|
||||
Some(v.trim().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.next()
|
||||
{
|
||||
if let Some(session_id) = cookie_header.split(';').find_map(|pair| {
|
||||
let pair = pair.trim();
|
||||
let (k, v) = pair.split_once('=')?;
|
||||
if k.trim() == "fc_session" {
|
||||
Some(v.trim().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
state.sessions.remove(&session_id);
|
||||
}
|
||||
}
|
||||
|
|
@ -1556,12 +1530,13 @@ async fn starred_page(
|
|||
Vec::new()
|
||||
};
|
||||
|
||||
if let Some(build) = builds.first() {
|
||||
let (text, class) = status_badge(&build.status);
|
||||
(text, class, Some(build.id))
|
||||
} else {
|
||||
("No builds".to_string(), "pending".to_string(), None)
|
||||
}
|
||||
builds.first().map_or_else(
|
||||
|| ("No builds".to_string(), "pending".to_string(), None),
|
||||
|build| {
|
||||
let (text, class) = status_badge(&build.status);
|
||||
(text, class, Some(build.id))
|
||||
},
|
||||
)
|
||||
} else {
|
||||
("No builds".to_string(), "pending".to_string(), None)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -93,9 +93,9 @@ async fn stream_build_log(
|
|||
if active_path.exists() { active_path.clone() } else { final_path.clone() }
|
||||
};
|
||||
|
||||
let file = if let Ok(f) = tokio::fs::File::open(&path).await { f } else {
|
||||
yield Ok(Event::default().data("Failed to open log file"));
|
||||
return;
|
||||
let Ok(file) = tokio::fs::File::open(&path).await else {
|
||||
yield Ok(Event::default().data("Failed to open log file"));
|
||||
return;
|
||||
};
|
||||
|
||||
let mut reader = BufReader::new(file);
|
||||
|
|
@ -106,7 +106,7 @@ async fn stream_build_log(
|
|||
line.clear();
|
||||
match reader.read_line(&mut line).await {
|
||||
Ok(0) => {
|
||||
// EOF — check if build is still running
|
||||
// EOF - check if build is still running
|
||||
consecutive_empty += 1;
|
||||
if consecutive_empty > 5 {
|
||||
// Check build status
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ struct TimeseriesQuery {
|
|||
bucket: i32,
|
||||
}
|
||||
|
||||
fn default_hours() -> i32 {
|
||||
const fn default_hours() -> i32 {
|
||||
24
|
||||
}
|
||||
|
||||
fn default_bucket() -> i32 {
|
||||
const fn default_bucket() -> i32 {
|
||||
60
|
||||
}
|
||||
|
||||
|
|
@ -64,21 +64,19 @@ fn escape_prometheus_label(s: &str) -> String {
|
|||
}
|
||||
|
||||
async fn prometheus_metrics(State(state): State<AppState>) -> Response {
|
||||
let stats = match fc_common::repo::builds::get_stats(&state.pool).await {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||
},
|
||||
use std::fmt::Write;
|
||||
|
||||
let Ok(build_stats) = fc_common::repo::builds::get_stats(&state.pool).await
|
||||
else {
|
||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||
};
|
||||
|
||||
let eval_count: i64 =
|
||||
match sqlx::query_as::<_, (i64,)>("SELECT COUNT(*) FROM evaluations")
|
||||
sqlx::query_as::<_, (i64,)>("SELECT COUNT(*) FROM evaluations")
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(row) => row.0,
|
||||
Err(_) => 0,
|
||||
};
|
||||
.ok()
|
||||
.map_or(0, |row| row.0);
|
||||
|
||||
let eval_by_status: Vec<(String, i64)> = sqlx::query_as(
|
||||
"SELECT status::text, COUNT(*) FROM evaluations GROUP BY status",
|
||||
|
|
@ -124,8 +122,6 @@ async fn prometheus_metrics(State(state): State<AppState>) -> Response {
|
|||
.await
|
||||
.unwrap_or((None, None, None));
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut output = String::with_capacity(2048);
|
||||
|
||||
// Build counts by status
|
||||
|
|
@ -134,27 +130,27 @@ async fn prometheus_metrics(State(state): State<AppState>) -> Response {
|
|||
let _ = writeln!(
|
||||
output,
|
||||
"fc_builds_total{{status=\"succeeded\"}} {}",
|
||||
stats.completed_builds.unwrap_or(0)
|
||||
build_stats.completed_builds.unwrap_or(0)
|
||||
);
|
||||
let _ = writeln!(
|
||||
output,
|
||||
"fc_builds_total{{status=\"failed\"}} {}",
|
||||
stats.failed_builds.unwrap_or(0)
|
||||
build_stats.failed_builds.unwrap_or(0)
|
||||
);
|
||||
let _ = writeln!(
|
||||
output,
|
||||
"fc_builds_total{{status=\"running\"}} {}",
|
||||
stats.running_builds.unwrap_or(0)
|
||||
build_stats.running_builds.unwrap_or(0)
|
||||
);
|
||||
let _ = writeln!(
|
||||
output,
|
||||
"fc_builds_total{{status=\"pending\"}} {}",
|
||||
stats.pending_builds.unwrap_or(0)
|
||||
build_stats.pending_builds.unwrap_or(0)
|
||||
);
|
||||
let _ = writeln!(
|
||||
output,
|
||||
"fc_builds_total{{status=\"all\"}} {}",
|
||||
stats.total_builds.unwrap_or(0)
|
||||
build_stats.total_builds.unwrap_or(0)
|
||||
);
|
||||
|
||||
// Build duration stats
|
||||
|
|
@ -166,7 +162,7 @@ async fn prometheus_metrics(State(state): State<AppState>) -> Response {
|
|||
let _ = writeln!(
|
||||
output,
|
||||
"fc_builds_avg_duration_seconds {:.2}",
|
||||
stats.avg_duration_seconds.unwrap_or(0.0)
|
||||
build_stats.avg_duration_seconds.unwrap_or(0.0)
|
||||
);
|
||||
|
||||
output.push_str(
|
||||
|
|
@ -214,7 +210,7 @@ async fn prometheus_metrics(State(state): State<AppState>) -> Response {
|
|||
let _ = writeln!(
|
||||
output,
|
||||
"fc_queue_depth {}",
|
||||
stats.pending_builds.unwrap_or(0)
|
||||
build_stats.pending_builds.unwrap_or(0)
|
||||
);
|
||||
|
||||
// Infrastructure
|
||||
|
|
|
|||
|
|
@ -44,13 +44,15 @@ use crate::{
|
|||
static STYLE_CSS: &str = include_str!("../../static/style.css");
|
||||
|
||||
/// Helper to generate secure cookie flags based on server configuration.
|
||||
/// Returns a string containing cookie security attributes: HttpOnly, SameSite,
|
||||
/// and optionally Secure.
|
||||
/// Returns a string containing cookie security attributes: `HttpOnly`,
|
||||
/// `SameSite`, and optionally Secure.
|
||||
///
|
||||
/// The Secure flag is set when:
|
||||
///
|
||||
/// 1. `force_secure_cookies` is enabled in config (for HTTPS reverse proxies),
|
||||
/// OR 2. The server is not bound to localhost/127.0.0.1 AND not in permissive
|
||||
/// mode
|
||||
/// 2. OR the server is not bound to localhost/127.0.0.1 AND not in permissive
|
||||
/// mode
|
||||
#[must_use]
|
||||
pub fn cookie_security_flags(
|
||||
config: &fc_common::config::ServerConfig,
|
||||
) -> String {
|
||||
|
|
|
|||
|
|
@ -89,12 +89,9 @@ fn build_github_client(config: &GitHubOAuthConfig) -> GitHubOAuthClient {
|
|||
}
|
||||
|
||||
async fn github_login(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let config = match &state.config.oauth.github {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return (StatusCode::NOT_FOUND, "GitHub OAuth not configured")
|
||||
.into_response();
|
||||
},
|
||||
let Some(config) = &state.config.oauth.github else {
|
||||
return (StatusCode::NOT_FOUND, "GitHub OAuth not configured")
|
||||
.into_response();
|
||||
};
|
||||
|
||||
let client = build_github_client(config);
|
||||
|
|
@ -141,13 +138,10 @@ async fn github_callback(
|
|||
headers: axum::http::HeaderMap,
|
||||
Query(params): Query<OAuthCallbackParams>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let config = match &state.config.oauth.github {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return Err(ApiError(fc_common::CiError::NotFound(
|
||||
"GitHub OAuth not configured".to_string(),
|
||||
)));
|
||||
},
|
||||
let Some(config) = &state.config.oauth.github else {
|
||||
return Err(ApiError(fc_common::CiError::NotFound(
|
||||
"GitHub OAuth not configured".to_string(),
|
||||
)));
|
||||
};
|
||||
|
||||
// Verify CSRF token from cookie
|
||||
|
|
@ -290,7 +284,7 @@ async fn github_callback(
|
|||
};
|
||||
|
||||
let clear_state =
|
||||
format!("fc_oauth_state=; {}; Path=/; Max-Age=0", security_flags);
|
||||
format!("fc_oauth_state=; {security_flags}; Path=/; Max-Age=0");
|
||||
let session_cookie = format!(
|
||||
"fc_user_session={}; {}; Path=/; Max-Age={}",
|
||||
session.0,
|
||||
|
|
@ -371,21 +365,21 @@ mod tests {
|
|||
fn test_secure_flag_detection() {
|
||||
// HTTP should not have Secure flag
|
||||
let http_uri = "http://localhost:3000/callback";
|
||||
let secure_flag = if http_uri.starts_with("https://") {
|
||||
let http_secure_flag = if http_uri.starts_with("https://") {
|
||||
"; Secure"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
assert_eq!(secure_flag, "");
|
||||
assert_eq!(http_secure_flag, "");
|
||||
|
||||
// HTTPS should have Secure flag
|
||||
let https_uri = "https://example.com/callback";
|
||||
let secure_flag = if https_uri.starts_with("https://") {
|
||||
let https_secure_flag = if https_uri.starts_with("https://") {
|
||||
"; Secure"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
assert_eq!(secure_flag, "; Secure");
|
||||
assert_eq!(https_secure_flag, "; Secure");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -437,7 +431,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_github_emails_find_primary_verified() {
|
||||
let emails = vec![
|
||||
let emails = [
|
||||
GitHubEmailResponse {
|
||||
email: "secondary@example.com".to_string(),
|
||||
primary: false,
|
||||
|
|
@ -467,7 +461,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_github_emails_fallback_to_verified() {
|
||||
// No primary email, should fall back to first verified
|
||||
let emails = vec![
|
||||
let emails = [
|
||||
GitHubEmailResponse {
|
||||
email: "unverified@example.com".to_string(),
|
||||
primary: false,
|
||||
|
|
@ -492,7 +486,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_github_emails_no_verified() {
|
||||
// No verified emails
|
||||
let emails = vec![GitHubEmailResponse {
|
||||
let emails = [GitHubEmailResponse {
|
||||
email: "unverified@example.com".to_string(),
|
||||
primary: true,
|
||||
verified: false,
|
||||
|
|
@ -540,8 +534,8 @@ mod tests {
|
|||
let max_age = 7 * 24 * 60 * 60;
|
||||
|
||||
let cookie = format!(
|
||||
"fc_user_session={}; HttpOnly; SameSite=Lax; Path=/; Max-Age={}{}",
|
||||
session_token, max_age, secure_flag
|
||||
"fc_user_session={session_token}; HttpOnly; SameSite=Lax; Path=/; \
|
||||
Max-Age={max_age}{secure_flag}"
|
||||
);
|
||||
|
||||
assert!(cookie.contains("fc_user_session=test-session-token"));
|
||||
|
|
|
|||
|
|
@ -159,17 +159,14 @@ async fn handle_github_webhook(
|
|||
.await
|
||||
.map_err(ApiError)?;
|
||||
|
||||
let webhook_config = match webhook_config {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return Ok((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(WebhookResponse {
|
||||
accepted: false,
|
||||
message: "No GitHub webhook configured for this project".to_string(),
|
||||
}),
|
||||
));
|
||||
},
|
||||
let Some(webhook_config) = webhook_config else {
|
||||
return Ok((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(WebhookResponse {
|
||||
accepted: false,
|
||||
message: "No GitHub webhook configured for this project".to_string(),
|
||||
}),
|
||||
));
|
||||
};
|
||||
|
||||
// Verify signature if secret is configured
|
||||
|
|
@ -299,17 +296,14 @@ async fn handle_github_pull_request(
|
|||
));
|
||||
}
|
||||
|
||||
let pr = match payload.pull_request {
|
||||
Some(pr) => pr,
|
||||
None => {
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
Json(WebhookResponse {
|
||||
accepted: true,
|
||||
message: "No pull request data, skipping".to_string(),
|
||||
}),
|
||||
));
|
||||
},
|
||||
let Some(pr) = payload.pull_request else {
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
Json(WebhookResponse {
|
||||
accepted: true,
|
||||
message: "No pull request data, skipping".to_string(),
|
||||
}),
|
||||
));
|
||||
};
|
||||
|
||||
// Skip draft PRs
|
||||
|
|
@ -513,6 +507,8 @@ async fn handle_gitlab_webhook(
|
|||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Result<(StatusCode, Json<WebhookResponse>), ApiError> {
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
// Check webhook config exists
|
||||
let webhook_config = repo::webhook_configs::get_by_project_and_forge(
|
||||
&state.pool,
|
||||
|
|
@ -522,17 +518,14 @@ async fn handle_gitlab_webhook(
|
|||
.await
|
||||
.map_err(ApiError)?;
|
||||
|
||||
let webhook_config = match webhook_config {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return Ok((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(WebhookResponse {
|
||||
accepted: false,
|
||||
message: "No GitLab webhook configured for this project".to_string(),
|
||||
}),
|
||||
));
|
||||
},
|
||||
let Some(webhook_config) = webhook_config else {
|
||||
return Ok((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(WebhookResponse {
|
||||
accepted: false,
|
||||
message: "No GitLab webhook configured for this project".to_string(),
|
||||
}),
|
||||
));
|
||||
};
|
||||
|
||||
// Verify token if secret is configured
|
||||
|
|
@ -544,7 +537,6 @@ async fn handle_gitlab_webhook(
|
|||
.unwrap_or("");
|
||||
|
||||
// Use constant-time comparison to prevent timing attacks
|
||||
use subtle::ConstantTimeEq;
|
||||
let token_matches = token.len() == secret.len()
|
||||
&& token.as_bytes().ct_eq(secret.as_bytes()).into();
|
||||
|
||||
|
|
@ -656,17 +648,14 @@ async fn handle_gitlab_merge_request(
|
|||
)))
|
||||
})?;
|
||||
|
||||
let attrs = match payload.object_attributes {
|
||||
Some(a) => a,
|
||||
None => {
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
Json(WebhookResponse {
|
||||
accepted: true,
|
||||
message: "No merge request attributes, skipping".to_string(),
|
||||
}),
|
||||
));
|
||||
},
|
||||
let Some(attrs) = payload.object_attributes else {
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
Json(WebhookResponse {
|
||||
accepted: true,
|
||||
message: "No merge request attributes, skipping".to_string(),
|
||||
}),
|
||||
));
|
||||
};
|
||||
|
||||
// Skip draft/WIP merge requests
|
||||
|
|
@ -774,12 +763,13 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_verify_signature_valid() {
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
|
||||
let secret = "test-secret";
|
||||
let body = b"test-body";
|
||||
|
||||
// Compute expected signature
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
|
||||
mac.update(body);
|
||||
let expected = hex::encode(mac.finalize().into_bytes());
|
||||
|
|
@ -787,7 +777,7 @@ mod tests {
|
|||
assert!(verify_signature(
|
||||
secret,
|
||||
body,
|
||||
&format!("sha256={}", expected)
|
||||
&format!("sha256={expected}")
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -800,20 +790,16 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_verify_signature_wrong_secret() {
|
||||
let body = b"test-body";
|
||||
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
|
||||
let body = b"test-body";
|
||||
let mut mac = Hmac::<Sha256>::new_from_slice(b"secret1").unwrap();
|
||||
mac.update(body);
|
||||
let sig = hex::encode(mac.finalize().into_bytes());
|
||||
|
||||
// Verify with different secret should fail
|
||||
assert!(!verify_signature(
|
||||
"secret2",
|
||||
body,
|
||||
&format!("sha256={}", sig)
|
||||
));
|
||||
assert!(!verify_signature("secret2", body, &format!("sha256={sig}")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue