Skip to main content

mur_core/workflow/
cloud.rs

1//! Cloud workflow execution — run workflows on mur.run servers (Pro/Team only).
2
3use crate::types::ExecutionResult;
4use anyhow::{Context, Result};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Cloud execution client for mur.run.
9pub struct CloudExecutor {
10    api_token: String,
11    base_url: String,
12    client: reqwest::Client,
13}
14
15/// Request to execute a workflow on the cloud.
16#[derive(Debug, Serialize)]
17pub struct CloudRunRequest {
18    pub workflow_id: String,
19    pub variables: HashMap<String, String>,
20    pub shadow: bool,
21}
22
23/// Response from cloud execution.
24#[derive(Debug, Deserialize)]
25pub struct CloudRunResponse {
26    pub execution_id: String,
27    pub status: String,
28    pub result: Option<ExecutionResult>,
29}
30
31impl CloudExecutor {
32    pub fn new(api_token: String, base_url: Option<String>) -> Self {
33        Self {
34            api_token,
35            base_url: base_url.unwrap_or_else(|| "https://mur.run/api/v1".into()),
36            client: reqwest::Client::new(),
37        }
38    }
39
40    /// Submit a workflow for cloud execution.
41    pub async fn submit(&self, request: &CloudRunRequest) -> Result<CloudRunResponse> {
42        let response = self
43            .client
44            .post(format!("{}/workflows/run", self.base_url))
45            .header("Authorization", format!("Bearer {}", self.api_token))
46            .json(request)
47            .send()
48            .await
49            .context("Submitting to mur.run")?;
50
51        if !response.status().is_success() {
52            let status = response.status();
53            let body = response.text().await.unwrap_or_default();
54            anyhow::bail!("Cloud execution error {}: {}", status, body);
55        }
56
57        response.json().await.context("Parsing cloud response")
58    }
59
60    /// Check execution status.
61    pub async fn status(&self, execution_id: &str) -> Result<CloudRunResponse> {
62        let response = self
63            .client
64            .get(format!("{}/executions/{}", self.base_url, execution_id))
65            .header("Authorization", format!("Bearer {}", self.api_token))
66            .send()
67            .await
68            .context("Checking cloud execution status")?;
69
70        response.json().await.context("Parsing status response")
71    }
72
73    /// Upload a workflow YAML to mur.run.
74    pub async fn upload_workflow(&self, workflow_yaml: &str) -> Result<String> {
75        let response = self
76            .client
77            .post(format!("{}/workflows/upload", self.base_url))
78            .header("Authorization", format!("Bearer {}", self.api_token))
79            .header("Content-Type", "text/yaml")
80            .body(workflow_yaml.to_string())
81            .send()
82            .await
83            .context("Uploading workflow")?;
84
85        if !response.status().is_success() {
86            anyhow::bail!("Upload failed: {}", response.status());
87        }
88
89        #[derive(Deserialize)]
90        struct UploadResponse {
91            workflow_id: String,
92        }
93        let resp: UploadResponse = response.json().await?;
94        Ok(resp.workflow_id)
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_cloud_run_request() {
104        let req = CloudRunRequest {
105            workflow_id: "deploy".into(),
106            variables: HashMap::from([("env".into(), "staging".into())]),
107            shadow: true,
108        };
109        let json = serde_json::to_string(&req).unwrap();
110        assert!(json.contains("deploy"));
111        assert!(json.contains("shadow"));
112    }
113}