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

Commit 4249221

Browse files
authored
Merge pull request #111 from rtibblesbot/issue-107-0a1e6e
Add wait-for-builds polling for Launchpad build completion
2 parents 431d35e + 2540bd8 commit 4249221

4 files changed

Lines changed: 546 additions & 2 deletions

File tree

.github/workflows/build_debian.yml

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ on:
55
jobs:
66
check_version:
77
runs-on: ubuntu-latest
8+
outputs:
9+
version: ${{ steps.version.outputs.VERSION }}
810
steps:
911
- name: Checkout codebase
1012
uses: actions/checkout@v4
@@ -50,8 +52,35 @@ jobs:
5052
echo "Signing package..."
5153
make sign-and-upload
5254
echo "upload completed successfully!"
55+
wait_for_source_builds:
56+
needs:
57+
- check_version
58+
- build_package
59+
runs-on: ubuntu-latest
60+
steps:
61+
- name: Checkout codebase
62+
uses: actions/checkout@v4
63+
- name: Install dependencies
64+
run: |
65+
sudo apt-get update
66+
sudo apt-get install -y python3-launchpadlib
67+
- name: Write Launchpad credentials
68+
env:
69+
LP_CREDENTIALS: ${{ secrets.LP_CREDENTIALS }}
70+
run: |
71+
echo "$LP_CREDENTIALS" > /tmp/lp-creds.txt
72+
- name: Wait for source package builds
73+
env:
74+
LP_CREDENTIALS_FILE: /tmp/lp-creds.txt
75+
run: |
76+
python3 scripts/launchpad_copy.py wait-for-builds \
77+
--package kolibri-server \
78+
--version "${{ needs.check_version.outputs.version }}"
79+
- name: Cleanup Launchpad credentials
80+
if: always()
81+
run: rm -f /tmp/lp-creds.txt
5382
copy_to_other_distributions:
54-
needs: build_package
83+
needs: wait_for_source_builds
5584
runs-on: ubuntu-latest
5685
steps:
5786
- name: Checkout codebase
@@ -73,10 +102,37 @@ jobs:
73102
- name: Cleanup Launchpad credentials
74103
if: always()
75104
run: rm -f /tmp/lp-creds.txt
105+
wait_for_copy_builds:
106+
needs:
107+
- check_version
108+
- copy_to_other_distributions
109+
runs-on: ubuntu-latest
110+
steps:
111+
- name: Checkout codebase
112+
uses: actions/checkout@v4
113+
- name: Install dependencies
114+
run: |
115+
sudo apt-get update
116+
sudo apt-get install -y python3-launchpadlib
117+
- name: Write Launchpad credentials
118+
env:
119+
LP_CREDENTIALS: ${{ secrets.LP_CREDENTIALS }}
120+
run: |
121+
echo "$LP_CREDENTIALS" > /tmp/lp-creds.txt
122+
- name: Wait for copy builds to complete
123+
env:
124+
LP_CREDENTIALS_FILE: /tmp/lp-creds.txt
125+
run: |
126+
python3 scripts/launchpad_copy.py wait-for-builds \
127+
--package kolibri-server \
128+
--version "${{ needs.check_version.outputs.version }}"
129+
- name: Cleanup Launchpad credentials
130+
if: always()
131+
run: rm -f /tmp/lp-creds.txt
76132
block_release_step:
77133
if: ${{ !github.event.release.prerelease }}
78134
name: Job to block publish of a release until it has been manually approved
79-
needs: copy_to_other_distributions
135+
needs: wait_for_copy_builds
80136
runs-on: ubuntu-latest
81137
environment: release
82138
steps:

pyproject.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
[project]
2+
name = "kolibri-server"
3+
version = "0.0.0"
4+
requires-python = ">=3.10"
5+
dependencies = [
6+
"launchpadlib",
7+
"keyring",
8+
]
9+
10+
[project.optional-dependencies]
11+
test = [
12+
"pytest",
13+
"vcrpy",
14+
]
15+
116
[tool.ruff]
217
target-version = "py310"
318
line-length = 120

scripts/launchpad_copy.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,25 @@
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+
3049
log = logging.getLogger(APP_NAME)
3150

3251
STARTUP_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+
388504
def 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

405523
if __name__ == "__main__":

0 commit comments

Comments
 (0)