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