Skip to content

Commit 2620f2c

Browse files
authored
Merge pull request #217 from MagicRB/combined-build-reports-github
Combined build reports GitHub
2 parents c9b6d01 + 0ecf33f commit 2620f2c

File tree

6 files changed

+196
-21
lines changed

6 files changed

+196
-21
lines changed

buildbot_nix/__init__.py

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,14 @@ def __init__(
5959
builds_scheduler: str,
6060
skipped_builds_scheduler: str,
6161
jobs: list[dict[str, Any]],
62+
report_status: bool,
6263
**kwargs: Any,
6364
) -> None:
6465
if "name" not in kwargs:
6566
kwargs["name"] = "trigger"
6667
self.project = project
6768
self.jobs = jobs
69+
self.report_status = report_status
6870
self.config = None
6971
self.builds_scheduler = builds_scheduler
7072
self.skipped_builds_scheduler = skipped_builds_scheduler
@@ -102,6 +104,7 @@ def getSchedulersAndProperties(self) -> list[tuple[str, Properties]]: # noqa: N
102104
props.setProperty("virtual_builder_name", name, source)
103105
props.setProperty("status_name", f"nix-build .#checks.{attr}", source)
104106
props.setProperty("virtual_builder_tags", "", source)
107+
props.setProperty("report_status", self.report_status, source)
105108

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

147150

151+
class NixBuildCombined(steps.BuildStep):
152+
"""Shows the error message of a failed evaluation."""
153+
154+
name = "nix-build-combined"
155+
156+
def run(self) -> Generator[Any, object, Any]:
157+
return self.build.results
158+
159+
148160
class NixEvalCommand(buildstep.ShellMixin, steps.BuildStep):
149161
"""Parses the output of `nix-eval-jobs` and triggers a `nix-build` build for
150162
every attribute.
@@ -153,14 +165,19 @@ class NixEvalCommand(buildstep.ShellMixin, steps.BuildStep):
153165
project: GitProject
154166

155167
def __init__(
156-
self, project: GitProject, supported_systems: list[str], **kwargs: Any
168+
self,
169+
project: GitProject,
170+
supported_systems: list[str],
171+
job_report_limit: int | None,
172+
**kwargs: Any,
157173
) -> None:
158174
kwargs = self.setupShellMixin(kwargs)
159175
super().__init__(**kwargs)
160176
self.project = project
161177
self.observer = logobserver.BufferLogObserver()
162178
self.addLogObserver("stdio", self.observer)
163179
self.supported_systems = supported_systems
180+
self.job_report_limit = job_report_limit
164181

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

210+
self.number_of_jobs = len(filtered_jobs)
211+
193212
self.build.addStepsAfterCurrentStep(
194213
[
195214
BuildTrigger(
@@ -198,8 +217,28 @@ def run(self) -> Generator[Any, object, Any]:
198217
skipped_builds_scheduler=f"{project_id}-nix-skipped-build",
199218
name="build flake",
200219
jobs=filtered_jobs,
220+
report_status=(
221+
self.job_report_limit is None
222+
or self.number_of_jobs <= self.job_report_limit
223+
),
201224
),
202-
],
225+
]
226+
+ (
227+
[
228+
Trigger(
229+
waitForFinish=True,
230+
schedulerNames=[f"{project_id}-nix-build-combined"],
231+
haltOnFailure=True,
232+
flunkOnFailure=True,
233+
sourceStamps=[],
234+
alwaysUseLatest=False,
235+
updateSourceStamp=False,
236+
),
237+
]
238+
if self.job_report_limit is not None
239+
and self.number_of_jobs > self.job_report_limit
240+
else []
241+
),
203242
)
204243

205244
return result
@@ -360,6 +399,7 @@ def nix_eval_config(
360399
eval_lock: MasterLock,
361400
worker_count: int,
362401
max_memory_size: int,
402+
job_report_limit: int | None,
363403
) -> BuilderConfig:
364404
"""Uses nix-eval-jobs to evaluate hydraJobs from flake.nix in parallel.
365405
For each evaluated attribute a new build pipeline is started.
@@ -385,6 +425,7 @@ def nix_eval_config(
385425
env={},
386426
name="evaluate flake",
387427
supported_systems=supported_systems,
428+
job_report_limit=job_report_limit,
388429
command=[
389430
"nix-eval-jobs",
390431
"--workers",
@@ -492,6 +533,7 @@ def nix_build_config(
492533
updateSourceStamp=False,
493534
doStepIf=do_register_gcroot_if,
494535
copy_properties=["out_path", "attr"],
536+
set_properties={"report_status": False},
495537
),
496538
)
497539
factory.addStep(
@@ -556,6 +598,7 @@ def nix_skipped_build_config(
556598
updateSourceStamp=False,
557599
doStepIf=do_register_gcroot_if,
558600
copy_properties=["out_path", "attr"],
601+
set_properties={"report_status": False},
559602
),
560603
)
561604
return util.BuilderConfig(
@@ -601,6 +644,24 @@ def nix_register_gcroot_config(
601644
)
602645

603646

647+
def nix_build_combined_config(
648+
project: GitProject,
649+
worker_names: list[str],
650+
) -> BuilderConfig:
651+
factory = util.BuildFactory()
652+
factory.addStep(NixBuildCombined())
653+
654+
return util.BuilderConfig(
655+
name=f"{project.name}/nix-build-combined",
656+
project=project.name,
657+
workernames=worker_names,
658+
collapseRequests=False,
659+
env={},
660+
factory=factory,
661+
properties=dict(status_name="nix-build-combined"),
662+
)
663+
664+
604665
def config_for_project(
605666
config: dict[str, Any],
606667
project: GitProject,
@@ -610,6 +671,7 @@ def config_for_project(
610671
nix_eval_max_memory_size: int,
611672
eval_lock: MasterLock,
612673
post_build_steps: list[steps.BuildStep],
674+
job_report_limit: int | None,
613675
outputs_path: Path | None = None,
614676
build_retries: int = 1,
615677
) -> None:
@@ -653,6 +715,11 @@ def config_for_project(
653715
name=f"{project.project_id}-nix-skipped-build",
654716
builderNames=[f"{project.name}/nix-skipped-build"],
655717
),
718+
# this is triggered from `nix-eval` when the build contains too many outputs
719+
schedulers.Triggerable(
720+
name=f"{project.project_id}-nix-build-combined",
721+
builderNames=[f"{project.name}/nix-build-combined"],
722+
),
656723
schedulers.Triggerable(
657724
name=f"{project.project_id}-nix-register-gcroot",
658725
builderNames=[f"{project.name}/nix-register-gcroot"],
@@ -680,6 +747,7 @@ def config_for_project(
680747
worker_names,
681748
git_url=project.get_project_url(),
682749
supported_systems=nix_supported_systems,
750+
job_report_limit=job_report_limit,
683751
worker_count=nix_eval_worker_count,
684752
max_memory_size=nix_eval_max_memory_size,
685753
eval_lock=eval_lock,
@@ -693,6 +761,7 @@ def config_for_project(
693761
),
694762
nix_skipped_build_config(project, [SKIPPED_BUILDER_NAME]),
695763
nix_register_gcroot_config(project, worker_names),
764+
nix_build_combined_config(project, worker_names),
696765
],
697766
)
698767

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

844913
if self.config.gitea is not None:
845-
backends["gitea"] = GiteaBackend(self.config.gitea)
914+
backends["gitea"] = GiteaBackend(self.config.gitea, self.config.url)
846915

847916
auth: AuthBase | None = (
848917
backends[self.config.auth_backend].create_auth()
@@ -895,6 +964,7 @@ def configure(self, config: dict[str, Any]) -> None:
895964
self.config.eval_max_memory_size,
896965
eval_lock,
897966
[x.to_buildstep() for x in self.config.post_build_steps],
967+
self.config.job_report_limit,
898968
self.config.outputs_path,
899969
self.config.build_retries,
900970
)

buildbot_nix/common.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,11 @@ def model_validate_project_cache(cls: type[_T], project_cache_file: Path) -> lis
172172

173173
def model_dump_project_cache(repos: list[_T]) -> str:
174174
return json.dumps([repo.model_dump() for repo in repos])
175+
176+
177+
def filter_for_combined_builds(reports: Any) -> Any | None:
178+
properties = reports[0]["builds"][0]["properties"]
179+
180+
if "report_status" in properties and not properties["report_status"][0]:
181+
return None
182+
return reports

buildbot_nix/gitea_projects.py

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import signal
3+
from collections.abc import Callable
34
from pathlib import Path
45
from typing import Any
56
from urllib.parse import urlparse
@@ -13,12 +14,14 @@
1314
from buildbot_gitea.auth import GiteaAuth # type: ignore[import]
1415
from buildbot_gitea.reporter import GiteaStatusPush # type: ignore[import]
1516
from pydantic import BaseModel
17+
from twisted.internet import defer
1618
from twisted.logger import Logger
1719
from twisted.python import log
1820

1921
from .common import (
2022
ThreadDeferredBuildStep,
2123
atomic_write_file,
24+
filter_for_combined_builds,
2225
filter_repos_by_topic,
2326
http_request,
2427
model_dump_project_cache,
@@ -105,11 +108,43 @@ def belongs_to_org(self) -> bool:
105108
return False # self.data["owner"]["type"] == "Organization"
106109

107110

111+
class ModifyingGiteaStatusPush(GiteaStatusPush):
112+
def checkConfig(
113+
self,
114+
modifyingFilter: Callable[[Any], Any | None] = lambda x: x, # noqa: N803
115+
**kwargs: Any,
116+
) -> Any:
117+
self.modifyingFilter = modifyingFilter
118+
119+
return super().checkConfig(**kwargs)
120+
121+
def reconfigService(
122+
self,
123+
modifyingFilter: Callable[[Any], Any | None] = lambda x: x, # noqa: N803
124+
**kwargs: Any,
125+
) -> Any:
126+
self.modifyingFilter = modifyingFilter
127+
128+
return super().reconfigService(**kwargs)
129+
130+
@defer.inlineCallbacks
131+
def sendMessage(self, reports: Any) -> Any:
132+
reports = self.modifyingFilter(reports)
133+
if reports is None:
134+
return
135+
136+
result = yield super().sendMessage(reports)
137+
return result
138+
139+
108140
class GiteaBackend(GitBackend):
109141
config: GiteaConfig
142+
webhook_secret: str
143+
instance_url: str
110144

111-
def __init__(self, config: GiteaConfig) -> None:
145+
def __init__(self, config: GiteaConfig, instance_url: str) -> None:
112146
self.config = config
147+
self.instance_url = instance_url
113148

114149
def create_reload_builder(self, worker_names: list[str]) -> BuilderConfig:
115150
"""Updates the flake an opens a PR for it."""
@@ -118,7 +153,10 @@ def create_reload_builder(self, worker_names: list[str]) -> BuilderConfig:
118153
ReloadGiteaProjects(self.config, self.config.project_cache_file),
119154
)
120155
factory.addStep(
121-
CreateGiteaProjectHooks(self.config),
156+
CreateGiteaProjectHooks(
157+
self.config,
158+
self.instance_url,
159+
),
122160
)
123161
return util.BuilderConfig(
124162
name=self.reload_builder_name,
@@ -127,11 +165,12 @@ def create_reload_builder(self, worker_names: list[str]) -> BuilderConfig:
127165
)
128166

129167
def create_reporter(self) -> ReporterBase:
130-
return GiteaStatusPush(
131-
self.config.instance_url,
132-
Interpolate(self.config.token),
168+
return ModifyingGiteaStatusPush(
169+
baseURL=self.config.instance_url,
170+
token=Interpolate(self.config.token),
133171
context=Interpolate("buildbot/%(prop:status_name)s"),
134172
context_pr=Interpolate("buildbot/%(prop:status_name)s"),
173+
modifyingFilter=filter_for_combined_builds,
135174
)
136175

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

199238

200239
def create_repo_hook(
201-
token: str, webhook_secret: str, owner: str, repo: str, webhook_url: str
240+
token: str,
241+
webhook_secret: str,
242+
owner: str,
243+
repo: str,
244+
gitea_url: str,
245+
instance_url: str,
202246
) -> None:
203247
hooks = paginated_github_request(
204-
f"{webhook_url}/api/v1/repos/{owner}/{repo}/hooks?limit=100",
248+
f"{gitea_url}/api/v1/repos/{owner}/{repo}/hooks?limit=100",
205249
token,
206250
)
207251
config = dict(
208-
url=webhook_url + "change_hook/gitea",
252+
url=instance_url + "change_hook/gitea",
209253
content_type="json",
210254
insecure_ssl="0",
211255
secret=webhook_secret,
@@ -223,13 +267,13 @@ def create_repo_hook(
223267
"Content-Type": "application/json",
224268
}
225269
for hook in hooks:
226-
if hook["config"]["url"] == webhook_url + "change_hook/gitea":
270+
if hook["config"]["url"] == instance_url + "change_hook/gitea":
227271
log.msg(f"hook for {owner}/{repo} already exists")
228272
return
229273

230274
log.msg(f"creating hook for {owner}/{repo}")
231275
http_request(
232-
f"{webhook_url}/api/v1/repos/{owner}/{repo}/hooks",
276+
f"{gitea_url}/api/v1/repos/{owner}/{repo}/hooks",
233277
method="POST",
234278
headers=headers,
235279
data=data,
@@ -240,25 +284,29 @@ class CreateGiteaProjectHooks(ThreadDeferredBuildStep):
240284
name = "create_gitea_project_hooks"
241285

242286
config: GiteaConfig
287+
instance_url: str
243288

244289
def __init__(
245290
self,
246291
config: GiteaConfig,
292+
instance_url: str,
247293
**kwargs: Any,
248294
) -> None:
249295
self.config = config
296+
self.instance_url = instance_url
250297
super().__init__(**kwargs)
251298

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

255302
for repo in repos:
256303
create_repo_hook(
257-
self.config.token,
258-
self.config.webhook_secret,
259-
repo.owner.login,
260-
repo.name,
261-
self.config.instance_url,
304+
token=self.config.token,
305+
webhook_secret=self.config.webhook_secret,
306+
owner=repo.owner.login,
307+
repo=repo.name,
308+
gitea_url=self.config.instance_url,
309+
instance_url=self.instance_url,
262310
)
263311

264312
def run_post(self) -> Any:

0 commit comments

Comments
 (0)