Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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