Skip to main content

crypto/ci_verdict/
body.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Canonical content model for a CI verdict.
3//!
4//! Canonical bytes are `serde_json` over these structs in declaration order.
5//! Maps are [`BTreeMap`]s, absent optional fields are omitted, and every schema
6//! change that moves the bytes must bump [`CI_VERDICT_BODY_SCHEMA_VERSION`].
7
8use std::collections::BTreeMap;
9
10use heddle_object_model::object::ContentHash;
11use serde::{Deserialize, Serialize};
12
13use super::body_details::{Execution, LogRef, Outcome, Repro};
14
15/// Current canonical [`CiVerdictBody`] schema version.
16/// Schema 2 makes runner environment runtime-only. Schema 1 is rejected rather
17/// than reusing or publishing evidence containing private runner details.
18pub const CI_VERDICT_BODY_SCHEMA_VERSION: u32 = 2;
19
20/// The complete conclusion-bearing content of a CI verdict.
21#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub struct CiVerdictBody {
24    /// Canonical body schema version.
25    pub schema_version: u32,
26    /// Repository this verdict describes.
27    pub repo: String,
28    /// Source state that was evaluated.
29    pub state: StateRef,
30    /// Branch or speculative-merge basis actually evaluated.
31    pub basis: Basis,
32    /// Check identity and resolved parameters.
33    pub check: CheckDescriptor,
34    /// Terminal conclusion and optional failure detail.
35    pub outcome: Outcome,
36    /// Runner and timing metadata.
37    pub execution: Execution,
38    /// Finalized log reference; log bytes are never inlined.
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub log: Option<LogRef>,
41    /// Portable reproduction recipe without the runner's private environment.
42    pub repro: Repro,
43    /// Canonical CheckSet digest used with `check.node_id` for authoritative gates.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub check_set_digest: Option<String>,
46}
47
48impl CiVerdictBody {
49    /// Deterministic bytes hashed into [`Self::content_hash`].
50    #[must_use]
51    pub fn canonical_bytes(&self) -> Vec<u8> {
52        serde_json::to_vec(self).expect("CiVerdictBody is always serializable")
53    }
54
55    /// BLAKE3 [`ContentHash`] of the canonical body bytes.
56    #[must_use]
57    pub fn content_hash(&self) -> ContentHash {
58        ContentHash::compute(&self.canonical_bytes())
59    }
60}
61
62/// Reference to the immutable source state described by the body.
63#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub struct StateRef {
66    /// Transfer-stable source-state content digest.
67    pub content_hash: String,
68    /// Physical source-state identifier.
69    pub change_id: String,
70    /// Optional rewrite-stable lineage identity.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub logical_change_id: Option<String>,
73}
74
75/// Exact tree evaluated and how it relates to a merge target.
76#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub struct Basis {
79    /// Branch-versus-merge discriminator.
80    pub kind: BasisKind,
81    /// Digest of the exact evaluated tree, including speculative merges.
82    pub evaluated_tree_digest: String,
83}
84
85/// Branch-versus-speculative-merge discriminator.
86#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum BasisKind {
89    /// The branch tree was evaluated as pushed.
90    #[default]
91    Branch,
92    /// The branch was evaluated after merging it with a target.
93    MergedWith {
94        /// Target state used for the speculative merge.
95        target_state: String,
96        /// Number of commits the branch was behind that target.
97        behind_count: u32,
98        /// Merge implementation version used to materialize the tree.
99        #[serde(default, skip_serializing_if = "Option::is_none")]
100        merge_algorithm_version: Option<String>,
101        /// Conflict policy applied while materializing the tree.
102        #[serde(default, skip_serializing_if = "Option::is_none")]
103        conflict_policy: Option<String>,
104    },
105}
106
107/// Check identity, command, and resolved inputs.
108#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub struct CheckDescriptor {
111    /// Unique check name within the repository definition.
112    pub name: String,
113    /// Whether the check gates, advises, or only informs.
114    pub class: CheckClass,
115    /// Digest of the authored check definition.
116    pub definition_digest: String,
117    /// Exact argument vector executed by the check.
118    pub command: Vec<String>,
119    /// Optional immutable container image digest.
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub image_digest: Option<String>,
122    /// Optional toolchain identifier.
123    #[serde(default, skip_serializing_if = "Option::is_none")]
124    pub toolchain: Option<String>,
125    /// Sorted resolved parameters, preventing cheap-check substitution.
126    pub params: BTreeMap<String, String>,
127    /// Service containers required by the check.
128    pub services: Vec<String>,
129    /// Check node within the body-level CheckSet.
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub node_id: Option<String>,
132}
133
134/// Whether a check gates a merge or only contributes advisory context.
135#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "snake_case")]
137pub enum CheckClass {
138    /// A non-green verdict blocks the merge when signer policy also allows it.
139    Required,
140    /// Reported but never gates.
141    #[default]
142    Advisory,
143    /// Context-only check, such as a metric.
144    Informational,
145}