From ce0dbf6b249956700c6a1705bf4ad85a09d53e8c Mon Sep 17 00:00:00 2001 From: Dawid Rycerz Date: Sun, 15 Feb 2026 21:27:00 +0100 Subject: feat: witryna 0.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch, cleanup, and status CLI commands. Persistent build state via state.json. Post-deploy hooks on success and failure with WITRYNA_BUILD_STATUS. Dependency diet (axum→tiny_http, clap→argh, tracing→log). Drop built-in rate limiting. Nix flake with NixOS module. Arch Linux PKGBUILD. Centralized version management. Co-Authored-By: Claude Opus 4.6 --- src/logs.rs | 291 ++---------------------------------------------------------- 1 file changed, 6 insertions(+), 285 deletions(-) (limited to 'src/logs.rs') diff --git a/src/logs.rs b/src/logs.rs index bddcc9d..e25262a 100644 --- a/src/logs.rs +++ b/src/logs.rs @@ -1,9 +1,9 @@ use anyhow::{Context as _, Result}; +use log::debug; use std::path::{Path, PathBuf}; use std::time::Duration; use tokio::io::AsyncWriteExt as _; use tokio::process::Command; -use tracing::{debug, warn}; use crate::hook::HookResult; @@ -107,9 +107,9 @@ pub async fn save_build_log( let _ = tokio::fs::remove_file(stderr_file).await; debug!( - path = %log_file.display(), - site = %meta.site_name, - "build log saved" + "[{}] build log saved: {}", + meta.site_name, + log_file.display() ); Ok(log_file) @@ -239,11 +239,7 @@ pub async fn save_hook_log( let _ = tokio::fs::remove_file(&hook_result.stdout_file).await; let _ = tokio::fs::remove_file(&hook_result.stderr_file).await; - debug!( - path = %log_file.display(), - site = %site_name, - "hook log saved" - ); + debug!("[{site_name}] hook log saved: {}", log_file.display()); Ok(log_file) } @@ -271,17 +267,6 @@ fn format_hook_log_header(site_name: &str, timestamp: &str, result: &HookResult) ) } -/// Parsed header from a build log file. -#[derive(Debug, Clone, serde::Serialize)] -pub struct ParsedLogHeader { - pub site_name: String, - pub timestamp: String, - pub git_commit: String, - pub image: String, - pub duration: String, - pub status: String, -} - /// Combined deployment status (build + optional hook). #[derive(Debug, Clone, serde::Serialize)] pub struct DeploymentStatus { @@ -291,182 +276,7 @@ pub struct DeploymentStatus { pub duration: String, pub status: String, pub log: String, -} - -/// Parse the header section of a build log file. -/// -/// Expects lines like: -/// ```text -/// === BUILD LOG === -/// Site: my-site -/// Timestamp: 20260126-143000-123456 -/// Git Commit: abc123d -/// Image: node:20-alpine -/// Duration: 45s -/// Status: success -/// ``` -/// -/// Returns `None` if the header is malformed. -#[must_use] -pub fn parse_log_header(content: &str) -> Option { - let mut site_name = None; - let mut timestamp = None; - let mut git_commit = None; - let mut image = None; - let mut duration = None; - let mut status = None; - - for line in content.lines().take(10) { - if let Some(val) = line.strip_prefix("Site: ") { - site_name = Some(val.to_owned()); - } else if let Some(val) = line.strip_prefix("Timestamp: ") { - timestamp = Some(val.to_owned()); - } else if let Some(val) = line.strip_prefix("Git Commit: ") { - git_commit = Some(val.to_owned()); - } else if let Some(val) = line.strip_prefix("Image: ") { - image = Some(val.to_owned()); - } else if let Some(val) = line.strip_prefix("Duration: ") { - duration = Some(val.to_owned()); - } else if let Some(val) = line.strip_prefix("Status: ") { - status = Some(val.to_owned()); - } - } - - Some(ParsedLogHeader { - site_name: site_name?, - timestamp: timestamp?, - git_commit: git_commit.unwrap_or_else(|| "unknown".to_owned()), - image: image.unwrap_or_else(|| "unknown".to_owned()), - duration: duration?, - status: status?, - }) -} - -/// Parse the status line from a hook log. -/// -/// Returns `Some(true)` for success, `Some(false)` for failure, -/// `None` if the content cannot be parsed. -#[must_use] -pub fn parse_hook_status(content: &str) -> Option { - for line in content.lines().take(10) { - if let Some(val) = line.strip_prefix("Status: ") { - return Some(val == "success"); - } - } - None -} - -/// List build log files for a site, sorted newest-first. -/// -/// Returns `(timestamp, path)` pairs. Excludes `*-hook.log` and `*.tmp` files. -/// -/// # Errors -/// -/// Returns an error if the directory cannot be read (except for not-found, -/// which returns an empty list). -pub async fn list_site_logs(log_dir: &Path, site_name: &str) -> Result> { - let site_log_dir = log_dir.join(site_name); - - if !site_log_dir.is_dir() { - return Ok(Vec::new()); - } - - let mut entries = tokio::fs::read_dir(&site_log_dir) - .await - .with_context(|| format!("failed to read log directory: {}", site_log_dir.display()))?; - - let mut logs = Vec::new(); - - while let Some(entry) = entries.next_entry().await? { - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - - // Skip hook logs and temp files - if name_str.ends_with("-hook.log") || name_str.ends_with(".tmp") { - continue; - } - - if let Some(timestamp) = name_str.strip_suffix(".log") { - logs.push((timestamp.to_owned(), entry.path())); - } - } - - // Sort descending (newest first) — timestamps are lexicographically sortable - logs.sort_by(|a, b| b.0.cmp(&a.0)); - - Ok(logs) -} - -/// Get the deployment status for a single build log. -/// -/// Reads the build log header and checks for an accompanying hook log -/// to determine overall deployment status. -/// -/// # Errors -/// -/// Returns an error if the build log cannot be read. -pub async fn get_deployment_status( - log_dir: &Path, - site_name: &str, - timestamp: &str, - log_path: &Path, -) -> Result { - let content = tokio::fs::read_to_string(log_path) - .await - .with_context(|| format!("failed to read build log: {}", log_path.display()))?; - - let header = parse_log_header(&content); - - let (git_commit, duration, build_status) = match &header { - Some(h) => (h.git_commit.clone(), h.duration.clone(), h.status.clone()), - None => { - warn!(path = %log_path.display(), "malformed build log header"); - ( - "unknown".to_owned(), - "-".to_owned(), - "(parse error)".to_owned(), - ) - } - }; - - // Check for accompanying hook log - let hook_log_path = log_dir - .join(site_name) - .join(format!("{timestamp}-hook.log")); - - let status = if hook_log_path.is_file() { - match tokio::fs::read_to_string(&hook_log_path).await { - Ok(hook_content) => match parse_hook_status(&hook_content) { - Some(true) => { - if build_status.starts_with("failed") { - build_status - } else { - "success".to_owned() - } - } - Some(false) => { - if build_status.starts_with("failed") { - build_status - } else { - "hook failed".to_owned() - } - } - None => build_status, - }, - Err(_) => build_status, - } - } else { - build_status - }; - - Ok(DeploymentStatus { - site_name: site_name.to_owned(), - timestamp: timestamp.to_owned(), - git_commit, - duration, - status, - log: log_path.to_string_lossy().to_string(), - }) + pub current_build: String, } #[cfg(test)] @@ -827,93 +637,4 @@ mod tests { cleanup(&base_dir).await; } - - // --- parse_log_header tests --- - - #[test] - fn parse_log_header_success() { - let content = "\ -=== BUILD LOG === -Site: my-site -Timestamp: 20260126-143000-123456 -Git Commit: abc123d -Image: node:20-alpine -Duration: 45s -Status: success - -=== STDOUT === -build output -"; - let header = parse_log_header(content).unwrap(); - assert_eq!(header.site_name, "my-site"); - assert_eq!(header.timestamp, "20260126-143000-123456"); - assert_eq!(header.git_commit, "abc123d"); - assert_eq!(header.image, "node:20-alpine"); - assert_eq!(header.duration, "45s"); - assert_eq!(header.status, "success"); - } - - #[test] - fn parse_log_header_failed_build() { - let content = "\ -=== BUILD LOG === -Site: fail-site -Timestamp: 20260126-160000-000000 -Git Commit: def456 -Image: node:18 -Duration: 2m 0s -Status: failed (exit code: 42): build error -"; - let header = parse_log_header(content).unwrap(); - assert_eq!(header.status, "failed (exit code: 42): build error"); - assert_eq!(header.duration, "2m 0s"); - } - - #[test] - fn parse_log_header_unknown_commit() { - let content = "\ -=== BUILD LOG === -Site: test-site -Timestamp: 20260126-150000-000000 -Git Commit: unknown -Image: alpine:latest -Duration: 5s -Status: success -"; - let header = parse_log_header(content).unwrap(); - assert_eq!(header.git_commit, "unknown"); - } - - #[test] - fn parse_log_header_malformed() { - let content = "This is not a valid log file\nSome random text\n"; - let header = parse_log_header(content); - assert!(header.is_none()); - } - - #[test] - fn parse_hook_status_success() { - let content = "\ -=== HOOK LOG === -Site: test-site -Timestamp: 20260202-120000-000000 -Command: touch marker -Duration: 1s -Status: success -"; - assert_eq!(parse_hook_status(content), Some(true)); - } - - #[test] - fn parse_hook_status_failed() { - let content = "\ -=== HOOK LOG === -Site: test-site -Timestamp: 20260202-120000-000000 -Command: false -Duration: 0s -Status: failed (exit code 1) -"; - assert_eq!(parse_hook_status(content), Some(false)); - } } -- cgit v1.2.3