diff --git a/buildbot_nix/buildbot_nix/build_trigger.py b/buildbot_nix/buildbot_nix/build_trigger.py index 7857b0eab..a53511ec5 100644 --- a/buildbot_nix/buildbot_nix/build_trigger.py +++ b/buildbot_nix/buildbot_nix/build_trigger.py @@ -8,7 +8,13 @@ from buildbot.plugins import steps, util from buildbot.process import buildstep from buildbot.process.properties import Properties -from buildbot.process.results import ALL_RESULTS, SUCCESS, statusToString, worst_status +from buildbot.process.results import ( + ALL_RESULTS, + CANCELLED, + SUCCESS, + statusToString, + worst_status, +) from buildbot.reporters.utils import getURLForBuild, getURLForBuildrequest from twisted.internet import defer @@ -95,6 +101,7 @@ class SchedulingContext: scheduled: list[BuildTrigger.ScheduledJob] scheduler_log: StreamLog schedule_now: list[NixEvalJobSuccess] + seen_drvs: dict[str, str] # Maps drvPath to attr name of first occurrence def __init__( self, @@ -530,7 +537,18 @@ async def _schedule_ready_jobs( ) -> list[NixEvalJobSuccess]: """Schedule jobs that are ready to run. Returns list of skipped jobs.""" skipped_jobs = [] - for job in ctx.schedule_now: + for job in sorted(ctx.schedule_now, key=lambda j: j.attr): + # Check if we've already scheduled a build for this derivation (alias detection) + if job.drvPath in ctx.seen_drvs: + original_attr = ctx.seen_drvs[job.drvPath] + ctx.scheduler_log.addStdout( + f"\t- {job.attr} (skipped, alias of {original_attr})\n" + ) + skipped_jobs.append(job) + self._skipped_count += 1 + continue + ctx.seen_drvs[job.drvPath] = job.attr + schedule_result = self.schedule_success(build_props, job) if schedule_result is not None: ctx.scheduler_log.addStdout(f"\t- {job.attr}\n") @@ -573,7 +591,15 @@ async def _wait_and_process_completed( fireOnOneCallback=True, fireOnOneErrback=True, ) - results, index = await self.wait_for_finish_deferred # type: ignore[assignment] + try: + results, index = await self.wait_for_finish_deferred # type: ignore[assignment] + except defer.FirstError as e: + # DeferredList wraps the actual error in FirstError + if isinstance(e.subFailure.value, defer.CancelledError): + ctx.scheduler_log.addStdout("Build was cancelled\n") + return CANCELLED + # Re-raise other errors + raise # Get the completed job scheduled_job = ctx.scheduled.pop(index) @@ -715,6 +741,7 @@ async def run(self) -> int: scheduled=scheduled, schedule_now=[], scheduler_log=scheduler_log, + seen_drvs={}, ) # Main scheduling loop