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-
4930log = logging .getLogger (APP_NAME )
5031
5132STARTUP_TIME = LAST_LOG_TIME = time .time ()
@@ -237,35 +218,38 @@ def queue_copy(self, name, version, source_series, target_series, pocket):
237218
238219 def perform_queued_copies (self , ppa ):
239220 first = True
221+ failures = []
240222 for (source_series , target_series , pocket ), packages in self .queue .items ():
241223 if not packages :
242224 continue
243- for name , version in sorted (packages ):
244- if first :
245- log .info ("" )
246- first = False
247- log .info ("Copying %s %s to %s" , name , version , target_series )
248- try :
249- ppa .copyPackage (
250- from_archive = ppa ,
251- include_binaries = True ,
252- to_series = target_series ,
253- to_pocket = pocket ,
254- source_name = name ,
255- version = version ,
256- )
257- except lre .BadRequest as e :
258- msg = str (e )
259- if "same version already published" in msg :
260- log .info ("Already copied to %s — skipping" , target_series )
261- elif "is obsolete and will not accept new uploads" in msg :
262- log .info ("Skip obsolete series %s for %s %s" , target_series , name , version )
263- else :
264- raise
225+ if first :
226+ log .info ("" )
227+ first = False
228+ names = sorted (name for name , version in packages )
229+ log .info ("Copying %s to %s" , ", " .join (names ), target_series )
230+ try :
231+ ppa .syncSources (
232+ from_archive = ppa ,
233+ to_series = target_series ,
234+ to_pocket = pocket ,
235+ include_binaries = True ,
236+ source_names = names ,
237+ )
238+ except lre .BadRequest as e :
239+ msg = str (e )
240+ if "same version already published" in msg :
241+ log .info ("Already copied to %s — skipping" , target_series )
242+ else :
243+ log .error ("Failed to copy to %s: %s" , target_series , msg )
244+ failures .append (target_series )
245+ if failures :
246+ log .error ("Copy failed for series: %s" , ", " .join (failures ))
247+ return 1
248+ return 0
265249
266- def copy_to_series (self ):
250+ def copy_to_series (self , source_series = None ):
267251 """Copy packages from source series to all other supported Ubuntu series."""
268- source_series = get_current_series ()
252+ source_series = source_series or get_current_series ()
269253 log .info (
270254 "Spinning up the Launchpad API to copy targets in %s (source series: %s)" ,
271255 ", " .join (PACKAGE_WHITELIST ),
@@ -311,9 +295,9 @@ def copy_to_series(self):
311295 for notice in notices :
312296 log .info (notice )
313297
314- self .perform_queued_copies (ppa )
298+ result = self .perform_queued_copies (ppa )
315299 log .debug ("All done" )
316- return 0
300+ return result
317301
318302 def check_source (self , package , version , ppa_name = None ):
319303 """Check if a source package version exists in a PPA.
@@ -333,77 +317,78 @@ def check_source(self, package, version, ppa_name=None):
333317 log .info ("%s %s not found in %s" , package , version , ppa_name )
334318 return 1
335319
336- def wait_for_builds (self , package , version , ppa_name = None , timeout = 1800 , interval = 60 ):
337- """Wait for all builds of a source package to reach a terminal state .
320+ def wait_for_published (self , package , version , ppa_name = None , series = None , timeout = 1800 , interval = 60 ):
321+ """Wait for published binaries to appear for a package .
338322
339- Returns 0 if all builds succeed, 1 on failure or timeout.
323+ If series is given, waits for those specific series to have published binaries.
324+ If series is None, discovers all series that have a published source for this
325+ package+version and waits until every one of them also has published binaries.
326+ Returns 0 if all expected series are published, 1 on failure or timeout.
340327 """
341328 ppa_name = ppa_name or PROPOSED_PPA_NAME
342329 ppa = self .get_ppa (ppa_name )
343330 deadline = time .time () + timeout
331+ expected = set (series ) if series else None
332+
333+ log .info (
334+ "Waiting for %s %s to be published in %s%s..." ,
335+ package ,
336+ version ,
337+ ppa_name ,
338+ " for series: %s" % ", " .join (sorted (expected )) if expected else "" ,
339+ )
344340
345- # Phase 1: Wait for source to appear
346- log .info ("Waiting for %s %s to appear in %s..." , package , version , ppa_name )
347- sources = []
348341 while time .time () < deadline :
349- published = ppa .getPublishedSources (
350- source_name = package ,
342+ # If no explicit series, discover from published sources
343+ if expected is None :
344+ sources = ppa .getPublishedSources (
345+ source_name = package ,
346+ version = version ,
347+ order_by_date = True ,
348+ )
349+ source_series = set ()
350+ for s in sources :
351+ if s .status not in ("Deleted" , "Superseded" , "Obsolete" ):
352+ series_name = s .distro_series_link .rstrip ("/" ).split ("/" )[- 1 ]
353+ source_series .add (series_name )
354+ if not source_series :
355+ log .info ("No published sources yet." )
356+ remaining = int (deadline - time .time ())
357+ log .info ("Retrying in %ds (%ds remaining)..." , interval , remaining )
358+ time .sleep (interval )
359+ continue
360+ expected = source_series
361+ log .info ("Discovered %d series with sources: %s" , len (expected ), ", " .join (sorted (expected )))
362+
363+ # Check published binaries
364+ bins = ppa .getPublishedBinaries (
365+ binary_name = package ,
351366 version = version ,
352367 order_by_date = True ,
353368 )
354- sources = [s for s in published if s .status not in ("Deleted" , "Superseded" , "Obsolete" )]
355- if sources :
356- log .info ("Found %d source(s) for %s %s" , len (sources ), package , version )
357- break
358- remaining = int (deadline - time .time ())
359- log .info ("Source not yet available. Retrying in %ds (%ds remaining)..." , interval , remaining )
360- time .sleep (interval )
361- else :
362- log .error ("Timeout: %s %s did not appear in %s within %ds" , package , version , ppa_name , timeout )
363- return 1
364-
365- # Phase 2: Wait for all builds to complete
366- return self ._poll_builds (sources , package , version , deadline , interval )
367-
368- def _poll_builds (self , sources , package , version , deadline , interval ):
369- """Poll builds for all sources until terminal state or timeout."""
370- log .info ("Waiting for builds to complete..." )
371- while time .time () < deadline :
372- all_terminal = True
373- total = 0
374- succeeded = 0
375- failed = []
376- building = 0
377-
378- for source in sources :
379- builds = source .getBuilds ()
380- for build in builds :
381- total += 1
382- state = build .buildstate
383- if state == "Successfully built" :
384- succeeded += 1
385- elif state in FAILED_BUILD_STATES :
386- failed .append ((build .arch_tag , state , build .web_link ))
387- else :
388- building += 1
389- all_terminal = False
390-
391- if failed :
392- log .error ("Build failures detected:" )
393- for arch , state , link in failed :
394- log .error (" %s: %s - %s" , arch , state , link )
395- return 1
396-
397- if total > 0 and all_terminal :
398- log .info ("All %d build(s) completed successfully." , total )
369+ published_series = set ()
370+ for b in bins :
371+ if b .status == "Published" :
372+ # distro_arch_series_link: .../ubuntu/noble/amd64
373+ series_name = b .distro_arch_series_link .rstrip ("/" ).split ("/" )[- 2 ]
374+ published_series .add (series_name )
375+
376+ missing = expected - published_series
377+ if not missing :
378+ log .info ("All %d series published: %s" , len (expected ), ", " .join (sorted (published_series )))
399379 return 0
380+ log .info (
381+ "Published in %d/%d series. Missing: %s" ,
382+ len (expected ) - len (missing ),
383+ len (expected ),
384+ ", " .join (sorted (missing )),
385+ )
400386
401- log .info ("Waiting for builds: %d/%d complete, %d building..." , succeeded , total , building )
402387 remaining = int (deadline - time .time ())
403388 log .info ("Retrying in %ds (%ds remaining)..." , interval , remaining )
404389 time .sleep (interval )
405390
406- log .error ("Timeout: builds for %s %s did not complete within timeout " , package , version )
391+ log .error ("Timeout: %s %s not published within %ds " , package , version , timeout )
407392 return 1
408393
409394 def promote (self ):
@@ -476,19 +461,20 @@ def build_parser():
476461 parser .add_argument ("--debug" , action = "store_true" , help = "Enable HTTP debug output." )
477462 subparsers = parser .add_subparsers (dest = "command" , required = True )
478463
479- subparsers .add_parser (
464+ copy_parser = subparsers .add_parser (
480465 "copy-to-series" ,
481466 help = "Copy packages from source series to all other supported series within a PPA." ,
482467 )
468+ copy_parser .add_argument ("--series" , default = None , help = "Source series override (default: auto-detect from OS)." )
483469
484470 subparsers .add_parser (
485471 "promote" ,
486472 help = "Promote published packages from kolibri-proposed to kolibri PPA." ,
487473 )
488474
489475 wait_parser = subparsers .add_parser (
490- "wait-for-builds " ,
491- help = "Wait for Launchpad builds to complete for a source package." ,
476+ "wait-for-published " ,
477+ help = "Wait for published binaries to appear for a source package." ,
492478 )
493479 wait_parser .add_argument ("--package" , required = True , help = "Source package name." )
494480 wait_parser .add_argument ("--version" , required = True , help = "Expected version string." )
@@ -497,6 +483,7 @@ def build_parser():
497483 wait_parser .add_argument (
498484 "--interval" , type = int , default = 60 , help = "Polling interval in seconds (default: %(default)s)."
499485 )
486+ wait_parser .add_argument ("--series" , nargs = "+" , default = None , help = "Series to wait for (default: any)." )
500487
501488 check_parser = subparsers .add_parser (
502489 "check-source" ,
@@ -526,16 +513,17 @@ def configure_logging(args):
526513def cmd_copy_to_series (args ):
527514 """Copy packages from source series to all other supported Ubuntu series."""
528515 lp = LaunchpadWrapper ()
529- return lp .copy_to_series ()
516+ return lp .copy_to_series (source_series = args . series )
530517
531518
532- def cmd_wait_for_builds (args ):
533- """Wait for Launchpad builds to complete ."""
519+ def cmd_wait_for_published (args ):
520+ """Wait for published binaries to appear ."""
534521 lp = LaunchpadWrapper ()
535- return lp .wait_for_builds (
522+ return lp .wait_for_published (
536523 package = args .package ,
537524 version = args .version ,
538525 ppa_name = args .ppa ,
526+ series = args .series ,
539527 timeout = args .timeout ,
540528 interval = args .interval ,
541529 )
@@ -568,8 +556,8 @@ def main():
568556 return cmd_check_source (args )
569557 elif args .command == "promote" :
570558 return cmd_promote (args )
571- elif args .command == "wait-for-builds " :
572- return cmd_wait_for_builds (args )
559+ elif args .command == "wait-for-published " :
560+ return cmd_wait_for_published (args )
573561
574562
575563if __name__ == "__main__" :
0 commit comments