2023-09-10 08:11:56 +00:00
|
|
|
import json
|
2024-04-22 13:58:38 +00:00
|
|
|
import os
|
2024-04-27 15:06:17 +00:00
|
|
|
import signal
|
2024-05-18 19:36:22 +00:00
|
|
|
from abc import ABC, abstractmethod
|
2024-04-27 15:06:17 +00:00
|
|
|
from dataclasses import dataclass
|
2024-07-03 15:01:08 +00:00
|
|
|
from itertools import starmap
|
2023-09-10 08:11:56 +00:00
|
|
|
from pathlib import Path
|
2024-07-11 18:56:15 +00:00
|
|
|
from typing import Any, Callable
|
2024-04-22 13:58:38 +00:00
|
|
|
|
|
|
|
from buildbot.config.builder import BuilderConfig
|
2024-04-27 15:06:17 +00:00
|
|
|
from buildbot.plugins import util
|
2024-04-22 13:58:38 +00:00
|
|
|
from buildbot.process.buildstep import BuildStep
|
2024-07-13 09:36:57 +00:00
|
|
|
from buildbot.process.properties import Interpolate, Properties, WithProperties
|
2024-04-22 13:58:38 +00:00
|
|
|
from buildbot.reporters.base import ReporterBase
|
|
|
|
from buildbot.reporters.github import GitHubStatusPush
|
2024-05-18 19:36:22 +00:00
|
|
|
from buildbot.secrets.providers.base import SecretProviderBase
|
2024-04-22 13:58:38 +00:00
|
|
|
from buildbot.www.auth import AuthBase
|
2024-04-27 15:06:17 +00:00
|
|
|
from buildbot.www.avatar import AvatarBase, AvatarGitHub
|
2024-04-22 13:58:38 +00:00
|
|
|
from buildbot.www.oauth2 import GitHubAuth
|
2024-07-19 20:20:42 +00:00
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
2024-05-18 19:36:22 +00:00
|
|
|
from twisted.logger import Logger
|
2024-04-27 15:06:17 +00:00
|
|
|
from twisted.python import log
|
2024-07-11 18:56:15 +00:00
|
|
|
from twisted.internet import defer
|
2024-04-30 15:38:50 +00:00
|
|
|
|
2024-05-03 12:02:17 +00:00
|
|
|
from .common import (
|
2024-07-03 15:01:08 +00:00
|
|
|
ThreadDeferredBuildStep,
|
2024-05-18 19:36:22 +00:00
|
|
|
atomic_write_file,
|
2024-07-03 15:01:08 +00:00
|
|
|
filter_repos_by_topic,
|
2024-05-03 12:02:17 +00:00
|
|
|
http_request,
|
2024-07-19 20:20:42 +00:00
|
|
|
model_dump_project_cache,
|
|
|
|
model_validate_project_cache,
|
2024-05-03 12:02:17 +00:00
|
|
|
paginated_github_request,
|
|
|
|
slugify_project_name,
|
|
|
|
)
|
2024-05-18 19:36:22 +00:00
|
|
|
from .github.installation_token import InstallationToken
|
|
|
|
from .github.jwt_token import JWTToken
|
|
|
|
from .github.legacy_token import (
|
|
|
|
LegacyToken,
|
|
|
|
)
|
|
|
|
from .github.repo_token import (
|
|
|
|
RepoToken,
|
|
|
|
)
|
2024-07-19 20:20:42 +00:00
|
|
|
from .models import (
|
|
|
|
GitHubAppConfig,
|
|
|
|
GitHubConfig,
|
|
|
|
GitHubLegacyConfig,
|
|
|
|
)
|
2024-04-27 15:06:17 +00:00
|
|
|
from .projects import GitBackend, GitProject
|
|
|
|
|
2024-05-18 19:36:22 +00:00
|
|
|
tlog = Logger()
|
|
|
|
|
|
|
|
|
2024-07-19 20:20:42 +00:00
|
|
|
class RepoOwnerData(BaseModel):
|
|
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
|
|
|
|
login: str
|
|
|
|
ttype: str = Field(alias="type")
|
|
|
|
|
|
|
|
|
|
|
|
class RepoData(BaseModel):
|
|
|
|
name: str
|
|
|
|
owner: RepoOwnerData
|
|
|
|
full_name: str
|
|
|
|
html_url: str
|
|
|
|
default_branch: str
|
|
|
|
topics: list[str]
|
|
|
|
installation_id: int | None
|
|
|
|
|
|
|
|
|
2024-05-18 19:36:22 +00:00
|
|
|
def get_installations(jwt_token: JWTToken) -> list[int]:
|
|
|
|
installations = paginated_github_request(
|
|
|
|
"https://api.github.com/app/installations?per_page=100", jwt_token.get()
|
|
|
|
)
|
|
|
|
|
|
|
|
return [installation["id"] for installation in installations]
|
|
|
|
|
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
class CreateGitHubInstallationHooks(ThreadDeferredBuildStep):
|
|
|
|
name = "create_github_installation_hooks"
|
|
|
|
|
|
|
|
jwt_token: JWTToken
|
|
|
|
project_cache_file: Path
|
|
|
|
installation_token_map_name: Path
|
|
|
|
webhook_secret: str
|
|
|
|
webhook_url: str
|
|
|
|
topic: str | None
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
jwt_token: JWTToken,
|
|
|
|
project_cache_file: Path,
|
|
|
|
installation_token_map_name: Path,
|
|
|
|
webhook_secret: str,
|
|
|
|
webhook_url: str,
|
|
|
|
topic: str | None,
|
|
|
|
**kwargs: Any,
|
|
|
|
) -> None:
|
|
|
|
self.jwt_token = jwt_token
|
|
|
|
self.project_cache_file = project_cache_file
|
|
|
|
self.installation_token_map_name = installation_token_map_name
|
|
|
|
self.webhook_secret = webhook_secret
|
|
|
|
self.webhook_url = webhook_url
|
|
|
|
self.topic = topic
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
|
|
|
def run_deferred(self) -> None:
|
2024-07-19 20:20:42 +00:00
|
|
|
repos = model_validate_project_cache(RepoData, self.project_cache_file)
|
2024-07-03 15:01:08 +00:00
|
|
|
installation_token_map: dict[int, InstallationToken] = dict(
|
|
|
|
starmap(
|
|
|
|
lambda k, v: (
|
|
|
|
int(k),
|
|
|
|
InstallationToken.from_json(
|
|
|
|
self.jwt_token, int(k), self.installation_token_map_name, v
|
|
|
|
),
|
|
|
|
),
|
|
|
|
json.loads(self.installation_token_map_name.read_text()).items(),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
for repo in repos:
|
2024-07-19 20:20:42 +00:00
|
|
|
if repo.installation_id is None:
|
|
|
|
continue
|
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
create_project_hook(
|
2024-07-19 20:20:42 +00:00
|
|
|
installation_token_map[repo.installation_id],
|
2024-07-03 15:01:08 +00:00
|
|
|
self.webhook_secret,
|
2024-07-19 20:20:42 +00:00
|
|
|
repo.owner.login,
|
|
|
|
repo.name,
|
2024-07-03 15:01:08 +00:00
|
|
|
self.webhook_url,
|
|
|
|
)
|
|
|
|
|
|
|
|
def run_post(self) -> Any:
|
|
|
|
# reload the buildbot config
|
|
|
|
os.kill(os.getpid(), signal.SIGHUP)
|
|
|
|
return util.SUCCESS
|
|
|
|
|
|
|
|
|
|
|
|
class ReloadGithubInstallations(ThreadDeferredBuildStep):
|
2024-05-18 19:36:22 +00:00
|
|
|
name = "reload_github_projects"
|
|
|
|
|
|
|
|
jwt_token: JWTToken
|
|
|
|
project_cache_file: Path
|
|
|
|
installation_token_map_name: Path
|
|
|
|
project_id_map_name: Path
|
2024-07-03 15:01:08 +00:00
|
|
|
topic: str | None
|
2024-05-18 19:36:22 +00:00
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
jwt_token: JWTToken,
|
|
|
|
project_cache_file: Path,
|
|
|
|
installation_token_map_name: Path,
|
|
|
|
project_id_map_name: Path,
|
2024-07-03 15:01:08 +00:00
|
|
|
topic: str | None,
|
2024-05-18 19:36:22 +00:00
|
|
|
**kwargs: Any,
|
|
|
|
) -> None:
|
|
|
|
self.jwt_token = jwt_token
|
|
|
|
self.installation_token_map_name = installation_token_map_name
|
|
|
|
self.project_id_map_name = project_id_map_name
|
|
|
|
self.project_cache_file = project_cache_file
|
2024-07-03 15:01:08 +00:00
|
|
|
self.topic = topic
|
2024-05-18 19:36:22 +00:00
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
def run_deferred(self) -> None:
|
2024-05-18 19:36:22 +00:00
|
|
|
installation_token_map = GithubBackend.create_missing_installations(
|
|
|
|
self.jwt_token,
|
|
|
|
self.installation_token_map_name,
|
|
|
|
GithubBackend.load_installations(
|
|
|
|
self.jwt_token,
|
|
|
|
self.installation_token_map_name,
|
|
|
|
),
|
|
|
|
get_installations(self.jwt_token),
|
|
|
|
)
|
|
|
|
|
2024-07-19 20:20:42 +00:00
|
|
|
repos: list[RepoData] = []
|
2024-05-18 19:36:22 +00:00
|
|
|
project_id_map: dict[str, int] = {}
|
|
|
|
|
|
|
|
repos = []
|
|
|
|
|
|
|
|
for k, v in installation_token_map.items():
|
2024-07-03 15:01:08 +00:00
|
|
|
new_repos = filter_repos_by_topic(
|
|
|
|
self.topic,
|
|
|
|
refresh_projects(
|
|
|
|
v.get(),
|
|
|
|
self.project_cache_file,
|
|
|
|
clear=True,
|
|
|
|
api_endpoint="/installation/repositories",
|
|
|
|
subkey="repositories",
|
|
|
|
require_admin=False,
|
|
|
|
),
|
2024-07-19 20:20:42 +00:00
|
|
|
lambda repo: repo.topics,
|
2024-05-18 19:36:22 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
for repo in new_repos:
|
2024-07-19 20:20:42 +00:00
|
|
|
repo.installation_id = k
|
2024-05-18 19:36:22 +00:00
|
|
|
|
2024-07-19 20:20:42 +00:00
|
|
|
project_id_map[repo.full_name] = k
|
2024-05-18 19:36:22 +00:00
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
repos.extend(new_repos)
|
|
|
|
|
2024-07-19 20:20:42 +00:00
|
|
|
atomic_write_file(self.project_cache_file, model_dump_project_cache(repos))
|
2024-05-18 19:36:22 +00:00
|
|
|
atomic_write_file(self.project_id_map_name, json.dumps(project_id_map))
|
|
|
|
|
|
|
|
tlog.info(
|
2024-07-03 15:01:08 +00:00
|
|
|
f"Fetched {len(repos)} repositories from {len(installation_token_map.items())} installation tokens."
|
2024-05-18 19:36:22 +00:00
|
|
|
)
|
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
def run_post(self) -> Any:
|
|
|
|
return util.SUCCESS
|
2024-05-18 19:36:22 +00:00
|
|
|
|
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
class CreateGitHubProjectHooks(ThreadDeferredBuildStep):
|
|
|
|
name = "create_github_project_hooks"
|
2024-04-22 13:58:38 +00:00
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
token: RepoToken
|
|
|
|
project_cache_file: Path
|
|
|
|
webhook_secret: str
|
|
|
|
webhook_url: str
|
|
|
|
topic: str | None
|
2024-04-22 13:58:38 +00:00
|
|
|
|
2024-05-18 19:36:22 +00:00
|
|
|
def __init__(
|
2024-07-03 15:01:08 +00:00
|
|
|
self,
|
|
|
|
token: RepoToken,
|
|
|
|
project_cache_file: Path,
|
|
|
|
webhook_secret: str,
|
|
|
|
webhook_url: str,
|
|
|
|
topic: str | None,
|
|
|
|
**kwargs: Any,
|
2024-05-18 19:36:22 +00:00
|
|
|
) -> None:
|
2024-04-22 13:58:38 +00:00
|
|
|
self.token = token
|
|
|
|
self.project_cache_file = project_cache_file
|
2024-07-03 15:01:08 +00:00
|
|
|
self.webhook_secret = webhook_secret
|
|
|
|
self.webhook_url = webhook_url
|
|
|
|
self.topic = topic
|
2024-04-22 13:58:38 +00:00
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
def run_deferred(self) -> None:
|
2024-07-19 20:20:42 +00:00
|
|
|
repos = model_validate_project_cache(RepoData, self.project_cache_file)
|
2024-05-18 19:36:22 +00:00
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
for repo in repos:
|
|
|
|
create_project_hook(
|
|
|
|
self.token,
|
|
|
|
self.webhook_secret,
|
2024-07-19 20:20:42 +00:00
|
|
|
repo.owner.login,
|
|
|
|
repo.name,
|
2024-07-03 15:01:08 +00:00
|
|
|
self.webhook_url,
|
|
|
|
)
|
2024-05-18 19:36:22 +00:00
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
def run_post(self) -> Any:
|
|
|
|
# reload the buildbot config
|
|
|
|
os.kill(os.getpid(), signal.SIGHUP)
|
|
|
|
return util.SUCCESS
|
2024-04-22 13:58:38 +00:00
|
|
|
|
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
class ReloadGithubProjects(ThreadDeferredBuildStep):
|
|
|
|
name = "reload_github_projects"
|
2024-04-22 13:58:38 +00:00
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
token: RepoToken
|
|
|
|
project_cache_file: Path
|
|
|
|
topic: str | None
|
2024-04-22 13:58:38 +00:00
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
token: RepoToken,
|
|
|
|
project_cache_file: Path,
|
|
|
|
topic: str | None,
|
|
|
|
**kwargs: Any,
|
|
|
|
) -> None:
|
|
|
|
self.token = token
|
|
|
|
self.project_cache_file = project_cache_file
|
|
|
|
self.topic = topic
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
|
|
|
def run_deferred(self) -> None:
|
2024-07-19 20:20:42 +00:00
|
|
|
repos: list[RepoData] = filter_repos_by_topic(
|
2024-07-03 15:01:08 +00:00
|
|
|
self.topic,
|
|
|
|
refresh_projects(self.token.get(), self.project_cache_file),
|
2024-07-19 20:20:42 +00:00
|
|
|
lambda repo: repo.topics,
|
2024-07-03 15:01:08 +00:00
|
|
|
)
|
|
|
|
|
2024-07-19 20:20:42 +00:00
|
|
|
atomic_write_file(self.project_cache_file, model_dump_project_cache(repos))
|
2024-07-03 15:01:08 +00:00
|
|
|
|
|
|
|
def run_post(self) -> Any:
|
|
|
|
return util.SUCCESS
|
2024-04-22 13:58:38 +00:00
|
|
|
|
2024-04-27 15:06:17 +00:00
|
|
|
|
2024-05-18 19:36:22 +00:00
|
|
|
class GithubAuthBackend(ABC):
|
|
|
|
@abstractmethod
|
|
|
|
def get_general_token(self) -> RepoToken:
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
2024-07-19 20:20:42 +00:00
|
|
|
def get_repo_token(self, repo: RepoData) -> RepoToken:
|
2024-05-18 19:36:22 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def create_secret_providers(self) -> list[SecretProviderBase]:
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def create_reporter(self) -> ReporterBase:
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
2024-07-03 15:01:08 +00:00
|
|
|
def create_reload_builder_steps(
|
|
|
|
self,
|
|
|
|
project_cache_file: Path,
|
|
|
|
webhook_secret: str,
|
|
|
|
webhook_url: str,
|
|
|
|
topic: str | None,
|
|
|
|
) -> list[BuildStep]:
|
2024-05-18 19:36:22 +00:00
|
|
|
pass
|
|
|
|
|
2024-07-11 18:56:15 +00:00
|
|
|
class ModifyingGitHubStatusPush(GitHubStatusPush):
|
|
|
|
def checkConfig(self, modifyingFilter: Callable[[Any], Any | None] = lambda x: x, **kwargs: Any) -> Any:
|
|
|
|
self.modifyingFilter = modifyingFilter
|
|
|
|
|
|
|
|
return super().checkConfig(**kwargs)
|
|
|
|
|
|
|
|
def reconfigService(self, modifyingFilter: Callable[[Any], Any | None] = lambda x: x, **kwargs: Any) -> Any:
|
|
|
|
self.modifyingFilter = modifyingFilter
|
|
|
|
|
|
|
|
return super().reconfigService(**kwargs)
|
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
|
|
|
def sendMessage(self, reports: Any) -> Any:
|
|
|
|
reports = self.modifyingFilter(reports)
|
|
|
|
if reports is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
result = yield super().sendMessage(reports)
|
|
|
|
return result
|
|
|
|
|
|
|
|
def filter_for_combined_builds(reports: Any) -> Any | None:
|
|
|
|
properties = reports[0]["builds"][0]["properties"]
|
|
|
|
|
|
|
|
if "report_status" in properties and not properties["report_status"][0]:
|
|
|
|
return None
|
|
|
|
return reports
|
2024-05-18 19:36:22 +00:00
|
|
|
|
|
|
|
class GithubLegacyAuthBackend(GithubAuthBackend):
|
2024-07-19 20:20:42 +00:00
|
|
|
auth_type: GitHubLegacyConfig
|
2024-05-18 19:36:22 +00:00
|
|
|
|
|
|
|
token: LegacyToken
|
|
|
|
|
2024-07-19 20:20:42 +00:00
|
|
|
def __init__(self, auth_type: GitHubLegacyConfig) -> None:
|
2024-05-18 19:36:22 +00:00
|
|
|
self.auth_type = auth_type
|
2024-07-19 20:20:42 +00:00
|
|
|
self.token = LegacyToken(auth_type.token)
|
2024-05-18 19:36:22 +00:00
|
|
|
|
|
|
|
def get_general_token(self) -> RepoToken:
|
|
|
|
return self.token
|
|
|
|
|
2024-07-19 20:20:42 +00:00
|
|
|
def get_repo_token(self, repo: RepoData) -> RepoToken:
|
2024-05-18 19:36:22 +00:00
|
|
|
return self.token
|
|
|
|
|
|
|
|
def create_secret_providers(self) -> list[SecretProviderBase]:
|
2024-06-07 14:55:36 +00:00
|
|
|
return [GitHubLegacySecretService(self.token)]
|
2024-05-18 19:36:22 +00:00
|
|
|
|
|
|
|
def create_reporter(self) -> ReporterBase:
|
2024-07-11 18:56:15 +00:00
|
|
|
return ModifyingGitHubStatusPush(
|
2024-05-18 19:36:22 +00:00
|
|
|
token=self.token.get(),
|
|
|
|
# Since we dynamically create build steps,
|
|
|
|
# we use `virtual_builder_name` in the webinterface
|
|
|
|
# so that we distinguish what has beeing build
|
|
|
|
context=Interpolate("buildbot/%(prop:status_name)s"),
|
2024-07-11 18:56:15 +00:00
|
|
|
modifyingFilter=filter_for_combined_builds,
|
2024-05-18 19:36:22 +00:00
|
|
|
)
|
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
def create_reload_builder_steps(
|
|
|
|
self,
|
|
|
|
project_cache_file: Path,
|
|
|
|
webhook_secret: str,
|
|
|
|
webhook_url: str,
|
|
|
|
topic: str | None,
|
|
|
|
) -> list[BuildStep]:
|
|
|
|
return [
|
|
|
|
ReloadGithubProjects(
|
|
|
|
token=self.token,
|
|
|
|
project_cache_file=project_cache_file,
|
|
|
|
topic=topic,
|
|
|
|
),
|
|
|
|
CreateGitHubProjectHooks(
|
|
|
|
token=self.token,
|
|
|
|
project_cache_file=project_cache_file,
|
|
|
|
webhook_secret=webhook_secret,
|
|
|
|
webhook_url=webhook_url,
|
|
|
|
topic=topic,
|
|
|
|
),
|
|
|
|
]
|
2024-05-18 19:36:22 +00:00
|
|
|
|
|
|
|
|
2024-06-07 14:55:36 +00:00
|
|
|
class GitHubLegacySecretService(SecretProviderBase):
|
2024-05-18 19:36:22 +00:00
|
|
|
name = "GitHubLegacySecretService"
|
|
|
|
token: LegacyToken
|
|
|
|
|
|
|
|
def reconfigService(self, token: LegacyToken) -> None:
|
|
|
|
self.token = token
|
|
|
|
|
|
|
|
def get(self, entry: str) -> str | None:
|
|
|
|
"""
|
|
|
|
get the value from the file identified by 'entry'
|
|
|
|
"""
|
|
|
|
if entry.startswith("github-token"):
|
|
|
|
return self.token.get()
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
class GithubAppAuthBackend(GithubAuthBackend):
|
2024-07-19 20:20:42 +00:00
|
|
|
auth_type: GitHubAppConfig
|
2024-05-18 19:36:22 +00:00
|
|
|
|
|
|
|
jwt_token: JWTToken
|
|
|
|
installation_tokens: dict[int, InstallationToken]
|
|
|
|
project_id_map: dict[str, int]
|
|
|
|
|
2024-07-19 20:20:42 +00:00
|
|
|
def __init__(self, auth_type: GitHubAppConfig) -> None:
|
2024-05-18 19:36:22 +00:00
|
|
|
self.auth_type = auth_type
|
2024-07-19 20:20:42 +00:00
|
|
|
self.jwt_token = JWTToken(self.auth_type.id, self.auth_type.secret_key_file)
|
2024-05-18 19:36:22 +00:00
|
|
|
self.installation_tokens = GithubBackend.load_installations(
|
|
|
|
self.jwt_token,
|
2024-07-19 20:20:42 +00:00
|
|
|
self.auth_type.installation_token_map_file,
|
2024-05-18 19:36:22 +00:00
|
|
|
)
|
2024-07-19 20:20:42 +00:00
|
|
|
if self.auth_type.project_id_map_file.exists():
|
2024-05-18 19:36:22 +00:00
|
|
|
self.project_id_map = json.loads(
|
2024-07-19 20:20:42 +00:00
|
|
|
self.auth_type.project_id_map_file.read_text()
|
2024-05-18 19:36:22 +00:00
|
|
|
)
|
|
|
|
else:
|
2024-06-12 15:36:43 +00:00
|
|
|
tlog.info(
|
|
|
|
"~project-id-map~ is not present, GitHub project reload will follow."
|
|
|
|
)
|
2024-05-18 19:36:22 +00:00
|
|
|
self.project_id_map = {}
|
|
|
|
|
|
|
|
def get_general_token(self) -> RepoToken:
|
|
|
|
return self.jwt_token
|
|
|
|
|
2024-07-19 20:20:42 +00:00
|
|
|
def get_repo_token(self, repo: RepoData) -> RepoToken:
|
|
|
|
assert repo.installation_id is not None, f"Missing installation_id in {repo}"
|
|
|
|
return self.installation_tokens[repo.installation_id]
|
2024-05-18 19:36:22 +00:00
|
|
|
|
|
|
|
def create_secret_providers(self) -> list[SecretProviderBase]:
|
|
|
|
return [GitHubAppSecretService(self.installation_tokens, self.jwt_token)]
|
|
|
|
|
|
|
|
def create_reporter(self) -> ReporterBase:
|
2024-07-13 09:36:57 +00:00
|
|
|
def get_github_token(props: Properties) -> str:
|
|
|
|
return self.installation_tokens[
|
|
|
|
self.project_id_map[props["projectname"]]
|
|
|
|
].get()
|
|
|
|
|
2024-07-11 18:56:15 +00:00
|
|
|
return ModifyingGitHubStatusPush(
|
2024-07-13 09:36:57 +00:00
|
|
|
token=WithProperties("%(github_token)s", github_token=get_github_token),
|
2024-05-18 19:36:22 +00:00
|
|
|
# Since we dynamically create build steps,
|
|
|
|
# we use `virtual_builder_name` in the webinterface
|
|
|
|
# so that we distinguish what has beeing build
|
|
|
|
context=Interpolate("buildbot/%(prop:status_name)s"),
|
2024-07-11 18:56:15 +00:00
|
|
|
modifyingFilter=filter_for_combined_builds,
|
2024-05-18 19:36:22 +00:00
|
|
|
)
|
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
def create_reload_builder_steps(
|
|
|
|
self,
|
|
|
|
project_cache_file: Path,
|
|
|
|
webhook_secret: str,
|
|
|
|
webhook_url: str,
|
|
|
|
topic: str | None,
|
|
|
|
) -> list[BuildStep]:
|
|
|
|
return [
|
|
|
|
ReloadGithubInstallations(
|
|
|
|
self.jwt_token,
|
|
|
|
project_cache_file,
|
2024-07-19 20:20:42 +00:00
|
|
|
self.auth_type.installation_token_map_file,
|
|
|
|
self.auth_type.project_id_map_file,
|
2024-07-03 15:01:08 +00:00
|
|
|
topic,
|
|
|
|
),
|
|
|
|
CreateGitHubInstallationHooks(
|
|
|
|
self.jwt_token,
|
|
|
|
project_cache_file,
|
2024-07-19 20:20:42 +00:00
|
|
|
self.auth_type.installation_token_map_file,
|
2024-07-03 15:01:08 +00:00
|
|
|
webhook_secret=webhook_secret,
|
|
|
|
webhook_url=webhook_url,
|
|
|
|
topic=topic,
|
|
|
|
),
|
|
|
|
]
|
2024-05-18 19:36:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
class GitHubAppSecretService(SecretProviderBase):
|
|
|
|
name = "GitHubAppSecretService"
|
|
|
|
installation_tokens: dict[int, InstallationToken]
|
|
|
|
jwt_token: JWTToken
|
|
|
|
|
|
|
|
def reconfigService(
|
|
|
|
self, installation_tokens: dict[int, InstallationToken], jwt_token: JWTToken
|
|
|
|
) -> None:
|
|
|
|
self.installation_tokens = installation_tokens
|
|
|
|
self.jwt_token = jwt_token
|
|
|
|
|
|
|
|
def get(self, entry: str) -> str | None:
|
|
|
|
"""
|
|
|
|
get the value from the file identified by 'entry'
|
|
|
|
"""
|
|
|
|
if entry.startswith("github-token-"):
|
|
|
|
return self.installation_tokens[
|
|
|
|
int(entry.removeprefix("github-token-"))
|
|
|
|
].get()
|
|
|
|
if entry == "github-jwt-token":
|
|
|
|
return self.jwt_token.get()
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2024-04-22 13:58:38 +00:00
|
|
|
@dataclass
|
|
|
|
class GithubBackend(GitBackend):
|
2024-07-19 20:20:42 +00:00
|
|
|
config: GitHubConfig
|
2024-04-27 15:06:17 +00:00
|
|
|
webhook_secret: str
|
2024-07-03 15:01:08 +00:00
|
|
|
webhook_url: str
|
2024-04-22 13:58:38 +00:00
|
|
|
|
2024-05-18 19:36:22 +00:00
|
|
|
auth_backend: GithubAuthBackend
|
|
|
|
|
2024-07-19 20:20:42 +00:00
|
|
|
def __init__(self, config: GitHubConfig, webhook_url: str) -> None:
|
2024-04-22 13:58:38 +00:00
|
|
|
self.config = config
|
2024-07-19 20:20:42 +00:00
|
|
|
self.webhook_secret = self.config.webhook_secret
|
2024-07-03 15:01:08 +00:00
|
|
|
self.webhook_url = webhook_url
|
2024-04-22 13:58:38 +00:00
|
|
|
|
2024-07-19 20:20:42 +00:00
|
|
|
if isinstance(self.config.auth_type, GitHubLegacyConfig):
|
2024-05-18 19:36:22 +00:00
|
|
|
self.auth_backend = GithubLegacyAuthBackend(self.config.auth_type)
|
2024-07-19 20:20:42 +00:00
|
|
|
elif isinstance(self.config.auth_type, GitHubAppConfig):
|
2024-05-18 19:36:22 +00:00
|
|
|
self.auth_backend = GithubAppAuthBackend(self.config.auth_type)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def load_installations(
|
|
|
|
jwt_token: JWTToken, installations_token_map_name: Path
|
|
|
|
) -> dict[int, InstallationToken]:
|
|
|
|
initial_installations_map: dict[str, Any]
|
|
|
|
if installations_token_map_name.exists():
|
|
|
|
initial_installations_map = json.loads(
|
|
|
|
installations_token_map_name.read_text()
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
initial_installations_map = {}
|
|
|
|
|
|
|
|
installations_map: dict[int, InstallationToken] = {}
|
|
|
|
|
|
|
|
for iid, installation in initial_installations_map.items():
|
2024-07-03 15:01:08 +00:00
|
|
|
installations_map[int(iid)] = InstallationToken.from_json(
|
|
|
|
jwt_token, int(iid), installations_token_map_name, installation
|
2024-05-18 19:36:22 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return installations_map
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def create_missing_installations(
|
|
|
|
jwt_token: JWTToken,
|
|
|
|
installations_token_map_name: Path,
|
|
|
|
installations_map: dict[int, InstallationToken],
|
|
|
|
installations: list[int],
|
|
|
|
) -> dict[int, InstallationToken]:
|
|
|
|
for installation in set(installations) - installations_map.keys():
|
2024-07-03 15:01:08 +00:00
|
|
|
installations_map[installation] = InstallationToken.new(
|
2024-05-18 19:36:22 +00:00
|
|
|
jwt_token,
|
|
|
|
installation,
|
|
|
|
installations_token_map_name,
|
|
|
|
)
|
|
|
|
|
|
|
|
return installations_map
|
|
|
|
|
2024-04-27 15:06:17 +00:00
|
|
|
def create_reload_builder(self, worker_names: list[str]) -> BuilderConfig:
|
2024-04-22 13:58:38 +00:00
|
|
|
"""Updates the flake an opens a PR for it."""
|
|
|
|
factory = util.BuildFactory()
|
2024-07-03 15:01:08 +00:00
|
|
|
steps = self.auth_backend.create_reload_builder_steps(
|
|
|
|
self.config.project_cache_file,
|
|
|
|
self.webhook_secret,
|
|
|
|
self.webhook_url,
|
|
|
|
self.config.topic,
|
2024-04-22 13:58:38 +00:00
|
|
|
)
|
2024-07-03 15:01:08 +00:00
|
|
|
for step in steps:
|
|
|
|
factory.addStep(step)
|
|
|
|
|
2024-04-22 13:58:38 +00:00
|
|
|
return util.BuilderConfig(
|
|
|
|
name=self.reload_builder_name,
|
|
|
|
workernames=worker_names,
|
|
|
|
factory=factory,
|
|
|
|
)
|
|
|
|
|
|
|
|
def create_reporter(self) -> ReporterBase:
|
2024-05-18 19:36:22 +00:00
|
|
|
return self.auth_backend.create_reporter()
|
2024-04-27 15:06:17 +00:00
|
|
|
|
|
|
|
def create_change_hook(self) -> dict[str, Any]:
|
2024-04-22 13:58:38 +00:00
|
|
|
return {
|
2024-04-27 15:06:17 +00:00
|
|
|
"secret": self.webhook_secret,
|
2024-04-22 13:58:38 +00:00
|
|
|
"strict": True,
|
2024-05-18 19:36:22 +00:00
|
|
|
"token": self.auth_backend.get_general_token().get(),
|
2024-05-02 04:02:00 +00:00
|
|
|
"github_property_whitelist": ["github.base.sha", "github.head.sha"],
|
2024-04-22 13:58:38 +00:00
|
|
|
}
|
|
|
|
|
2024-04-27 15:06:17 +00:00
|
|
|
def create_avatar_method(self) -> AvatarBase | None:
|
2024-07-11 18:52:01 +00:00
|
|
|
return AvatarGitHub()
|
2024-04-22 13:58:38 +00:00
|
|
|
|
|
|
|
def create_auth(self) -> AuthBase:
|
2024-04-30 13:45:39 +00:00
|
|
|
assert self.config.oauth_id is not None, "GitHub OAuth ID is required"
|
2024-04-27 15:06:17 +00:00
|
|
|
return GitHubAuth(
|
|
|
|
self.config.oauth_id,
|
2024-07-19 20:20:42 +00:00
|
|
|
self.config.oauth_secret,
|
2024-04-27 15:06:17 +00:00
|
|
|
apiVersion=4,
|
|
|
|
)
|
2024-04-22 13:58:38 +00:00
|
|
|
|
2024-05-18 19:36:22 +00:00
|
|
|
def create_secret_providers(self) -> list[SecretProviderBase]:
|
|
|
|
return self.auth_backend.create_secret_providers()
|
|
|
|
|
2024-04-22 13:58:38 +00:00
|
|
|
def load_projects(self) -> list["GitProject"]:
|
|
|
|
if not self.config.project_cache_file.exists():
|
|
|
|
return []
|
|
|
|
|
2024-07-19 20:20:42 +00:00
|
|
|
repos: list[RepoData] = filter_repos_by_topic(
|
2024-07-03 15:01:08 +00:00
|
|
|
self.config.topic,
|
|
|
|
sorted(
|
2024-07-19 20:20:42 +00:00
|
|
|
model_validate_project_cache(RepoData, self.config.project_cache_file),
|
|
|
|
key=lambda repo: repo.full_name,
|
2024-07-03 15:01:08 +00:00
|
|
|
),
|
2024-07-19 20:20:42 +00:00
|
|
|
lambda repo: repo.topics,
|
2024-04-27 15:06:17 +00:00
|
|
|
)
|
2024-06-12 15:19:11 +00:00
|
|
|
|
|
|
|
if isinstance(self.auth_backend, GithubAppAuthBackend):
|
|
|
|
dropped_repos = list(
|
2024-07-19 20:20:42 +00:00
|
|
|
filter(lambda repo: repo.installation_id is None, repos)
|
2024-06-12 15:19:11 +00:00
|
|
|
)
|
|
|
|
if dropped_repos:
|
|
|
|
tlog.info(
|
|
|
|
"Dropped projects follow, refresh will follow after initialisation:"
|
|
|
|
)
|
|
|
|
for dropped_repo in dropped_repos:
|
2024-07-19 20:20:42 +00:00
|
|
|
tlog.info(f"\tDropping repo {dropped_repo.full_name}")
|
|
|
|
repos = list(filter(lambda repo: repo.installation_id is not None, repos))
|
2024-06-12 15:19:11 +00:00
|
|
|
|
2024-07-19 20:20:42 +00:00
|
|
|
repo_names: list[str] = [repo.owner.login + "/" + repo.name for repo in repos]
|
2024-07-03 15:01:08 +00:00
|
|
|
|
|
|
|
tlog.info(
|
|
|
|
f"Loading {len(repos)} cached repositories: [{', '.join(repo_names)}]"
|
2024-04-22 13:58:38 +00:00
|
|
|
)
|
2024-07-03 15:01:08 +00:00
|
|
|
return [
|
|
|
|
GithubProject(
|
|
|
|
self.auth_backend.get_repo_token(repo),
|
|
|
|
self.config,
|
|
|
|
self.webhook_secret,
|
2024-07-19 20:20:42 +00:00
|
|
|
RepoData.model_validate(repo),
|
2024-07-03 15:01:08 +00:00
|
|
|
)
|
|
|
|
for repo in repos
|
|
|
|
]
|
2024-04-22 13:58:38 +00:00
|
|
|
|
|
|
|
def are_projects_cached(self) -> bool:
|
2024-06-12 15:19:11 +00:00
|
|
|
if not self.config.project_cache_file.exists():
|
|
|
|
return False
|
|
|
|
|
2024-06-12 15:36:43 +00:00
|
|
|
if (
|
2024-07-19 20:20:42 +00:00
|
|
|
isinstance(self.config.auth_type, GitHubAppConfig)
|
|
|
|
and not self.config.auth_type.project_id_map_file.exists()
|
2024-06-12 15:36:43 +00:00
|
|
|
):
|
|
|
|
return False
|
|
|
|
|
2024-06-12 15:19:11 +00:00
|
|
|
all_have_installation_id = True
|
2024-07-19 20:20:42 +00:00
|
|
|
for project in model_validate_project_cache(
|
|
|
|
RepoData, self.config.project_cache_file
|
|
|
|
):
|
|
|
|
if project.installation_id is not None:
|
2024-06-12 15:19:11 +00:00
|
|
|
all_have_installation_id = False
|
|
|
|
break
|
|
|
|
|
|
|
|
return all_have_installation_id
|
2024-04-22 13:58:38 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def type(self) -> str:
|
|
|
|
return "github"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def pretty_type(self) -> str:
|
|
|
|
return "GitHub"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def reload_builder_name(self) -> str:
|
|
|
|
return "reload-github-projects"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def change_hook_name(self) -> str:
|
|
|
|
return "github"
|
|
|
|
|
2024-04-27 15:06:17 +00:00
|
|
|
|
2024-07-03 15:01:08 +00:00
|
|
|
def create_project_hook(
|
|
|
|
token: RepoToken, webhook_secret: str, owner: str, repo: str, webhook_url: str
|
|
|
|
) -> None:
|
|
|
|
hooks = paginated_github_request(
|
|
|
|
f"https://api.github.com/repos/{owner}/{repo}/hooks?per_page=100",
|
|
|
|
token.get(),
|
|
|
|
)
|
|
|
|
config = dict(
|
|
|
|
url=webhook_url + "change_hook/github",
|
|
|
|
content_type="json",
|
|
|
|
insecure_ssl="0",
|
|
|
|
secret=webhook_secret,
|
|
|
|
)
|
|
|
|
data = dict(name="web", active=True, events=["push", "pull_request"], config=config)
|
|
|
|
headers = {
|
|
|
|
"Authorization": f"Bearer {token.get()}",
|
|
|
|
"Accept": "application/vnd.github+json",
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
"X-GitHub-Api-Version": "2022-11-28",
|
|
|
|
}
|
|
|
|
for hook in hooks:
|
|
|
|
if hook["config"]["url"] == webhook_url + "change_hook/github":
|
|
|
|
log.msg(f"hook for {owner}/{repo} already exists")
|
|
|
|
return
|
|
|
|
|
|
|
|
http_request(
|
|
|
|
f"https://api.github.com/repos/{owner}/{repo}/hooks",
|
|
|
|
method="POST",
|
|
|
|
headers=headers,
|
|
|
|
data=data,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-04-22 13:58:38 +00:00
|
|
|
class GithubProject(GitProject):
|
2024-07-19 20:20:42 +00:00
|
|
|
config: GitHubConfig
|
2024-05-18 19:36:22 +00:00
|
|
|
webhook_secret: str
|
2024-07-19 20:20:42 +00:00
|
|
|
data: RepoData
|
2024-05-18 19:36:22 +00:00
|
|
|
token: RepoToken
|
2024-04-22 13:58:38 +00:00
|
|
|
|
2024-04-27 15:06:17 +00:00
|
|
|
def __init__(
|
2024-05-18 19:36:22 +00:00
|
|
|
self,
|
|
|
|
token: RepoToken,
|
2024-07-19 20:20:42 +00:00
|
|
|
config: GitHubConfig,
|
2024-05-18 19:36:22 +00:00
|
|
|
webhook_secret: str,
|
2024-07-19 20:20:42 +00:00
|
|
|
data: RepoData,
|
2024-04-27 15:06:17 +00:00
|
|
|
) -> None:
|
2024-05-18 19:36:22 +00:00
|
|
|
self.token = token
|
2024-04-22 13:58:38 +00:00
|
|
|
self.config = config
|
2024-04-27 15:06:17 +00:00
|
|
|
self.webhook_secret = webhook_secret
|
2024-04-22 13:58:38 +00:00
|
|
|
self.data = data
|
|
|
|
|
|
|
|
def get_project_url(self) -> str:
|
2024-05-18 19:36:22 +00:00
|
|
|
return f"https://git:{self.token.get_as_secret()}s@github.com/{self.name}"
|
2024-04-22 13:58:38 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def pretty_type(self) -> str:
|
|
|
|
return "GitHub"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def type(self) -> str:
|
|
|
|
return "github"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def repo(self) -> str:
|
2024-07-19 20:20:42 +00:00
|
|
|
return self.data.name
|
2024-04-22 13:58:38 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def owner(self) -> str:
|
2024-07-19 20:20:42 +00:00
|
|
|
return self.data.owner.login
|
2024-04-22 13:58:38 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
2024-07-19 20:20:42 +00:00
|
|
|
return self.data.full_name
|
2024-04-22 13:58:38 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def url(self) -> str:
|
2024-07-19 20:20:42 +00:00
|
|
|
return self.data.html_url
|
2024-04-22 13:58:38 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def project_id(self) -> str:
|
2024-07-19 20:20:42 +00:00
|
|
|
return slugify_project_name(self.data.full_name)
|
2024-04-22 13:58:38 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def default_branch(self) -> str:
|
2024-07-19 20:20:42 +00:00
|
|
|
return self.data.default_branch
|
2024-04-22 13:58:38 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def topics(self) -> list[str]:
|
2024-07-19 20:20:42 +00:00
|
|
|
return self.data.topics
|
2023-09-10 08:11:56 +00:00
|
|
|
|
2024-04-22 13:58:38 +00:00
|
|
|
@property
|
|
|
|
def belongs_to_org(self) -> bool:
|
2024-07-19 20:20:42 +00:00
|
|
|
return self.data.owner.ttype == "Organization"
|
2023-09-10 08:11:56 +00:00
|
|
|
|
2023-10-26 12:54:01 +00:00
|
|
|
|
2024-05-18 19:36:22 +00:00
|
|
|
def refresh_projects(
|
|
|
|
github_token: str,
|
|
|
|
repo_cache_file: Path,
|
|
|
|
repos: list[Any] | None = None,
|
|
|
|
clear: bool = True,
|
|
|
|
api_endpoint: str = "/user/repos",
|
|
|
|
subkey: None | str = None,
|
|
|
|
require_admin: bool = True,
|
2024-07-19 20:20:42 +00:00
|
|
|
) -> list[RepoData]:
|
2024-05-18 19:36:22 +00:00
|
|
|
if repos is None:
|
|
|
|
repos = []
|
2023-10-26 09:08:07 +00:00
|
|
|
|
|
|
|
for repo in paginated_github_request(
|
2024-05-18 19:36:22 +00:00
|
|
|
f"https://api.github.com{api_endpoint}?per_page=100",
|
2023-10-27 08:49:40 +00:00
|
|
|
github_token,
|
2024-05-18 19:36:22 +00:00
|
|
|
subkey=subkey,
|
2023-10-26 09:08:07 +00:00
|
|
|
):
|
2024-05-18 19:36:22 +00:00
|
|
|
# TODO actually check for this properly
|
|
|
|
if not repo["permissions"]["admin"] and require_admin:
|
2023-10-27 08:49:40 +00:00
|
|
|
name = repo["full_name"]
|
|
|
|
log.msg(
|
2023-12-26 20:56:36 +00:00
|
|
|
f"skipping {name} because we do not have admin privileges, needed for hook management",
|
2023-10-27 08:49:40 +00:00
|
|
|
)
|
2023-10-26 09:08:07 +00:00
|
|
|
else:
|
2024-07-19 20:20:42 +00:00
|
|
|
repo["installation_id"] = None
|
|
|
|
repos.append(RepoData.model_validate(repo))
|
2023-10-26 09:08:07 +00:00
|
|
|
|
2024-05-18 19:36:22 +00:00
|
|
|
return repos
|