Skip to main content

hm_plugin_protocol/
events.rs

1//! Build-time events. Produced by the orchestrator (host) and fanned
2//! out to output formatters, lifecycle hooks, and (via the host
3//! re-broadcast of `hm_emit_step_log`) any subscriber.
4
5use chrono::{DateTime, Utc};
6use schemars::JsonSchema as DeriveJsonSchema;
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use crate::executor::SnapshotRef;
11
12#[derive(
13    Debug,
14    Clone,
15    Copy,
16    PartialEq,
17    Eq,
18    Serialize,
19    Deserialize,
20    DeriveJsonSchema,
21    derive_more::IsVariant,
22)]
23#[serde(rename_all = "snake_case")]
24pub enum StdStream {
25    Stdout,
26    Stderr,
27}
28
29#[derive(
30    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema, derive_more::IsVariant,
31)]
32#[serde(tag = "kind", rename_all = "snake_case")]
33#[non_exhaustive]
34pub enum BuildEvent {
35    BuildStart {
36        run_id: Uuid,
37        plan: PlanSummary,
38        started_at: DateTime<Utc>,
39    },
40    /// Emitted once, early, when the build has an identity. Replaces the
41    /// ad-hoc "Build #N submitted" log line. `watch_url` is `Some` for cloud.
42    BuildAccepted {
43        build: BuildRef,
44        watch_url: Option<String>,
45    },
46    StepQueued {
47        step_id: Uuid,
48        key: String,
49        chain_idx: usize,
50        /// Key of this step's `BuildsIn` parent, if any. Lets renderers
51        /// nest progress bars to reflect the pipeline's DAG structure.
52        parent_key: Option<String>,
53        /// Human-readable name for display. Falls back to a truncated
54        /// command when no explicit label was set in the pipeline DSL.
55        display_name: String,
56    },
57    StepStart {
58        step_id: Uuid,
59        runner: String,
60        image: Option<String>,
61    },
62    StepLog {
63        step_id: Uuid,
64        stream: StdStream,
65        line: String,
66        ts: DateTime<Utc>,
67    },
68    StepCacheHit {
69        step_id: Uuid,
70        key: String,
71        tag: String,
72    },
73    StepEnd {
74        step_id: Uuid,
75        exit_code: i32,
76        duration_ms: u64,
77        snapshot: Option<SnapshotRef>,
78    },
79    /// Emitted when any step in a chain returns non-zero. Carries the
80    /// failing step's identity so output plugins can render a precise
81    /// diagnostic. Distinct from `StepEnd` (per-step) and `BuildEnd`
82    /// (per-run).
83    ChainFailed {
84        chain_idx: usize,
85        failed_step_id: Uuid,
86        failed_step_key: String,
87        exit_code: i32,
88        message: String,
89        ts: DateTime<Utc>,
90    },
91    BuildEnd {
92        exit_code: i32,
93        duration_ms: u64,
94    },
95}
96
97/// Stable identity for a build, shared by `BuildAccepted` and `hm_exec::BuildOutcome`.
98/// Local builds have a `run_id` only; cloud builds also have `number`/`org`.
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
100pub struct BuildRef {
101    pub run_id: Uuid,
102    pub number: Option<i64>,
103    pub org: Option<String>,
104    pub pipeline: String,
105}
106
107/// Compact summary of the resolved IR included in `BuildStart`. Lets
108/// output formatters print a header without needing the full pipeline.
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
110pub struct PlanSummary {
111    pub step_count: usize,
112    pub chain_count: usize,
113    pub default_runner: String,
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[allow(clippy::unwrap_used)]
121    #[test]
122    fn build_accepted_round_trips() {
123        let ev = BuildEvent::BuildAccepted {
124            build: BuildRef {
125                run_id: uuid::Uuid::nil(),
126                number: Some(42),
127                org: Some("acme".into()),
128                pipeline: "ci".into(),
129            },
130            watch_url: Some("https://app.harmont.dev/acme/ci/builds/42".into()),
131        };
132        let s = serde_json::to_string(&ev).unwrap();
133        let back: BuildEvent = serde_json::from_str(&s).unwrap();
134        assert!(matches!(back, BuildEvent::BuildAccepted { .. }));
135    }
136}