diff --git a/Cargo.toml b/Cargo.toml index 3275779..6800c0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/action.rs b/src/action.rs index a785275..7b8523e 100644 --- a/src/action.rs +++ b/src/action.rs @@ -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: /// and -#[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, } -#[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, } -#[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, } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Composite { // "composite" @@ -65,7 +67,7 @@ pub struct Composite { pub steps: Vec, } -#[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, pub r#if: Option, #[serde(default)] - pub env: HashMap, + pub env: Env, #[serde(default)] pub continue_on_error: bool, pub working_directory: Option, } -/// 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, } -#[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, + pub env: Env, pub entrypoint: Option, pub pre_entrypoint: Option, // Defaults to `always()` diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000..3cf1814 --- /dev/null +++ b/src/common.rs @@ -0,0 +1,68 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +pub type Env = HashMap; + +/// 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 { + Literal(T), + Expr(String), +} + +impl Default for LoE +where + T: Default, +{ + fn default() -> Self { + Self::Literal(T::default()) + } +} + +pub type BoE = LoE; + +/// 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 { + One(T), + Many(Vec), +} + +impl From> for SoV { + fn from(value: Vec) -> Self { + Self::Many(value) + } +} + +impl From for SoV { + fn from(value: T) -> Self { + Self::One(value) + } +} diff --git a/src/lib.rs b/src/lib.rs index 8394cdc..2285676 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ pub mod action; +pub mod common; pub mod dependabot; pub mod workflow; diff --git a/src/workflow.rs b/src/workflow.rs deleted file mode 100644 index 495697b..0000000 --- a/src/workflow.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::collections::HashMap; - -use serde::{Deserialize, Serialize}; - -/// A single GitHub Actions workflow. -/// -/// See: -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct Workflow { - pub name: Option, - pub run_name: Option, - 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), - // `schedule:` events. - Schedule { schedule: Vec }, - WorkflowCall { workflow_call: Option }, - // "Rich" events, i.e. each event with its optional filters. - Events(HashMap>), -} - -#[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, - outputs: HashMap, - secrets: HashMap, -} - -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct WorkflowCallInput { - description: Option, - // TODO: model `default`? - #[serde(default)] - required: bool, - r#type: String, -} - -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct WorkflowCallOutput { - description: Option, - value: String, -} - -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct WorkflowCallSecret { - description: Option, - required: bool, -} - -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct RichEvent { - #[serde(default)] - types: Vec, - - // `push | pull_request | pull_request_target` only. - #[serde(default)] - branches: Vec, - - // `push | pull_request | pull_request_target` only. - #[serde(default)] - branches_ignore: Vec, - - // `push` only. - #[serde(default)] - tags: Vec, - - // `push` only. - #[serde(default)] - tags_ignore: Vec, - - // `push | pull_request | pull_request_target` only. - #[serde(default)] - paths: Vec, - - // `push | pull_request | pull_request_target` only. - #[serde(default)] - paths_ignore: Vec, -} - -#[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, -} diff --git a/src/workflow/event.rs b/src/workflow/event.rs new file mode 100644 index 0000000..fd0e5bd --- /dev/null +++ b/src/workflow/event.rs @@ -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, + pub check_run: OptionalBody, + pub check_suite: OptionalBody, + // TODO: create + delete + // TODO: deployment + deployment_status + pub discussion: OptionalBody, + pub discussion_comment: OptionalBody, + // TODO: fork + gollum + pub issue_comment: OptionalBody, + pub issues: OptionalBody, + pub label: OptionalBody, + pub merge_group: OptionalBody, + pub milestone: OptionalBody, + // TODO: page_build + pub project: OptionalBody, + pub project_card: OptionalBody, + pub project_column: OptionalBody, + // TODO: public + pub pull_request: OptionalBody, + pub pull_request_comment: OptionalBody, + pub pull_request_review: OptionalBody, + pub pull_request_review_comment: OptionalBody, + // NOTE: `pull_request_target` appears to have the same trigger filters as `pull_request`. + pub pull_request_target: OptionalBody, + pub push: OptionalBody, + pub registry_package: OptionalBody, + pub release: OptionalBody, + pub repository_dispatch: OptionalBody, + pub schedule: OptionalBody>, + // TODO: status + pub watch: OptionalBody, + pub workflow_call: OptionalBody, + // TODO: Custom type. + pub workflow_dispatch: OptionalBody, + pub workflow_run: OptionalBody, +} + +/// 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 { + Default, + Missing, + Body(T), +} + +impl<'de, T> Deserialize<'de> for OptionalBody +where + T: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Option::deserialize(deserializer).map(Into::into) + } +} + +impl From> for OptionalBody { + fn from(value: Option) -> Self { + match value { + Some(v) => OptionalBody::Body(v), + None => OptionalBody::Default, + } + } +} + +impl Default for OptionalBody { + fn default() -> Self { + OptionalBody::Missing + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct GenericEvent { + #[serde(default)] + pub types: Vec, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct PullRequest { + #[serde(default)] + pub types: Vec, + + #[serde(flatten)] + pub branch_filters: Option, + + #[serde(flatten)] + pub path_filters: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Push { + #[serde(flatten)] + pub branch_filters: Option, + + #[serde(flatten)] + pub path_filters: Option, + + #[serde(flatten)] + pub tag_filters: Option, +} + +#[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, + pub outputs: HashMap, + pub secrets: HashMap, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct WorkflowCallInput { + pub description: Option, + // 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, + pub value: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct WorkflowCallSecret { + pub description: Option, + pub required: bool, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct WorkflowDispatch { + #[serde(default)] + pub inputs: HashMap, // TODO: WorkflowDispatchInput +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct WorkflowDispatchInput { + pub description: Option, + // TODO: model `default`? + #[serde(default)] + pub required: bool, + pub r#type: String, + // Only present when `type` is `choice`. + #[serde(default)] + pub options: Vec, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct WorkflowRun { + pub workflows: Vec, + #[serde(default)] + pub types: Vec, + #[serde(flatten)] + pub branch_filters: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum BranchFilters { + Branches(Vec), + BranchesIgnore(Vec), +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum TagFilters { + Tags(Vec), + TagsIgnore(Vec), +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum PathFilters { + Paths(Vec), + PathsIgnore(Vec), +} diff --git a/src/workflow/job.rs b/src/workflow/job.rs new file mode 100644 index 0000000..d154b20 --- /dev/null +++ b/src/workflow/job.rs @@ -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, + #[serde(default)] + pub permissions: Permissions, + #[serde(default)] + pub needs: Vec, + pub r#if: Option, + pub runs_on: RunsOn, + pub environment: Option, + pub concurrency: Option, + #[serde(default)] + pub outputs: HashMap, + #[serde(default)] + pub env: Env, + pub defaults: Option, + pub steps: Vec, + pub timeout_minutes: Option, + pub strategy: Option, + #[serde(default)] + pub continue_on_error: BoE, + pub container: Option, + #[serde(default)] + pub services: HashMap, +} + +#[derive(Debug, Deserialize, PartialEq)] +#[serde(rename_all = "kebab-case", untagged)] +pub enum RunsOn { + Target(SoV), + Group { group: String }, + Label { label: SoV }, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case", untagged)] +pub enum DeploymentEnvironment { + Name(String), + NameURL { name: String, url: Option }, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Step { + pub id: Option, + pub r#if: Option, + pub name: Option, + pub timeout_minutes: Option, + #[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, + shell: Option, + #[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, + pub max_parallel: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Matrix { + #[serde(default)] + pub include: LoE>>, + #[serde(default)] + pub exclude: LoE>>, + #[serde(flatten)] + pub dimensions: LoE>>, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case", untagged)] +pub enum Container { + Name(String), + Container { + image: String, + credentials: Option, + #[serde(default)] + env: Env, + // TODO: model `ports`? + #[serde(default)] + volumes: Vec, + options: Option, + }, +} + +#[derive(Deserialize)] +pub struct DockerCredentials { + pub username: Option, + pub password: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct ReusableWorkflowCallJob { + pub name: Option, + #[serde(default)] + pub permissions: Permissions, + #[serde(default)] + pub needs: Vec, + pub r#if: Option, + pub uses: String, + #[serde(default)] + pub with: Env, + pub secrets: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum Secrets { + Inherit, + #[serde(untagged)] + Env(#[serde(default)] Env), +} diff --git a/src/workflow/mod.rs b/src/workflow/mod.rs new file mode 100644 index 0000000..e336949 --- /dev/null +++ b/src/workflow/mod.rs @@ -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: +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Workflow { + pub name: Option, + pub run_name: Option, + pub on: Trigger, + #[serde(default)] + pub permissions: Permissions, + #[serde(default)] + pub env: Env, + pub defaults: Option, + pub concurrency: Option, + pub jobs: HashMap, +} + +/// 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), + 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, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct RunDefaults { + pub shell: Option, + pub working_directory: Option, +} + +#[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::("read-all").unwrap(), + Permissions::Base(crate::workflow::BasePermission::ReadAll) + ); + + let perm = "security-events: write"; + assert!(matches!( + serde_yaml::from_str::(perm), + Ok(_) + )); + } +} diff --git a/tests/sample-workflows/pip-audit-scorecards.yml b/tests/sample-workflows/pip-audit-scorecards.yml new file mode 100644 index 0000000..7bb7a9e --- /dev/null +++ b/tests/sample-workflows/pip-audit-scorecards.yml @@ -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 diff --git a/tests/test_workflow.rs b/tests/test_workflow.rs index 455a69f..5688969 100644 --- a/tests/test_workflow.rs +++ b/tests/test_workflow.rs @@ -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_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"); + } +}