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}