use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum ReleaseEvent {
#[serde(rename = "run_start")]
RunStart {
ts: DateTime<Utc>,
run_id: String,
workspace: String,
},
#[serde(rename = "repo_start")]
RepoStart {
ts: DateTime<Utc>,
repo: String,
sha: String,
},
#[serde(rename = "phase_start")]
PhaseStart {
ts: DateTime<Utc>,
repo: String,
phase: String,
},
#[serde(rename = "phase_end")]
PhaseEnd {
ts: DateTime<Utc>,
repo: String,
phase: String,
ok: bool,
duration_ms: u64,
},
#[serde(rename = "binary_start")]
BinaryStart {
ts: DateTime<Utc>,
repo: String,
binary: String,
},
#[serde(rename = "test_pass")]
TestPass {
ts: DateTime<Utc>,
repo: String,
binary: String,
name: String,
},
#[serde(rename = "test_fail")]
TestFail {
ts: DateTime<Utc>,
repo: String,
binary: String,
name: String,
},
#[serde(rename = "binary_done")]
BinaryDone {
ts: DateTime<Utc>,
repo: String,
binary: String,
passed: u32,
failed: u32,
},
#[serde(rename = "repo_end")]
RepoEnd {
ts: DateTime<Utc>,
repo: String,
ok: bool,
},
#[serde(rename = "run_end")]
RunEnd {
ts: DateTime<Utc>,
run_id: String,
ok: bool,
},
}
impl ReleaseEvent {
pub fn timestamp(&self) -> DateTime<Utc> {
match self {
Self::RunStart { ts, .. }
| Self::RepoStart { ts, .. }
| Self::PhaseStart { ts, .. }
| Self::PhaseEnd { ts, .. }
| Self::BinaryStart { ts, .. }
| Self::TestPass { ts, .. }
| Self::TestFail { ts, .. }
| Self::BinaryDone { ts, .. }
| Self::RepoEnd { ts, .. }
| Self::RunEnd { ts, .. } => *ts,
}
}
}
#[derive(Clone, Debug)]
pub struct ProgressWriter {
inner: std::sync::Arc<Mutex<File>>,
path: PathBuf,
}
impl ProgressWriter {
pub fn open(log_dir: &Path, run_id: &str) -> Result<Self> {
std::fs::create_dir_all(log_dir)
.with_context(|| format!("mkdir {}", log_dir.display()))?;
let path = log_dir.join(format!("release-run-{run_id}.events.ndjson"));
let f = OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.with_context(|| format!("open {}", path.display()))?;
Ok(Self { inner: std::sync::Arc::new(Mutex::new(f)), path })
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn emit(&self, ev: &ReleaseEvent) {
let line = match serde_json::to_string(ev) {
Ok(s) => s,
Err(e) => {
eprintln!("progress: encode event failed: {e}");
return;
}
};
let mut guard = match self.inner.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
if let Err(e) = writeln!(*guard, "{line}") {
eprintln!("progress: write {} failed: {e}", self.path.display());
}
let _ = guard.flush();
}
}
#[derive(Clone, Default)]
pub struct NoopProgress;
impl NoopProgress {
pub fn emit(&self, _ev: &ReleaseEvent) {}
}
pub fn now() -> DateTime<Utc> {
Utc::now()
}