diff --git a/.github/workflows/cherry-pick.yaml b/.github/workflows/cherry-pick.yaml new file mode 100644 index 0000000..219bd6d --- /dev/null +++ b/.github/workflows/cherry-pick.yaml @@ -0,0 +1,63 @@ +name: Cherry Pick + +on: + workflow_call: + +jobs: + branches: + runs-on: ubuntu-latest + outputs: + branches: ${{ steps.cherry_pick_branches.outputs.result }} + steps: + - name: Check for cherry-pick labels + id: cherry_pick_branches + uses: actions/github-script@v7 + with: + script: | + const { data: issue } = await github.rest.issues.get({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + }); + + const labels = issue.labels.map(label => label.name); + console.log(`Labels: ${labels}`); + + const branches = labels.filter(label => label.startsWith('cherry-pick/')) + .map(label => label.substr(12)); + console.log(`Branches: ${branches}`); + + return branches + + cherry-pick: + runs-on: ubuntu-latest + needs: branches + if: ${{ needs.branches.outputs.branches != '[]' && needs.branches.outputs.branches != '' }} + strategy: + matrix: + branch: ${{ fromJson(needs.branches.outputs.branches) }} + steps: + - name: checkout + uses: actions/checkout@v4 + with: + ref: ${{ matrix.branch }} + fetch-depth: 0 + + - name: Get Token + id: get_workflow_token + uses: peter-murray/workflow-application-token-action@v3 + with: + application_id: ${{ vars.TRUSTIFICATION_BOT_ID }} + application_private_key: ${{ secrets.TRUSTIFICATION_BOT_KEY }} + + - name: Cherry-pick + env: + GH_TOKEN: ${{ steps.get_workflow_token.outputs.token }} + run: | + BRANCH_NAME="cherry-pick-pr${{ github.event.number }}-${{ matrix.branch }}" + git config --global user.email "noreply@github.com" + git config --global user.name "Cherry Picker" + git checkout -b ${BRANCH_NAME} + git cherry-pick -s ${{ github.sha }} + git push origin ${BRANCH_NAME} + gh pr create --base ${{ matrix.branch }} --fill \ No newline at end of file diff --git a/.github/workflows/ci-repo.yaml b/.github/workflows/ci-repo.yaml new file mode 100644 index 0000000..daaf12b --- /dev/null +++ b/.github/workflows/ci-repo.yaml @@ -0,0 +1,29 @@ +name: CI (repo level) + +on: + push: + branches: + - "main" + - "release/*" + pull_request: + branches: + - "main" + - "release/*" + workflow_dispatch: + workflow_call: + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Rust + uses: Swatinem/rust-cache@v2 + - name: Crate Format + run: cargo fmt --check + - name: Crate Check + run: cargo check + - name: Crate Clippy + run: cargo clippy --all-targets --all-features -- -D warnings + - name: Crate Test + run: cargo test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b39d31 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.idea/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..41b561e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,337 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "action" +version = "0.1.1" +dependencies = [ + "envy", + "serde", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "envy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" +dependencies = [ + "serde", +] + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi", + "windows-targets", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "once_cell" +version = "1.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + +[[package]] +name = "pr" +version = "0.1.1" +dependencies = [ + "regex", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" + +[[package]] +name = "verify-pr" +version = "0.1.1" +dependencies = [ + "action", + "pr", + "serde", + "serde_json", + "tempfile", +] + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e55fb02 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[workspace] +resolver = "2" +members = [ + "pkg/action", + "pkg/pr", + "cmd/verify-pr", +] + +[workspace.package] +version = "0.1.1" +edition = "2021" +publish = false +license = "Apache-2.0" + +[workspace.dependencies] +envy = "0.4.2" +serde = { version = "1" } +serde_json = { version = "1" } +regex = { version = "1" } +tempfile = { version = "3" } + +action = { path = "./pkg/action" } +pr = { path = "./pkg/pr" } + +[patch.crates-io] diff --git a/cmd/verify-pr/Cargo.toml b/cmd/verify-pr/Cargo.toml new file mode 100644 index 0000000..d38fd16 --- /dev/null +++ b/cmd/verify-pr/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "verify-pr" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +action = { workspace = true } +pr = { workspace = true } + +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } \ No newline at end of file diff --git a/cmd/verify-pr/action.yml b/cmd/verify-pr/action.yml new file mode 100644 index 0000000..29e04fd --- /dev/null +++ b/cmd/verify-pr/action.yml @@ -0,0 +1,15 @@ +name: "Verify PR" +description: Verify PRs for Trustification organization +inputs: + github_token: + description: "the github_token provided by the actions runner" + required: true +runs: + using: composite + steps: + - name: Set up Rust + uses: Swatinem/rust-cache@v2 + - name: Run verify + run: cd ${GITHUB_ACTION_PATH} && cargo run --bin verify-pr + shell: bash + \ No newline at end of file diff --git a/cmd/verify-pr/src/main.rs b/cmd/verify-pr/src/main.rs new file mode 100644 index 0000000..b296390 --- /dev/null +++ b/cmd/verify-pr/src/main.rs @@ -0,0 +1,66 @@ +use action::context::vars_from_env; +use pr::prefix::{PRType, PRTypeError}; +use serde::Deserialize; +use std::fs; + +#[derive(Debug, Deserialize)] +struct Event { + pub pull_request: PullRequestEvent, +} + +#[derive(Debug, Deserialize)] +struct PullRequestEvent { + title: String, +} + +fn main() -> Result<(), PRTypeError> { + let gh_context = vars_from_env().expect("Failed to get github env vars"); + + // Parse the event + let event_file = + fs::read_to_string(gh_context.github_event_path).expect("Unable to read github event file"); + + let event: Event = + serde_json::from_str(&event_file).expect("unable to unmarshal PullRequest event"); + + // Check the title of the PR + let pr_type = PRType::from_title(&event.pull_request.title)?; + + println!("{:?}", pr_type); + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::main; + use tempfile::NamedTempFile; + + #[test] + fn read_from_file() { + std::env::set_var("CI", "true"); + std::env::set_var("GITHUB_ACTIONS", "true"); + std::env::set_var("GITHUB_EVENT_NAME", "foo"); + + let event_that_generates_ok = "{\"pull_request\":{\"title\":\"WIP: :bug: Fix bug\"}}"; + let event_that_generates_error = + "{\"pull_request\":{\"title\":\"WIP: [docs] Update documentation\"}}"; + + // + let temp_file = NamedTempFile::new().expect("Failed to create temp file"); + let path = temp_file.path(); // Get the temporary file's path + std::fs::write(path, event_that_generates_ok).expect("Failed to write to temp file"); + + std::env::set_var("GITHUB_EVENT_PATH", path); + let result = main(); + assert!(result.is_ok()); + + // + let temp_file = NamedTempFile::new().expect("Failed to create temp file"); + let path = temp_file.path(); // Get the temporary file's path + std::fs::write(path, event_that_generates_error).expect("Failed to write to temp file"); + + std::env::set_var("GITHUB_EVENT_PATH", path); + let result = main(); + assert!(result.is_err()); + } +} diff --git a/pkg/action/Cargo.toml b/pkg/action/Cargo.toml new file mode 100644 index 0000000..ae64718 --- /dev/null +++ b/pkg/action/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "action" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +serde = { workspace = true, features = ["derive"] } +envy = { workspace = true } diff --git a/pkg/action/src/context.rs b/pkg/action/src/context.rs new file mode 100644 index 0000000..dab6969 --- /dev/null +++ b/pkg/action/src/context.rs @@ -0,0 +1,14 @@ +use envy::Error; +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +pub struct GitHubVariables { + pub ci: bool, + pub github_actions: bool, + pub github_event_name: String, + pub github_event_path: String, +} + +pub fn vars_from_env() -> Result { + envy::from_env::() +} diff --git a/pkg/action/src/lib.rs b/pkg/action/src/lib.rs new file mode 100644 index 0000000..9efb2ab --- /dev/null +++ b/pkg/action/src/lib.rs @@ -0,0 +1 @@ +pub mod context; diff --git a/pkg/pr/Cargo.toml b/pkg/pr/Cargo.toml new file mode 100644 index 0000000..9d05ac4 --- /dev/null +++ b/pkg/pr/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "pr" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +regex = { workspace = true } diff --git a/pkg/pr/src/lib.rs b/pkg/pr/src/lib.rs new file mode 100644 index 0000000..0465c37 --- /dev/null +++ b/pkg/pr/src/lib.rs @@ -0,0 +1,3 @@ +extern crate core; + +pub mod prefix; diff --git a/pkg/pr/src/prefix.rs b/pkg/pr/src/prefix.rs new file mode 100644 index 0000000..d37aa88 --- /dev/null +++ b/pkg/pr/src/prefix.rs @@ -0,0 +1,181 @@ +// Motivated by, and largely copied from, +// https://github.com/kubernetes-sigs/kubebuilder-release-tools + +use core::fmt; +use regex::Regex; + +const PREFIX_FEATURE: (&str, &str) = (":sparkles:", "✨"); +const PREFIX_BUG_FIX: (&str, &str) = (":bug:", "🐛"); +const PREFIX_DOCS: (&str, &str) = (":book:", "📖"); +const PREFIX_INFRA: (&str, &str) = (":seedling:", "🌱"); +const PREFIX_BREAKING: (&str, &str) = (":warning:", "⚠"); +const PREFIX_NO_NOTE: (&str, &str) = (":ghost:", "👻"); + +// Title, Emoji +pub type PRTypeError = (String, Option); + +#[derive(Debug, PartialEq)] +pub enum PRType { + Feature(String), + BugFix(String), + Docs(String), + Infra(String), + Breaking(String), + NoNote(String), +} + +impl fmt::Display for PRType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Feature(title) => write!(f, "PR type 'Feature'\n PR title '{}'", title), + Self::BugFix(title) => write!(f, "PR type 'Bug'\n PR title '{}'", title), + Self::Docs(title) => write!(f, "PR type 'Docs'\n PR title '{}'", title), + Self::Infra(title) => write!(f, "PR type 'Infra'\n PR title '{}'", title), + Self::Breaking(title) => write!(f, "PR type 'Breaking'\n PR title '{}'", title), + Self::NoNote(title) => write!(f, "PR type 'NoNote'\n PR title '{}'", title), + } + } +} + +impl PRType { + pub fn from_title(value: &str) -> Result { + let wip_regex = Regex::new(r"(?i)^\W?WIP\W").unwrap(); + let tag_regex = Regex::new(r"^\[[\w.-]*]").unwrap(); + + // Remove the WIP prefix if found. + let value = wip_regex.replace_all(value, ""); + + // Trim to remove spaces after WIP. + let value = value.trim(); + + // Remove a tag prefix if found. + let value = tag_regex.replace_all(value, ""); + let value = value.trim(); + + if value.is_empty() { + return Err((value.to_string(), None)); + } + + // Trusting those that came before... + // https://github.com/kubernetes-sigs/kubebuilder-release-tools/blob/4f3d1085b4458a49ed86918b4b55505716715b77/notes/common/prefix.go#L123-L125 + // strip the variation selector from the title, if present + // (some systems sneak it in -- my guess is OSX) + fn trust(title: &str) -> String { + let result = if title.starts_with('\u{FE0F}') { + let result: String = title.chars().skip(1).collect(); + result + } else { + title.to_string() + }; + result.trim().to_string() + } + + if let Some(title) = value.strip_prefix(PREFIX_FEATURE.0) { + Ok(PRType::Feature(trust(title))) + } else if let Some(title) = value.strip_prefix(PREFIX_BUG_FIX.0) { + Ok(PRType::BugFix(trust(title))) + } else if let Some(title) = value.strip_prefix(PREFIX_DOCS.0) { + Ok(PRType::Docs(trust(title))) + } else if let Some(title) = value.strip_prefix(PREFIX_INFRA.0) { + Ok(PRType::Infra(trust(title))) + } else if let Some(title) = value.strip_prefix(PREFIX_BREAKING.0) { + Ok(PRType::Breaking(trust(title))) + } else if let Some(title) = value.strip_prefix(PREFIX_NO_NOTE.0) { + Ok(PRType::NoNote(trust(title))) + } else if value.strip_prefix(PREFIX_FEATURE.1).is_some() + || value.strip_prefix(PREFIX_BUG_FIX.1).is_some() + || value.strip_prefix(PREFIX_DOCS.1).is_some() + || value.strip_prefix(PREFIX_INFRA.1).is_some() + || value.strip_prefix(PREFIX_BREAKING.1).is_some() + || value.strip_prefix(PREFIX_NO_NOTE.1).is_some() + { + let emoji = value.chars().next().map(|c| c.to_string()); + return Err((trust(value), emoji)); + } else { + return Err((trust(value), None)); + } + } + + pub fn title(&self) -> String { + match self { + PRType::Feature(title) => title.to_string(), + PRType::BugFix(title) => title.to_string(), + PRType::Docs(title) => title.to_string(), + PRType::Infra(title) => title.to_string(), + PRType::Breaking(title) => title.to_string(), + PRType::NoNote(title) => title.to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use crate::prefix::{PRType, PRTypeError}; + + struct TestCase { + pub title: &'static str, + pub expected_result: Result, + } + + #[test] + fn title_cases() { + let test_cases = vec![ + TestCase { + title: "WIP: [docs] Update documentation", + expected_result: Err(("Update documentation".to_string(), None)), + }, + TestCase { + title: "WIP: :sparkles: Add new feature", + expected_result: Ok(PRType::Feature("Add new feature".to_string())), + }, + TestCase { + title: "WIP: :warning: Breaking change", + expected_result: Ok(PRType::Breaking("Breaking change".to_string())), + }, + TestCase { + title: "WIP: :bug: Fix bug", + expected_result: Ok(PRType::BugFix("Fix bug".to_string())), + }, + TestCase { + title: ":ghost: Don't put me in release notes", + expected_result: Ok(PRType::NoNote("Don't put me in release notes".to_string())), + }, + TestCase { + title: "WIP: :seedling: Infrastructure change", + expected_result: Ok(PRType::Infra("Infrastructure change".to_string())), + }, + TestCase { + title: "WIP: No prefix in title", + expected_result: Err(("No prefix in title".to_string(), None)), + }, + TestCase { + title: "No prefix in title", + expected_result: Err(("No prefix in title".to_string(), None)), + }, + TestCase { + title: "WIP:", + expected_result: Err(("".to_string(), None)), + }, + TestCase { + title: "", + expected_result: Err(("".to_string(), None)), + }, + TestCase { + title: "WIP: [tag] :sparkles: Add new feature", + expected_result: Ok(PRType::Feature("Add new feature".to_string())), + }, + TestCase { + title: "👻 I should have used the alias", + expected_result: Err(( + "👻 I should have used the alias".to_string(), + Some("👻".to_string()), + )), + }, + ]; + + for tc in test_cases { + let pr = PRType::from_title(tc.title); + assert_eq!(tc.expected_result, pr); + } + } +}