Skip to content

Commit

Permalink
Merge pull request #217 from MagicRB/combined-build-reports-github
Browse files Browse the repository at this point in the history
Combined build reports GitHub
  • Loading branch information
Mic92 authored Jul 26, 2024
2 parents c9b6d01 + 0ecf33f commit 2620f2c
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 21 deletions.
76 changes: 73 additions & 3 deletions buildbot_nix/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,14 @@ def __init__(
builds_scheduler: str,
skipped_builds_scheduler: str,
jobs: list[dict[str, Any]],
report_status: bool,
**kwargs: Any,
) -> None:
if "name" not in kwargs:
kwargs["name"] = "trigger"
self.project = project
self.jobs = jobs
self.report_status = report_status
self.config = None
self.builds_scheduler = builds_scheduler
self.skipped_builds_scheduler = skipped_builds_scheduler
Expand Down Expand Up @@ -102,6 +104,7 @@ def getSchedulersAndProperties(self) -> list[tuple[str, Properties]]: # noqa: N
props.setProperty("virtual_builder_name", name, source)
props.setProperty("status_name", f"nix-build .#checks.{attr}", source)
props.setProperty("virtual_builder_tags", "", source)
props.setProperty("report_status", self.report_status, source)

drv_path = job.get("drvPath")
system = job.get("system")
Expand Down Expand Up @@ -145,6 +148,15 @@ def getCurrentSummary(self) -> dict[str, str]: # noqa: N802
return {"step": f"({', '.join(summary)})"}


class NixBuildCombined(steps.BuildStep):
"""Shows the error message of a failed evaluation."""

name = "nix-build-combined"

def run(self) -> Generator[Any, object, Any]:
return self.build.results


class NixEvalCommand(buildstep.ShellMixin, steps.BuildStep):
"""Parses the output of `nix-eval-jobs` and triggers a `nix-build` build for
every attribute.
Expand All @@ -153,14 +165,19 @@ class NixEvalCommand(buildstep.ShellMixin, steps.BuildStep):
project: GitProject

def __init__(
self, project: GitProject, supported_systems: list[str], **kwargs: Any
self,
project: GitProject,
supported_systems: list[str],
job_report_limit: int | None,
**kwargs: Any,
) -> None:
kwargs = self.setupShellMixin(kwargs)
super().__init__(**kwargs)
self.project = project
self.observer = logobserver.BufferLogObserver()
self.addLogObserver("stdio", self.observer)
self.supported_systems = supported_systems
self.job_report_limit = job_report_limit

@defer.inlineCallbacks
def run(self) -> Generator[Any, object, Any]:
Expand Down Expand Up @@ -190,6 +207,8 @@ def run(self) -> Generator[Any, object, Any]:
if not system or system in self.supported_systems: # report eval errors
filtered_jobs.append(job)

self.number_of_jobs = len(filtered_jobs)

self.build.addStepsAfterCurrentStep(
[
BuildTrigger(
Expand All @@ -198,8 +217,28 @@ def run(self) -> Generator[Any, object, Any]:
skipped_builds_scheduler=f"{project_id}-nix-skipped-build",
name="build flake",
jobs=filtered_jobs,
report_status=(
self.job_report_limit is None
or self.number_of_jobs <= self.job_report_limit
),
),
],
]
+ (
[
Trigger(
waitForFinish=True,
schedulerNames=[f"{project_id}-nix-build-combined"],
haltOnFailure=True,
flunkOnFailure=True,
sourceStamps=[],
alwaysUseLatest=False,
updateSourceStamp=False,
),
]
if self.job_report_limit is not None
and self.number_of_jobs > self.job_report_limit
else []
),
)

return result
Expand Down Expand Up @@ -360,6 +399,7 @@ def nix_eval_config(
eval_lock: MasterLock,
worker_count: int,
max_memory_size: int,
job_report_limit: int | None,
) -> BuilderConfig:
"""Uses nix-eval-jobs to evaluate hydraJobs from flake.nix in parallel.
For each evaluated attribute a new build pipeline is started.
Expand All @@ -385,6 +425,7 @@ def nix_eval_config(
env={},
name="evaluate flake",
supported_systems=supported_systems,
job_report_limit=job_report_limit,
command=[
"nix-eval-jobs",
"--workers",
Expand Down Expand Up @@ -492,6 +533,7 @@ def nix_build_config(
updateSourceStamp=False,
doStepIf=do_register_gcroot_if,
copy_properties=["out_path", "attr"],
set_properties={"report_status": False},
),
)
factory.addStep(
Expand Down Expand Up @@ -556,6 +598,7 @@ def nix_skipped_build_config(
updateSourceStamp=False,
doStepIf=do_register_gcroot_if,
copy_properties=["out_path", "attr"],
set_properties={"report_status": False},
),
)
return util.BuilderConfig(
Expand Down Expand Up @@ -601,6 +644,24 @@ def nix_register_gcroot_config(
)


def nix_build_combined_config(
project: GitProject,
worker_names: list[str],
) -> BuilderConfig:
factory = util.BuildFactory()
factory.addStep(NixBuildCombined())

return util.BuilderConfig(
name=f"{project.name}/nix-build-combined",
project=project.name,
workernames=worker_names,
collapseRequests=False,
env={},
factory=factory,
properties=dict(status_name="nix-build-combined"),
)


def config_for_project(
config: dict[str, Any],
project: GitProject,
Expand All @@ -610,6 +671,7 @@ def config_for_project(
nix_eval_max_memory_size: int,
eval_lock: MasterLock,
post_build_steps: list[steps.BuildStep],
job_report_limit: int | None,
outputs_path: Path | None = None,
build_retries: int = 1,
) -> None:
Expand Down Expand Up @@ -653,6 +715,11 @@ def config_for_project(
name=f"{project.project_id}-nix-skipped-build",
builderNames=[f"{project.name}/nix-skipped-build"],
),
# this is triggered from `nix-eval` when the build contains too many outputs
schedulers.Triggerable(
name=f"{project.project_id}-nix-build-combined",
builderNames=[f"{project.name}/nix-build-combined"],
),
schedulers.Triggerable(
name=f"{project.project_id}-nix-register-gcroot",
builderNames=[f"{project.name}/nix-register-gcroot"],
Expand Down Expand Up @@ -680,6 +747,7 @@ def config_for_project(
worker_names,
git_url=project.get_project_url(),
supported_systems=nix_supported_systems,
job_report_limit=job_report_limit,
worker_count=nix_eval_worker_count,
max_memory_size=nix_eval_max_memory_size,
eval_lock=eval_lock,
Expand All @@ -693,6 +761,7 @@ def config_for_project(
),
nix_skipped_build_config(project, [SKIPPED_BUILDER_NAME]),
nix_register_gcroot_config(project, worker_names),
nix_build_combined_config(project, worker_names),
],
)

Expand Down Expand Up @@ -842,7 +911,7 @@ def configure(self, config: dict[str, Any]) -> None:
backends["github"] = GithubBackend(self.config.github, self.config.url)

if self.config.gitea is not None:
backends["gitea"] = GiteaBackend(self.config.gitea)
backends["gitea"] = GiteaBackend(self.config.gitea, self.config.url)

auth: AuthBase | None = (
backends[self.config.auth_backend].create_auth()
Expand Down Expand Up @@ -895,6 +964,7 @@ def configure(self, config: dict[str, Any]) -> None:
self.config.eval_max_memory_size,
eval_lock,
[x.to_buildstep() for x in self.config.post_build_steps],
self.config.job_report_limit,
self.config.outputs_path,
self.config.build_retries,
)
Expand Down
8 changes: 8 additions & 0 deletions buildbot_nix/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,11 @@ def model_validate_project_cache(cls: type[_T], project_cache_file: Path) -> lis

def model_dump_project_cache(repos: list[_T]) -> str:
return json.dumps([repo.model_dump() for repo in repos])


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
78 changes: 63 additions & 15 deletions buildbot_nix/gitea_projects.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import signal
from collections.abc import Callable
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
Expand All @@ -13,12 +14,14 @@
from buildbot_gitea.auth import GiteaAuth # type: ignore[import]
from buildbot_gitea.reporter import GiteaStatusPush # type: ignore[import]
from pydantic import BaseModel
from twisted.internet import defer
from twisted.logger import Logger
from twisted.python import log

from .common import (
ThreadDeferredBuildStep,
atomic_write_file,
filter_for_combined_builds,
filter_repos_by_topic,
http_request,
model_dump_project_cache,
Expand Down Expand Up @@ -105,11 +108,43 @@ def belongs_to_org(self) -> bool:
return False # self.data["owner"]["type"] == "Organization"


class ModifyingGiteaStatusPush(GiteaStatusPush):
def checkConfig(
self,
modifyingFilter: Callable[[Any], Any | None] = lambda x: x, # noqa: N803
**kwargs: Any,
) -> Any:
self.modifyingFilter = modifyingFilter

return super().checkConfig(**kwargs)

def reconfigService(
self,
modifyingFilter: Callable[[Any], Any | None] = lambda x: x, # noqa: N803
**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


class GiteaBackend(GitBackend):
config: GiteaConfig
webhook_secret: str
instance_url: str

def __init__(self, config: GiteaConfig) -> None:
def __init__(self, config: GiteaConfig, instance_url: str) -> None:
self.config = config
self.instance_url = instance_url

def create_reload_builder(self, worker_names: list[str]) -> BuilderConfig:
"""Updates the flake an opens a PR for it."""
Expand All @@ -118,7 +153,10 @@ def create_reload_builder(self, worker_names: list[str]) -> BuilderConfig:
ReloadGiteaProjects(self.config, self.config.project_cache_file),
)
factory.addStep(
CreateGiteaProjectHooks(self.config),
CreateGiteaProjectHooks(
self.config,
self.instance_url,
),
)
return util.BuilderConfig(
name=self.reload_builder_name,
Expand All @@ -127,11 +165,12 @@ def create_reload_builder(self, worker_names: list[str]) -> BuilderConfig:
)

def create_reporter(self) -> ReporterBase:
return GiteaStatusPush(
self.config.instance_url,
Interpolate(self.config.token),
return ModifyingGiteaStatusPush(
baseURL=self.config.instance_url,
token=Interpolate(self.config.token),
context=Interpolate("buildbot/%(prop:status_name)s"),
context_pr=Interpolate("buildbot/%(prop:status_name)s"),
modifyingFilter=filter_for_combined_builds,
)

def create_change_hook(self) -> dict[str, Any]:
Expand Down Expand Up @@ -198,14 +237,19 @@ def change_hook_name(self) -> str:


def create_repo_hook(
token: str, webhook_secret: str, owner: str, repo: str, webhook_url: str
token: str,
webhook_secret: str,
owner: str,
repo: str,
gitea_url: str,
instance_url: str,
) -> None:
hooks = paginated_github_request(
f"{webhook_url}/api/v1/repos/{owner}/{repo}/hooks?limit=100",
f"{gitea_url}/api/v1/repos/{owner}/{repo}/hooks?limit=100",
token,
)
config = dict(
url=webhook_url + "change_hook/gitea",
url=instance_url + "change_hook/gitea",
content_type="json",
insecure_ssl="0",
secret=webhook_secret,
Expand All @@ -223,13 +267,13 @@ def create_repo_hook(
"Content-Type": "application/json",
}
for hook in hooks:
if hook["config"]["url"] == webhook_url + "change_hook/gitea":
if hook["config"]["url"] == instance_url + "change_hook/gitea":
log.msg(f"hook for {owner}/{repo} already exists")
return

log.msg(f"creating hook for {owner}/{repo}")
http_request(
f"{webhook_url}/api/v1/repos/{owner}/{repo}/hooks",
f"{gitea_url}/api/v1/repos/{owner}/{repo}/hooks",
method="POST",
headers=headers,
data=data,
Expand All @@ -240,25 +284,29 @@ class CreateGiteaProjectHooks(ThreadDeferredBuildStep):
name = "create_gitea_project_hooks"

config: GiteaConfig
instance_url: str

def __init__(
self,
config: GiteaConfig,
instance_url: str,
**kwargs: Any,
) -> None:
self.config = config
self.instance_url = instance_url
super().__init__(**kwargs)

def run_deferred(self) -> None:
repos = model_validate_project_cache(RepoData, self.config.project_cache_file)

for repo in repos:
create_repo_hook(
self.config.token,
self.config.webhook_secret,
repo.owner.login,
repo.name,
self.config.instance_url,
token=self.config.token,
webhook_secret=self.config.webhook_secret,
owner=repo.owner.login,
repo=repo.name,
gitea_url=self.config.instance_url,
instance_url=self.instance_url,
)

def run_post(self) -> Any:
Expand Down
Loading

0 comments on commit 2620f2c

Please sign in to comment.