diff --git a/Cargo.toml b/Cargo.toml index d090a748b..8b7167616 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,10 +24,10 @@ members = [ "packages/fuels-macros", "packages/fuels-programs", "packages/fuels-test-helpers", + "scripts/change-log", "scripts/check-docs", "scripts/fuel-core-version", "scripts/versions-replacer", - "scripts/change-log", "wasm-tests", ] @@ -44,6 +44,7 @@ version = "0.70.0" [workspace.dependencies] Inflector = "0.11.4" anyhow = { version = "1.0", default-features = false } +dialoguer = { version = "0.11", default-features = false } async-trait = { version = "0.1.74", default-features = false } bech32 = "0.9.1" bytes = { version = "1.5.0", default-features = false } @@ -83,7 +84,7 @@ trybuild = "1.0.85" uint = { version = "0.9.5", default-features = false } which = { version = "6.0.0", default-features = false } zeroize = "1.7.0" -octocrab = { version = "0.39", default-features = false } +octocrab = { version = "0.43", default-features = false } dotenv = { version = "0.15", default-features = false } toml = { version = "0.8", default-features = false } mockall = { version = "0.13", default-features = false } diff --git a/scripts/change-log/Cargo.toml b/scripts/change-log/Cargo.toml index f00bb874e..94b33dec9 100644 --- a/scripts/change-log/Cargo.toml +++ b/scripts/change-log/Cargo.toml @@ -10,7 +10,9 @@ repository = { workspace = true } rust-version = { workspace = true } [dependencies] -regex = { workspace = true } +dialoguer = { version = "0.11", features = ["fuzzy-select"] } dotenv = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } octocrab = { workspace = true, features = ["default"] } +regex = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/scripts/change-log/src/adapters.rs b/scripts/change-log/src/adapters.rs new file mode 100644 index 000000000..ba55c4328 --- /dev/null +++ b/scripts/change-log/src/adapters.rs @@ -0,0 +1 @@ +pub mod octocrab; diff --git a/scripts/change-log/src/adapters/octocrab.rs b/scripts/change-log/src/adapters/octocrab.rs new file mode 100644 index 000000000..a19d71b5d --- /dev/null +++ b/scripts/change-log/src/adapters/octocrab.rs @@ -0,0 +1,236 @@ +use octocrab::{models::pulls::PullRequest, Octocrab}; +use regex::Regex; +use serde_json::Value; + +use crate::{ + domain::{changelog::capitalize, models::ChangelogInfo}, + ports::github::GitHubPort, +}; + +pub struct OctocrabAdapter { + client: Octocrab, +} + +impl OctocrabAdapter { + pub fn new(token: &str) -> Self { + let client = Octocrab::builder() + .personal_token(token.to_string()) + .build() + .unwrap(); + Self { client } + } + + /// Retrieve the pull request associated with a commit SHA. + async fn get_pr_for_commit( + &self, + owner: &str, + repo: &str, + commit_sha: &str, + ) -> Result> { + let pr_info = self + .client + .repos(owner, repo) + .list_pulls(commit_sha.to_string()) + .send() + .await?; + + if pr_info.items.is_empty() { + return Err("No PR found for this commit SHA".into()); + } + + let pr = pr_info.items.into_iter().next().unwrap(); + + // Ignore PRs from "fuel-service-user" + if pr.user.as_ref().map_or("", |u| &u.login) == "fuel-service-user" { + return Err("PR from fuel-service-user ignored".into()); + } + + Ok(pr) + } + + pub async fn search_branches( + &self, + owner: &str, + repo: &str, + query: &str, + ) -> Result, Box> { + let payload = serde_json::json!({ + "query": r#" + query($owner: String!, $repo: String!, $query: String!) { + repository(owner: $owner, name: $repo) { + refs(refPrefix: "refs/heads/", query: $query, first: 100) { + nodes { + name + } + } + } + } + "#, + "variables": { + "owner": owner, + "repo": repo, + "query": query, + } + }); + + let response: Value = self.client.graphql(&payload).await?; + + let nodes = response["data"]["repository"]["refs"]["nodes"] + .as_array() + .ok_or("Could not parse branch nodes from response")?; + + let branch_names = nodes + .iter() + .filter_map(|node| node["name"].as_str().map(|s| s.to_owned())) + .collect(); + + Ok(branch_names) + } + + /// Query GitHub for all releases in the repository. + pub async fn get_releases( + &self, + owner: &str, + repo: &str, + ) -> Result, Box> { + let releases = self + .client + .repos(owner, repo) + .releases() + .list() + .per_page(100) + .send() + .await?; + + let release_tags = releases + .items + .into_iter() + .map(|release| release.tag_name) + .collect(); + + Ok(release_tags) + } + + /// Build a ChangelogInfo instance from a commit. + async fn build_changelog_info( + &self, + owner: &str, + repo: &str, + commit_sha: &str, + ) -> Result> { + let pr = self.get_pr_for_commit(owner, repo, commit_sha).await?; + + let pr_title_full = pr.title.as_ref().unwrap_or(&"".to_string()).clone(); + let pr_type = pr_title_full + .split(':') + .next() + .unwrap_or("misc") + .to_string(); + let is_breaking = pr_title_full.contains('!'); + let title_description = pr_title_full + .split(':') + .nth(1) + .unwrap_or("") + .trim() + .to_string(); + let pr_number = pr.number; + let pr_author = pr.user.as_ref().map_or("", |u| &u.login).to_string(); + let pr_url = pr.html_url.map(|u| u.to_string()).unwrap_or_default(); + + let bullet_point = format!( + "- [#{}]({}) - {}, by @{}", + pr_number, pr_url, title_description, pr_author + ); + + let breaking_changes_regex = Regex::new(r"(?s)# Breaking Changes\s*(.*)")?; + let breaking_changes = breaking_changes_regex + .captures(pr.body.as_ref().unwrap_or(&String::new())) + .and_then(|cap| cap.get(1)) + .map(|m| { + m.as_str() + .split("\n# ") + .next() + .unwrap_or("") + .trim() + .to_string() + }) + .unwrap_or_default(); + + let release_notes_regex = Regex::new(r"(?s)In this release, we:\s*(.*)")?; + let release_notes = release_notes_regex + .captures(pr.body.as_ref().unwrap_or(&String::new())) + .and_then(|cap| cap.get(1)) + .map(|m| { + m.as_str() + .split("\n# ") + .next() + .unwrap_or("") + .trim() + .to_string() + }) + .unwrap_or_default(); + + let migration_note = format!( + "### [{} - {}]({})\n\n{}", + pr_number, + capitalize(&title_description), + pr_url, + breaking_changes + ); + + Ok(ChangelogInfo { + is_breaking, + pr_type, + bullet_point, + migration_note, + release_notes, + }) + } +} + +impl GitHubPort for OctocrabAdapter { + async fn get_changelog_infos( + &self, + owner: &str, + repo: &str, + base: &str, + head: &str, + ) -> Result, Box> { + let comparison = self + .client + .commits(owner, repo) + .compare(base, head) + .send() + .await?; + + let mut changelogs = Vec::new(); + + for commit in comparison.commits { + match self.build_changelog_info(owner, repo, &commit.sha).await { + Ok(info) => changelogs.push(info), + Err(e) => { + eprintln!("Error retrieving PR for commit {}: {}", commit.sha, e); + continue; + } + } + } + + changelogs.sort_by(|a, b| a.pr_type.cmp(&b.pr_type)); + + Ok(changelogs) + } + + async fn get_latest_release_tag( + &self, + owner: &str, + repo: &str, + ) -> Result> { + let latest_release = self + .client + .repos(owner, repo) + .releases() + .get_latest() + .await?; + Ok(latest_release.tag_name) + } +} diff --git a/scripts/change-log/src/domain.rs b/scripts/change-log/src/domain.rs new file mode 100644 index 000000000..8dc472880 --- /dev/null +++ b/scripts/change-log/src/domain.rs @@ -0,0 +1,2 @@ +pub mod changelog; +pub mod models; diff --git a/scripts/change-log/src/domain/changelog.rs b/scripts/change-log/src/domain/changelog.rs new file mode 100644 index 000000000..bd6fb0f4b --- /dev/null +++ b/scripts/change-log/src/domain/changelog.rs @@ -0,0 +1,159 @@ +use std::collections::{HashMap, HashSet}; + +use crate::domain::models::ChangelogInfo; + +fn category_from_pr_type(pr_type: &str) -> Option<&'static str> { + match pr_type.trim_end_matches('!') { + "feat" => Some("Features"), + "fix" => Some("Fixes"), + "chore" => Some("Chores"), + _ => None, + } +} + +pub fn generate_changelog(changelogs: Vec) -> String { + let mut content = String::new(); + + let mut non_breaking: HashMap<&str, Vec> = HashMap::new(); + let mut breaking: HashMap<&str, Vec> = HashMap::new(); + let mut migration_notes: Vec = Vec::new(); + let mut summary_set: HashSet = HashSet::new(); + + for changelog in &changelogs { + if !changelog.release_notes.is_empty() { + summary_set.insert(changelog.release_notes.clone()); + } + if let Some(category) = category_from_pr_type(&changelog.pr_type) { + if changelog.is_breaking { + breaking + .entry(category) + .or_default() + .push(changelog.bullet_point.clone()); + migration_notes.push(changelog.migration_note.clone()); + } else { + non_breaking + .entry(category) + .or_default() + .push(changelog.bullet_point.clone()); + } + } + } + + if !summary_set.is_empty() { + content.push_str("# Summary\n\nIn this release, we:\n"); + let mut summary_lines: Vec = summary_set.into_iter().collect(); + summary_lines.sort(); + for line in summary_lines { + content.push_str(&format!("{}\n", line)); + } + content.push('\n'); + } + + let categories = ["Features", "Fixes", "Chores"]; + if !breaking.is_empty() { + content.push_str("# Breaking\n\n"); + for cat in &categories { + if let Some(items) = breaking.get(cat) { + content.push_str(&format!("- {}\n", cat)); + + let indented = items + .iter() + .map(|s| format!("\t{}", s)) + .collect::>() + .join("\n"); + content.push_str(&format!("{}\n\n", indented)); + } + } + } + + let mut write_section = |title: &str, items: &[String]| { + if !items.is_empty() { + content.push_str(&format!("# {}\n\n", title)); + content.push_str(&format!("{}\n\n", items.join("\n\n"))); + } + }; + + for cat in &categories { + if let Some(items) = non_breaking.get(cat) { + write_section(cat, items); + } + } + + if !migration_notes.is_empty() { + write_section("Migration Notes", &migration_notes); + } + + content.trim().to_string() +} + +/// Utility function to capitalize a string. +pub fn capitalize(s: &str) -> String { + let mut c = s.chars(); + match c.next() { + None => String::new(), + Some(f) => f.to_uppercase().collect::() + c.as_str(), + } +} +#[cfg(test)] +mod tests { + use super::*; + use crate::domain::models::ChangelogInfo; + + #[test] + fn test_generate_changelog_exact() { + let changelog1 = ChangelogInfo { + is_breaking: false, + pr_type: "feat".to_string(), + bullet_point: "- [#1](http://example.com) - Added feature, by @alice".to_string(), + migration_note: "".to_string(), + release_notes: "Added feature".to_string(), + }; + + let changelog2 = ChangelogInfo { + is_breaking: true, + pr_type: "fix!".to_string(), + bullet_point: "- [#2](http://example.com) - Fixed bug, by @bob".to_string(), + migration_note: "### [2 - Fixed bug](http://example.com)\n\nCritical fix".to_string(), + release_notes: "Fixed bug".to_string(), + }; + + let changelog3 = ChangelogInfo { + is_breaking: false, + pr_type: "chore".to_string(), + bullet_point: "- [#3](http://example.com) - Update dependencies, by @carol".to_string(), + migration_note: "".to_string(), + release_notes: "".to_string(), + }; + + let changelogs = vec![changelog1, changelog2, changelog3]; + let markdown = generate_changelog(changelogs); + + let expected = "\ +# Summary + +In this release, we: +Added feature +Fixed bug + +# Breaking + +- Fixes +\t- [#2](http://example.com) - Fixed bug, by @bob + +# Features + +- [#1](http://example.com) - Added feature, by @alice + +# Chores + +- [#3](http://example.com) - Update dependencies, by @carol + +# Migration Notes + +### [2 - Fixed bug](http://example.com) + +Critical fix"; + + assert_eq!(markdown, expected); + } +} diff --git a/scripts/change-log/src/domain/models.rs b/scripts/change-log/src/domain/models.rs new file mode 100644 index 000000000..5252cc6e9 --- /dev/null +++ b/scripts/change-log/src/domain/models.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone)] +pub struct ChangelogInfo { + pub is_breaking: bool, + pub pr_type: String, + pub bullet_point: String, + pub migration_note: String, + pub release_notes: String, +} diff --git a/scripts/change-log/src/get_full_changelog.rs b/scripts/change-log/src/get_full_changelog.rs deleted file mode 100644 index bcc4fa117..000000000 --- a/scripts/change-log/src/get_full_changelog.rs +++ /dev/null @@ -1,246 +0,0 @@ -use octocrab::Octocrab; -use regex::Regex; -use std::collections::HashSet; -use std::fs::File; -use std::io::{self, Write}; - -#[derive(Debug)] -pub struct ChangelogInfo { - pub is_breaking: bool, - pub pr_type: String, - pub bullet_point: String, - pub migration_note: String, - pub release_notes: String, - pub _pr_number: u64, - pub _pr_title: String, - pub _pr_author: String, - pub _pr_url: String, -} - -pub fn capitalize(s: &str) -> String { - let mut c = s.chars(); - match c.next() { - None => String::new(), - Some(f) => f.to_uppercase().collect::() + c.as_str(), - } -} - -pub async fn get_changelog_info( - octocrab: &Octocrab, - owner: &str, - repo: &str, - commit_sha: &str, -) -> Result> { - let pr_info = octocrab - .repos(owner, repo) - .list_pulls(commit_sha.to_string()) - .send() - .await?; - - if pr_info.items.is_empty() { - return Err("No PR found for this commit SHA".into()); - } - - let pr = &pr_info.items[0]; - - // Skip PRs from the user "fuel-service-user" - if pr.user.as_ref().map_or("", |user| &user.login) == "fuel-service-user" { - return Err("PR from fuel-service-user ignored".into()); - } - - let pr_type = pr - .title - .as_ref() - .map_or("misc", |title| title.split(':').next().unwrap_or("misc")) - .to_string(); - let is_breaking = pr.title.as_ref().is_some_and(|title| title.contains('!')); - let title_description = pr - .title - .as_ref() - .map_or("", |title| title.split(':').nth(1).unwrap_or("")) - .trim() - .to_string(); - let pr_number = pr.number; - let pr_title = title_description.clone(); - let pr_author = pr.user.as_ref().map_or("", |user| &user.login).to_string(); - let pr_url = pr - .html_url - .as_ref() - .map_or("", |url| url.as_str()) - .to_string(); - - let bullet_point = format!( - "- [#{}]({}) - {}, by @{}", - pr_number, pr_url, pr_title, pr_author - ); - - let breaking_changes_regex = Regex::new(r"(?s)# Breaking Changes\s*(.*)").unwrap(); - let breaking_changes = breaking_changes_regex - .captures(pr.body.as_ref().unwrap_or(&String::new())) - .map_or_else(String::new, |cap| { - cap.get(1).map_or(String::new(), |m| { - m.as_str() - .split("\n# ") - .next() - .unwrap_or("") - .trim() - .to_string() - }) - }); - - let release_notes_regex = Regex::new(r"(?s)In this release, we:\s*(.*)").unwrap(); - let release_notes = release_notes_regex - .captures(pr.body.as_ref().unwrap_or(&String::new())) - .map_or_else(String::new, |cap| { - cap.get(1).map_or(String::new(), |m| { - m.as_str() - .split("\n# ") - .next() - .unwrap_or("") - .trim() - .to_string() - }) - }); - - let migration_note = format!( - "### [{} - {}]({})\n\n{}", - pr_number, - capitalize(&title_description), - pr_url, - breaking_changes - ); - - Ok(ChangelogInfo { - is_breaking, - pr_type, - bullet_point, - migration_note, - release_notes, - _pr_number: pr_number, - _pr_title: pr_title, - _pr_author: pr_author, - _pr_url: pr_url, - }) -} - -pub async fn get_changelogs( - octocrab: &Octocrab, - owner: &str, - repo: &str, - base: &str, - head: &str, -) -> Result, Box> { - let comparison = octocrab - .commits(owner, repo) - .compare(base, head) - .send() - .await?; - - let mut changelogs = Vec::new(); - - for commit in comparison.commits { - match get_changelog_info(octocrab, owner, repo, &commit.sha).await { - Ok(info) => changelogs.push(info), - Err(e) => { - println!("Error retrieving PR for commit {}: {}", commit.sha, e); - continue; - } - } - } - - changelogs.sort_by(|a, b| a.pr_type.cmp(&b.pr_type)); - - Ok(changelogs) -} - -pub fn generate_changelog(changelogs: Vec) -> String { - let mut content = String::new(); - - // Categorize PRs by type - let mut features = Vec::new(); - let mut fixes = Vec::new(); - let mut chores = Vec::new(); - let mut breaking_features = Vec::new(); - let mut breaking_fixes = Vec::new(); - let mut breaking_chores = Vec::new(); - let mut migration_notes = Vec::new(); - let mut summary_set: HashSet = HashSet::new(); - - for changelog in &changelogs { - if changelog.is_breaking { - match changelog.pr_type.as_str() { - "feat!" => breaking_features.push(changelog.bullet_point.clone()), - "fix!" => breaking_fixes.push(changelog.bullet_point.clone()), - "chore!" => breaking_chores.push(changelog.bullet_point.clone()), - _ => {} - } - migration_notes.push(changelog.migration_note.clone()); - } else { - match changelog.pr_type.as_str() { - "feat" => features.push(changelog.bullet_point.clone()), - "fix" => fixes.push(changelog.bullet_point.clone()), - "chore" => chores.push(changelog.bullet_point.clone()), - _ => {} - } - } - - if !changelog.release_notes.is_empty() && !summary_set.contains(&changelog.release_notes) { - summary_set.insert(changelog.release_notes.clone().to_string()); - } - } - - if !summary_set.is_empty() { - content.push_str("# Summary\n\nIn this release, we:\n"); - let mut summary_lines: Vec = summary_set.into_iter().collect(); - summary_lines.sort(); - for line in summary_lines { - content.push_str(&format!("{}\n", line)); - } - content.push('\n'); - } - - // Generate the breaking changes section - if !breaking_features.is_empty() || !breaking_fixes.is_empty() || !breaking_chores.is_empty() { - content.push_str("# Breaking\n\n"); - if !breaking_features.is_empty() { - content.push_str("- Features\n"); - content.push_str(&format!("\t{}\n\n", breaking_features.join("\n\t"))); - } - if !breaking_fixes.is_empty() { - content.push_str("- Fixes\n"); - content.push_str(&format!("\t{}\n\n", breaking_fixes.join("\n\t"))); - } - if !breaking_chores.is_empty() { - content.push_str("- Chores\n"); - content.push_str(&format!("\t{}\n\n", breaking_chores.join("\n\t"))); - } - } - - // Generate the categorized sections for non-breaking changes - if !features.is_empty() { - content.push_str("# Features\n\n"); - content.push_str(&format!("{}\n\n", features.join("\n\n"))); - } - if !fixes.is_empty() { - content.push_str("# Fixes\n\n"); - content.push_str(&format!("{}\n\n", fixes.join("\n\n"))); - } - if !chores.is_empty() { - content.push_str("# Chores\n\n"); - content.push_str(&format!("{}\n\n", chores.join("\n\n"))); - } - - // Generate the migration notes section - if !migration_notes.is_empty() { - content.push_str("# Migration Notes\n\n"); - content.push_str(&format!("{}\n\n", migration_notes.join("\n\n"))); - } - - content.trim().to_string() -} - -pub fn write_changelog_to_file(changelog: &str, file_path: &str) -> io::Result<()> { - let mut file = File::create(file_path)?; - file.write_all(changelog.as_bytes())?; - Ok(()) -} diff --git a/scripts/change-log/src/get_latest_release.rs b/scripts/change-log/src/get_latest_release.rs deleted file mode 100644 index 1f1edc717..000000000 --- a/scripts/change-log/src/get_latest_release.rs +++ /dev/null @@ -1,27 +0,0 @@ -use dotenv::dotenv; -use octocrab::Octocrab; - -pub async fn get_latest_release_tag() -> Result> { - dotenv().ok(); - - let github_token = std::env::var("GITHUB_TOKEN").ok(); - - if let Some(token) = github_token { - let octocrab = Octocrab::builder().personal_token(token).build()?; - - let repo_owner = - std::env::var("GITHUB_REPOSITORY_OWNER").expect("Repository owner not found"); - let repo_name = std::env::var("GITHUB_REPOSITORY_NAME").expect("Repository name not found"); - - let latest_release = octocrab - .repos(&repo_owner, &repo_name) - .releases() - .get_latest() - .await?; - - Ok(latest_release.tag_name) - } else { - eprintln!("Please add GITHUB_TOKEN to the environment"); - std::process::exit(1); - } -} diff --git a/scripts/change-log/src/lib.rs b/scripts/change-log/src/lib.rs new file mode 100644 index 000000000..def9028cf --- /dev/null +++ b/scripts/change-log/src/lib.rs @@ -0,0 +1,3 @@ +pub mod adapters; +pub mod domain; +pub mod ports; diff --git a/scripts/change-log/src/main.rs b/scripts/change-log/src/main.rs index d8c50a705..8b0d04692 100644 --- a/scripts/change-log/src/main.rs +++ b/scripts/change-log/src/main.rs @@ -1,34 +1,66 @@ -mod get_full_changelog; -mod get_latest_release; +use std::env; -use get_full_changelog::{generate_changelog, get_changelogs, write_changelog_to_file}; -use get_latest_release::get_latest_release_tag; -use octocrab::Octocrab; +use change_log::{ + adapters::octocrab::OctocrabAdapter, domain::changelog::generate_changelog, + ports::github::GitHubPort, +}; +use dialoguer::FuzzySelect; +use dotenv::dotenv; #[tokio::main] async fn main() -> Result<(), Box> { - dotenv::dotenv().ok(); + dotenv().ok(); + let github_token = - std::env::var("GITHUB_TOKEN").expect("GITHUB_TOKEN is not set in the environment"); - let repo_owner = std::env::var("GITHUB_REPOSITORY_OWNER").expect("Repository owner not found"); - let repo_name = std::env::var("GITHUB_REPOSITORY_NAME").expect("Repository name not found"); + env::var("GITHUB_TOKEN").expect("GITHUB_TOKEN is not set in the environment"); + let repo_owner = env::var("GITHUB_REPOSITORY_OWNER").unwrap_or_else(|_| "FuelLabs".to_string()); + let repo_name = env::var("GITHUB_REPOSITORY_NAME").unwrap_or_else(|_| "fuels-rs".to_string()); + + let github_adapter = OctocrabAdapter::new(&github_token); + + let branches = { + let mut branches = vec!["master".to_string()]; + let lts_branches = github_adapter + .search_branches(&repo_owner, &repo_name, "lts/") + .await?; + branches.extend(lts_branches); + branches + }; + + let branch_selection = FuzzySelect::new() + .with_prompt("Select the target branch (start typing to filter)") + .items(&branches) + .default(0) + .interact()?; + + let target_branch = branches[branch_selection].clone(); - let octocrab = Octocrab::builder().personal_token(github_token).build()?; + let releases = github_adapter.get_releases(&repo_owner, &repo_name).await?; + if releases.is_empty() { + return Err("No releases found for the repository".into()); + } + let release_selection = FuzzySelect::new() + .with_prompt("Select the previous release tag") + .items(&releases) + .default(0) + .interact()?; + let previous_release_tag = releases[release_selection].clone(); - let latest_release_tag = get_latest_release_tag().await?; + eprintln!("Using branch: {}", target_branch); + eprintln!("Using previous release: {}", previous_release_tag); - let changelogs = get_changelogs( - &octocrab, - &repo_owner, - &repo_name, - &latest_release_tag, - "master", - ) - .await?; + let changelog_infos = github_adapter + .get_changelog_infos( + &repo_owner, + &repo_name, + &previous_release_tag, + &target_branch, + ) + .await?; - let full_changelog = generate_changelog(changelogs); + let changelog_markdown = generate_changelog(changelog_infos); - write_changelog_to_file(&full_changelog, "output_changelog.md")?; + println!("{changelog_markdown}"); Ok(()) } diff --git a/scripts/change-log/src/ports.rs b/scripts/change-log/src/ports.rs new file mode 100644 index 000000000..72246d32d --- /dev/null +++ b/scripts/change-log/src/ports.rs @@ -0,0 +1 @@ +pub mod github; diff --git a/scripts/change-log/src/ports/github.rs b/scripts/change-log/src/ports/github.rs new file mode 100644 index 000000000..f6c641840 --- /dev/null +++ b/scripts/change-log/src/ports/github.rs @@ -0,0 +1,20 @@ +use crate::domain::models::ChangelogInfo; + +#[allow(async_fn_in_trait)] +pub trait GitHubPort { + /// Retrieve a collection of changelog infos based on the commit comparison between `base` and `head`. + async fn get_changelog_infos( + &self, + owner: &str, + repo: &str, + base: &str, + head: &str, + ) -> Result, Box>; + + /// Retrieve the latest release tag for the given repository. + async fn get_latest_release_tag( + &self, + owner: &str, + repo: &str, + ) -> Result>; +}