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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
//! A command's lifecycle, from its start to its exit code, for every command
//! that does factory work: a Run or Spec run started on an Issue URL,
//! or a Pass. A child Run goes through it too, as part
//! of the command that started it.
//!
//! A command [`start`]s: it begins its record in the logs, loads the User
//! config, offering Setup, installs the interrupt handler and checks its Run
//! notification, in that order. It does its work, then [`Started::finish`]es
//! with its [`Ending`]: the Factory skills it wrote out for its sessions
//! removed, then the Activity log's end line, the terminal display and the
//! Run notification, in that order, only the display of those three for a
//! skipped pass.
use std::process::ExitCode;
use std::time::Instant;
use crate::architect::{self, Reviewed};
use crate::config::UserConfig;
use crate::failed_run::FailedRun;
use crate::harness::Choice;
use crate::interrupt;
use crate::issue::IssueUrl;
use crate::logs::{self, Begin};
use crate::notification::{About, NotificationAsk, RunNotification};
use crate::pickup;
use crate::progress;
use crate::run::Ended;
use crate::run_ending;
use crate::setup;
use crate::skills;
/// How a command that did factory work, or was skipped before any, ended.
pub enum Ending {
/// A Run or a Spec run ended: started by hand, or dispatched by a Pickup
/// run.
Run(Ended),
/// An Architect run was not skipped: how its Architecture review ended,
/// failing if anything before it did, and how the run it dispatched its
/// plan as ended, if it dispatched one.
Architect {
review: Result<Reviewed, FailedRun>,
dispatched: Option<Ended>,
},
/// A Security run completed its audit, or failed.
Security(crate::security::Ended),
SecurityFix {
ended: Ended,
findings: Vec<crate::security::RecordedFinding>,
},
/// A Pass was skipped before any work.
Skipped(Skip),
}
/// Why a pass was skipped, and what it prints.
pub struct Skip {
/// The line that says why, on stderr.
pub reason: String,
/// The URLs it puts on stdout, a line each: those of the issues it was
/// skipped for, if any.
pub urls: Vec<String>,
}
impl From<architect::Skipped> for Skip {
/// An Architect run's skip: its reason, and the URLs of the issues it
/// was skipped for.
fn from(skipped: architect::Skipped) -> Self {
Skip {
reason: skipped.to_string(),
urls: skipped.urls().into_iter().map(String::from).collect(),
}
}
}
impl From<pickup::Skipped> for Skip {
/// A Pickup run's skip: its reason, with nothing on stdout.
fn from(skipped: pickup::Skipped) -> Self {
Skip {
reason: skipped.to_string(),
urls: Vec::new(),
}
}
}
/// A command started, with its Run notification checked if it asked for
/// one, to finish with its [`Ending`].
pub struct Started {
notification: Option<RunNotification>,
/// Only the top-level command reports elapsed wall time.
started: Option<Instant>,
}
/// Start a command that begins in the logs as `begin` says: load the User
/// config, after offering Setup where there is none; install the interrupt
/// handler; then check the Run notification that `ask` of that config asks
/// for, if any, about `about`. Returns the User config, for the command's
/// own asks, and the command to finish, or, if any of that fails, the
/// failure to exit with, its error reported.
pub fn start(
begin: Begin,
ask: impl FnOnce(&UserConfig) -> NotificationAsk,
about: About,
) -> Result<(UserConfig, Started), ExitCode> {
let started = (!matches!(begin, Begin::ChildRun(..))).then(Instant::now);
logs::begin(begin);
let config = setup::offer()
.and_then(|()| UserConfig::load())
.map_err(|error| failure(&error))?;
logs::configured(&config);
// First, so no interrupt can end the command once its notification is
// checked.
interrupt::install().map_err(|error| failure(&error))?;
let notification = match ask(&config) {
NotificationAsk::Send(to) => {
Some(RunNotification::new(to, &config.email, about).map_err(|error| failure(&error))?)
}
NotificationAsk::Skip => None,
};
Ok((
config,
Started {
notification,
started,
},
))
}
impl Started {
/// Take note that the command's sessions run on `harness`, which its Run
/// notification names.
pub fn built_with(&mut self, harness: &Choice) {
if let Some(notification) = &mut self.notification {
notification.built_with(harness);
}
}
/// Take note that the Pickup run took the Ready issue `issue`, titled
/// `title`, which its Run notification is then about.
pub fn took(&mut self, issue: &IssueUrl, title: String) {
if let Some(notification) = &mut self.notification {
notification.took(issue, title);
}
}
/// Finish the command that ended as `ending`: remove the Factory skills
/// it wrote out for its sessions, if it did, write the Activity log's
/// end line, show how it ended, then send its Run notification, if it
/// asked for one. A skipped pass only shows how it ended: its skip is
/// recorded where it was decided, and it sends no notification. Returns
/// its exit code.
pub fn finish(self, ending: Ending) -> ExitCode {
skills::remove_written();
let account = match run_ending::read(&ending) {
Ok(account) => account,
Err(skip) => return run_ending::show_skip(skip),
};
logs::ended(account.summary());
logs::finished(&account);
let elapsed = match &ending {
Ending::Run(_)
| Ending::Architect {
dispatched: Some(_),
..
}
| Ending::SecurityFix { .. } => self.started.map(|started| started.elapsed()),
_ => None,
};
let code = account.show(elapsed);
if let Some(notification) = self.notification {
notification.send(&account);
}
code
}
}
/// `error` on stderr, after any lines a pass held from the terminal, and the
/// exit code of a failure.
pub fn failure(error: &anyhow::Error) -> ExitCode {
logs::show_held();
progress::step(format_args!("{error:#}"));
ExitCode::FAILURE
}