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}