Skip to main content

ambient_ci/action_impl/
custom.rs

1use std::{collections::HashMap, path::Path, process::Command};
2
3use clingwrap::runner::{CommandError, CommandRunner};
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    action::{ActionError, Context},
8    action_impl::ActionImpl,
9    runlog::RunLogSource,
10};
11
12/// Execute a custom action.
13///
14/// Run the named executable from `.ambient` at the root of the source
15/// tree. The arguments in `args` are used to set environment variables
16/// and are also passed in via the standard input, both as JSON. Note that
17/// there is no way for Ambient to check that the necessary arguments are
18/// provided.
19///
20/// An `args` value of `foo` results in `AMBIENT_CI_foo` being set in the
21/// environment.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct Custom {
24    /// Name of custom action executable in `.ambient`.
25    pub name: String,
26
27    /// Arguments to custom action.
28    #[serde(default)]
29    pub args: HashMap<String, serde_norway::Value>,
30}
31
32impl Custom {
33    /// Create a new custom action.
34    pub fn new(name: String, args: HashMap<String, serde_norway::Value>) -> Self {
35        Self { name, args }
36    }
37}
38
39impl ActionImpl for Custom {
40    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
41        let source = context.source_dir().to_path_buf();
42        let exe = Path::new(".ambient").join(&self.name);
43        let exe_exists = exe.exists();
44        let json = serde_json::to_string(&self.args).map_err(CustomError::ArgsToJson)?;
45
46        context.runlog().custom_action_starts(
47            RunLogSource::Plan,
48            source.clone(),
49            self.clone(),
50            exe.clone(),
51            exe_exists,
52        );
53
54        let mut cmd = Command::new(exe);
55        cmd.current_dir(&source);
56        cmd.envs(context.env());
57        for (key, value) in self.args.iter() {
58            let key = format!("AMBIENT_CI_{key}");
59            let value = serde_json::to_string(value).map_err(CustomError::ArgsToJson)?;
60            cmd.env(key, value);
61        }
62        let mut runner = CommandRunner::new(cmd);
63        runner.feed_stdin(json.as_bytes());
64        runner.capture_stdout();
65        runner.capture_stderr();
66
67        let result = runner.execute();
68        if let Ok(output) = result {
69            // Output text to stdout for test suite. Use debug formatting to avoid
70            // accidentally looking like a JSON line.
71            println!("{:?}", String::from_utf8_lossy(&output.stdout));
72            context
73                .runlog()
74                .custom_action_output(RunLogSource::Plan, output.stdout, output.stderr);
75        } else if let Err(err) = result {
76            if let CommandError::CommandFailed { output, .. } = &err {
77                context.runlog().custom_action_output(
78                    RunLogSource::Plan,
79                    output.stdout.clone(),
80                    output.stderr.clone(),
81                );
82            }
83            Err(CustomError::Custom(self.name.to_string(), err))?;
84        }
85
86        Ok(())
87    }
88}
89
90/// Errors from a custom action.
91#[derive(Debug, thiserror::Error)]
92pub enum CustomError {
93    /// Can't run custom action.
94    #[error("failed to run custom action {0:?}")]
95    Custom(String, #[source] clingwrap::runner::CommandError),
96
97    /// Can't convert custom action arguments to JSON.
98    #[error("failed to convert custom action into JSON")]
99    ArgsToJson(#[source] serde_json::Error),
100}
101
102impl From<CustomError> for ActionError {
103    fn from(value: CustomError) -> Self {
104        Self::Custom(value)
105    }
106}