Skip to content

Commit

Permalink
feature: write annotations to a branch
Browse files Browse the repository at this point in the history
  • Loading branch information
ideepu committed Jun 23, 2024
1 parent 0ee8cda commit d523386
Show file tree
Hide file tree
Showing 7 changed files with 549 additions and 54 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ Permissions needed for the Github Token:
`Pull requests:read`
`Pull requests:write`

If you have given `ANNOTATIONS_DATA_BRANCH` branch then Github Token also requires content write permissions.
Read more on how to use this here.

`Contents:write`

**install:**

```bash
Expand Down
78 changes: 72 additions & 6 deletions codecov/github.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import base64
import dataclasses
import json
import pathlib
Expand All @@ -7,9 +8,10 @@
from codecov import github_client, groups, log, settings

GITHUB_CODECOV_LOGIN = 'CI-codecov[bot]'
COMMIT_MESSAGE = 'Update annotations data'


class CannotDeterminePR(Exception):
class CannotGetBranch(Exception):
pass


Expand Down Expand Up @@ -58,6 +60,13 @@ def default(self, o):
return super().default(o)


@dataclasses.dataclass
class User:
name: str
email: str
login: str


@dataclasses.dataclass
class RepositoryInfo:
default_branch: str
Expand All @@ -76,16 +85,21 @@ def get_repository_info(github: github_client.GitHub, repository: str) -> Reposi
return RepositoryInfo(default_branch=response.default_branch, visibility=response.visibility)


def get_my_login(github: github_client.GitHub) -> str:
def get_my_login(github: github_client.GitHub) -> User:
try:
response = github.user.get()
user = User(
name=response.name,
email=response.email or f'{response.id}+{response.login}@users.noreply.github.com',
login=response.login,
)
except github_client.Forbidden:
# The GitHub actions user cannot access its own details
# and I'm not sure there's a way to see that we're using
# the GitHub actions user except noting that it fails
return GITHUB_CODECOV_LOGIN
return User(name=GITHUB_CODECOV_LOGIN, email='', login=GITHUB_CODECOV_LOGIN)

return response.login
return user


def get_pr_number(github: github_client.GitHub, config: settings.Config) -> int:
Expand Down Expand Up @@ -138,7 +152,7 @@ def get_pr_diff(github: github_client.GitHub, repository: str, pr_number: int) -

def post_comment( # pylint: disable=too-many-arguments
github: github_client.GitHub,
me: str,
user: User,
repository: str,
pr_number: int,
contents: str,
Expand All @@ -151,7 +165,7 @@ def post_comment( # pylint: disable=too-many-arguments
comments_path = github.repos(repository).issues.comments

for comment in issue_comments_path.get():
if comment.user.login == me and marker in comment.body:
if comment.user.login == user.login and marker in comment.body:
log.info('Update previous comment')
try:
comments_path(comment.id).patch(body=contents)
Expand Down Expand Up @@ -198,3 +212,55 @@ def create_missing_coverage_annotations(
)
)
return formatted_annotations


def write_annotations_to_branch(
github: github_client.GitHub, user: User, pr_number: int, config: settings.Config, annotations: list[Annotation]
) -> None:
log.info('Getting the annotations data branch.')
try:
data_branch = github.repos(config.GITHUB_REPOSITORY).branches(config.ANNOTATIONS_DATA_BRANCH).get()
if data_branch.protected:
raise github_client.NotFound
except github_client.Forbidden as exc:
raise CannotGetBranch from exc
except github_client.NotFound as exc:
log.warning(f'Branch "{config.GITHUB_REPOSITORY}/{config.ANNOTATIONS_DATA_BRANCH}" does not exist.')
raise CannotGetBranch from exc

log.info('Writing annotations to branch.')
file_name = f'{pr_number}-annotations.json'
file_sha: str | None = None
try:
file = github.repos(config.GITHUB_REPOSITORY).contents(file_name).get(ref=config.ANNOTATIONS_DATA_BRANCH)
file_sha = file.sha
except github_client.NotFound:
pass
except github_client.Forbidden as exc:
log.error(f'Forbidden access to branch "{config.GITHUB_REPOSITORY}/{config.ANNOTATIONS_DATA_BRANCH}".')
raise CannotGetBranch from exc

try:
encoded_content = base64.b64encode(json.dumps(annotations, cls=AnnotationEncoder).encode()).decode()
github.repos(config.GITHUB_REPOSITORY).contents(file_name).put(
message=COMMIT_MESSAGE,
branch=config.ANNOTATIONS_DATA_BRANCH,
sha=file_sha,
committer={
'name': user.name,
'email': user.email,
},
content=encoded_content,
)
except github_client.NotFound as exc:
log.error(f'Branch "{config.GITHUB_REPOSITORY}/{config.ANNOTATIONS_DATA_BRANCH}" does not exist.')
raise CannotGetBranch from exc
except github_client.Forbidden as exc:
log.error(f'Forbidden access to branch "{config.GITHUB_REPOSITORY}/{config.ANNOTATIONS_DATA_BRANCH}".')
raise CannotGetBranch from exc
except github_client.Conflict as exc:
log.error(f'Conflict writing to branch "{config.GITHUB_REPOSITORY}/{config.ANNOTATIONS_DATA_BRANCH}".')
raise CannotGetBranch from exc
except github_client.ValidationFailed as exc:
log.error('Validation failed on committer name or email.')
raise CannotGetBranch from exc
11 changes: 11 additions & 0 deletions codecov/github_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def _http(self, method: str, path: str, *, use_bytes: bool = False, use_text: bo
headers=headers,
**requests_kwargs,
)
contents: str | bytes | JsonObject
if use_bytes:
contents = response.content
elif use_text:
Expand All @@ -76,6 +77,8 @@ def _http(self, method: str, path: str, *, use_bytes: bool = False, use_text: bo
cls: type[ApiError] = {
403: Forbidden,
404: NotFound,
409: Conflict,
422: ValidationFailed,
}.get(exc.response.status_code, ApiError)

raise cls(str(contents)) from exc
Expand Down Expand Up @@ -113,3 +116,11 @@ class NotFound(ApiError):

class Forbidden(ApiError):
pass


class Conflict(ApiError):
pass


class ValidationFailed(ApiError):
pass
99 changes: 66 additions & 33 deletions codecov/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ def action(config: settings.Config, github_session: httpx.Client) -> int:
try:
pr_number = github.get_pr_number(github=gh, config=config)
except github.CannotGetPullRequest:
log.debug('Cannot get pull request number. Exiting.', exc_info=True)
log.info(
log.error('Cannot get pull request number. Exiting.', exc_info=True)
log.error(
'This worflow is not triggered on a pull_request event, '
"nor on a push event on a branch. Consequently, there's nothing to do. "
'Exiting.'
Expand Down Expand Up @@ -74,37 +74,18 @@ def process_pr( # pylint: disable=too-many-locals
added_lines = coverage_module.parse_diff_output(diff=pr_diff)
diff_coverage = coverage_module.get_diff_coverage_info(added_lines=added_lines, coverage=coverage)

if config.ANNOTATE_MISSING_LINES:
log.info('Generating annotations for missing lines.')
annotations = diff_grouper.get_diff_missing_groups(coverage=coverage, diff_coverage=diff_coverage)
formatted_annotations = github.create_missing_coverage_annotations(
annotation_type=config.ANNOTATION_TYPE,
annotations=annotations,
user: github.User = github.get_my_login(github=gh)
try:
generate_annotations(
config=config, user=user, pr_number=pr_number, gh=gh, coverage=coverage, diff_coverage=diff_coverage
)

if config.BRANCH_COVERAGE:
branch_annotations = diff_grouper.get_branch_missing_groups(coverage=coverage, diff_coverage=diff_coverage)
formatted_annotations.extend(
github.create_missing_coverage_annotations(
annotation_type=config.ANNOTATION_TYPE,
annotations=branch_annotations,
branch=True,
)
)

# Print to console
yellow = '\033[93m'
reset = '\033[0m'
print(yellow, end='')
print(*formatted_annotations, sep='\n')
print(reset, end='')

# Save to file
if config.ANNOTATIONS_OUTPUT_PATH:
log.info('Writing annotations to file.')
with config.ANNOTATIONS_OUTPUT_PATH.open('w+') as annotations_file:
json.dump(formatted_annotations, annotations_file, cls=github.AnnotationEncoder)
log.info('Annotations generated.')
except github.CannotGetBranch:
log.error(
'Cannot retrieve the annotation data branch.'
'Please ensure it exists and that you have sufficient permissions and branch protection is disabled. Exiting.',
exc_info=True,
)
return 1

if config.SKIP_COVERAGE:
log.info('Skipping coverage report generation')
Expand Down Expand Up @@ -163,7 +144,7 @@ def process_pr( # pylint: disable=too-many-locals
try:
github.post_comment(
github=gh,
me=github.get_my_login(github=gh),
user=user,
repository=config.GITHUB_REPOSITORY,
pr_number=pr_number,
contents=comment,
Expand All @@ -178,3 +159,55 @@ def process_pr( # pylint: disable=too-many-locals

log.debug('Comment created on PR')
return 0


def generate_annotations( # pylint: disable=too-many-arguments
config: settings.Config, user: github.User, pr_number: int, gh: github_client.GitHub, coverage, diff_coverage
):
if not config.ANNOTATE_MISSING_LINES:
return

log.info('Generating annotations for missing lines.')
annotations = diff_grouper.get_diff_missing_groups(coverage=coverage, diff_coverage=diff_coverage)
formatted_annotations = github.create_missing_coverage_annotations(
annotation_type=config.ANNOTATION_TYPE,
annotations=annotations,
)

if config.BRANCH_COVERAGE:
branch_annotations = diff_grouper.get_branch_missing_groups(coverage=coverage, diff_coverage=diff_coverage)
formatted_annotations.extend(
github.create_missing_coverage_annotations(
annotation_type=config.ANNOTATION_TYPE,
annotations=branch_annotations,
branch=True,
)
)

if not formatted_annotations:
log.info('No annotations to generate. Exiting.')
return

# Print to console
yellow = '\033[93m'
reset = '\033[0m'
print(yellow, end='')
print(*formatted_annotations, sep='\n')
print(reset, end='')

# Save to file
if config.ANNOTATIONS_OUTPUT_PATH:
log.info('Writing annotations to file.')
with config.ANNOTATIONS_OUTPUT_PATH.open('w+') as annotations_file:
json.dump(formatted_annotations, annotations_file, cls=github.AnnotationEncoder)

if config.ANNOTATIONS_DATA_BRANCH:
log.info('Writing annotations to branch.')
github.write_annotations_to_branch(
github=gh,
user=user,
pr_number=pr_number,
config=config,
annotations=formatted_annotations,
)
log.info('Annotations generated.')
1 change: 1 addition & 0 deletions codecov/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class Config:
ANNOTATE_MISSING_LINES: bool = False
ANNOTATION_TYPE: str = 'warning'
ANNOTATIONS_OUTPUT_PATH: pathlib.Path | None = None
ANNOTATIONS_DATA_BRANCH: str | None = None
MAX_FILES_IN_COMMENT: int = 25
COMPLETE_PROJECT_REPORT: bool = False
COVERAGE_REPORT_URL: str | None = None
Expand Down
Loading

0 comments on commit d523386

Please sign in to comment.