| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- #
- # Copyright (c) 2022 Project CHIP Authors
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- #
- """Utility wrapper for GitHub operations."""
- import itertools
- import logging
- import os
- from typing import Iterable, Mapping, Optional
- import dateutil # type: ignore
- import dateutil.parser # type: ignore
- import ghapi.all # type: ignore
- from memdf import Config, ConfigDescription
- def postprocess_config(config: Config, _key: str, _info: Mapping) -> None:
- """Postprocess --github-repository."""
- if config['github.repository']:
- owner, repo = config.get('github.repository').split('/', 1)
- config.put('github.owner', owner)
- config.put('github.repo', repo)
- if not config['github.token']:
- config['github.token'] = os.environ.get('GITHUB_TOKEN')
- if not config['github.token']:
- logging.error('Missing --github-token')
- CONFIG: ConfigDescription = {
- Config.group_def('github'): {
- 'title': 'github options',
- },
- 'github.token': {
- 'help': 'Github API token, or "SKIP" to suppress connecting to github',
- 'metavar': 'TOKEN',
- 'default': '',
- 'argparse': {
- 'alias': ['--github-api-token', '--token'],
- },
- },
- 'github.repository': {
- 'help': 'Github repostiory',
- 'metavar': 'OWNER/REPO',
- 'default': '',
- 'argparse': {
- 'alias': ['--repo'],
- },
- 'postprocess': postprocess_config,
- },
- 'github.dryrun-comment': {
- 'help': "Don't actually post comments",
- 'default': False,
- },
- 'github.keep': {
- 'help': "Don't remove PR artifacts",
- 'default': False,
- 'argparse': {
- 'alias': ['--keep'],
- },
- },
- 'github.limit-artifact-pages': {
- 'help': 'Examine no more than COUNT pages of artifacts',
- 'metavar': 'COUNT',
- 'default': 0,
- 'argparse': {
- 'type': int,
- },
- },
- }
- class Gh:
- """Utility wrapper for GitHub operations."""
- def __init__(self, config: Config):
- self.config = config
- self.ghapi: Optional[ghapi.all.GhApi] = None
- self.deleted_artifacts: set[int] = set()
- owner = config['github.owner']
- repo = config['github.repo']
- token = config['github.token']
- if owner and repo and token and token != 'SKIP':
- self.ghapi = ghapi.all.GhApi(owner=owner, repo=repo, token=token)
- def __bool__(self):
- return self.ghapi is not None
- def get_comments_for_pr(self, pr: int):
- """Iterate PR comments."""
- assert self.ghapi
- try:
- return itertools.chain.from_iterable(
- ghapi.all.paged(self.ghapi.issues.list_comments, pr))
- except Exception as e:
- logging.error('Failed to get comments for PR #%d: %s', pr, e)
- return []
- def get_commits_for_pr(self, pr: int):
- """Iterate PR commits."""
- assert self.ghapi
- try:
- return itertools.chain.from_iterable(
- ghapi.all.paged(self.ghapi.pulls.list_commits, pr))
- except Exception as e:
- logging.error('Failed to get commits for PR #%d: %s', pr, e)
- return []
- def get_artifacts(self, page_limit: int = -1, per_page: int = -1):
- """Iterate artifact descriptions."""
- if page_limit < 0:
- page_limit = self.config['github.limit-artifact-pages']
- if per_page < 0:
- per_page = self.config['github.artifacts-per-page'] or 100
- assert self.ghapi
- try:
- page = 0
- for i in ghapi.all.paged(
- self.ghapi.actions.list_artifacts_for_repo,
- per_page=per_page):
- if not i.artifacts:
- break
- for a in i.artifacts:
- yield a
- page += 1
- logging.debug('ASP: artifact page %d of %d', page, page_limit)
- if page_limit and page >= page_limit:
- break
- except Exception as e:
- logging.error('Failed to get artifact list: %s', e)
- def get_size_artifacts(self,
- page_limit: int = -1,
- per_page: int = -1,
- label: str = ''):
- """Iterate size artifact descriptions."""
- for a in self.get_artifacts(page_limit, per_page):
- # Size artifacts have names of the form:
- # Size,{group},{pr},{commit_hash},{parent_hash}[,{event}]
- # This information is added to the attribute record from GitHub.
- if a.name.startswith('Size,') and a.name.count(',') >= 4:
- _, group, pr, commit, parent, *etc = a.name.split(',')
- if label and group != label:
- continue
- a.group = group
- a.commit = commit
- a.parent = parent
- a.pr = pr
- a.created_at = dateutil.parser.isoparse(a.created_at)
- # Old artifact names don't include the event.
- if etc:
- event = etc[0]
- else:
- event = 'push' if pr == '0' else 'pull_request'
- a.event = event
- yield a
- def download_artifact(self, artifact_id: int):
- """Download a GitHub artifact, returning a binary zip object."""
- logging.debug('Downloading artifact %d', artifact_id)
- try:
- assert self.ghapi
- return self.ghapi.actions.download_artifact(artifact_id, 'zip')
- except Exception as e:
- logging.error('Failed to download artifact %d: %s', artifact_id, e)
- return None
- def delete_artifact(self, artifact_id: int) -> bool:
- """Delete a GitHub artifact."""
- if not artifact_id or artifact_id in self.deleted_artifacts:
- return True
- self.deleted_artifacts.add(artifact_id)
- if self.config['github.keep']:
- logging.info('Suppressed deleting artifact %d', artifact_id)
- return False
- try:
- assert self.ghapi
- logging.info('Deleting artifact %d', artifact_id)
- self.ghapi.actions.delete_artifact(artifact_id)
- return True
- except Exception as e:
- # During manual testing we sometimes lose the race against CI.
- logging.error('Failed to delete artifact %d: %s', artifact_id, e)
- return False
- def delete_artifacts(self, artifacts: Iterable[int]):
- for artifact_id in artifacts:
- self.delete_artifact(artifact_id)
- def create_comment(self, issue_id: int, text: str) -> bool:
- """Create a GitHub comment."""
- if self.config['github.dryrun-comment']:
- logging.info('Suppressed creating comment on #%d', issue_id)
- logging.debug('%s', text)
- return False
- assert self.ghapi
- logging.info('Creating comment on #%d', issue_id)
- try:
- self.ghapi.issues.create_comment(issue_id, text)
- return True
- except Exception as e:
- logging.error('Failed to created comment on #%d: %s', issue_id, e)
- return False
- def update_comment(self, comment_id: int, text: str) -> bool:
- """Update a GitHub comment."""
- if self.config['github.dryrun-comment']:
- logging.info('Suppressed updating comment #%d', comment_id)
- logging.debug('%s', text)
- return False
- logging.info('Updating comment #%d', comment_id)
- try:
- assert self.ghapi
- self.ghapi.issues.update_comment(comment_id, text)
- return True
- except Exception as e:
- logging.error('Failed to update comment %d: %s', comment_id, e)
- return False
|