hackety hack
This commit is contained in:
parent
d1f298a0c6
commit
0d39e51123
10 changed files with 719 additions and 213 deletions
|
@ -7,6 +7,6 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
serde_yaml = "0.9.29"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_yaml = "0.9.29"
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::common::Env;
|
||||
|
||||
/// A GitHub Actions action definition.
|
||||
///
|
||||
/// See: <https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions>
|
||||
/// and <https://json.schemastore.org/github-action.json>
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Action {
|
||||
pub name: String,
|
||||
|
@ -19,7 +21,7 @@ pub struct Action {
|
|||
pub runs: Runs,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Input {
|
||||
pub description: String,
|
||||
|
@ -27,7 +29,7 @@ pub struct Input {
|
|||
pub default: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Output {
|
||||
pub description: String,
|
||||
|
@ -35,7 +37,7 @@ pub struct Output {
|
|||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", untagged)]
|
||||
pub enum Runs {
|
||||
JavaScript(JavaScript),
|
||||
|
@ -43,7 +45,7 @@ pub enum Runs {
|
|||
Docker(Docker),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct JavaScript {
|
||||
// "node12" | "node16" | "node20"
|
||||
|
@ -57,7 +59,7 @@ pub struct JavaScript {
|
|||
pub post_if: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Composite {
|
||||
// "composite"
|
||||
|
@ -65,7 +67,7 @@ pub struct Composite {
|
|||
pub steps: Vec<Step>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", untagged)]
|
||||
pub enum Step {
|
||||
/// A step that runs a command in a shell.
|
||||
|
@ -74,7 +76,7 @@ pub enum Step {
|
|||
UseAction(UseAction),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct RunShell {
|
||||
pub run: String,
|
||||
|
@ -83,34 +85,13 @@ pub struct RunShell {
|
|||
pub id: Option<String>,
|
||||
pub r#if: Option<String>,
|
||||
#[serde(default)]
|
||||
pub env: HashMap<String, EnvValue>,
|
||||
pub env: Env,
|
||||
#[serde(default)]
|
||||
pub continue_on_error: bool,
|
||||
pub working_directory: Option<String>,
|
||||
}
|
||||
|
||||
/// Environment variable values are always strings, but GitHub Actions
|
||||
/// allows users to configure them as various native YAML types before
|
||||
/// internal stringification.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case", untagged)]
|
||||
pub enum EnvValue {
|
||||
String(String),
|
||||
Number(f64),
|
||||
Boolean(bool),
|
||||
}
|
||||
|
||||
impl ToString for EnvValue {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Self::String(s) => s.clone(),
|
||||
Self::Number(n) => n.to_string(),
|
||||
Self::Boolean(b) => b.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct UseAction {
|
||||
pub uses: String,
|
||||
|
@ -119,14 +100,14 @@ pub struct UseAction {
|
|||
pub r#if: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Docker {
|
||||
// "docker"
|
||||
pub using: String,
|
||||
pub image: String,
|
||||
#[serde(default)]
|
||||
pub env: HashMap<String, EnvValue>,
|
||||
pub env: Env,
|
||||
pub entrypoint: Option<String>,
|
||||
pub pre_entrypoint: Option<String>,
|
||||
// Defaults to `always()`
|
||||
|
|
68
src/common.rs
Normal file
68
src/common.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
pub type Env = HashMap<String, EnvValue>;
|
||||
|
||||
/// Environment variable values are always strings, but GitHub Actions
|
||||
/// allows users to configure them as various native YAML types before
|
||||
/// internal stringification.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum EnvValue {
|
||||
String(String),
|
||||
Number(f64),
|
||||
Boolean(bool),
|
||||
}
|
||||
|
||||
impl ToString for EnvValue {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Self::String(s) => s.clone(),
|
||||
Self::Number(n) => n.to_string(),
|
||||
Self::Boolean(b) => b.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A "literal or expr" type, for places in GitHub Actions where a
|
||||
/// key can either have a literal value (array, object, etc.) or an
|
||||
/// expression string.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum LoE<T> {
|
||||
Literal(T),
|
||||
Expr(String),
|
||||
}
|
||||
|
||||
impl<T> Default for LoE<T>
|
||||
where
|
||||
T: Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::Literal(T::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub type BoE = LoE<bool>;
|
||||
|
||||
/// A "scalar or vector" type, for places in GitHub Actions where a
|
||||
/// key can have either a scalar value or an array of values.
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum SoV<T> {
|
||||
One(T),
|
||||
Many(Vec<T>),
|
||||
}
|
||||
|
||||
impl<T> From<Vec<T>> for SoV<T> {
|
||||
fn from(value: Vec<T>) -> Self {
|
||||
Self::Many(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for SoV<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Self::One(value)
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod action;
|
||||
pub mod common;
|
||||
pub mod dependabot;
|
||||
pub mod workflow;
|
||||
|
|
177
src/workflow.rs
177
src/workflow.rs
|
@ -1,177 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A single GitHub Actions workflow.
|
||||
///
|
||||
/// See: <https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions>
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Workflow {
|
||||
pub name: Option<String>,
|
||||
pub run_name: Option<String>,
|
||||
pub on: Trigger,
|
||||
#[serde(default)]
|
||||
pub permissions: Permissions,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case", untagged)]
|
||||
pub enum Trigger {
|
||||
// A single "bare" event, like `on: push`.
|
||||
BareEvent(BareEvent),
|
||||
// Multiple "bare" events, like `on: [push, pull_request]`
|
||||
BareEvents(Vec<BareEvent>),
|
||||
// `schedule:` events.
|
||||
Schedule { schedule: Vec<Cron> },
|
||||
WorkflowCall { workflow_call: Option<WorkflowCall> },
|
||||
// "Rich" events, i.e. each event with its optional filters.
|
||||
Events(HashMap<BareEvent, Option<RichEvent>>),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BareEvent {
|
||||
BranchProtectionRule,
|
||||
CheckRun,
|
||||
CheckSuite,
|
||||
Create,
|
||||
Delete,
|
||||
Deployment,
|
||||
DeploymentStatus,
|
||||
Discussion,
|
||||
DiscussionComment,
|
||||
Fork,
|
||||
Gollum,
|
||||
IssueComment,
|
||||
Issues,
|
||||
Label,
|
||||
MergeGroup,
|
||||
Milestone,
|
||||
PageBuild,
|
||||
Project,
|
||||
ProjectCard,
|
||||
ProjectColumn,
|
||||
Public,
|
||||
PullRequest,
|
||||
PullRequestComment,
|
||||
PullRequestReview,
|
||||
PullRequestReviewComment,
|
||||
PullRequestTarget,
|
||||
Push,
|
||||
RegistryPackage,
|
||||
Release,
|
||||
RepositoryDispatch,
|
||||
// NOTE: `schedule` is omitted, since it's never bare.
|
||||
Status,
|
||||
Watch,
|
||||
WorkflowCall,
|
||||
WorkflowDispatch,
|
||||
WorkflowRun,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Cron {
|
||||
cron: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct WorkflowCall {
|
||||
inputs: HashMap<String, WorkflowCallInput>,
|
||||
outputs: HashMap<String, WorkflowCallOutput>,
|
||||
secrets: HashMap<String, WorkflowCallSecret>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct WorkflowCallInput {
|
||||
description: Option<String>,
|
||||
// TODO: model `default`?
|
||||
#[serde(default)]
|
||||
required: bool,
|
||||
r#type: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct WorkflowCallOutput {
|
||||
description: Option<String>,
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct WorkflowCallSecret {
|
||||
description: Option<String>,
|
||||
required: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct RichEvent {
|
||||
#[serde(default)]
|
||||
types: Vec<String>,
|
||||
|
||||
// `push | pull_request | pull_request_target` only.
|
||||
#[serde(default)]
|
||||
branches: Vec<String>,
|
||||
|
||||
// `push | pull_request | pull_request_target` only.
|
||||
#[serde(default)]
|
||||
branches_ignore: Vec<String>,
|
||||
|
||||
// `push` only.
|
||||
#[serde(default)]
|
||||
tags: Vec<String>,
|
||||
|
||||
// `push` only.
|
||||
#[serde(default)]
|
||||
tags_ignore: Vec<String>,
|
||||
|
||||
// `push | pull_request | pull_request_target` only.
|
||||
#[serde(default)]
|
||||
paths: Vec<String>,
|
||||
|
||||
// `push | pull_request | pull_request_target` only.
|
||||
#[serde(default)]
|
||||
paths_ignore: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Default)]
|
||||
#[serde(rename_all = "kebab-case", untagged)]
|
||||
pub enum Permissions {
|
||||
/// Whatever default permissions come from the workflow's `GITHUB_TOKEN`.
|
||||
#[default]
|
||||
Token,
|
||||
ReadAll,
|
||||
WriteAll,
|
||||
Explicit(ExplicitPermissions),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ExplicitPermissions {
|
||||
pub actions: Permission,
|
||||
pub checks: Permission,
|
||||
pub contents: Permission,
|
||||
pub deployments: Permission,
|
||||
pub id_token: Permission,
|
||||
pub issues: Permission,
|
||||
pub discussions: Permission,
|
||||
pub packages: Permission,
|
||||
pub pages: Permission,
|
||||
pub pull_requests: Permission,
|
||||
pub repository_projects: Permission,
|
||||
pub security_events: Permission,
|
||||
pub statuses: Permission,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case", untagged)]
|
||||
pub enum Permission {
|
||||
Read,
|
||||
Write,
|
||||
None,
|
||||
}
|
246
src/workflow/event.rs
Normal file
246
src/workflow/event.rs
Normal file
|
@ -0,0 +1,246 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BareEvent {
|
||||
BranchProtectionRule,
|
||||
CheckRun,
|
||||
CheckSuite,
|
||||
Create,
|
||||
Delete,
|
||||
Deployment,
|
||||
DeploymentStatus,
|
||||
Discussion,
|
||||
DiscussionComment,
|
||||
Fork,
|
||||
Gollum,
|
||||
IssueComment,
|
||||
Issues,
|
||||
Label,
|
||||
MergeGroup,
|
||||
Milestone,
|
||||
PageBuild,
|
||||
Project,
|
||||
ProjectCard,
|
||||
ProjectColumn,
|
||||
Public,
|
||||
PullRequest,
|
||||
PullRequestComment,
|
||||
PullRequestReview,
|
||||
PullRequestReviewComment,
|
||||
PullRequestTarget,
|
||||
Push,
|
||||
RegistryPackage,
|
||||
Release,
|
||||
RepositoryDispatch,
|
||||
// NOTE: `schedule` is omitted, since it's never bare.
|
||||
Status,
|
||||
Watch,
|
||||
WorkflowCall,
|
||||
WorkflowDispatch,
|
||||
WorkflowRun,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
#[serde(default, rename_all = "snake_case")]
|
||||
pub struct Events {
|
||||
pub branch_protection_rule: OptionalBody<GenericEvent>,
|
||||
pub check_run: OptionalBody<GenericEvent>,
|
||||
pub check_suite: OptionalBody<GenericEvent>,
|
||||
// TODO: create + delete
|
||||
// TODO: deployment + deployment_status
|
||||
pub discussion: OptionalBody<GenericEvent>,
|
||||
pub discussion_comment: OptionalBody<GenericEvent>,
|
||||
// TODO: fork + gollum
|
||||
pub issue_comment: OptionalBody<GenericEvent>,
|
||||
pub issues: OptionalBody<GenericEvent>,
|
||||
pub label: OptionalBody<GenericEvent>,
|
||||
pub merge_group: OptionalBody<GenericEvent>,
|
||||
pub milestone: OptionalBody<GenericEvent>,
|
||||
// TODO: page_build
|
||||
pub project: OptionalBody<GenericEvent>,
|
||||
pub project_card: OptionalBody<GenericEvent>,
|
||||
pub project_column: OptionalBody<GenericEvent>,
|
||||
// TODO: public
|
||||
pub pull_request: OptionalBody<PullRequest>,
|
||||
pub pull_request_comment: OptionalBody<GenericEvent>,
|
||||
pub pull_request_review: OptionalBody<GenericEvent>,
|
||||
pub pull_request_review_comment: OptionalBody<GenericEvent>,
|
||||
// NOTE: `pull_request_target` appears to have the same trigger filters as `pull_request`.
|
||||
pub pull_request_target: OptionalBody<PullRequest>,
|
||||
pub push: OptionalBody<Push>,
|
||||
pub registry_package: OptionalBody<GenericEvent>,
|
||||
pub release: OptionalBody<GenericEvent>,
|
||||
pub repository_dispatch: OptionalBody<GenericEvent>,
|
||||
pub schedule: OptionalBody<Vec<Cron>>,
|
||||
// TODO: status
|
||||
pub watch: OptionalBody<GenericEvent>,
|
||||
pub workflow_call: OptionalBody<WorkflowCall>,
|
||||
// TODO: Custom type.
|
||||
pub workflow_dispatch: OptionalBody<WorkflowDispatch>,
|
||||
pub workflow_run: OptionalBody<WorkflowRun>,
|
||||
}
|
||||
|
||||
/// A generic container type for distinguishing between
|
||||
/// a missing key, an explicitly null key, and an explicit value `T`.
|
||||
///
|
||||
/// This is needed for modeling `on:` triggers, since GitHub distinguishes
|
||||
/// between the non-presence of an event (no trigger) and the presence
|
||||
/// of an empty event body (e.g. `pull_request:`), which means "trigger
|
||||
/// with the defaults for this event type."
|
||||
pub enum OptionalBody<T> {
|
||||
Default,
|
||||
Missing,
|
||||
Body(T),
|
||||
}
|
||||
|
||||
impl<'de, T> Deserialize<'de> for OptionalBody<T>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
Option::deserialize(deserializer).map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Option<T>> for OptionalBody<T> {
|
||||
fn from(value: Option<T>) -> Self {
|
||||
match value {
|
||||
Some(v) => OptionalBody::Body(v),
|
||||
None => OptionalBody::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for OptionalBody<T> {
|
||||
fn default() -> Self {
|
||||
OptionalBody::Missing
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct GenericEvent {
|
||||
#[serde(default)]
|
||||
pub types: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct PullRequest {
|
||||
#[serde(default)]
|
||||
pub types: Vec<String>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub branch_filters: Option<BranchFilters>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub path_filters: Option<PathFilters>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Push {
|
||||
#[serde(flatten)]
|
||||
pub branch_filters: Option<BranchFilters>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub path_filters: Option<PathFilters>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub tag_filters: Option<TagFilters>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Cron {
|
||||
pub cron: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct WorkflowCall {
|
||||
pub inputs: HashMap<String, WorkflowCallInput>,
|
||||
pub outputs: HashMap<String, WorkflowCallOutput>,
|
||||
pub secrets: HashMap<String, WorkflowCallSecret>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct WorkflowCallInput {
|
||||
pub description: Option<String>,
|
||||
// TODO: model `default`?
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
pub r#type: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct WorkflowCallOutput {
|
||||
pub description: Option<String>,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct WorkflowCallSecret {
|
||||
pub description: Option<String>,
|
||||
pub required: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct WorkflowDispatch {
|
||||
#[serde(default)]
|
||||
pub inputs: HashMap<String, WorkflowDispatchInput>, // TODO: WorkflowDispatchInput
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct WorkflowDispatchInput {
|
||||
pub description: Option<String>,
|
||||
// TODO: model `default`?
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
pub r#type: String,
|
||||
// Only present when `type` is `choice`.
|
||||
#[serde(default)]
|
||||
pub options: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct WorkflowRun {
|
||||
pub workflows: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub types: Vec<String>,
|
||||
#[serde(flatten)]
|
||||
pub branch_filters: Option<BranchFilters>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum BranchFilters {
|
||||
Branches(Vec<String>),
|
||||
BranchesIgnore(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum TagFilters {
|
||||
Tags(Vec<String>),
|
||||
TagsIgnore(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PathFilters {
|
||||
Paths(Vec<String>),
|
||||
PathsIgnore(Vec<String>),
|
||||
}
|
144
src/workflow/job.rs
Normal file
144
src/workflow/job.rs
Normal file
|
@ -0,0 +1,144 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_yaml::Value;
|
||||
|
||||
use crate::common::{BoE, Env, LoE, SoV};
|
||||
|
||||
use super::{Concurrency, Defaults, Permissions};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct NormalJob {
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub permissions: Permissions,
|
||||
#[serde(default)]
|
||||
pub needs: Vec<String>,
|
||||
pub r#if: Option<String>,
|
||||
pub runs_on: RunsOn,
|
||||
pub environment: Option<DeploymentEnvironment>,
|
||||
pub concurrency: Option<Concurrency>,
|
||||
#[serde(default)]
|
||||
pub outputs: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub env: Env,
|
||||
pub defaults: Option<Defaults>,
|
||||
pub steps: Vec<Step>,
|
||||
pub timeout_minutes: Option<u64>,
|
||||
pub strategy: Option<Strategy>,
|
||||
#[serde(default)]
|
||||
pub continue_on_error: BoE,
|
||||
pub container: Option<Container>,
|
||||
#[serde(default)]
|
||||
pub services: HashMap<String, Container>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "kebab-case", untagged)]
|
||||
pub enum RunsOn {
|
||||
Target(SoV<String>),
|
||||
Group { group: String },
|
||||
Label { label: SoV<String> },
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", untagged)]
|
||||
pub enum DeploymentEnvironment {
|
||||
Name(String),
|
||||
NameURL { name: String, url: Option<String> },
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Step {
|
||||
pub id: Option<String>,
|
||||
pub r#if: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub timeout_minutes: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub continue_on_error: BoE,
|
||||
#[serde(flatten)]
|
||||
pub body: StepBody,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", untagged)]
|
||||
pub enum StepBody {
|
||||
Uses {
|
||||
uses: String,
|
||||
working_directory: Option<String>,
|
||||
shell: Option<String>,
|
||||
#[serde(default)]
|
||||
env: Env,
|
||||
},
|
||||
Run {
|
||||
run: String,
|
||||
#[serde(default)]
|
||||
with: Env,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Strategy {
|
||||
pub matrix: Matrix,
|
||||
pub fail_fast: Option<BoE>,
|
||||
pub max_parallel: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Matrix {
|
||||
#[serde(default)]
|
||||
pub include: LoE<Vec<HashMap<String, Value>>>,
|
||||
#[serde(default)]
|
||||
pub exclude: LoE<Vec<HashMap<String, Value>>>,
|
||||
#[serde(flatten)]
|
||||
pub dimensions: LoE<HashMap<String, Vec<Value>>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", untagged)]
|
||||
pub enum Container {
|
||||
Name(String),
|
||||
Container {
|
||||
image: String,
|
||||
credentials: Option<DockerCredentials>,
|
||||
#[serde(default)]
|
||||
env: Env,
|
||||
// TODO: model `ports`?
|
||||
#[serde(default)]
|
||||
volumes: Vec<String>,
|
||||
options: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DockerCredentials {
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ReusableWorkflowCallJob {
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub permissions: Permissions,
|
||||
#[serde(default)]
|
||||
pub needs: Vec<String>,
|
||||
pub r#if: Option<String>,
|
||||
pub uses: String,
|
||||
#[serde(default)]
|
||||
pub with: Env,
|
||||
pub secrets: Option<Secrets>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum Secrets {
|
||||
Inherit,
|
||||
#[serde(untagged)]
|
||||
Env(#[serde(default)] Env),
|
||||
}
|
169
src/workflow/mod.rs
Normal file
169
src/workflow/mod.rs
Normal file
|
@ -0,0 +1,169 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::common::{BoE, Env};
|
||||
|
||||
pub mod event;
|
||||
pub mod job;
|
||||
|
||||
/// A single GitHub Actions workflow.
|
||||
///
|
||||
/// See: <https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions>
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Workflow {
|
||||
pub name: Option<String>,
|
||||
pub run_name: Option<String>,
|
||||
pub on: Trigger,
|
||||
#[serde(default)]
|
||||
pub permissions: Permissions,
|
||||
#[serde(default)]
|
||||
pub env: Env,
|
||||
pub defaults: Option<Defaults>,
|
||||
pub concurrency: Option<Concurrency>,
|
||||
pub jobs: HashMap<String, Job>,
|
||||
}
|
||||
|
||||
/// The triggering condition or conditions for a workflow.
|
||||
///
|
||||
/// Workflow triggers take three forms:
|
||||
///
|
||||
/// 1. A single webhook event name:
|
||||
///
|
||||
/// ```yaml
|
||||
/// on: push
|
||||
/// ```
|
||||
/// 2. A list of webhook event names:
|
||||
///
|
||||
/// ```yaml
|
||||
/// on: [push, fork]
|
||||
/// ```
|
||||
///
|
||||
/// 3. A mapping of event names with (optional) configurations:
|
||||
///
|
||||
/// ```yaml
|
||||
/// on:
|
||||
/// push:
|
||||
/// branches: [main]
|
||||
/// pull_request:
|
||||
/// ```
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "snake_case", untagged)]
|
||||
pub enum Trigger {
|
||||
BareEvent(event::BareEvent),
|
||||
BareEvents(Vec<event::BareEvent>),
|
||||
Events(event::Events),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "kebab-case", untagged)]
|
||||
pub enum Permissions {
|
||||
Base(BasePermission),
|
||||
Explicit(ExplicitPermissions),
|
||||
}
|
||||
|
||||
impl Default for Permissions {
|
||||
fn default() -> Self {
|
||||
Self::Base(BasePermission::Default)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug, PartialEq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum BasePermission {
|
||||
/// Whatever default permissions come from the workflow's `GITHUB_TOKEN`.
|
||||
#[default]
|
||||
Default,
|
||||
ReadAll,
|
||||
WriteAll,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ExplicitPermissions {
|
||||
#[serde(default)]
|
||||
pub actions: Permission,
|
||||
#[serde(default)]
|
||||
pub checks: Permission,
|
||||
#[serde(default)]
|
||||
pub contents: Permission,
|
||||
#[serde(default)]
|
||||
pub deployments: Permission,
|
||||
#[serde(default)]
|
||||
pub id_token: Permission,
|
||||
#[serde(default)]
|
||||
pub issues: Permission,
|
||||
#[serde(default)]
|
||||
pub discussions: Permission,
|
||||
#[serde(default)]
|
||||
pub packages: Permission,
|
||||
#[serde(default)]
|
||||
pub pages: Permission,
|
||||
#[serde(default)]
|
||||
pub pull_requests: Permission,
|
||||
#[serde(default)]
|
||||
pub repository_projects: Permission,
|
||||
#[serde(default)]
|
||||
pub security_events: Permission,
|
||||
#[serde(default)]
|
||||
pub statuses: Permission,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug, PartialEq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum Permission {
|
||||
Read,
|
||||
Write,
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Defaults {
|
||||
pub run: Option<RunDefaults>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct RunDefaults {
|
||||
pub shell: Option<String>,
|
||||
pub working_directory: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Concurrency {
|
||||
pub group: String,
|
||||
#[serde(default)]
|
||||
pub cancel_in_progress: BoE,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", untagged)]
|
||||
pub enum Job {
|
||||
NormalJob(job::NormalJob),
|
||||
ReusableWorkflowCallJob(job::ReusableWorkflowCallJob),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::workflow::ExplicitPermissions;
|
||||
|
||||
use super::Permissions;
|
||||
|
||||
#[test]
|
||||
fn permissions_deserializes() {
|
||||
assert_eq!(
|
||||
serde_yaml::from_str::<Permissions>("read-all").unwrap(),
|
||||
Permissions::Base(crate::workflow::BasePermission::ReadAll)
|
||||
);
|
||||
|
||||
let perm = "security-events: write";
|
||||
assert!(matches!(
|
||||
serde_yaml::from_str::<ExplicitPermissions>(perm),
|
||||
Ok(_)
|
||||
));
|
||||
}
|
||||
}
|
54
tests/sample-workflows/pip-audit-scorecards.yml
Normal file
54
tests/sample-workflows/pip-audit-scorecards.yml
Normal file
|
@ -0,0 +1,54 @@
|
|||
# https://github.com/pypa/pip-audit/blob/1fd67af0653a8e66b9470adab2e408a435632f19/.github/workflows/scorecards.yml
|
||||
name: Scorecards supply-chain security
|
||||
on:
|
||||
# Only the default branch is supported.
|
||||
branch_protection_rule:
|
||||
schedule:
|
||||
- cron: "19 4 * * 0"
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecards analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload the results to code-scanning dashboard.
|
||||
security-events: write
|
||||
# Used to receive a badge. (Upcoming feature)
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4.1.1 # tag=v3.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # tag=v2.3.1
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
# Publish the results for public repositories to enable scorecard badges. For more details, see
|
||||
# https://github.com/ossf/scorecard-action#publishing-results.
|
||||
# For private repositories, `publish_results` will automatically be set to `false`, regardless
|
||||
# of the value entered here.
|
||||
publish_results: true
|
||||
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # tag=v3.1.3
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@cdcdbb579706841c47f7063dda365e292e5cad7a # tag=v2.13.4
|
||||
with:
|
||||
sarif_file: results.sarif
|
|
@ -1,6 +1,6 @@
|
|||
use std::{env, path::Path};
|
||||
|
||||
use glomar_models::workflow::Workflow;
|
||||
use glomar_models::workflow::{event::OptionalBody, job::RunsOn, Job, Trigger, Workflow};
|
||||
|
||||
fn load_workflow(name: &str) -> Workflow {
|
||||
let workflow_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
|
@ -20,3 +20,23 @@ fn test_load_all() {
|
|||
serde_yaml::from_str::<Workflow>(&workflow_contents).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pip_audit_ci() {
|
||||
let workflow = load_workflow("pip-audit-ci.yml");
|
||||
|
||||
assert!(
|
||||
matches!(workflow.on, Trigger::Events(events) if matches!(events.pull_request, OptionalBody::Default))
|
||||
);
|
||||
|
||||
let test_job = &workflow.jobs["test"];
|
||||
if let Job::NormalJob(test_job) = test_job {
|
||||
assert_eq!(test_job.name, None);
|
||||
assert_eq!(
|
||||
test_job.runs_on,
|
||||
RunsOn::Target(String::from("ubuntu-latest").into())
|
||||
);
|
||||
} else {
|
||||
panic!("oops");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue