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

Commit 058f5df

Browse files
authored
Merge pull request #125 from learningequality/fix/use-copyPackage-for-series-copy
fix: use copyPackage instead of syncSources for series copies
2 parents 877bb31 + b0f9062 commit 058f5df

2 files changed

Lines changed: 66 additions & 43 deletions

File tree

scripts/launchpad_copy.py

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def get_builds_for(self, ppa, name, version, series_name):
202202

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

207207
def get_usable_sources(self, ppa, package_names, series_name):
208208
res = []
@@ -232,31 +232,36 @@ def get_usable_sources(self, ppa, package_names, series_name):
232232
res.append((name, version))
233233
return res
234234

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

238238
def perform_queued_copies(self, ppa):
239239
first = True
240-
for (source_series, target_series, pocket), names in self.queue.items():
241-
if not names:
240+
for (source_series, target_series, pocket), packages in self.queue.items():
241+
if not packages:
242242
continue
243-
if first:
244-
log.info("")
245-
first = False
246-
log.info("Copying %s to %s", ", ".join(sorted(names)), target_series)
247-
try:
248-
ppa.syncSources(
249-
from_archive=ppa,
250-
to_series=target_series,
251-
to_pocket=pocket,
252-
include_binaries=True,
253-
source_names=sorted(names),
254-
)
255-
except lre.BadRequest as e:
256-
if "same version already published" in str(e):
257-
log.info("Already copied to %s — skipping", target_series)
258-
else:
259-
raise
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
260265

261266
def copy_to_series(self):
262267
"""Copy packages from source series to all other supported Ubuntu series."""
@@ -279,7 +284,7 @@ def copy_to_series(self):
279284
mentioned = True
280285
log.info("%s %s missing from %s", name, version, target_series_name)
281286
if self.has_published_binaries(ppa, name, version, source_series):
282-
self.queue_copy(name, source_series, target_series_name, POCKET)
287+
self.queue_copy(name, version, source_series, target_series_name, POCKET)
283288
else:
284289
builds = self.get_builds_for(ppa, name, version, source_series)
285290
if builds:

tests/test_launchpad_copy.py

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -149,52 +149,53 @@ def test_raises_on_missing_command(self):
149149
class TestLaunchpadWrapper:
150150
"""Test LaunchpadWrapper queue and filtering logic."""
151151

152-
def test_queue_copy_accumulates_names(self):
152+
def test_queue_copy_accumulates_packages(self):
153153
wrapper = LaunchpadWrapper()
154-
wrapper.queue_copy("kolibri-server", "jammy", "noble", "Release")
155-
wrapper.queue_copy("kolibri-server", "jammy", "focal", "Release")
154+
wrapper.queue_copy("kolibri-server", "0.5.1-0ubuntu1", "jammy", "noble", "Release")
155+
wrapper.queue_copy("kolibri-server", "0.5.1-0ubuntu1", "jammy", "focal", "Release")
156156

157157
assert ("jammy", "noble", "Release") in wrapper.queue
158158
assert ("jammy", "focal", "Release") in wrapper.queue
159-
assert "kolibri-server" in wrapper.queue[("jammy", "noble", "Release")]
159+
assert ("kolibri-server", "0.5.1-0ubuntu1") in wrapper.queue[("jammy", "noble", "Release")]
160160

161161
def test_queue_starts_empty(self):
162162
wrapper = LaunchpadWrapper()
163163
assert len(wrapper.queue) == 0
164164

165-
def test_perform_queued_copies_calls_sync_sources(self):
165+
def test_perform_queued_copies_calls_copy_package(self):
166166
wrapper = LaunchpadWrapper()
167-
wrapper.queue_copy("kolibri-server", "jammy", "noble", "Release")
167+
wrapper.queue_copy("kolibri-server", "0.5.1-0ubuntu1", "jammy", "noble", "Release")
168168

169169
mock_ppa = MagicMock()
170170
wrapper.perform_queued_copies(mock_ppa)
171171

172-
mock_ppa.syncSources.assert_called_once_with(
172+
mock_ppa.copyPackage.assert_called_once_with(
173173
from_archive=mock_ppa,
174+
include_binaries=True,
174175
to_series="noble",
175176
to_pocket="Release",
176-
include_binaries=True,
177-
source_names=["kolibri-server"],
177+
source_name="kolibri-server",
178+
version="0.5.1-0ubuntu1",
178179
)
179180

180181
def test_perform_queued_copies_skips_empty_queues(self):
181182
wrapper = LaunchpadWrapper()
182183
mock_ppa = MagicMock()
183184
wrapper.perform_queued_copies(mock_ppa)
184185

185-
mock_ppa.syncSources.assert_not_called()
186+
mock_ppa.copyPackage.assert_not_called()
186187

187-
def test_perform_queued_copies_handles_already_synced(self):
188-
"""Idempotency: syncSources errors for already-copied packages are handled gracefully."""
188+
def test_perform_queued_copies_handles_already_published(self):
189+
"""Idempotency: copyPackage errors for already-copied packages are handled gracefully."""
189190
wrapper = LaunchpadWrapper()
190-
wrapper.queue_copy("kolibri-server", "jammy", "noble", "Release")
191+
wrapper.queue_copy("kolibri-server", "0.5.1-0ubuntu1", "jammy", "noble", "Release")
191192

192193
class MockBadRequest(Exception):
193194
pass
194195

195196
mock_ppa = MagicMock()
196-
mock_ppa.syncSources.side_effect = MockBadRequest(
197-
"kolibri-server 0.9.0 in noble (same version already published)"
197+
mock_ppa.copyPackage.side_effect = MockBadRequest(
198+
"kolibri-server 0.5.1-0ubuntu1 in noble (same version already published)"
198199
)
199200

200201
with patch("launchpad_copy.lre") as mock_lre:
@@ -203,17 +204,34 @@ class MockBadRequest(Exception):
203204

204205
# Should not raise — the error is handled gracefully
205206

206-
def test_perform_queued_copies_logs_already_synced(self, caplog):
207-
"""Idempotency: logs a message when syncSources finds package already exists."""
207+
def test_perform_queued_copies_handles_obsolete_series(self):
208+
"""copyPackage errors for obsolete series are handled gracefully."""
209+
wrapper = LaunchpadWrapper()
210+
wrapper.queue_copy("kolibri-server", "0.5.1-0ubuntu1", "jammy", "trusty", "Release")
211+
212+
class MockBadRequest(Exception):
213+
pass
214+
215+
mock_ppa = MagicMock()
216+
mock_ppa.copyPackage.side_effect = MockBadRequest("trusty is obsolete and will not accept new uploads")
217+
218+
with patch("launchpad_copy.lre") as mock_lre:
219+
mock_lre.BadRequest = MockBadRequest
220+
wrapper.perform_queued_copies(mock_ppa)
221+
222+
# Should not raise — the error is handled gracefully
223+
224+
def test_perform_queued_copies_logs_already_published(self, caplog):
225+
"""Idempotency: logs a message when copyPackage finds package already exists."""
208226
wrapper = LaunchpadWrapper()
209-
wrapper.queue_copy("kolibri-server", "jammy", "noble", "Release")
227+
wrapper.queue_copy("kolibri-server", "0.5.1-0ubuntu1", "jammy", "noble", "Release")
210228

211229
class MockBadRequest(Exception):
212230
pass
213231

214232
mock_ppa = MagicMock()
215-
mock_ppa.syncSources.side_effect = MockBadRequest(
216-
"kolibri-server 0.9.0 in noble (same version already published)"
233+
mock_ppa.copyPackage.side_effect = MockBadRequest(
234+
"kolibri-server 0.5.1-0ubuntu1 in noble (same version already published)"
217235
)
218236

219237
with (

0 commit comments

Comments
 (0)