Skip to main content

hm_pipeline_ir/
graph.rs

1use std::collections::BTreeMap;
2
3use daggy::Dag;
4
5use schemars::JsonSchema as DeriveJsonSchema;
6use serde::{Deserialize, Serialize};
7
8/// A single build command within a pipeline.
9///
10/// Serialized as a JSON object inside each graph node's `step` field.
11/// The `key` is the unique identifier used to reference this step in
12/// edges and log output.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
14pub struct CommandStep {
15    /// Unique identifier for this step within the pipeline.
16    pub key: String,
17    /// Human-readable label shown in build output.
18    #[serde(default)]
19    pub label: Option<String>,
20    /// Shell command to execute inside the container.
21    pub cmd: String,
22    /// Docker image to boot from. Root steps without an image inherit
23    /// `PipelineGraph::default_image`; child steps boot from their
24    /// parent's committed snapshot.
25    #[serde(default)]
26    pub image: Option<String>,
27    /// Per-step environment variables merged on top of the pipeline env.
28    #[serde(default)]
29    pub env: Option<BTreeMap<String, String>>,
30    /// Maximum wall-clock seconds before the step is killed.
31    #[serde(default)]
32    pub timeout_seconds: Option<u32>,
33    /// Cache configuration for this step's committed snapshot.
34    #[serde(default)]
35    pub cache: Option<Cache>,
36    /// Step-executor plugin name. `None` falls back to the default
37    /// runner (Docker in the shipped configuration).
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub runner: Option<String>,
40    /// Plugin-specific extra fields passed verbatim to the runner.
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub runner_args: Option<serde_json::Value>,
43}
44
45/// Snapshot cache configuration for a step.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
47pub struct Cache {
48    /// Cache policy name (e.g. `"content-hash"`).
49    pub policy: String,
50    /// Explicit cache key override; derived from the step if absent.
51    #[serde(default)]
52    pub key: Option<String>,
53}
54
55/// A graph node: a [`CommandStep`] paired with its resolved environment.
56///
57/// The `env` map is the final merged result of pipeline-level defaults
58/// and per-step overrides — ready to hand to the executor as-is.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct Transition {
61    pub step: CommandStep,
62    pub env: BTreeMap<String, String>,
63}
64
65/// Edge label in the pipeline DAG.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum EdgeKind {
69    /// Container lineage: the child boots from the parent's committed
70    /// snapshot rather than from a fresh image.
71    BuildsIn,
72    /// Ordering-only dependency (emitted by `wait` barriers). The
73    /// child waits for the parent to finish but does not inherit its
74    /// snapshot.
75    DependsOn,
76}
77
78/// Top-level pipeline graph, deserialized directly from the v0 wire
79/// format (petgraph-serde JSON).
80///
81/// Callers access the underlying [`Dag`] via [`dag()`](Self::dag) and
82/// traverse it with petgraph's standard visitor traits.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct PipelineGraph {
85    #[serde(default = "default_version")]
86    version: String,
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    default_image: Option<String>,
89    #[serde(rename = "graph")]
90    inner: Dag<Transition, EdgeKind>,
91}
92
93fn default_version() -> String {
94    "0".to_string()
95}
96
97impl PipelineGraph {
98    /// Number of steps (nodes) in the graph.
99    #[must_use]
100    pub fn node_count(&self) -> usize {
101        self.inner.node_count()
102    }
103
104    /// Pipeline-wide fallback image for root steps that don't declare one.
105    #[must_use]
106    pub fn default_image(&self) -> Option<&str> {
107        self.default_image.as_deref()
108    }
109
110    /// The underlying DAG for direct traversal.
111    #[must_use]
112    pub const fn dag(&self) -> &Dag<Transition, EdgeKind> {
113        &self.inner
114    }
115}