1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
//! What:
//! The service's handler for a `review` work item: read the landed commit, ask the decision gateway, and append the
//! entry to the repository's live shadow log, all inside a fixed time budget.
//! Why:
//! This replaces the repository's post-commit hook that ran `mise run shadow-review` (`doc/runbook/standards-review-gate.md`).
//! The review shares the service's work thread with pushes and upkeep, so every step is bounded: the gateway call by a
//! deadline, the Git reads and the write by the budget around the whole run. A review that fails leaves its item in
//! `failed` as a plain record and the log shows the gateway's failure as an entry; nothing is ever left claimed.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const outcome = await Promise.race([reviewLanded(names, sha, subject, read, options), wait(budget)]);
//! ```
use super::standards_review_input::has_shadow_log;
use super::standards_review_run::{ReviewOptions, ReviewOutcome, options_from_environment, review_landed, run_within};
use super::standards_review_source::{read_landed_commit, read_message, read_names};
use super::standards_review_text::commit_subject;
use super::service_review_key::{KEY_VARIABLES, ReviewKey};
use super::service_work::Verdict;
use super::transaction_git::GitContext;
use super::work_item::WorkItem;
use std::path::{Path, PathBuf};
use std::time::Duration;
/// How long the gateway call may take: several times the largest figure on record (1.4 s in
/// `doc/audit/standards-review-gate-shadow.md`), and a third of the script's 30 s, because the work thread is shared.
pub const CALL_DEADLINE: Duration = Duration::from_secs(10);
/// How long the whole review may take, Git reads and write included; past it the item is failed and the work thread moves on.
pub const BUDGET: Duration = Duration::from_secs(20);
/// What:
/// The common Git directory of a worktree's Git directory: the directory its `commondir` file names, else the directory
/// itself.
/// Why:
/// A linked worktree's items carry that worktree's own Git directory (`.git/worktrees/<name>`), which Git deletes with the
/// worktree, while the repository's one log lives in the common directory. Git strips every trailing carriage return and
/// line feed and resolves a relative path against the Git directory (`get_common_dir_noenv` in `path.c`), as the layout
/// reader does; a path that cannot be resolved is used as written, so the entry still goes to the common directory.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// existsSync(join(gitDir, 'commondir')) ? realpath(resolve(gitDir, readFile(...).trimEnd())) : gitDir
/// ```
pub fn common_dir_of(git_dir: &Path) -> PathBuf {
let Ok(text) = std::fs::read(git_dir.join("commondir")) else {
return git_dir.to_path_buf();
};
let end: usize = text.iter().rposition(|byte| { return *byte != b'\r' && *byte != b'\n'; }).map_or(0, |last| { return last + 1; });
if end == 0 {
return git_dir.to_path_buf();
}
#[cfg(unix)]
let named: PathBuf = PathBuf::from(<std::ffi::OsStr as std::os::unix::ffi::OsStrExt>::from_bytes(&text[..end]));
#[cfg(not(unix))]
let named: PathBuf = PathBuf::from(String::from_utf8_lossy(&text[..end]).into_owned());
let joined: PathBuf = git_dir.join(named);
return std::fs::canonicalize(&joined).unwrap_or(joined);
}
/// What:
/// The options for one item: the script's environment variables read from the service's environment, and the call
/// deadline; the log is the item's repository's live log in its common Git directory.
/// Why:
/// Every worktree of a repository appends to that repository's one log, and a service that serves several repositories
/// writes each one's own; the item names its worktree's Git directory, and the common one follows from it with no Git start.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// optionsFor(item, process.env)
/// ```
pub fn options_for(item: &WorkItem, variable: &dyn Fn(&str) -> Option<String>) -> ReviewOptions {
let common: PathBuf = common_dir_of(Path::new(item.git_dir.as_str()));
let mut options: ReviewOptions = options_from_environment(common.as_path(), variable);
options.deadline = CALL_DEADLINE;
return options;
}
/// What:
/// The variable lookup the review's options read: the credential variables answer from the key that came in the wake
/// message and from nowhere else, and every other variable answers from the service's environment.
/// Why:
/// The credential is never in the service's environment or in a unit file, so a service that did not receive a key in
/// the message must record "no key" and not pick up a stray value; the service's environment carries no secret.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// (name) => keyNames.includes(name) ? fromMessage(name) : process.env[name]
/// ```
pub fn review_variables(key: Option<&ReviewKey>) -> impl Fn(&str) -> Option<String> {
let exposed: Option<String> = key.map(|held| { return held.expose().to_string(); });
return move |name: &str| {
if name == KEY_VARIABLES[0] {
return exposed.clone();
}
if name == KEY_VARIABLES[1] {
return None;
}
return std::env::var(name).ok();
};
}
/// The Git reads, the subject and the review, without the budget.
fn run_review(context: &GitContext, item: &WorkItem, options: &ReviewOptions, now_millis: &dyn Fn() -> u64) -> ReviewOutcome {
let cwd: PathBuf = PathBuf::from(item.repository.as_str());
let names: Vec<String> = match read_names(context, cwd.as_path(), item.oid.as_str()) {
Ok(found) => found,
Err(message) => return ReviewOutcome::Unread(message),
};
let subject: String = match read_message(context, cwd.as_path(), item.oid.as_str()) {
Ok(message) => commit_subject(message.as_str()),
Err(message) => return ReviewOutcome::Unread(message),
};
let read = |files: &[String]| { return read_landed_commit(context, cwd.as_path(), item.oid.as_str(), subject.as_str(), files); };
return review_landed(names.as_slice(), item.oid.as_str(), subject.as_str(), &read, options, now_millis);
}
/// The wall clock in milliseconds since 1970.
fn wall_clock_millis() -> u64 {
return std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |elapsed| { return u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX); });
}
/// What:
/// Review one item within `budget`: `Done` when an entry was written, was not needed, or the worktree does not
/// hold the tracked shadow log that switches the review on;
/// `Failed` with the reason when the commit could not be read, the log could not be written, or the budget ran out.
/// Why:
/// The caller settles `Failed` items into `failed`, so a review that cannot finish is a plain record and never a stuck claim.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await reviewItem(context, item, options, budget)
/// ```
pub fn review_item(context: &GitContext, item: &WorkItem, options: ReviewOptions, budget: Duration) -> Verdict {
if !has_shadow_log(Path::new(item.repository.as_str())) {
return Verdict::Done;
}
// The service serves many repositories, so its own context names none; the Git command that takes input needs the
// item's Git directory for its input file, as the landing sets it for each request.
let owned_context: GitContext = GitContext { common_dir: Some(PathBuf::from(item.git_dir.as_str())), ..context.clone() };
let owned: WorkItem = item.clone();
let outcome: Option<ReviewOutcome> = run_within(budget, move || { return run_review(&owned_context, &owned, &options, &wall_clock_millis); });
return match outcome {
None => Verdict::Failed(format!("the review of {} did not finish within {} s", item.oid, budget.as_secs())),
Some(ReviewOutcome::Unread(message) | ReviewOutcome::Unwritable(message)) => Verdict::Failed(message),
Some(ReviewOutcome::NoEntry | ReviewOutcome::Skipped | ReviewOutcome::Reviewed(_) | ReviewOutcome::AlreadyLogged) => Verdict::Done,
};
}
/// Review controls stay out of the release executable.
#[cfg(test)]
#[path = "service_review_tests.rs"]
mod tests;