use std::collections::BTreeMap; use globset::{Glob, GlobSet, GlobSetBuilder}; use serde::{Deserialize, Serialize}; pub const WORKFLOW_DIR: &str = ".github/workflows"; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(deny_unknown_fields)] pub struct WorkflowDefinition { #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, pub on: WorkflowTriggers, #[serde(default)] pub env: BTreeMap, #[serde(default, skip_serializing_if = "Option::is_none")] pub concurrency: Option, pub jobs: BTreeMap, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(deny_unknown_fields)] pub struct WorkflowTriggers { #[serde(default, skip_serializing_if = "Option::is_none")] pub push: Option, #[serde( default, rename = "pull_request", skip_serializing_if = "Option::is_none" )] pub pull_request: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub workflow_dispatch: Option, } impl WorkflowTriggers { #[must_use] pub fn is_empty(&self) -> bool { self.push.is_none() && self.pull_request.is_none() && self.workflow_dispatch.is_none() } } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(deny_unknown_fields)] pub struct PushTrigger { #[serde(default)] pub branches: Vec, #[serde(default)] pub tags: Vec, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(deny_unknown_fields)] pub struct PullRequestTrigger { #[serde(default)] pub branches: Vec, #[serde(default)] pub types: Vec, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(deny_unknown_fields)] pub struct WorkflowDispatchTrigger { #[serde(default)] pub inputs: BTreeMap, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "snake_case")] pub enum PullRequestAction { Opened, Synchronize, Reopened, Merged, Closed, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(deny_unknown_fields)] pub struct Concurrency { pub group: String, #[serde(default)] pub cancel_in_progress: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(deny_unknown_fields)] pub struct Job { #[serde(default = "default_runs_on", rename = "runs-on")] pub runs_on: Vec, #[serde(default, rename = "if", skip_serializing_if = "Option::is_none")] pub condition: Option, #[serde(default)] pub needs: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub environment: Option, #[serde(default)] pub permissions: MarkedPermissions, #[serde(default, skip_serializing_if = "Option::is_none")] pub container: Option, #[serde( default, rename = "timeout-minutes", skip_serializing_if = "Option::is_none" )] pub timeout_minutes: Option, #[serde(default)] pub env: BTreeMap, #[serde(default)] pub artifacts: Vec, pub steps: Vec, } fn default_runs_on() -> Vec { vec!["self-hosted".to_string()] } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(deny_unknown_fields)] pub struct MarkedPermissions { #[serde(default, skip_serializing_if = "Option::is_none")] pub contents: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "snake_case")] pub enum PermissionLevel { Read, Write, None, } impl MarkedPermissions { #[must_use] pub fn contents_write(&self) -> bool { matches!(self.contents, Some(PermissionLevel::Write)) } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(deny_unknown_fields)] pub struct ArtifactSpec { pub name: String, pub path: String, #[serde(default)] pub always: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(deny_unknown_fields)] pub struct Step { #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, pub run: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub shell: Option, #[serde( default, rename = "working-directory", skip_serializing_if = "Option::is_none" )] pub working_directory: Option, #[serde(default)] pub env: BTreeMap, #[serde( default, rename = "timeout-minutes", skip_serializing_if = "Option::is_none" )] pub timeout_minutes: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] pub struct WorkflowSummary { pub file: String, pub name: String, pub valid: bool, pub errors: Vec, pub triggers: TriggerSummary, #[serde(default)] pub inputs: BTreeMap, #[serde(default)] pub engine: String, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] pub struct TriggerSummary { pub push_branches: Vec, pub push_tags: Vec, pub pull_request_branches: Vec, pub pull_request_types: Vec, pub workflow_dispatch: bool, } impl TriggerSummary { #[must_use] pub fn from_triggers(triggers: &WorkflowTriggers) -> Self { Self { push_branches: triggers .push .as_ref() .map(|p| p.branches.clone()) .unwrap_or_default(), push_tags: triggers .push .as_ref() .map(|p| p.tags.clone()) .unwrap_or_default(), pull_request_branches: triggers .pull_request .as_ref() .map(|p| p.branches.clone()) .unwrap_or_default(), pull_request_types: triggers .pull_request .as_ref() .map(|p| p.types.clone()) .unwrap_or_default(), workflow_dispatch: triggers.workflow_dispatch.is_some(), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RefKind { Branch, Tag, } pub fn build_glob_set(patterns: &[String]) -> Option { if patterns.is_empty() { return None; } let mut builder = GlobSetBuilder::new(); let mut any = false; for pattern in patterns { if let Ok(glob) = Glob::new(pattern) { builder.add(glob); any = true; } } if !any { return None; } builder.build().ok() } #[must_use] pub fn matches_patterns(patterns: &[String], value: &str) -> bool { match build_glob_set(patterns) { Some(set) => set.is_match(value), None => false, } }