def get_base_commit_sha(self, pull_request: PullRequest) -> Optional[str]: if self._settings.pull_request_build == pull_request_build_mode_merge: if self._settings.event: # for pull request events we take the other parent of the merge commit (base) if self._settings.event_name == 'pull_request': return self._settings.event.get('pull_request', {}).get('base', {}).get('sha') # for workflow run events we should take the same as for pull request events, # but we have no way to figure out the actual merge commit and its parents # we do not take the base sha from pull_request as it is not immutable if self._settings.event_name == 'workflow_run': return None try: # we always fall back to where the branch merged off base ref logger.debug( f'comparing {pull_request.base.ref} with {self._settings.commit}' ) compare = self._repo.compare(pull_request.base.ref, self._settings.commit) return compare.merge_base_commit.sha except: logger.warning(f'could not find best common ancestor ' f'between base {pull_request.base.sha} ' f'and commit {self._settings.commit}') return None
def get_check_run(self, commit_sha: str) -> Optional[CheckRun]: if commit_sha is None or commit_sha == '0000000000000000000000000000000000000000': return None commit = None try: commit = self._repo.get_commit(commit_sha) except GithubException as e: if e.status == 422: self._gha.warning(str(e.data)) else: raise e if commit is None: self._gha.error(f'Could not find commit {commit_sha}') return None runs = commit.get_check_runs() logger.debug( f'found {runs.totalCount} check runs for commit {commit_sha}') runs = list( [run for run in runs if run.name == self._settings.check_name]) logger.debug( f'found {len(runs)} check runs for commit {commit_sha} with title {self._settings.check_name}' ) if len(runs) != 1: return None return runs[0]
def publish_comment(self, title: str, stats: UnitTestRunResults, pull_request: PullRequest, check_run: Optional[CheckRun] = None, cases: Optional[UnitTestCaseResults] = None) -> PullRequest: # compare them with earlier stats base_check_run = None if self._settings.compare_earlier: base_commit_sha = self.get_base_commit_sha(pull_request) logger.debug(f'comparing against base={base_commit_sha}') base_check_run = self.get_check_run(base_commit_sha) base_stats = self.get_stats_from_check_run(base_check_run) if base_check_run is not None else None stats_with_delta = get_stats_delta(stats, base_stats, 'base') if base_stats is not None else stats logger.debug(f'stats with delta: {stats_with_delta}') # gather test lists from check run and cases before_all_tests, before_skipped_tests = self.get_test_lists_from_check_run(base_check_run) all_tests, skipped_tests = get_all_tests_list(cases), get_skipped_tests_list(cases) test_changes = SomeTestChanges(before_all_tests, all_tests, before_skipped_tests, skipped_tests) details_url = check_run.html_url if check_run else None summary = get_long_summary_md(stats_with_delta, details_url, test_changes, self._settings.test_changes_limit) body = f'## {title}\n{summary}' # reuse existing commend when comment_mode == comment_mode_update # if none exists or comment_mode != comment_mode_update, create new comment if self._settings.comment_mode != comment_mode_update or not self.reuse_comment(pull_request, body): logger.info('creating comment') pull_request.create_issue_comment(body) return pull_request
def reuse_comment(self, pull: PullRequest, body: str) -> bool: # get comments of this pull request comments = self.get_pull_request_comments(pull, order_by_updated=True) # get all comments that come from this action and are not hidden comments = self.get_action_comments(comments) # if there is no such comment, stop here if len(comments) == 0: return False # edit last comment comment_id = comments[-1].get("databaseId") logger.info(f'editing comment {comment_id}') if ':recycle:' not in body: body = f'{body}\n:recycle: This comment has been updated with latest results.' try: pull.get_issue_comment(comment_id).edit(body) except Exception as e: self._gha.warning(f'Failed to edit existing comment #{comment_id}') logger.debug('editing existing comment failed', exc_info=e) return True
def get_stats_from_check_run(self, check_run: CheckRun) -> Optional[UnitTestRunResults]: summary = check_run.output.summary if summary is None: return None for line in summary.split('\n'): logger.debug(f'summary: {line}') pos = summary.index(digest_prefix) if digest_prefix in summary else None if pos: digest = summary[pos + len(digest_prefix):] logger.debug(f'digest: {digest}') stats = get_stats_from_digest(digest) logger.debug(f'stats: {stats}') return stats
def publish_check(self, stats: UnitTestRunResults, cases: UnitTestCaseResults, compare_earlier: bool, conclusion: str) -> CheckRun: # get stats from earlier commits before_stats = None if compare_earlier: before_commit_sha = self._settings.event.get('before') logger.debug(f'comparing against before={before_commit_sha}') before_stats = self.get_stats_from_commit(before_commit_sha) stats_with_delta = get_stats_delta( stats, before_stats, 'earlier') if before_stats is not None else stats logger.debug(f'stats with delta: {stats_with_delta}') error_annotations = get_error_annotations(stats.errors) case_annotations = get_case_annotations( cases, self._settings.report_individual_runs) file_list_annotations = self.get_test_list_annotations(cases) all_annotations = error_annotations + case_annotations + file_list_annotations # we can send only 50 annotations at once, so we split them into chunks of 50 check_run = None all_annotations = [ all_annotations[x:x + 50] for x in range(0, len(all_annotations), 50) ] or [[]] for annotations in all_annotations: output = dict(title=get_short_summary(stats), summary=get_long_summary_with_digest_md( stats_with_delta, stats), annotations=[ annotation.to_dict() for annotation in annotations ]) logger.info('creating check') check_run = self._repo.create_check_run( name=self._settings.check_name, head_sha=self._settings.commit, status='completed', conclusion=conclusion, output=output) logger.debug(f'created check {check_run}') return check_run
def debug(self, message: str) -> str: logger.debug(message) return self._command(self._file, 'debug', message)
def get_pull(self, commit: str) -> Optional[PullRequest]: issues = self._gh.search_issues( f'type:pr repo:"{self._settings.repo}" {commit}') logger.debug( f'found {issues.totalCount} pull requests in repo {self._settings.repo} for commit {commit}' ) if issues.totalCount == 0: return None for issue in issues: pr = issue.as_pull_request() logger.debug(pr) logger.debug(pr.raw_data) logger.debug( f'PR {pr.html_url}: {pr.head.repo.full_name} -> {pr.base.repo.full_name}' ) # we can only publish the comment to PRs that are in the same repository as this action is executed in # so pr.base.repo.full_name must be same as GITHUB_REPOSITORY / self._settings.repo # we won't have permission otherwise pulls = list([ pr for issue in issues for pr in [issue.as_pull_request()] if pr.base.repo.full_name == self._settings.repo ]) if len(pulls) == 0: logger.debug( f'found no pull requests in repo {self._settings.repo} for commit {commit}' ) return None if len(pulls) > 1: pulls = [pull for pull in pulls if pull.state == 'open'] if len(pulls) == 0: logger.debug( f'found no open pull request in repo {self._settings.repo} for commit {commit}' ) return None if len(pulls) > 1: self._gha.error( f'Found multiple open pull requests for commit {commit}') return None pull = pulls[0] logger.debug(f'found pull request #{pull.number} for commit {commit}') return pull