2727POCKET = "Release"
2828APP_NAME = "ppa-kolibri-server-copy-packages"
2929
30+ TERMINAL_BUILD_STATES = frozenset (
31+ {
32+ "Successfully built" ,
33+ "Failed to build" ,
34+ "Chroot problem" ,
35+ "Failed to upload" ,
36+ "Cancelled build" ,
37+ }
38+ )
39+
40+ FAILED_BUILD_STATES = frozenset (
41+ {
42+ "Failed to build" ,
43+ "Chroot problem" ,
44+ "Failed to upload" ,
45+ "Cancelled build" ,
46+ }
47+ )
48+
3049log = logging .getLogger (APP_NAME )
3150
3251STARTUP_TIME = LAST_LOG_TIME = time .time ()
@@ -288,6 +307,79 @@ def copy_to_series(self):
288307 log .debug ("All done" )
289308 return 0
290309
310+ def wait_for_builds (self , package , version , ppa_name = None , timeout = 1800 , interval = 60 ):
311+ """Wait for all builds of a source package to reach a terminal state.
312+
313+ Returns 0 if all builds succeed, 1 on failure or timeout.
314+ """
315+ ppa_name = ppa_name or PROPOSED_PPA_NAME
316+ ppa = self .get_ppa (ppa_name )
317+ deadline = time .time () + timeout
318+
319+ # Phase 1: Wait for source to appear
320+ log .info ("Waiting for %s %s to appear in %s..." , package , version , ppa_name )
321+ sources = []
322+ while time .time () < deadline :
323+ published = ppa .getPublishedSources (
324+ source_name = package ,
325+ version = version ,
326+ order_by_date = True ,
327+ )
328+ sources = [s for s in published if s .status not in ("Deleted" , "Superseded" , "Obsolete" )]
329+ if sources :
330+ log .info ("Found %d source(s) for %s %s" , len (sources ), package , version )
331+ break
332+ remaining = int (deadline - time .time ())
333+ log .info ("Source not yet available. Retrying in %ds (%ds remaining)..." , interval , remaining )
334+ time .sleep (interval )
335+ else :
336+ log .error ("Timeout: %s %s did not appear in %s within %ds" , package , version , ppa_name , timeout )
337+ return 1
338+
339+ # Phase 2: Wait for all builds to complete
340+ return self ._poll_builds (sources , package , version , deadline , interval )
341+
342+ def _poll_builds (self , sources , package , version , deadline , interval ):
343+ """Poll builds for all sources until terminal state or timeout."""
344+ log .info ("Waiting for builds to complete..." )
345+ while time .time () < deadline :
346+ all_terminal = True
347+ total = 0
348+ succeeded = 0
349+ failed = []
350+ building = 0
351+
352+ for source in sources :
353+ builds = source .getBuilds ()
354+ for build in builds :
355+ total += 1
356+ state = build .buildstate
357+ if state == "Successfully built" :
358+ succeeded += 1
359+ elif state in FAILED_BUILD_STATES :
360+ failed .append ((build .arch_tag , state , build .web_link ))
361+ else :
362+ building += 1
363+ all_terminal = False
364+
365+ if failed :
366+ log .error ("Build failures detected:" )
367+ for arch , state , link in failed :
368+ log .error (" %s: %s - %s" , arch , state , link )
369+ return 1
370+
371+ if total > 0 and all_terminal :
372+ log .info ("All %d build(s) completed successfully." , total )
373+ return 0
374+
375+ log .info ("Waiting for builds: %d/%d complete, %d building..." , succeeded , total , building )
376+ remaining = int (deadline - time .time ())
377+ log .info ("Retrying in %ds (%ds remaining)..." , interval , remaining )
378+ time .sleep (interval )
379+
380+ log .error ("Timeout: builds for %s %s did not complete within timeout" , package , version )
381+ return 1
382+
291383 def promote (self ):
292384 """Promote published packages from kolibri-proposed to kolibri PPA."""
293385 log .info ("Promoting packages from %s to %s" , PROPOSED_PPA_NAME , RELEASE_PPA_NAME )
@@ -362,6 +454,18 @@ def build_parser():
362454 help = "Promote published packages from kolibri-proposed to kolibri PPA." ,
363455 )
364456
457+ wait_parser = subparsers .add_parser (
458+ "wait-for-builds" ,
459+ help = "Wait for Launchpad builds to complete for a source package." ,
460+ )
461+ wait_parser .add_argument ("--package" , required = True , help = "Source package name." )
462+ wait_parser .add_argument ("--version" , required = True , help = "Expected version string." )
463+ wait_parser .add_argument ("--ppa" , default = PROPOSED_PPA_NAME , help = "PPA name to poll (default: %(default)s)." )
464+ wait_parser .add_argument ("--timeout" , type = int , default = 1800 , help = "Max wait in seconds (default: %(default)s)." )
465+ wait_parser .add_argument (
466+ "--interval" , type = int , default = 60 , help = "Polling interval in seconds (default: %(default)s)."
467+ )
468+
365469 return parser
366470
367471
@@ -385,6 +489,18 @@ def cmd_copy_to_series(args):
385489 return lp .copy_to_series ()
386490
387491
492+ def cmd_wait_for_builds (args ):
493+ """Wait for Launchpad builds to complete."""
494+ lp = LaunchpadWrapper ()
495+ return lp .wait_for_builds (
496+ package = args .package ,
497+ version = args .version ,
498+ ppa_name = args .ppa ,
499+ timeout = args .timeout ,
500+ interval = args .interval ,
501+ )
502+
503+
388504def cmd_promote (args ):
389505 """Promote published packages from kolibri-proposed to kolibri PPA."""
390506 lp = LaunchpadWrapper ()
@@ -400,6 +516,8 @@ def main():
400516 return cmd_copy_to_series (args )
401517 elif args .command == "promote" :
402518 return cmd_promote (args )
519+ elif args .command == "wait-for-builds" :
520+ return cmd_wait_for_builds (args )
403521
404522
405523if __name__ == "__main__" :
0 commit comments