Skip to content
Clone
eba4fd1
254 lines 7.2 KB Rust
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<String>,
pub on: WorkflowTriggers,
#[serde(default)]
pub env: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub concurrency: Option<Concurrency>,
pub jobs: BTreeMap<String, Job>,
}
#[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<PushTrigger>,
#[serde(
default,
rename = "pull_request",
skip_serializing_if = "Option::is_none"
)]
pub pull_request: Option<PullRequestTrigger>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workflow_dispatch: Option<WorkflowDispatchTrigger>,
}
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<String>,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(deny_unknown_fields)]
pub struct PullRequestTrigger {
#[serde(default)]
pub branches: Vec<String>,
#[serde(default)]
pub types: Vec<PullRequestAction>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkflowDispatchTrigger {
#[serde(default)]
pub inputs: BTreeMap<String, String>,
}
#[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<String>,
#[serde(default, rename = "if", skip_serializing_if = "Option::is_none")]
pub condition: Option<String>,
#[serde(default)]
pub needs: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub environment: Option<String>,
#[serde(default)]
pub permissions: MarkedPermissions,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub container: Option<String>,
#[serde(
default,
rename = "timeout-minutes",
skip_serializing_if = "Option::is_none"
)]
pub timeout_minutes: Option<u64>,
#[serde(default)]
pub env: BTreeMap<String, String>,
#[serde(default)]
pub artifacts: Vec<ArtifactSpec>,
pub steps: Vec<Step>,
}
fn default_runs_on() -> Vec<String> {
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<PermissionLevel>,
}
#[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<String>,
pub run: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shell: Option<String>,
#[serde(
default,
rename = "working-directory",
skip_serializing_if = "Option::is_none"
)]
pub working_directory: Option<String>,
#[serde(default)]
pub env: BTreeMap<String, String>,
#[serde(
default,
rename = "timeout-minutes",
skip_serializing_if = "Option::is_none"
)]
pub timeout_minutes: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
pub struct WorkflowSummary {
pub file: String,
pub name: String,
pub valid: bool,
pub errors: Vec<String>,
pub triggers: TriggerSummary,
#[serde(default)]
pub inputs: BTreeMap<String, crate::actions::InputDefinition>,
#[serde(default)]
pub engine: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
pub struct TriggerSummary {
pub push_branches: Vec<String>,
pub push_tags: Vec<String>,
pub pull_request_branches: Vec<String>,
pub pull_request_types: Vec<PullRequestAction>,
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<GlobSet> {
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,
}
}