1use chrono::Utc;
2use serde_json;
3use serde_yaml::Value;
4use std::{collections::HashMap, fs, io, path::Path};
5use wrkflw_matrix::MatrixCombination;
6use wrkflw_parser::workflow::WorkflowDefinition;
7
8pub fn setup_github_environment_files(workspace_dir: &Path) -> io::Result<()> {
9 let github_dir = workspace_dir.join("github");
11 fs::create_dir_all(&github_dir)?;
12
13 let github_output = github_dir.join("output");
15 let github_env = github_dir.join("env");
16 let github_path = github_dir.join("path");
17 let github_step_summary = github_dir.join("step_summary");
18
19 fs::write(&github_output, "")?;
21 fs::write(&github_env, "")?;
22 fs::write(&github_path, "")?;
23 fs::write(&github_step_summary, "")?;
24
25 Ok(())
26}
27
28pub fn create_github_context(
29 workflow: &WorkflowDefinition,
30 workspace_dir: &Path,
31) -> HashMap<String, String> {
32 let mut env = HashMap::new();
33
34 env.insert("GITHUB_WORKFLOW".to_string(), workflow.name.clone());
36 env.insert("GITHUB_ACTION".to_string(), "run".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 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 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 env.insert("GITHUB_RUN_ATTEMPT".to_string(), "1".to_string());
82
83 env.insert("GITHUB_ACTIONS".to_string(), "true".to_string());
85 env.insert("CI".to_string(), "true".to_string());
86
87 env.insert(
89 "GITHUB_SERVER_URL".to_string(),
90 "https://github.com".to_string(),
91 );
92 env.insert(
93 "GITHUB_API_URL".to_string(),
94 "https://api.github.com".to_string(),
95 );
96 env.insert(
97 "GITHUB_GRAPHQL_URL".to_string(),
98 "https://api.github.com/graphql".to_string(),
99 );
100
101 let full_ref = env.get("GITHUB_REF").cloned().unwrap_or_default();
103 env.insert("GITHUB_REF_NAME".to_string(), get_ref_name(&full_ref));
104 env.insert("GITHUB_REF_TYPE".to_string(), get_ref_type(&full_ref));
105
106 env.insert("GITHUB_HEAD_REF".to_string(), String::new());
108 env.insert("GITHUB_BASE_REF".to_string(), String::new());
109
110 let actor = get_actor();
112 env.insert("GITHUB_ACTOR".to_string(), actor.clone());
113 env.insert("GITHUB_TRIGGERING_ACTOR".to_string(), actor);
114
115 let repo = env.get("GITHUB_REPOSITORY").cloned().unwrap_or_default();
117 env.insert(
118 "GITHUB_REPOSITORY_OWNER".to_string(),
119 get_repository_owner(&repo),
120 );
121
122 env.insert("GITHUB_RETENTION_DAYS".to_string(), "90".to_string());
124
125 env.insert("RUNNER_OS".to_string(), get_runner_os());
127 env.insert("RUNNER_ARCH".to_string(), get_runner_arch());
128 env.insert("RUNNER_NAME".to_string(), "wrkflw-local".to_string());
129 env.insert("RUNNER_ENVIRONMENT".to_string(), "local".to_string());
130 env.insert("RUNNER_TEMP".to_string(), get_temp_dir());
131 env.insert("RUNNER_TOOL_CACHE".to_string(), get_tool_cache_dir());
132
133 env
134}
135
136pub fn add_job_context(env: &mut HashMap<String, String>, job_name: &str) {
138 env.insert("GITHUB_JOB".to_string(), job_name.to_string());
139}
140
141pub fn add_matrix_context(
143 env: &mut HashMap<String, String>,
144 matrix_combination: &MatrixCombination,
145) {
146 for (key, value) in &matrix_combination.values {
148 let env_key = format!("MATRIX_{}", key.to_uppercase());
149 let env_value = value_to_string(value);
150 env.insert(env_key, env_value);
151 }
152
153 if let Ok(json_value) = serde_json::to_string(&matrix_combination.values) {
155 env.insert("MATRIX_CONTEXT".to_string(), json_value);
156 }
157}
158
159fn value_to_string(value: &Value) -> String {
161 match value {
162 Value::String(s) => s.clone(),
163 Value::Number(n) => n.to_string(),
164 Value::Bool(b) => b.to_string(),
165 Value::Sequence(seq) => {
166 let items = seq
167 .iter()
168 .map(value_to_string)
169 .collect::<Vec<_>>()
170 .join(",");
171 items
172 }
173 Value::Mapping(map) => {
174 let items = map
175 .iter()
176 .map(|(k, v)| format!("{}={}", value_to_string(k), value_to_string(v)))
177 .collect::<Vec<_>>()
178 .join(",");
179 items
180 }
181 Value::Null => "".to_string(),
182 _ => "".to_string(),
183 }
184}
185
186fn get_repo_name() -> String {
187 if let Ok(output) = std::process::Command::new("git")
189 .args(["remote", "get-url", "origin"])
190 .output()
191 {
192 if output.status.success() {
193 let url = String::from_utf8_lossy(&output.stdout);
194 if let Some(repo) = extract_repo_from_url(&url) {
195 return repo;
196 }
197 }
198 }
199
200 let current_dir = std::env::current_dir().unwrap_or_default();
202 format!(
203 "wrkflw/{}",
204 current_dir
205 .file_name()
206 .unwrap_or_default()
207 .to_string_lossy()
208 )
209}
210
211fn extract_repo_from_url(url: &str) -> Option<String> {
212 let url = url.trim();
214
215 if url.starts_with("git@") {
217 let parts: Vec<&str> = url.split(':').collect();
218 if parts.len() == 2 {
219 let repo_part = parts[1].trim_end_matches(".git");
220 return Some(repo_part.to_string());
221 }
222 }
223
224 if url.starts_with("http") {
226 let without_protocol = url.split("://").nth(1)?;
227 let parts: Vec<&str> = without_protocol.split('/').collect();
228 if parts.len() >= 3 {
229 let owner = parts[1];
230 let repo = parts[2].trim_end_matches(".git");
231 return Some(format!("{}/{}", owner, repo));
232 }
233 }
234
235 None
236}
237
238fn get_event_name(workflow: &WorkflowDefinition) -> String {
239 if let Some(first_trigger) = workflow.on.first() {
241 return first_trigger.clone();
242 }
243 "workflow_dispatch".to_string()
244}
245
246fn get_workspace_path() -> String {
247 std::env::current_dir()
248 .unwrap_or_default()
249 .to_string_lossy()
250 .to_string()
251}
252
253fn get_current_sha() -> String {
254 if let Ok(output) = std::process::Command::new("git")
255 .args(["rev-parse", "HEAD"])
256 .output()
257 {
258 if output.status.success() {
259 return String::from_utf8_lossy(&output.stdout).trim().to_string();
260 }
261 }
262
263 "0000000000000000000000000000000000000000".to_string()
264}
265
266fn get_current_ref() -> String {
267 if let Ok(output) = std::process::Command::new("git")
268 .args(["symbolic-ref", "--short", "HEAD"])
269 .output()
270 {
271 if output.status.success() {
272 return format!(
273 "refs/heads/{}",
274 String::from_utf8_lossy(&output.stdout).trim()
275 );
276 }
277 }
278
279 "refs/heads/main".to_string()
280}
281
282fn get_runner_os() -> String {
283 match std::env::consts::OS {
284 "macos" => "macOS".to_string(),
285 "linux" => "Linux".to_string(),
286 "windows" => "Windows".to_string(),
287 other => other.to_string(),
288 }
289}
290
291fn get_runner_arch() -> String {
292 match std::env::consts::ARCH {
293 "x86_64" | "x86" => "X64".to_string(),
294 "aarch64" => "ARM64".to_string(),
295 other => other.to_string(),
296 }
297}
298
299fn get_temp_dir() -> String {
300 let temp_dir = std::env::temp_dir();
301 temp_dir.join("wrkflw").to_string_lossy().to_string()
302}
303
304fn get_tool_cache_dir() -> String {
305 let home_dir = dirs::home_dir().unwrap_or_default();
306 home_dir
307 .join(".wrkflw")
308 .join("tools")
309 .to_string_lossy()
310 .to_string()
311}
312
313fn get_ref_name(full_ref: &str) -> String {
314 if let Some(name) = full_ref.strip_prefix("refs/heads/") {
315 name.to_string()
316 } else if let Some(name) = full_ref.strip_prefix("refs/tags/") {
317 name.to_string()
318 } else if let Some(name) = full_ref.strip_prefix("refs/pull/") {
319 name.to_string()
320 } else {
321 full_ref.to_string()
322 }
323}
324
325fn get_ref_type(full_ref: &str) -> String {
326 if full_ref.starts_with("refs/tags/") {
327 "tag".to_string()
328 } else {
329 "branch".to_string()
330 }
331}
332
333fn get_repository_owner(repo: &str) -> String {
334 repo.split('/').next().unwrap_or("").to_string()
335}
336
337fn get_actor() -> String {
338 if let Ok(output) = std::process::Command::new("git")
340 .args(["config", "user.name"])
341 .output()
342 {
343 if output.status.success() {
344 let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
345 if !name.is_empty() {
346 return name;
347 }
348 }
349 }
350
351 if let Ok(user) = std::env::var("USER") {
353 if !user.is_empty() {
354 return user;
355 }
356 }
357 if let Ok(user) = std::env::var("USERNAME") {
358 if !user.is_empty() {
359 return user;
360 }
361 }
362
363 "wrkflw".to_string()
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 #[test]
371 fn ref_name_strips_heads_prefix() {
372 assert_eq!(get_ref_name("refs/heads/main"), "main");
373 assert_eq!(get_ref_name("refs/heads/feature/foo"), "feature/foo");
374 }
375
376 #[test]
377 fn ref_name_strips_tags_prefix() {
378 assert_eq!(get_ref_name("refs/tags/v1.0.0"), "v1.0.0");
379 }
380
381 #[test]
382 fn ref_name_returns_input_for_unknown_prefix() {
383 assert_eq!(get_ref_name("some/other/ref"), "some/other/ref");
384 }
385
386 #[test]
387 fn ref_type_detects_tag() {
388 assert_eq!(get_ref_type("refs/tags/v1.0.0"), "tag");
389 }
390
391 #[test]
392 fn ref_type_defaults_to_branch() {
393 assert_eq!(get_ref_type("refs/heads/main"), "branch");
394 assert_eq!(get_ref_type("something-else"), "branch");
395 }
396
397 #[test]
398 fn repository_owner_extracts_owner() {
399 assert_eq!(get_repository_owner("octocat/hello-world"), "octocat");
400 }
401
402 #[test]
403 fn repository_owner_handles_no_slash() {
404 assert_eq!(get_repository_owner("myrepo"), "myrepo");
405 }
406
407 #[test]
408 fn repository_owner_handles_empty() {
409 assert_eq!(get_repository_owner(""), "");
410 }
411
412 #[test]
413 fn extract_repo_from_ssh_url() {
414 assert_eq!(
415 extract_repo_from_url("git@github.com:owner/repo.git"),
416 Some("owner/repo".to_string())
417 );
418 }
419
420 #[test]
421 fn extract_repo_from_https_url() {
422 assert_eq!(
423 extract_repo_from_url("https://github.com/owner/repo.git"),
424 Some("owner/repo".to_string())
425 );
426 }
427
428 #[test]
429 fn extract_repo_from_invalid_url() {
430 assert_eq!(extract_repo_from_url("not-a-url"), None);
431 }
432}