summaryrefslogtreecommitdiff
path: root/src/logs.rs
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2026-02-15 21:27:00 +0100
committerDawid Rycerz <dawid@rycerz.xyz>2026-02-15 21:27:00 +0100
commitce0dbf6b249956700c6a1705bf4ad85a09d53e8c (patch)
treed7c3236807cfbf75d7f3a355eb5df5a5e2cc4ad7 /src/logs.rs
parent064a1d01c5c14f5ecc032fa9b8346a4a88b893f6 (diff)
feat: witryna 0.2.0HEADv0.2.0main
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 <noreply@anthropic.com>
Diffstat (limited to 'src/logs.rs')
-rw-r--r--src/logs.rs291
1 files changed, 6 insertions, 285 deletions
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<ParsedLogHeader> {
- 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<bool> {
- 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<Vec<(String, PathBuf)>> {
- 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<DeploymentStatus> {
- 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));
- }
}