Skip to content
This repository was archived by the owner on Jul 16, 2026. It is now read-only.

Commit caa1e1f

Browse files
rtibblesclaude
andcommitted
fix: replace wait-for-builds with wait-for-published and fix copy errors
- Replace wait-for-builds with wait-for-published: checks getPublishedBinaries status instead of build states, since "Successfully built" != "published" - Auto-discovery mode: without --series, discovers all series with published sources and waits until every one also has published binaries - Per-series error handling in syncSources copies: one series failing no longer crashes the entire run; failures are collected and reported at end - Fix has_published_binaries: 'not builds' returned True when no builds existed - Add --series flag to copy-to-series for local testing on non-Ubuntu systems - Update workflow: wait_for_source_published and wait_for_copies_published Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 877bb31 commit caa1e1f

3 files changed

Lines changed: 216 additions & 352 deletions

File tree

.github/workflows/build_debian.yml

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ jobs:
8787
- name: Cleanup credentials
8888
if: always()
8989
run: rm -f /tmp/lp-creds.txt /tmp/.gpg-passphrase
90-
wait_for_source_builds:
90+
wait_for_source_published:
9191
needs:
9292
- check_version
9393
- build_package
@@ -104,18 +104,20 @@ jobs:
104104
LP_CREDENTIALS: ${{ secrets.LP_CREDENTIALS }}
105105
run: |
106106
echo "$LP_CREDENTIALS" > /tmp/lp-creds.txt
107-
- name: Wait for source package builds
107+
- name: Wait for published binaries in source series
108108
env:
109109
LP_CREDENTIALS_FILE: /tmp/lp-creds.txt
110110
run: |
111-
python3 scripts/launchpad_copy.py wait-for-builds \
111+
SERIES=$(. /etc/os-release && echo "$VERSION_CODENAME")
112+
python3 scripts/launchpad_copy.py wait-for-published \
112113
--package kolibri-server \
113-
--version "${{ needs.check_version.outputs.version }}"
114+
--version "${{ needs.check_version.outputs.version }}" \
115+
--series "$SERIES"
114116
- name: Cleanup Launchpad credentials
115117
if: always()
116118
run: rm -f /tmp/lp-creds.txt
117119
copy_to_other_distributions:
118-
needs: wait_for_source_builds
120+
needs: wait_for_source_published
119121
runs-on: ubuntu-latest
120122
steps:
121123
- name: Checkout codebase
@@ -137,7 +139,7 @@ jobs:
137139
- name: Cleanup Launchpad credentials
138140
if: always()
139141
run: rm -f /tmp/lp-creds.txt
140-
wait_for_copy_builds:
142+
wait_for_copies_published:
141143
needs:
142144
- check_version
143145
- copy_to_other_distributions
@@ -154,19 +156,19 @@ jobs:
154156
LP_CREDENTIALS: ${{ secrets.LP_CREDENTIALS }}
155157
run: |
156158
echo "$LP_CREDENTIALS" > /tmp/lp-creds.txt
157-
- name: Wait for copy builds to complete
159+
- name: Wait for published binaries in all series
158160
env:
159161
LP_CREDENTIALS_FILE: /tmp/lp-creds.txt
160162
run: |
161-
python3 scripts/launchpad_copy.py wait-for-builds \
163+
python3 scripts/launchpad_copy.py wait-for-published \
162164
--package kolibri-server \
163165
--version "${{ needs.check_version.outputs.version }}"
164166
- name: Cleanup Launchpad credentials
165167
if: always()
166168
run: rm -f /tmp/lp-creds.txt
167169
block_release_step:
168170
name: Job to block publish of a release until it has been manually approved
169-
needs: wait_for_copy_builds
171+
needs: wait_for_copies_published
170172
runs-on: ubuntu-latest
171173
environment: release
172174
steps:

scripts/launchpad_copy.py

Lines changed: 94 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,6 @@
2727
POCKET = "Release"
2828
APP_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-
4930
log = logging.getLogger(APP_NAME)
5031

5132
STARTUP_TIME = LAST_LOG_TIME = time.time()
@@ -202,7 +183,7 @@ def get_builds_for(self, ppa, name, version, series_name):
202183

203184
def has_published_binaries(self, ppa, name, version, series_name):
204185
builds = self.get_builds_for(ppa, name, version, series_name)
205-
return not builds or builds[0].buildstate == "Successfully built"
186+
return bool(builds) and builds[0].buildstate == "Successfully built"
206187

207188
def get_usable_sources(self, ppa, package_names, series_name):
208189
res = []
@@ -232,35 +213,43 @@ def get_usable_sources(self, ppa, package_names, series_name):
232213
res.append((name, version))
233214
return res
234215

235-
def queue_copy(self, name, source_series, target_series, pocket):
236-
self.queue[source_series, target_series, pocket].add(name)
216+
def queue_copy(self, name, version, source_series, target_series, pocket):
217+
self.queue[source_series, target_series, pocket].add((name, version))
237218

238219
def perform_queued_copies(self, ppa):
239220
first = True
240-
for (source_series, target_series, pocket), names in self.queue.items():
241-
if not names:
221+
failures = []
222+
for (source_series, target_series, pocket), packages in self.queue.items():
223+
if not packages:
242224
continue
243225
if first:
244226
log.info("")
245227
first = False
246-
log.info("Copying %s to %s", ", ".join(sorted(names)), target_series)
228+
names = sorted(name for name, version in packages)
229+
log.info("Copying %s to %s", ", ".join(names), target_series)
247230
try:
248231
ppa.syncSources(
249232
from_archive=ppa,
250233
to_series=target_series,
251234
to_pocket=pocket,
252235
include_binaries=True,
253-
source_names=sorted(names),
236+
source_names=names,
254237
)
255238
except lre.BadRequest as e:
256-
if "same version already published" in str(e):
239+
msg = str(e)
240+
if "same version already published" in msg:
257241
log.info("Already copied to %s — skipping", target_series)
258242
else:
259-
raise
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
260249

261-
def copy_to_series(self):
250+
def copy_to_series(self, source_series=None):
262251
"""Copy packages from source series to all other supported Ubuntu series."""
263-
source_series = get_current_series()
252+
source_series = source_series or get_current_series()
264253
log.info(
265254
"Spinning up the Launchpad API to copy targets in %s (source series: %s)",
266255
", ".join(PACKAGE_WHITELIST),
@@ -279,7 +268,7 @@ def copy_to_series(self):
279268
mentioned = True
280269
log.info("%s %s missing from %s", name, version, target_series_name)
281270
if self.has_published_binaries(ppa, name, version, source_series):
282-
self.queue_copy(name, source_series, target_series_name, POCKET)
271+
self.queue_copy(name, version, source_series, target_series_name, POCKET)
283272
else:
284273
builds = self.get_builds_for(ppa, name, version, source_series)
285274
if builds:
@@ -306,9 +295,9 @@ def copy_to_series(self):
306295
for notice in notices:
307296
log.info(notice)
308297

309-
self.perform_queued_copies(ppa)
298+
result = self.perform_queued_copies(ppa)
310299
log.debug("All done")
311-
return 0
300+
return result
312301

313302
def check_source(self, package, version, ppa_name=None):
314303
"""Check if a source package version exists in a PPA.
@@ -328,77 +317,78 @@ def check_source(self, package, version, ppa_name=None):
328317
log.info("%s %s not found in %s", package, version, ppa_name)
329318
return 1
330319

331-
def wait_for_builds(self, package, version, ppa_name=None, timeout=1800, interval=60):
332-
"""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.
333322
334-
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.
335327
"""
336328
ppa_name = ppa_name or PROPOSED_PPA_NAME
337329
ppa = self.get_ppa(ppa_name)
338330
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+
)
339340

340-
# Phase 1: Wait for source to appear
341-
log.info("Waiting for %s %s to appear in %s...", package, version, ppa_name)
342-
sources = []
343341
while time.time() < deadline:
344-
published = ppa.getPublishedSources(
345-
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,
346366
version=version,
347367
order_by_date=True,
348368
)
349-
sources = [s for s in published if s.status not in ("Deleted", "Superseded", "Obsolete")]
350-
if sources:
351-
log.info("Found %d source(s) for %s %s", len(sources), package, version)
352-
break
353-
remaining = int(deadline - time.time())
354-
log.info("Source not yet available. Retrying in %ds (%ds remaining)...", interval, remaining)
355-
time.sleep(interval)
356-
else:
357-
log.error("Timeout: %s %s did not appear in %s within %ds", package, version, ppa_name, timeout)
358-
return 1
359-
360-
# Phase 2: Wait for all builds to complete
361-
return self._poll_builds(sources, package, version, deadline, interval)
362-
363-
def _poll_builds(self, sources, package, version, deadline, interval):
364-
"""Poll builds for all sources until terminal state or timeout."""
365-
log.info("Waiting for builds to complete...")
366-
while time.time() < deadline:
367-
all_terminal = True
368-
total = 0
369-
succeeded = 0
370-
failed = []
371-
building = 0
372-
373-
for source in sources:
374-
builds = source.getBuilds()
375-
for build in builds:
376-
total += 1
377-
state = build.buildstate
378-
if state == "Successfully built":
379-
succeeded += 1
380-
elif state in FAILED_BUILD_STATES:
381-
failed.append((build.arch_tag, state, build.web_link))
382-
else:
383-
building += 1
384-
all_terminal = False
385-
386-
if failed:
387-
log.error("Build failures detected:")
388-
for arch, state, link in failed:
389-
log.error(" %s: %s - %s", arch, state, link)
390-
return 1
391-
392-
if total > 0 and all_terminal:
393-
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)))
394379
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+
)
395386

396-
log.info("Waiting for builds: %d/%d complete, %d building...", succeeded, total, building)
397387
remaining = int(deadline - time.time())
398388
log.info("Retrying in %ds (%ds remaining)...", interval, remaining)
399389
time.sleep(interval)
400390

401-
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)
402392
return 1
403393

404394
def promote(self):
@@ -471,19 +461,22 @@ def build_parser():
471461
parser.add_argument("--debug", action="store_true", help="Enable HTTP debug output.")
472462
subparsers = parser.add_subparsers(dest="command", required=True)
473463

474-
subparsers.add_parser(
464+
copy_parser = subparsers.add_parser(
475465
"copy-to-series",
476466
help="Copy packages from source series to all other supported series within a PPA.",
477467
)
468+
copy_parser.add_argument(
469+
"--series", default=None, help="Source series override (default: auto-detect from OS)."
470+
)
478471

479472
subparsers.add_parser(
480473
"promote",
481474
help="Promote published packages from kolibri-proposed to kolibri PPA.",
482475
)
483476

484477
wait_parser = subparsers.add_parser(
485-
"wait-for-builds",
486-
help="Wait for Launchpad builds to complete for a source package.",
478+
"wait-for-published",
479+
help="Wait for published binaries to appear for a source package.",
487480
)
488481
wait_parser.add_argument("--package", required=True, help="Source package name.")
489482
wait_parser.add_argument("--version", required=True, help="Expected version string.")
@@ -492,6 +485,9 @@ def build_parser():
492485
wait_parser.add_argument(
493486
"--interval", type=int, default=60, help="Polling interval in seconds (default: %(default)s)."
494487
)
488+
wait_parser.add_argument(
489+
"--series", nargs="+", default=None, help="Series to wait for (default: any)."
490+
)
495491

496492
check_parser = subparsers.add_parser(
497493
"check-source",
@@ -521,16 +517,17 @@ def configure_logging(args):
521517
def cmd_copy_to_series(args):
522518
"""Copy packages from source series to all other supported Ubuntu series."""
523519
lp = LaunchpadWrapper()
524-
return lp.copy_to_series()
520+
return lp.copy_to_series(source_series=args.series)
525521

526522

527-
def cmd_wait_for_builds(args):
528-
"""Wait for Launchpad builds to complete."""
523+
def cmd_wait_for_published(args):
524+
"""Wait for published binaries to appear."""
529525
lp = LaunchpadWrapper()
530-
return lp.wait_for_builds(
526+
return lp.wait_for_published(
531527
package=args.package,
532528
version=args.version,
533529
ppa_name=args.ppa,
530+
series=args.series,
534531
timeout=args.timeout,
535532
interval=args.interval,
536533
)
@@ -563,8 +560,8 @@ def main():
563560
return cmd_check_source(args)
564561
elif args.command == "promote":
565562
return cmd_promote(args)
566-
elif args.command == "wait-for-builds":
567-
return cmd_wait_for_builds(args)
563+
elif args.command == "wait-for-published":
564+
return cmd_wait_for_published(args)
568565

569566

570567
if __name__ == "__main__":

0 commit comments

Comments
 (0)