use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use crate::dsl::{Concurrency, Job, MarkedPermissions, Step, WorkflowDefinition, WorkflowTriggers}; pub const ACTIONS_WORKFLOW_DIR: &str = ".github/workflows"; pub const SNAPSHOT_KEY: &str = "_gitdata_actions"; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] pub struct ActionsWorkflow { pub file: String, pub source: String, pub document: Value, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] pub struct InputDefinition { #[serde(default)] pub description: Option, #[serde(default)] pub required: bool, #[serde(default, rename = "type")] pub kind: Option, #[serde(default)] pub default: Option, #[serde(default)] pub options: Vec, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] pub struct NeedResult { pub result: String, #[serde(default)] pub outputs: BTreeMap, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] pub struct EngineInstance { pub index: usize, pub matrix: Value, pub name: String, pub runs_on: Vec, pub environment: String, pub timeout_seconds: u64, pub continue_on_error: bool, pub fail_fast: bool, pub max_parallel: usize, pub concurrency: Value, pub permissions: Value, #[serde(rename = "with")] pub inputs: Value, pub uses: String, pub group: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct EngineExpansion { pub skipped: bool, pub instances: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] pub struct ActionsExecution { pub workflow: ActionsWorkflow, pub job: String, pub matrix: Value, pub github: Value, pub event: Value, pub needs: BTreeMap, pub inputs: BTreeMap, pub vars: BTreeMap, pub token: String, pub runtime_url: String, pub runtime_token: String, } #[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)] pub struct EngineEvent { pub event: String, #[serde(default)] pub data: Value, #[serde(default)] pub error: Option, #[serde(default)] pub lease_token: Option, } impl ActionsWorkflow { pub fn name(&self) -> Option { self.document .get("name") .and_then(Value::as_str) .map(str::to_owned) } pub fn triggers(&self) -> &Value { self.document.get("on").unwrap_or(&Value::Null) } pub fn inputs(&self) -> BTreeMap { self.triggers() .get("workflow_dispatch") .and_then(|v| v.get("inputs")) .and_then(|v| serde_json::from_value(v.clone()).ok()) .unwrap_or_default() } pub fn projection(&self) -> WorkflowDefinition { let on = self.triggers(); let triggers = WorkflowTriggers { push: on.get("push").map(|value| crate::dsl::PushTrigger { branches: strings(value.get("branches")), tags: strings(value.get("tags")), }), pull_request: on .get("pull_request") .map(|value| crate::dsl::PullRequestTrigger { branches: strings(value.get("branches")), types: strings(value.get("types")) .into_iter() .filter_map(|s| serde_json::from_value(json!(s)).ok()) .collect(), }), workflow_dispatch: on .get("workflow_dispatch") .map(|_| crate::dsl::WorkflowDispatchTrigger::default()), }; let jobs = self .document .get("jobs") .and_then(Value::as_object) .into_iter() .flatten() .map(|(key, value)| { ( key.clone(), Job { runs_on: strings(value.get("runs-on")), condition: None, needs: strings(value.get("needs")), environment: value .get("environment") .and_then(|v| { v.as_str().or_else(|| v.get("name").and_then(Value::as_str)) }) .map(str::to_owned), permissions: MarkedPermissions::default(), container: None, timeout_minutes: value.get("timeout-minutes").and_then(Value::as_u64), env: env_map(value.get("env")), artifacts: Vec::new(), steps: value .get("steps") .and_then(Value::as_array) .into_iter() .flatten() .map(|step| Step { name: step.get("name").and_then(Value::as_str).map(str::to_owned), run: step .get("run") .or_else(|| step.get("uses")) .and_then(Value::as_str) .unwrap_or_default() .to_owned(), shell: step.get("shell").and_then(Value::as_str).map(str::to_owned), working_directory: step .get("working-directory") .and_then(Value::as_str) .map(str::to_owned), env: env_map(step.get("env")), timeout_minutes: step .get("timeout-minutes") .and_then(Value::as_u64), }) .collect(), }, ) }) .collect(); WorkflowDefinition { name: self.name(), on: triggers, env: env_map(self.document.get("env")), concurrency: self .document .get("concurrency") .and_then(|value| { value .as_str() .or_else(|| value.get("group").and_then(Value::as_str)) }) .map(|group| Concurrency { group: group.to_owned(), cancel_in_progress: self.document["concurrency"]["cancel-in-progress"] .as_bool() .unwrap_or(false), }), jobs, } } } pub fn strings(value: Option<&Value>) -> Vec { match value { Some(Value::String(value)) => vec![value.clone()], Some(Value::Array(values)) => values .iter() .filter_map(Value::as_str) .map(str::to_owned) .collect(), _ => Vec::new(), } } pub fn env_map(value: Option<&Value>) -> BTreeMap { value .and_then(Value::as_object) .into_iter() .flatten() .map(|(name, value)| { ( name.clone(), value .as_str() .map(str::to_owned) .unwrap_or_else(|| value.to_string()), ) }) .collect() } pub fn from_snapshot(value: &Value) -> Option { serde_json::from_value(value.get(SNAPSHOT_KEY)?.clone()).ok() }