ferrous_actions/actions/
core.rs

1use crate::node::path::Path;
2use js_sys::{JsString, Number, Object};
3use wasm_bindgen::JsValue;
4
5/// Formats and outputs a GitHub actions log line at debug level
6#[macro_export]
7macro_rules! debug {
8    ($($arg:tt)*) => {{
9        $crate::actions::core::debug(std::format!($($arg)*).as_str());
10    }};
11}
12
13/// Formats and outputs a GitHub actions log line at info level
14#[macro_export]
15macro_rules! info {
16    ($($arg:tt)*) => {{
17        $crate::actions::core::info(std::format!($($arg)*).as_str());
18    }};
19}
20
21/// Formats and outputs a GitHub actions log line at notice level (this will be
22/// an annotation)
23#[macro_export]
24macro_rules! notice {
25    ($($arg:tt)*) => {{
26        $crate::actions::core::notice(std::format!($($arg)*).as_str());
27    }};
28}
29
30/// Formats and outputs a GitHub actions log line at warning level (this will be
31/// an annotation)
32#[macro_export]
33macro_rules! warning {
34    ($($arg:tt)*) => {{
35        $crate::actions::core::warning(std::format!($($arg)*).as_str());
36    }};
37}
38
39/// Formats and outputs a GitHub actions log line at error level (this will be
40/// an annotation)
41#[macro_export]
42macro_rules! error {
43    ($($arg:tt)*) => {{
44        $crate::actions::core::error(std::format!($($arg)*).as_str());
45    }};
46}
47
48/// Outputs a GitHub actions log line at debug level
49pub fn debug<S: Into<JsString>>(message: S) {
50    ffi::debug(&message.into());
51}
52
53/// Outputs a GitHub actions log line at info level
54pub fn info<S: Into<JsString>>(message: S) {
55    ffi::info(&message.into());
56}
57
58/// Outputs a GitHub actions log line at notice level (this will be
59/// an annotation)
60pub fn notice<A: Into<Annotation>>(message: A) {
61    message.into().notice();
62}
63
64/// Outputs a GitHub actions log line at warning level (this will be
65/// an annotation)
66pub fn warning<A: Into<Annotation>>(message: A) {
67    message.into().warning();
68}
69
70/// Formats and outputs a GitHub actions log line at error level (this will be
71/// an annotation)
72pub fn error<A: Into<Annotation>>(message: A) {
73    message.into().error();
74}
75
76/// Sets a named action output to the specified value
77pub fn set_output<N: Into<JsString>, V: Into<JsString>>(name: N, value: V) {
78    ffi::set_output(&name.into(), &value.into());
79}
80
81/// Builder for retrieving action inputs
82#[derive(Debug)]
83pub struct Input {
84    name: JsString,
85    required: bool,
86    trim_whitespace: bool,
87}
88
89impl<N: Into<JsString>> From<N> for Input {
90    /// Construct a builder to access the specified input name
91    fn from(name: N) -> Input {
92        Input {
93            name: name.into(),
94            required: false,
95            trim_whitespace: true,
96        }
97    }
98}
99
100impl Input {
101    /// Mark this input as required. The `Input` will return an error on
102    /// retrieval if the input is not defined.
103    pub fn required(&mut self, value: bool) -> &mut Input {
104        self.required = value;
105        self
106    }
107
108    /// Specifies that whitespace should be trimmed from the retrieved input
109    pub fn trim_whitespace(&mut self, value: bool) -> &mut Input {
110        self.trim_whitespace = value;
111        self
112    }
113
114    fn to_ffi(&self) -> ffi::InputOptions {
115        ffi::InputOptions {
116            required: Some(self.required),
117            trim_whitespace: Some(self.trim_whitespace),
118        }
119    }
120
121    /// Gets the specified input (if defined)
122    pub fn get(&mut self) -> Result<Option<String>, JsValue> {
123        let ffi = self.to_ffi();
124        let value = String::from(ffi::get_input(&self.name, Some(ffi))?);
125        Ok(if value.is_empty() { None } else { Some(value) })
126    }
127
128    /// Gets the specified input, returning an error if it is not defined
129    pub fn get_required(&mut self) -> Result<String, JsValue> {
130        let mut ffi = self.to_ffi();
131        ffi.required = Some(true);
132        ffi::get_input(&self.name, Some(ffi)).map(String::from)
133    }
134}
135
136/// Builder for outputting annotations
137#[derive(Debug)]
138pub struct Annotation {
139    message: String,
140    title: Option<String>,
141    file: Option<Path>,
142    start_line: Option<usize>,
143    end_line: Option<usize>,
144    start_column: Option<usize>,
145    end_column: Option<usize>,
146}
147
148impl<M: Into<String>> From<M> for Annotation {
149    /// Constructs an annotation with the specified message
150    fn from(message: M) -> Annotation {
151        Annotation {
152            message: message.into(),
153            title: None,
154            file: None,
155            start_line: None,
156            end_line: None,
157            start_column: None,
158            end_column: None,
159        }
160    }
161}
162
163/// Annotation levels
164#[derive(Copy, Clone, Debug)]
165pub enum AnnotationLevel {
166    /// Notice
167    Notice,
168
169    /// Warning
170    Warning,
171
172    /// Error
173    Error,
174}
175
176impl Annotation {
177    /// Sets the title of the annotation
178    pub fn title(&mut self, title: &str) -> &mut Annotation {
179        self.title = Some(title.to_string());
180        self
181    }
182
183    /// Sets the path to a file to which the annotation is relevant
184    pub fn file(&mut self, path: &Path) -> &mut Annotation {
185        self.file = Some(path.clone());
186        self
187    }
188
189    /// Sets the line in the file the annotation should start
190    pub fn start_line(&mut self, start_line: usize) -> &mut Annotation {
191        self.start_line = Some(start_line);
192        self
193    }
194
195    /// Sets the line in the file the annotation should end
196    pub fn end_line(&mut self, end_line: usize) -> &mut Annotation {
197        self.end_line = Some(end_line);
198        self
199    }
200
201    /// Sets the column in the file the annotation should start
202    pub fn start_column(&mut self, start_column: usize) -> &mut Annotation {
203        self.start_column = Some(start_column);
204        self
205    }
206
207    /// Sets the column in the file the annotation should end
208    pub fn end_column(&mut self, end_column: usize) -> &mut Annotation {
209        self.end_column = Some(end_column);
210        self
211    }
212
213    fn build_js_properties(&self) -> Object {
214        let properties = js_sys::Map::new();
215        if let Some(title) = &self.title {
216            properties.set(&"title".into(), JsString::from(title.as_str()).as_ref());
217        }
218        if let Some(file) = &self.file {
219            properties.set(&"file".into(), file.to_js_string().as_ref());
220        }
221        for (name, value) in [
222            ("startLine", &self.start_line),
223            ("endLine", &self.end_line),
224            ("startColumn", &self.start_column),
225            ("endColumn", &self.end_column),
226        ] {
227            if let Some(number) = value.and_then(|n| TryInto::<u32>::try_into(n).ok()) {
228                properties.set(&name.into(), Number::from(number).as_ref());
229            }
230        }
231        Object::from_entries(&properties).expect("Failed to convert options map to object")
232    }
233
234    /// Outputs the annotation as an error
235    pub fn error(&self) {
236        self.output(AnnotationLevel::Error);
237    }
238
239    /// Outputs the annotation at notice level
240    pub fn notice(&self) {
241        self.output(AnnotationLevel::Notice);
242    }
243
244    /// Outputs the annotation as a warning
245    pub fn warning(&self) {
246        self.output(AnnotationLevel::Warning);
247    }
248
249    /// Outputs the annotation at the specified level
250    pub fn output(&self, level: AnnotationLevel) {
251        let message = JsString::from(self.message.as_str());
252        let properties = self.build_js_properties();
253        match level {
254            AnnotationLevel::Error => ffi::error(&message, Some(properties)),
255            AnnotationLevel::Warning => ffi::warning(&message, Some(properties)),
256            AnnotationLevel::Notice => ffi::notice(&message, Some(properties)),
257        }
258    }
259}
260
261/// Retrieves the action input of the specified name
262pub fn get_input<I: Into<Input>>(input: I) -> Result<Option<String>, JsValue> {
263    let mut input = input.into();
264    input.get()
265}
266
267/// Mark this action as failed for the specified reason
268pub fn set_failed<M: Into<JsString>>(message: M) {
269    ffi::set_failed(&message.into());
270}
271
272/// Adds the specified path into `$PATH` for use by later actions
273pub fn add_path(path: &Path) {
274    ffi::add_path(&path.into());
275}
276
277/// Exports an environment variable from the action
278pub fn export_variable<N: Into<JsString>, V: Into<JsString>>(name: N, value: V) {
279    let name = name.into();
280    let value = value.into();
281    ffi::export_variable(&name, &value);
282}
283
284/// Saves state for use by the action in a later phase
285pub fn save_state<N: Into<JsString>, V: Into<JsString>>(name: N, value: V) {
286    let name = name.into();
287    let value = value.into();
288    ffi::save_state(&name, &value);
289}
290
291/// Retrieves previously saved action state
292pub fn get_state<N: Into<JsString>>(name: N) -> Option<String> {
293    let name = name.into();
294    let value: String = ffi::get_state(&name).into();
295    let value = value.trim();
296    if value.is_empty() {
297        None
298    } else {
299        Some(value.into())
300    }
301}
302
303/// Starts a foldable group
304pub fn start_group<N: Into<JsString>>(name: N) {
305    ffi::start_group(&name.into());
306}
307
308/// Ends a foldable group
309pub fn end_group() {
310    ffi::end_group();
311}
312
313/// Low-level bindings to the GitHub Actions Toolkit "core" API
314#[allow(clippy::drop_non_drop)]
315pub mod ffi {
316    use js_sys::{JsString, Object};
317    use wasm_bindgen::prelude::*;
318
319    #[wasm_bindgen]
320    pub struct InputOptions {
321        pub required: Option<bool>,
322
323        #[wasm_bindgen(js_name = "trimWhitespace")]
324        pub trim_whitespace: Option<bool>,
325    }
326
327    #[wasm_bindgen(module = "@actions/core")]
328    extern "C" {
329        /// Gets the value of an input. The value is also trimmed.
330        #[wasm_bindgen(js_name = "getInput", catch)]
331        pub fn get_input(name: &JsString, options: Option<InputOptions>) -> Result<JsString, JsValue>;
332
333        /// Writes info
334        #[wasm_bindgen]
335        pub fn info(message: &JsString);
336
337        /// Writes debug
338        #[wasm_bindgen]
339        pub fn debug(message: &JsString);
340
341        /// Writes an error with an optional annotation
342        #[wasm_bindgen]
343        pub fn error(message: &JsString, annotation: Option<Object>);
344
345        /// Writes a warning with an optional annotation
346        #[wasm_bindgen]
347        pub fn warning(message: &JsString, annotation: Option<Object>);
348
349        /// Writes a notice with an optional annotation
350        #[wasm_bindgen]
351        pub fn notice(message: &JsString, annotation: Option<Object>);
352
353        /// Sets the action status to failed.
354        /// When the action exits it will be with an exit code of 1.
355        #[wasm_bindgen(js_name = "setFailed")]
356        pub fn set_failed(message: &JsString);
357
358        /// Sets the value of an output.
359        #[wasm_bindgen(js_name = "setOutput")]
360        pub fn set_output(name: &JsString, value: &JsString);
361
362        #[wasm_bindgen(js_name = "addPath")]
363        pub fn add_path(path: &JsString);
364
365        #[wasm_bindgen(js_name = "exportVariable")]
366        pub fn export_variable(name: &JsString, value: &JsString);
367
368        #[wasm_bindgen(js_name = "saveState")]
369        pub fn save_state(name: &JsString, value: &JsString);
370
371        #[wasm_bindgen(js_name = "getState")]
372        pub fn get_state(name: &JsString) -> JsString;
373
374        #[wasm_bindgen(js_name = "startGroup")]
375        pub fn start_group(name: &JsString);
376
377        #[wasm_bindgen(js_name = "endGroup")]
378        pub fn end_group();
379    }
380}