Skip to main content

wrkflw_executor/
environment.rs

1use chrono::Utc;
2use serde_yaml::Value;
3use std::{collections::HashMap, fs, io, path::Path};
4use wrkflw_matrix::MatrixCombination;
5use wrkflw_parser::workflow::WorkflowDefinition;
6
7pub fn setup_github_environment_files(workspace_dir: &Path) -> io::Result<()> {
8    // Create necessary directories
9    let github_dir = workspace_dir.join("github");
10    fs::create_dir_all(&github_dir)?;
11
12    // Create common GitHub environment files
13    let github_output = github_dir.join("output");
14    let github_env = github_dir.join("env");
15    let github_path = github_dir.join("path");
16    let github_step_summary = github_dir.join("step_summary");
17
18    // Initialize files with empty content
19    fs::write(&github_output, "")?;
20    fs::write(&github_env, "")?;
21    fs::write(&github_path, "")?;
22    fs::write(&github_step_summary, "")?;
23
24    Ok(())
25}
26
27pub fn create_github_context(
28    workflow: &WorkflowDefinition,
29    workspace_dir: &Path,
30) -> HashMap<String, String> {
31    let mut env = HashMap::new();
32
33    // Basic GitHub environment variables
34    env.insert("GITHUB_WORKFLOW".to_string(), workflow.name.clone());
35    env.insert("GITHUB_ACTION".to_string(), "run".to_string());
36    env.insert("GITHUB_ACTOR".to_string(), "wrkflw".to_string());
37    env.insert("GITHUB_REPOSITORY".to_string(), get_repo_name());
38    env.insert("GITHUB_EVENT_NAME".to_string(), get_event_name(workflow));
39    env.insert("GITHUB_WORKSPACE".to_string(), get_workspace_path());
40    env.insert("GITHUB_SHA".to_string(), get_current_sha());
41    env.insert("GITHUB_REF".to_string(), get_current_ref());
42
43    // File paths for GitHub Actions
44    env.insert(
45        "GITHUB_OUTPUT".to_string(),
46        workspace_dir
47            .join("github")
48            .join("output")
49            .to_string_lossy()
50            .to_string(),
51    );
52    env.insert(
53        "GITHUB_ENV".to_string(),
54        workspace_dir
55            .join("github")
56            .join("env")
57            .to_string_lossy()
58            .to_string(),
59    );
60    env.insert(
61        "GITHUB_PATH".to_string(),
62        workspace_dir
63            .join("github")
64            .join("path")
65            .to_string_lossy()
66            .to_string(),
67    );
68    env.insert(
69        "GITHUB_STEP_SUMMARY".to_string(),
70        workspace_dir
71            .join("github")
72            .join("step_summary")
73            .to_string_lossy()
74            .to_string(),
75    );
76
77    // Time-related variables
78    let now = Utc::now();
79    env.insert("GITHUB_RUN_ID".to_string(), format!("{}", now.timestamp()));
80    env.insert("GITHUB_RUN_NUMBER".to_string(), "1".to_string());
81
82    // Path-related variables
83    env.insert("RUNNER_TEMP".to_string(), get_temp_dir());
84    env.insert("RUNNER_TOOL_CACHE".to_string(), get_tool_cache_dir());
85
86    env
87}
88
89/// Add matrix context variables to the environment
90pub fn add_matrix_context(
91    env: &mut HashMap<String, String>,
92    matrix_combination: &MatrixCombination,
93) {
94    // Add each matrix parameter as an environment variable
95    for (key, value) in &matrix_combination.values {
96        let env_key = format!("MATRIX_{}", key.to_uppercase());
97        let env_value = value_to_string(value);
98        env.insert(env_key, env_value);
99    }
100
101    // Also serialize the whole matrix as JSON for potential use
102    if let Ok(json_value) = serde_json::to_string(&matrix_combination.values) {
103        env.insert("MATRIX_CONTEXT".to_string(), json_value);
104    }
105}
106
107/// Convert a serde_yaml::Value to a string for environment variables
108fn value_to_string(value: &Value) -> String {
109    match value {
110        Value::String(s) => s.clone(),
111        Value::Number(n) => n.to_string(),
112        Value::Bool(b) => b.to_string(),
113        Value::Sequence(seq) => {
114            let items = seq
115                .iter()
116                .map(value_to_string)
117                .collect::<Vec<_>>()
118                .join(",");
119            items
120        }
121        Value::Mapping(map) => {
122            let items = map
123                .iter()
124                .map(|(k, v)| format!("{}={}", value_to_string(k), value_to_string(v)))
125                .collect::<Vec<_>>()
126                .join(",");
127            items
128        }
129        Value::Null => "".to_string(),
130        _ => "".to_string(),
131    }
132}
133
134fn get_repo_name() -> String {
135    // Try to detect from git if available
136    if let Ok(output) = std::process::Command::new("git")
137        .args(["remote", "get-url", "origin"])
138        .output()
139    {
140        if output.status.success() {
141            let url = String::from_utf8_lossy(&output.stdout);
142            if let Some(repo) = extract_repo_from_url(&url) {
143                return repo;
144            }
145        }
146    }
147
148    // Fallback to directory name
149    let current_dir = std::env::current_dir().unwrap_or_default();
150    format!(
151        "wrkflw/{}",
152        current_dir
153            .file_name()
154            .unwrap_or_default()
155            .to_string_lossy()
156    )
157}
158
159fn extract_repo_from_url(url: &str) -> Option<String> {
160    // Extract owner/repo from common git URLs
161    let url = url.trim();
162
163    // Handle SSH URLs: git@github.com:owner/repo.git
164    if url.starts_with("git@") {
165        let parts: Vec<&str> = url.split(':').collect();
166        if parts.len() == 2 {
167            let repo_part = parts[1].trim_end_matches(".git");
168            return Some(repo_part.to_string());
169        }
170    }
171
172    // Handle HTTPS URLs: https://github.com/owner/repo.git
173    if url.starts_with("http") {
174        let without_protocol = url.split("://").nth(1)?;
175        let parts: Vec<&str> = without_protocol.split('/').collect();
176        if parts.len() >= 3 {
177            let owner = parts[1];
178            let repo = parts[2].trim_end_matches(".git");
179            return Some(format!("{}/{}", owner, repo));
180        }
181    }
182
183    None
184}
185
186fn get_event_name(workflow: &WorkflowDefinition) -> String {
187    // Try to extract from the workflow trigger
188    if let Some(first_trigger) = workflow.on.first() {
189        return first_trigger.clone();
190    }
191    "workflow_dispatch".to_string()
192}
193
194fn get_workspace_path() -> String {
195    std::env::current_dir()
196        .unwrap_or_default()
197        .to_string_lossy()
198        .to_string()
199}
200
201fn get_current_sha() -> String {
202    if let Ok(output) = std::process::Command::new("git")
203        .args(["rev-parse", "HEAD"])
204        .output()
205    {
206        if output.status.success() {
207            return String::from_utf8_lossy(&output.stdout).trim().to_string();
208        }
209    }
210
211    "0000000000000000000000000000000000000000".to_string()
212}
213
214fn get_current_ref() -> String {
215    if let Ok(output) = std::process::Command::new("git")
216        .args(["symbolic-ref", "--short", "HEAD"])
217        .output()
218    {
219        if output.status.success() {
220            return format!(
221                "refs/heads/{}",
222                String::from_utf8_lossy(&output.stdout).trim()
223            );
224        }
225    }
226
227    "refs/heads/main".to_string()
228}
229
230fn get_temp_dir() -> String {
231    let temp_dir = std::env::temp_dir();
232    temp_dir.join("wrkflw").to_string_lossy().to_string()
233}
234
235fn get_tool_cache_dir() -> String {
236    let home_dir = dirs::home_dir().unwrap_or_default();
237    home_dir
238        .join(".wrkflw")
239        .join("tools")
240        .to_string_lossy()
241        .to_string()
242}