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

Commit fe435ac

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 058f5df commit fe435ac

3 files changed

Lines changed: 210 additions & 369 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: 101 additions & 109 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()
@@ -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,22 @@ 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(
469+
"--series", default=None, help="Source series override (default: auto-detect from OS)."
470+
)
483471

484472
subparsers.add_parser(
485473
"promote",
486474
help="Promote published packages from kolibri-proposed to kolibri PPA.",
487475
)
488476

489477
wait_parser = subparsers.add_parser(
490-
"wait-for-builds",
491-
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.",
492480
)
493481
wait_parser.add_argument("--package", required=True, help="Source package name.")
494482
wait_parser.add_argument("--version", required=True, help="Expected version string.")
@@ -497,6 +485,9 @@ def build_parser():
497485
wait_parser.add_argument(
498486
"--interval", type=int, default=60, help="Polling interval in seconds (default: %(default)s)."
499487
)
488+
wait_parser.add_argument(
489+
"--series", nargs="+", default=None, help="Series to wait for (default: any)."
490+
)
500491

501492
check_parser = subparsers.add_parser(
502493
"check-source",
@@ -526,16 +517,17 @@ def configure_logging(args):
526517
def cmd_copy_to_series(args):
527518
"""Copy packages from source series to all other supported Ubuntu series."""
528519
lp = LaunchpadWrapper()
529-
return lp.copy_to_series()
520+
return lp.copy_to_series(source_series=args.series)
530521

531522

532-
def cmd_wait_for_builds(args):
533-
"""Wait for Launchpad builds to complete."""
523+
def cmd_wait_for_published(args):
524+
"""Wait for published binaries to appear."""
534525
lp = LaunchpadWrapper()
535-
return lp.wait_for_builds(
526+
return lp.wait_for_published(
536527
package=args.package,
537528
version=args.version,
538529
ppa_name=args.ppa,
530+
series=args.series,
539531
timeout=args.timeout,
540532
interval=args.interval,
541533
)
@@ -568,8 +560,8 @@ def main():
568560
return cmd_check_source(args)
569561
elif args.command == "promote":
570562
return cmd_promote(args)
571-
elif args.command == "wait-for-builds":
572-
return cmd_wait_for_builds(args)
563+
elif args.command == "wait-for-published":
564+
return cmd_wait_for_published(args)
573565

574566

575567
if __name__ == "__main__":

0 commit comments

Comments
 (0)