From 09f876f3f92c5bbf1db44558344112e1052a710b Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Sat, 4 Oct 2025 19:17:46 +0200 Subject: [PATCH 1/3] #174 - Add logic for fetching cross-repo sub issues's PRs - Added mining of Pull Requests for cross-repo issues. --- release_notes_generator/data/miner.py | 24 +++++++++++++++++-- release_notes_generator/generator.py | 6 ++++- release_notes_generator/model/mined_data.py | 2 ++ .../record/factory/default_record_factory.py | 9 +++++++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/release_notes_generator/data/miner.py b/release_notes_generator/data/miner.py index 584c27bf..2887a2a1 100644 --- a/release_notes_generator/data/miner.py +++ b/release_notes_generator/data/miner.py @@ -27,6 +27,7 @@ from github import Github from github.GitRelease import GitRelease from github.Issue import Issue +from github.PullRequest import PullRequest from github.Repository import Repository from release_notes_generator.action_inputs import ActionInputs @@ -83,7 +84,7 @@ def mine_data(self) -> MinedData: return de_duplicated_data - def mine_missing_sub_issues(self, data: MinedData) -> dict[Issue, Repository]: + def mine_missing_sub_issues(self, data: MinedData) -> tuple[dict[Issue, Repository], dict[str, list[PullRequest]]]: """ Mines missing sub-issues from GitHub. Parameters: @@ -95,7 +96,12 @@ def mine_missing_sub_issues(self, data: MinedData) -> dict[Issue, Repository]: data.parents_sub_issues = self._scan_sub_issues_for_parents([get_id(i, r) for i, r in data.issues.items()]) logger.info("Fetching missing issues...") - return self._fetch_missing_issues_and_prs(data) + fetched_issues = self._fetch_missing_issues_and_prs(data) + + logger.info("Getting PRs and Commits for missing issues...") + prs_of_fetched_cross_repo_issues = self._fetch_prs_for_fetched_cross_issues(fetched_issues, data) + + return fetched_issues, prs_of_fetched_cross_repo_issues def _scan_sub_issues_for_parents(self, parents_to_check: list[str]) -> dict[str, list[str]]: """ @@ -167,6 +173,8 @@ def _fetch_missing_issues_and_prs(self, data: MinedData) -> dict[Issue, Reposito else: logger.debug("Skipping issue %s since it does not meet criteria.", parent_id) issues_for_remove.append(parent_id) + else: + logger.error("Cannot get repository for issue %s. Skipping...", parent_id) # remove issue which does not meet criteria for iid in issues_for_remove: @@ -350,3 +358,15 @@ def __filter_duplicated_issues(data: MinedData) -> "MinedData": data.issues = filtered_issues return data + + def _fetch_prs_for_fetched_cross_issues(self, issues: dict[Issue, Repository], data: MinedData) -> dict[str, list[PullRequest]]: + prs_of_cross_repo_issues: dict[str, list[PullRequest]] = {} + for i, repo in issues.items(): + prs_of_cross_repo_issues[iid := get_id(i, repo)] = [] + for ev in i.get_timeline(): # timeline includes cross-references + if ev.event == "cross-referenced" and getattr(ev, "source", None): + src_issue = ev.source.issue # <- this is a github.Issue.Issue + if getattr(src_issue, "pull_request", None): + pr = src_issue.as_pull_request() # github.PullRequest.PullRequest + prs_of_cross_repo_issues[iid].append(pr) + return prs_of_cross_repo_issues diff --git a/release_notes_generator/generator.py b/release_notes_generator/generator.py index 7b0832b0..c5cc9e0e 100644 --- a/release_notes_generator/generator.py +++ b/release_notes_generator/generator.py @@ -24,6 +24,8 @@ from typing import Optional from github import Github +from github.Issue import Issue +from github.Repository import Repository from release_notes_generator.data.filter import FilterByRelease from release_notes_generator.data.miner import DataMiner @@ -91,7 +93,9 @@ def generate(self) -> Optional[str]: # data expansion when hierarchy is enabled if ActionInputs.get_hierarchy(): - data_filtered_by_release.issues.update(miner.mine_missing_sub_issues(data_filtered_by_release)) + fetched_issues, prs_of_fetched_issues = miner.mine_missing_sub_issues(data_filtered_by_release) + data_filtered_by_release.issues.update(fetched_issues) + data_filtered_by_release.pull_requests_of_fetched_cross_issues = prs_of_fetched_issues else: # fill flat structure with empty lists, no hierarchy for i, repo in data_filtered_by_release.issues.items(): diff --git a/release_notes_generator/model/mined_data.py b/release_notes_generator/model/mined_data.py index a47c2a2e..ed09c1bb 100644 --- a/release_notes_generator/model/mined_data.py +++ b/release_notes_generator/model/mined_data.py @@ -50,6 +50,8 @@ def __init__(self, repository: Repository): self.commits: dict[Commit, Repository] = {} self.parents_sub_issues: dict[str, list[str]] = {} # parent issue id -> list of its sub-issues ids + # dictionary of fetched cross issues and their pull requests + self.pull_requests_of_fetched_cross_issues: dict[str, list[PullRequest]] = {} @property def home_repository(self) -> Repository: diff --git a/release_notes_generator/record/factory/default_record_factory.py b/release_notes_generator/record/factory/default_record_factory.py index 4a5deeaa..aa0a884c 100644 --- a/release_notes_generator/record/factory/default_record_factory.py +++ b/release_notes_generator/record/factory/default_record_factory.py @@ -97,6 +97,11 @@ def generate(self, data: MinedData) -> dict[str, Record]: for pull, repo in data.pull_requests.items(): self._register_pull_and_its_commits_to_issue(pull, get_id(pull, repo), data, target_repository=repo) + if data.pull_requests_of_fetched_cross_issues.items(): + logger.debug("Register cross-repo Pull Requests to its issues") + for iid, prs in data.pull_requests_of_fetched_cross_issues.items(): + self._register_cross_repo_prs_to_issue(iid, prs) + logger.debug("Registering direct commits to records...") for commit, repo in data.commits.items(): if commit.sha not in self.__registered_commits: @@ -180,6 +185,10 @@ def _register_pull_and_its_commits_to_issue( self._records[pid] = pr_rec logger.debug("Created record for PR %s: %s", pid, pull.title) + def _register_cross_repo_prs_to_issue(self, iid: str, prs: list[PullRequest]) -> None: + for pr in prs: + cast(IssueRecord, self._records[iid]).register_pull_request(pr) + def _create_record_for_hierarchy_issue(self, i: Issue, iid: str, issue_labels: Optional[list[str]] = None) -> None: """ Create a hierarchy issue record and register sub-issues. From b1b178f2854a77e18c61a081bbcbe2710a96a777 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Sat, 4 Oct 2025 19:42:02 +0200 Subject: [PATCH 2/3] Fixed rabbit review comments. --- release_notes_generator/data/miner.py | 26 ++++++++++++------- release_notes_generator/generator.py | 2 -- .../record/factory/default_record_factory.py | 6 ++++- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/release_notes_generator/data/miner.py b/release_notes_generator/data/miner.py index 2887a2a1..0e1d25e2 100644 --- a/release_notes_generator/data/miner.py +++ b/release_notes_generator/data/miner.py @@ -91,15 +91,16 @@ def mine_missing_sub_issues(self, data: MinedData) -> tuple[dict[Issue, Reposito data (MinedData): The mined data containing origin sets of issues and pull requests. Returns: dict[Issue, Repository]: A dictionary mapping fetched issues to their repositories. + dict[str, list[PullRequest]]: A dictionary mapping fetched cross-repo issue with its pull requests. """ logger.info("Mapping sub-issues...") data.parents_sub_issues = self._scan_sub_issues_for_parents([get_id(i, r) for i, r in data.issues.items()]) logger.info("Fetching missing issues...") - fetched_issues = self._fetch_missing_issues_and_prs(data) + fetched_issues = self._fetch_missing_issues(data) logger.info("Getting PRs and Commits for missing issues...") - prs_of_fetched_cross_repo_issues = self._fetch_prs_for_fetched_cross_issues(fetched_issues, data) + prs_of_fetched_cross_repo_issues = self._fetch_prs_for_fetched_cross_issues(fetched_issues) return fetched_issues, prs_of_fetched_cross_repo_issues @@ -125,7 +126,7 @@ def _scan_sub_issues_for_parents(self, parents_to_check: list[str]) -> dict[str, return parents_sub_issues - def _fetch_missing_issues_and_prs(self, data: MinedData) -> dict[Issue, Repository]: + def _fetch_missing_issues(self, data: MinedData) -> dict[Issue, Repository]: """ Fetch missing issues. @@ -359,14 +360,19 @@ def __filter_duplicated_issues(data: MinedData) -> "MinedData": data.issues = filtered_issues return data - def _fetch_prs_for_fetched_cross_issues(self, issues: dict[Issue, Repository], data: MinedData) -> dict[str, list[PullRequest]]: + def _fetch_prs_for_fetched_cross_issues(self, issues: dict[Issue, Repository]) -> dict[str, list[PullRequest]]: prs_of_cross_repo_issues: dict[str, list[PullRequest]] = {} for i, repo in issues.items(): prs_of_cross_repo_issues[iid := get_id(i, repo)] = [] - for ev in i.get_timeline(): # timeline includes cross-references - if ev.event == "cross-referenced" and getattr(ev, "source", None): - src_issue = ev.source.issue # <- this is a github.Issue.Issue - if getattr(src_issue, "pull_request", None): - pr = src_issue.as_pull_request() # github.PullRequest.PullRequest - prs_of_cross_repo_issues[iid].append(pr) + try: + for ev in i.get_timeline(): # timeline includes cross-references + if ev.event == "cross-referenced" and getattr(ev, "source", None): + # <- this is a github.Issue.Issue + src_issue = ev.source.issue # type: ignore[union-attr] + if getattr(src_issue, "pull_request", None): + pr = src_issue.as_pull_request() # github.PullRequest.PullRequest + prs_of_cross_repo_issues[iid].append(pr) + except Exception as e: + logger.warning("Failed to fetch timeline events for issue %s: %s", iid, str(e)) + return prs_of_cross_repo_issues diff --git a/release_notes_generator/generator.py b/release_notes_generator/generator.py index c5cc9e0e..bb8cc989 100644 --- a/release_notes_generator/generator.py +++ b/release_notes_generator/generator.py @@ -24,8 +24,6 @@ from typing import Optional from github import Github -from github.Issue import Issue -from github.Repository import Repository from release_notes_generator.data.filter import FilterByRelease from release_notes_generator.data.miner import DataMiner diff --git a/release_notes_generator/record/factory/default_record_factory.py b/release_notes_generator/record/factory/default_record_factory.py index aa0a884c..c54b2f21 100644 --- a/release_notes_generator/record/factory/default_record_factory.py +++ b/release_notes_generator/record/factory/default_record_factory.py @@ -97,7 +97,7 @@ def generate(self, data: MinedData) -> dict[str, Record]: for pull, repo in data.pull_requests.items(): self._register_pull_and_its_commits_to_issue(pull, get_id(pull, repo), data, target_repository=repo) - if data.pull_requests_of_fetched_cross_issues.items(): + if data.pull_requests_of_fetched_cross_issues: logger.debug("Register cross-repo Pull Requests to its issues") for iid, prs in data.pull_requests_of_fetched_cross_issues.items(): self._register_cross_repo_prs_to_issue(iid, prs) @@ -186,6 +186,10 @@ def _register_pull_and_its_commits_to_issue( logger.debug("Created record for PR %s: %s", pid, pull.title) def _register_cross_repo_prs_to_issue(self, iid: str, prs: list[PullRequest]) -> None: + if iid not in self.__registered_issues: + logger.error("Issue '%s' not found among collected records.", iid) + return + for pr in prs: cast(IssueRecord, self._records[iid]).register_pull_request(pr) From 87a1d0c7ae006658a3aeba4215ab7bd6c58a36cc Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Sat, 4 Oct 2025 19:43:54 +0200 Subject: [PATCH 3/3] Black fixed. --- release_notes_generator/data/miner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release_notes_generator/data/miner.py b/release_notes_generator/data/miner.py index 0e1d25e2..98bdd2b0 100644 --- a/release_notes_generator/data/miner.py +++ b/release_notes_generator/data/miner.py @@ -368,7 +368,7 @@ def _fetch_prs_for_fetched_cross_issues(self, issues: dict[Issue, Repository]) - for ev in i.get_timeline(): # timeline includes cross-references if ev.event == "cross-referenced" and getattr(ev, "source", None): # <- this is a github.Issue.Issue - src_issue = ev.source.issue # type: ignore[union-attr] + src_issue = ev.source.issue # type: ignore[union-attr] if getattr(src_issue, "pull_request", None): pr = src_issue.as_pull_request() # github.PullRequest.PullRequest prs_of_cross_repo_issues[iid].append(pr)