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
//! What an action is given to work with.
use crate::error::{Result, ShlaneError};
use crate::runtime::context::{Cleanup, SharedCleanups, SharedFrame, SharedOutputs};
use crate::runtime::secrets::SharedSecrets;
use crate::runtime::shell::{self, Spawn};
use crate::runtime::ui::Ui;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::rc::Rc;
pub struct ActionContext<'a> {
pub lane: String,
pub env: &'a BTreeMap<String, String>,
pub workdir: PathBuf,
pub dry_run: bool,
pub ui: Rc<Ui>,
pub secrets: SharedSecrets,
/// The running lane's state, so a plugin written in Rhai can reach the same
/// builtins a lane's own script has.
pub frame: SharedFrame,
pub outputs: SharedOutputs,
/// Commands to run once the run is over, whatever its result.
pub cleanups: SharedCleanups,
/// So a Rhai plugin's `action()` can reach the same registry. Weak: the
/// registry holds the plugin, and a strong handle back would be a cycle.
pub registry: std::rc::Weak<crate::actions::Registry>,
/// Shared nesting depth, so a plugin calling its own action is bounded.
pub depth: Rc<std::cell::Cell<usize>>,
}
impl ActionContext<'_> {
pub fn workdir(&self) -> &Path {
&self.workdir
}
/// Ask for a command to be run once the run is over, whether it passed or
/// failed.
///
/// Registered rather than run in a `Drop`: a cleanup is a real command that
/// can fail and has something to say about it, and the lane's error hooks
/// have to see the machine as the failure left it.
pub fn on_finish(&self, what: impl Into<String>, command: impl Into<String>) {
self.cleanups.borrow_mut().push(Cleanup {
what: what.into(),
command: command.into(),
});
}
/// Hide a value wherever it appears in the output.
pub fn mark_secret(&self, value: &str) {
self.secrets.borrow_mut().add(value);
}
/// Run a command that changes something. Skipped by `--dry-run`.
pub fn sh(&self, command: &str) -> Result<shell::Outcome> {
self.spawn_with(command, false, true, &BTreeMap::new())
}
/// Like [`sh`](Self::sh), with extra environment for this command only.
///
/// Used to keep secrets off the command line, where `ps` and CI logs can
/// see them.
pub fn sh_with_env(
&self,
command: &str,
extra: &BTreeMap<String, String>,
) -> Result<shell::Outcome> {
self.spawn_with(command, false, true, extra)
}
/// Like [`require`](Self::require), with extra environment.
pub fn require_with_env(
&self,
command: &str,
extra: &BTreeMap<String, String>,
) -> Result<shell::Outcome> {
let outcome = self.sh_with_env(command, extra)?;
if !outcome.success {
return Err(self.failed(command, &outcome));
}
Ok(outcome)
}
/// Run a command and fail the action if it does not succeed.
pub fn require(&self, command: &str) -> Result<shell::Outcome> {
let outcome = self.sh(command)?;
if !outcome.success {
return Err(self.failed(command, &outcome));
}
Ok(outcome)
}
/// Read something: the command runs even under `--dry-run`.
///
/// A dry run that invents results is worse than useless -- it reports
/// problems that do not exist and hides the ones that do -- so reads
/// happen for real and only changes are skipped. An action whose decisions
/// depend on a change it just skipped has to handle `dry_run` itself.
pub fn capture(&self, command: &str) -> Result<String> {
let outcome = self.spawn_with(command, true, false, &BTreeMap::new())?;
if !outcome.success {
return Err(self.failed(command, &outcome));
}
Ok(outcome.stdout.trim_end().to_string())
}
/// Read something that is allowed to fail (no tags yet, not a repository).
/// Runs even under `--dry-run`, like [`capture`](Self::capture).
pub fn probe(&self, command: &str) -> Result<shell::Outcome> {
self.spawn_with(command, true, false, &BTreeMap::new())
}
fn spawn_with(
&self,
command: &str,
quiet: bool,
skip_on_dry_run: bool,
extra: &BTreeMap<String, String>,
) -> Result<shell::Outcome> {
if self.dry_run && skip_on_dry_run {
self.ui.say(&format!("Would run: {command}"));
return Ok(shell::Outcome {
code: Some(0),
success: true,
timed_out: false,
interrupted: false,
stdout: String::new(),
stderr: String::new(),
});
}
let secrets = self.secrets.borrow().clone();
let mut env = self.env.clone();
env.extend(extra.iter().map(|(k, v)| (k.clone(), v.clone())));
shell::run(Spawn {
command,
env: &env,
workdir: &self.workdir,
timeout: None,
quiet,
secrets: &secrets,
})
}
fn failed(&self, command: &str, outcome: &shell::Outcome) -> ShlaneError {
let detail = outcome.stderr.trim();
let detail = if detail.is_empty() {
String::new()
} else {
format!(": {detail}")
};
ShlaneError::Action {
action: self.lane.clone(),
message: format!(
"command failed with exit code {}{detail}\n command: {command}",
outcome.code.unwrap_or(-1)
),
}
}
/// Build an error for an action that could not do its job.
pub fn error(&self, action: &str, message: impl Into<String>) -> ShlaneError {
ShlaneError::Action {
action: action.to_string(),
message: message.into(),
}
}
}