Skip to content

Commit

Permalink
feat: add team-info task when processing pull requests
Browse files Browse the repository at this point in the history
  • Loading branch information
netomi committed Jan 29, 2024
1 parent 9fcf475 commit 70a2ff7
Show file tree
Hide file tree
Showing 15 changed files with 496 additions and 265 deletions.
4 changes: 3 additions & 1 deletion otterdog/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# which is available at http://www.eclipse.org/legal/epl-v20.html
# SPDX-License-Identifier: EPL-2.0
# *******************************************************************************

import logging
import os
from sys import exit

Expand All @@ -27,6 +27,8 @@

app = create_app(app_config) # type: ignore

logging.basicConfig(level=logging.INFO)

if os.path.exists(app_config.APP_ROOT):
os.chdir(app_config.APP_ROOT)
else:
Expand Down
12 changes: 9 additions & 3 deletions otterdog/providers/github/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ def app(self):
return AppClient(self)

@cached_property
def commits(self):
from .commits_client import CommitsClient
def commit(self):
from .commit_client import CommitClient

return CommitsClient(self)
return CommitClient(self)

@cached_property
def content(self):
Expand Down Expand Up @@ -91,6 +91,12 @@ def user(self):

return UserClient(self)

@cached_property
def team(self):
from .team_client import TeamClient

return TeamClient(self)


class RestClient(ABC):
def __init__(self, rest_api: RestApi):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from . import RestApi, RestClient


class CommitsClient(RestClient):
class CommitClient(RestClient):
def __init__(self, rest_api: RestApi):
super().__init__(rest_api)

Expand Down
44 changes: 44 additions & 0 deletions otterdog/providers/github/rest/team_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# *******************************************************************************
# Copyright (c) 2024 Eclipse Foundation and others.
# This program and the accompanying materials are made available
# under the terms of the Eclipse Public License 2.0
# which is available at http://www.eclipse.org/legal/epl-v20.html
# SPDX-License-Identifier: EPL-2.0
# *******************************************************************************

from typing import Any

from otterdog.providers.github.exception import GitHubException
from otterdog.providers.github.rest import RestApi, RestClient
from otterdog.utils import print_debug


class TeamClient(RestClient):
def __init__(self, rest_api: RestApi):
super().__init__(rest_api)

async def get_team_slugs(self, org_id: str) -> list[dict[str, Any]]:
print_debug(f"retrieving teams for org '{org_id}'")

try:
response = await self.requester.async_request_json("GET", f"/orgs/{org_id}/teams")
return list(map(lambda team: team["slug"], response))
except GitHubException as ex:
tb = ex.__traceback__
raise RuntimeError(f"failed retrieving teams:\n{ex}").with_traceback(tb)

async def is_user_member_of_team(self, org_id: str, team_slug: str, user: str) -> bool:
print_debug(f"retrieving membership of user '{user}' for team '{team_slug}' in org '{org_id}'")

status, body = await self.requester.async_request_raw(
"GET", f"/orgs/{org_id}/teams/{team_slug}/memberships/{user}"
)

if status == 200:
return True
elif status == 404:
return False
else:
raise RuntimeError(
f"failed retrieving team membership for user '{user}' in org '{org_id}'" f"\n{status}: {body}"
)
44 changes: 42 additions & 2 deletions otterdog/webapp/tasks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
# *******************************************************************************

import os.path
from abc import ABC, abstractmethod
from datetime import datetime
from logging import getLogger
from typing import Optional
from functools import cached_property
from logging import Logger, getLogger
from typing import Generic, Optional, TypeVar, Union

from quart import current_app

Expand All @@ -24,6 +26,44 @@

logger = getLogger(__name__)

T = TypeVar("T")


class Task(ABC, Generic[T]):
@cached_property
def logger(self) -> Logger:
return getLogger(type(self).__name__)

@staticmethod
async def get_rest_api(installation_id: int) -> RestApi:
return await get_rest_api_for_installation(installation_id)

async def execute(self) -> None:
self.logger.debug(f"executing task '{self!r}'")

await self._pre_execute()

try:
result = await self._execute()
await self._post_execute(result)
except RuntimeError as ex:
self.logger.exception(f"failed to execute task '{self!r}'", exc_info=ex)
await self._post_execute(ex)

async def _pre_execute(self) -> None:
pass

async def _post_execute(self, result_or_exception: Union[T, Exception]) -> None:
pass

@abstractmethod
async def _execute(self) -> T:
pass

@abstractmethod
def __repr__(self) -> str:
pass


def _create_rest_api_for_app() -> RestApi:
github_app_id = current_app.config["GITHUB_APP_ID"]
Expand Down
163 changes: 87 additions & 76 deletions otterdog/webapp/tasks/apply_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,96 +6,107 @@
# SPDX-License-Identifier: EPL-2.0
# *******************************************************************************

import dataclasses
import os
from io import StringIO
from logging import getLogger
from tempfile import TemporaryDirectory

from quart import render_template

from otterdog.config import OrganizationConfig, OtterdogConfig
from otterdog.config import OrganizationConfig
from otterdog.operations.apply import ApplyOperation
from otterdog.utils import IndentingPrinter, LogLevel
from otterdog.webapp.tasks import get_rest_api_for_installation
from otterdog.webapp.tasks import Task, get_otterdog_config
from otterdog.webapp.webhook.github_models import PullRequest, Repository

from .validate_pull_request import escape_for_github, get_config

logger = getLogger(__name__)


async def apply_changes(
org_id: str,
installation_id: int,
pull_request: PullRequest,
repository: Repository,
otterdog_config: OtterdogConfig,
) -> None:
@dataclasses.dataclass(repr=False)
class ApplyChangesTask(Task[None]):
"""Applies changes from a merged PR and adds the result as a comment."""

if pull_request.base.ref != repository.default_branch:
logger.info(
"pull request merged into '%s' which is not the default branch '%s', ignoring",
pull_request.base.ref,
repository.default_branch,
installation_id: int
org_id: str
repository: Repository
pull_request: PullRequest

async def _execute(self) -> None:
if self.pull_request.base.ref != self.repository.default_branch:
self.logger.debug(
"pull request merged into '%s' which is not the default branch '%s', ignoring",
self.pull_request.base.ref,
self.repository.default_branch,
)
return

assert self.pull_request.merged is True
assert self.pull_request.merge_commit_sha is not None

self.logger.info(
"applying merged pull request #%d of repo '%s'", self.pull_request.number, self.repository.full_name
)
return

assert pull_request.merged is True
assert pull_request.merge_commit_sha is not None

logger.info("applying merged pull request #%d for repo '%s'", pull_request.number, repository.full_name)

project_name = otterdog_config.get_project_name(org_id) or org_id
pull_request_number = str(pull_request.number)

rest_api = await get_rest_api_for_installation(installation_id)

with TemporaryDirectory(dir=otterdog_config.jsonnet_base_dir) as work_dir:
org_config = OrganizationConfig.of(
project_name, org_id, {"provider": "inmemory", "api_token": rest_api.token}, work_dir, otterdog_config
)

jsonnet_config = org_config.jsonnet_config

if not os.path.exists(jsonnet_config.org_dir):
os.makedirs(jsonnet_config.org_dir)

jsonnet_config.init_template()

# get config from merge commit sha
head_file = jsonnet_config.org_config_file
await get_config(
rest_api,
org_id,
org_id,
otterdog_config.default_config_repo,
head_file,
pull_request.merge_commit_sha,
)

output = StringIO()
printer = IndentingPrinter(output, log_level=LogLevel.ERROR)

# let's create an apply operation that forces processing but does not update
# any web UI settings and resources using credentials
operation = ApplyOperation(
force_processing=True,
no_web_ui=True,
update_webhooks=False,
update_secrets=False,
update_filter="",
delete_resources=True,
resolve_secrets=False,
include_resources_with_secrets=False,
)
operation.init(otterdog_config, printer)

await operation.execute(org_config)

text = output.getvalue()
logger.info(text)

result = await render_template("applied_changes.txt", result=escape_for_github(text))

await rest_api.issue.create_comment(org_id, otterdog_config.default_config_repo, pull_request_number, result)
otterdog_config = get_otterdog_config()
project_name = otterdog_config.get_project_name(self.org_id) or self.org_id
pull_request_number = str(self.pull_request.number)

rest_api = await self.get_rest_api(self.installation_id)

with TemporaryDirectory(dir=otterdog_config.jsonnet_base_dir) as work_dir:
org_config = OrganizationConfig.of(
project_name,
self.org_id,
{"provider": "inmemory", "api_token": rest_api.token},
work_dir,
otterdog_config,
)

jsonnet_config = org_config.jsonnet_config

if not os.path.exists(jsonnet_config.org_dir):
os.makedirs(jsonnet_config.org_dir)

jsonnet_config.init_template()

# get config from merge commit sha
head_file = jsonnet_config.org_config_file
await get_config(
rest_api,
self.org_id,
self.org_id,
otterdog_config.default_config_repo,
head_file,
self.pull_request.merge_commit_sha,
)

output = StringIO()
printer = IndentingPrinter(output, log_level=LogLevel.ERROR)

# let's create an apply operation that forces processing but does not update
# any web UI settings and resources using credentials
operation = ApplyOperation(
force_processing=True,
no_web_ui=True,
update_webhooks=False,
update_secrets=False,
update_filter="",
delete_resources=True,
resolve_secrets=False,
include_resources_with_secrets=False,
)
operation.init(otterdog_config, printer)

await operation.execute(org_config)

text = output.getvalue()
self.logger.info(text)

result = await render_template("applied_changes_comment.txt", result=escape_for_github(text))

await rest_api.issue.create_comment(
self.org_id, otterdog_config.default_config_repo, pull_request_number, result
)

def __repr__(self) -> str:
return f"ApplyChangesTask(repo={self.repository.full_name}, pull_request={self.pull_request.number})"
30 changes: 25 additions & 5 deletions otterdog/webapp/tasks/help_comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,32 @@
# SPDX-License-Identifier: EPL-2.0
# *******************************************************************************

import dataclasses

from quart import render_template

from otterdog.webapp.tasks import get_rest_api_for_installation
from otterdog.webapp.tasks import Task


@dataclasses.dataclass(repr=False)
class HelpCommentTask(Task[None]):
installation_id: int
org_id: str
repo_name: str
pull_request_number: int

async def _pre_execute(self) -> None:
self.logger.info(
"adding help text to pull request #%d of repo '%s/%s'",
self.pull_request_number,
self.org_id,
self.repo_name,
)

async def _execute(self) -> None:
rest_api = await self.get_rest_api(self.installation_id)
comment = await render_template("help_comment.txt")
await rest_api.issue.create_comment(self.org_id, self.repo_name, str(self.pull_request_number), comment)

async def create_help_comment(org_id: str, installation_id: int, repo_name: str, pull_request_number: int) -> None:
rest_api = await get_rest_api_for_installation(installation_id)
comment = await render_template("help_comment.txt")
await rest_api.issue.create_comment(org_id, repo_name, str(pull_request_number), comment)
def __repr__(self) -> str:
return f"HelpCommentTask(repo={self.org_id}/{self.repo_name}, pull_request={self.pull_request_number})"
Loading

0 comments on commit 70a2ff7

Please sign in to comment.