Use jiff::Timestamp for push and run timestamps
PushEvent.pushed_at was Unix-epoch seconds in a string while
RunMeta documented it as ISO 8601 — string passing hid the
mismatch. Switch to jiff::Timestamp end-to-end.
Assisted-by: Claude Opus 4.7 (1M context) via Claude Code
diff --git a/Cargo.toml b/Cargo.toml
index f18361d..976faf5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,7 +13,7 @@ base64 = "*"
clap = { version = "*", features = ["derive", "env"] }
clap_complete = "*"
fs-err = "*"
-jiff = "*"
+jiff = { version = "*", features = ["serde"] }
miette = { version = "*", features = ["fancy"] }
mlua = { version = "*", features = ["lua54", "serde", "vendored", "error-send"] }
regex = "*"
diff --git a/src/ci.rs b/src/ci.rs
index 81e6971..23655fb 100644
--- a/src/ci.rs
+++ b/src/ci.rs
@@ -34,20 +34,20 @@ pub struct RunMeta {
pub sha: String,
/// The full ref name (e.g. `refs/heads/main`).
pub r#ref: String,
- /// ISO 8601 timestamp of when the push occurred.
- pub pushed_at: String,
+ /// When the push occurred.
+ pub pushed_at: jiff::Timestamp,
}
/// Timestamps recorded across the run lifecycle. The directory name is the
/// authoritative state; this file records when transitions happened.
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RunTimes {
- /// ISO 8601 timestamp of when the run was picked up (moved to active).
+ /// When the run was picked up (moved to active).
#[serde(skip_serializing_if = "Option::is_none")]
- pub started_at: Option<String>,
- /// ISO 8601 timestamp of when the run finished (moved to complete/failed).
+ pub started_at: Option<jiff::Timestamp>,
+ /// When the run finished (moved to complete/failed).
#[serde(skip_serializing_if = "Option::is_none")]
- pub finished_at: Option<String>,
+ pub finished_at: Option<jiff::Timestamp>,
}
/// Access to CI runs for a single repo.
@@ -245,11 +245,11 @@ impl Run {
self.state = to;
let mut times = self.read_times()?;
- let now = || jiff::Zoned::now().to_string();
+ let now = jiff::Timestamp::now();
match to {
- RunState::Active if times.started_at.is_none() => times.started_at = Some(now()),
+ RunState::Active if times.started_at.is_none() => times.started_at = Some(now),
RunState::Complete | RunState::Failed if times.finished_at.is_none() => {
- times.finished_at = Some(now())
+ times.finished_at = Some(now)
}
_ => {}
}
@@ -466,7 +466,7 @@ pub fn trigger(quire: &crate::Quire, event: &PushEvent) {
};
for push_ref in event.updated_refs() {
- if let Err(e) = trigger_ref(&repo, &event.pushed_at, push_ref) {
+ if let Err(e) = trigger_ref(&repo, event.pushed_at, push_ref) {
tracing::error!(
repo = %event.repo,
sha = %push_ref.new_sha,
@@ -481,7 +481,7 @@ pub fn trigger(quire: &crate::Quire, event: &PushEvent) {
///
/// Returns `Ok(())` if CI ran (regardless of whether the run succeeded
/// or failed), or `Err` if the trigger itself failed.
-fn trigger_ref(repo: &Repo, pushed_at: &str, push_ref: &PushRef) -> Result<()> {
+fn trigger_ref(repo: &Repo, pushed_at: jiff::Timestamp, push_ref: &PushRef) -> Result<()> {
if !repo.has_ci_fnl(&push_ref.new_sha) {
return Ok(());
}
@@ -489,7 +489,7 @@ fn trigger_ref(repo: &Repo, pushed_at: &str, push_ref: &PushRef) -> Result<()> {
let meta = RunMeta {
sha: push_ref.new_sha.clone(),
r#ref: push_ref.r#ref.clone(),
- pushed_at: pushed_at.to_string(),
+ pushed_at,
};
let mut run = repo.runs().create(&meta)?;
@@ -557,7 +557,7 @@ mod tests {
RunMeta {
sha: "abc123".to_string(),
r#ref: "refs/heads/main".to_string(),
- pushed_at: "2026-04-28T12:00:00Z".to_string(),
+ pushed_at: "2026-04-28T12:00:00Z".parse().expect("parse timestamp"),
}
}
@@ -742,14 +742,15 @@ mod tests {
let runs = Runs::new(quire.base_dir().join("runs").join("test.git"));
let run = runs.create(&test_meta()).expect("create");
+ let started: jiff::Timestamp = "2026-04-28T12:00:01Z".parse().expect("parse");
run.write_times(&RunTimes {
- started_at: Some("2026-04-28T12:00:01Z".to_string()),
+ started_at: Some(started),
finished_at: None,
})
.expect("write state");
let loaded = run.read_times().expect("read state");
- assert_eq!(loaded.started_at.as_deref(), Some("2026-04-28T12:00:01Z"));
+ assert_eq!(loaded.started_at, Some(started));
// Meta is unchanged.
let loaded_meta = run.read_meta().expect("read meta");
diff --git a/src/event.rs b/src/event.rs
index 91b82e5..45fff90 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -11,7 +11,7 @@ pub struct PushRef {
pub struct PushEvent {
pub r#type: String,
pub repo: String,
- pub pushed_at: String,
+ pub pushed_at: jiff::Timestamp,
pub refs: Vec<PushRef>,
}
@@ -20,15 +20,10 @@ impl PushEvent {
///
/// `repo` is the repo name relative to the repos dir (e.g. "foo.git").
pub fn new(repo: String, refs: Vec<PushRef>) -> Self {
- let pushed_at = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .map(|d| d.as_secs().to_string())
- .unwrap_or_else(|_| "0".to_string());
-
Self {
r#type: "push".to_string(),
repo,
- pushed_at,
+ pushed_at: jiff::Timestamp::now(),
refs,
}
}
@@ -58,7 +53,7 @@ mod tests {
assert_eq!(event.r#type, "push");
assert_eq!(event.repo, "foo.git");
assert_eq!(event.refs, refs);
- assert_ne!(event.pushed_at, "0");
+ assert!(event.pushed_at > jiff::Timestamp::UNIX_EPOCH);
}
#[test]
diff --git a/tests/cli.rs b/tests/cli.rs
index f9ef74c..6862ac6 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -30,7 +30,7 @@ fn push_event_round_trips_through_socket() {
let event = quire::event::PushEvent {
r#type: "push".to_string(),
repo: "test.git".to_string(),
- pushed_at: "12345".to_string(),
+ pushed_at: jiff::Timestamp::from_second(12345).unwrap(),
refs: vec![quire::event::PushRef {
old_sha: "0000000000000000000000000000000000000000".to_string(),
new_sha: "abc123".to_string(),
@@ -77,7 +77,7 @@ fn push_event_multiple_refs_round_trip() {
let event = quire::event::PushEvent {
r#type: "push".to_string(),
repo: "work/project.git".to_string(),
- pushed_at: "99999".to_string(),
+ pushed_at: jiff::Timestamp::from_second(99999).unwrap(),
refs: vec![
quire::event::PushRef {
old_sha: "aaa".to_string(),