Merge ~cjwatson/launchpad:ci-build-upload-job into launchpad:master
- Git
- lp:~cjwatson/launchpad
- ci-build-upload-job
- Merge into master
Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | 5b43a35a8ece7e9dcf025fbbdfcc9acbf8bd9291 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~cjwatson/launchpad:ci-build-upload-job |
Merge into: | launchpad:master |
Diff against target: |
892 lines (+539/-6) 23 files modified
database/schema/security.cfg (+1/-1) lib/lp/_schema_circular_imports.py (+1/-0) lib/lp/services/config/schema-lazr.conf (+6/-0) lib/lp/soyuz/configure.zcml (+15/-4) lib/lp/soyuz/enums.py (+6/-0) lib/lp/soyuz/interfaces/archive.py (+12/-0) lib/lp/soyuz/interfaces/archivejob.py (+31/-0) lib/lp/soyuz/model/archive.py (+29/-0) lib/lp/soyuz/model/archivejob.py (+127/-0) lib/lp/soyuz/model/publishing.py (+1/-1) lib/lp/soyuz/tests/__init__.py (+14/-0) lib/lp/soyuz/tests/data/.gitignore (+2/-0) lib/lp/soyuz/tests/data/wheel-arch/README.md (+1/-0) lib/lp/soyuz/tests/data/wheel-arch/pyproject.toml (+3/-0) lib/lp/soyuz/tests/data/wheel-arch/setup.py (+14/-0) lib/lp/soyuz/tests/data/wheel-arch/test.c (+1/-0) lib/lp/soyuz/tests/data/wheel-indep/README.md (+1/-0) lib/lp/soyuz/tests/data/wheel-indep/pyproject.toml (+3/-0) lib/lp/soyuz/tests/data/wheel-indep/setup.cfg (+5/-0) lib/lp/soyuz/tests/test_archive.py (+67/-0) lib/lp/soyuz/tests/test_archivejob.py (+195/-0) requirements/launchpad.txt (+2/-0) setup.cfg (+2/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+423198@code.launchpad.net |
Commit message
Add method to upload a CI build to an archive
Description of the change
This new `Archive.
There's a new `CIBuildUploadJob` which fetches the output files from the librarian and works out how to turn them into `BinaryPackageR
Dependencies MP: https:/
William Grant (wgrant) : | # |
Preview Diff
1 | diff --git a/database/schema/security.cfg b/database/schema/security.cfg |
2 | index 0024832..f04f672 100644 |
3 | --- a/database/schema/security.cfg |
4 | +++ b/database/schema/security.cfg |
5 | @@ -1433,7 +1433,7 @@ public.archivesigningkey = SELECT, INSERT |
6 | public.binarypackagebuild = SELECT, INSERT, UPDATE |
7 | public.binarypackagefile = SELECT, INSERT |
8 | public.binarypackagename = SELECT, INSERT |
9 | -public.binarypackagepublishinghistory = SELECT |
10 | +public.binarypackagepublishinghistory = SELECT, INSERT |
11 | public.binarypackagerelease = SELECT, INSERT |
12 | public.binarysourcereference = SELECT, INSERT |
13 | public.bug = SELECT, UPDATE |
14 | diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py |
15 | index 1d3cb17..6bf443b 100644 |
16 | --- a/lib/lp/_schema_circular_imports.py |
17 | +++ b/lib/lp/_schema_circular_imports.py |
18 | @@ -397,6 +397,7 @@ patch_plain_parameter_type(IArchive, 'syncSource', 'from_archive', IArchive) |
19 | patch_plain_parameter_type(IArchive, 'copyPackage', 'from_archive', IArchive) |
20 | patch_plain_parameter_type( |
21 | IArchive, 'copyPackages', 'from_archive', IArchive) |
22 | +patch_plain_parameter_type(IArchive, 'uploadCIBuild', 'ci_build', ICIBuild) |
23 | patch_entry_return_type(IArchive, 'newSubscription', IArchiveSubscriber) |
24 | patch_plain_parameter_type( |
25 | IArchive, 'getArchiveDependency', 'dependency', IArchive) |
26 | diff --git a/lib/lp/services/config/schema-lazr.conf b/lib/lp/services/config/schema-lazr.conf |
27 | index 45bf05a..4fc8131 100644 |
28 | --- a/lib/lp/services/config/schema-lazr.conf |
29 | +++ b/lib/lp/services/config/schema-lazr.conf |
30 | @@ -1914,6 +1914,7 @@ dbuser: process-job-source-groups |
31 | # can be loaded from. |
32 | job_sources: |
33 | IBranchModifiedMailJobSource, |
34 | + ICIBuildUploadJobSource, |
35 | ICommercialExpiredJobSource, |
36 | IExpiringMembershipNotificationJobSource, |
37 | IGitRepositoryModifiedMailJobSource, |
38 | @@ -1966,6 +1967,11 @@ module: lp.charms.interfaces.charmrecipejob |
39 | dbuser: charm-build-job |
40 | crontab_group: MAIN |
41 | |
42 | +[ICIBuildUploadJobSource] |
43 | +module: lp.soyuz.interfaces.archivejob |
44 | +dbuser: uploader |
45 | +crontab_group: FREQUENT |
46 | + |
47 | [ICommercialExpiredJobSource] |
48 | module: lp.registry.interfaces.productjob |
49 | dbuser: product-job |
50 | diff --git a/lib/lp/soyuz/configure.zcml b/lib/lp/soyuz/configure.zcml |
51 | index 29cabca..7236f92 100644 |
52 | --- a/lib/lp/soyuz/configure.zcml |
53 | +++ b/lib/lp/soyuz/configure.zcml |
54 | @@ -433,21 +433,32 @@ |
55 | <class class="lp.soyuz.model.archivejob.ArchiveJob"> |
56 | <allow interface="lp.soyuz.interfaces.archivejob.IArchiveJob"/> |
57 | </class> |
58 | - <class class="lp.soyuz.model.archivejob.PackageUploadNotificationJob"> |
59 | - <allow interface="lp.soyuz.interfaces.archivejob.IArchiveJob"/> |
60 | - <allow interface="lp.soyuz.interfaces.archivejob.IPackageUploadNotificationJob"/> |
61 | - </class> |
62 | <securedutility |
63 | component="lp.soyuz.model.archivejob.ArchiveJob" |
64 | provides="lp.soyuz.interfaces.archivejob.IArchiveJobSource"> |
65 | <allow interface="lp.soyuz.interfaces.archivejob.IArchiveJobSource"/> |
66 | </securedutility> |
67 | + |
68 | + <class class="lp.soyuz.model.archivejob.PackageUploadNotificationJob"> |
69 | + <allow interface="lp.soyuz.interfaces.archivejob.IArchiveJob"/> |
70 | + <allow interface="lp.soyuz.interfaces.archivejob.IPackageUploadNotificationJob"/> |
71 | + </class> |
72 | <securedutility |
73 | component="lp.soyuz.model.archivejob.PackageUploadNotificationJob" |
74 | provides="lp.soyuz.interfaces.archivejob.IPackageUploadNotificationJobSource"> |
75 | <allow interface="lp.soyuz.interfaces.archivejob.IPackageUploadNotificationJobSource"/> |
76 | </securedutility> |
77 | |
78 | + <class class="lp.soyuz.model.archivejob.CIBuildUploadJob"> |
79 | + <allow interface="lp.soyuz.interfaces.archivejob.IArchiveJob"/> |
80 | + <allow interface="lp.soyuz.interfaces.archivejob.ICIBuildUploadJob"/> |
81 | + </class> |
82 | + <securedutility |
83 | + component="lp.soyuz.model.archivejob.CIBuildUploadJob" |
84 | + provides="lp.soyuz.interfaces.archivejob.ICIBuildUploadJobSource"> |
85 | + <allow interface="lp.soyuz.interfaces.archivejob.ICIBuildUploadJobSource"/> |
86 | + </securedutility> |
87 | + |
88 | <!-- ArchivePermission --> |
89 | |
90 | <class |
91 | diff --git a/lib/lp/soyuz/enums.py b/lib/lp/soyuz/enums.py |
92 | index 9953f43..39b8ceb 100644 |
93 | --- a/lib/lp/soyuz/enums.py |
94 | +++ b/lib/lp/soyuz/enums.py |
95 | @@ -57,6 +57,12 @@ class ArchiveJobType(DBEnumeratedType): |
96 | or held for approval. |
97 | """) |
98 | |
99 | + CI_BUILD_UPLOAD = DBItem(2, """ |
100 | + CI build upload |
101 | + |
102 | + Upload a CI build to this archive. |
103 | + """) |
104 | + |
105 | |
106 | class ArchivePermissionType(DBEnumeratedType): |
107 | """Archive Permission Type. |
108 | diff --git a/lib/lp/soyuz/interfaces/archive.py b/lib/lp/soyuz/interfaces/archive.py |
109 | index e011591..5ee645a 100644 |
110 | --- a/lib/lp/soyuz/interfaces/archive.py |
111 | +++ b/lib/lp/soyuz/interfaces/archive.py |
112 | @@ -1672,6 +1672,18 @@ class IArchiveView(IHasBuildRecords): |
113 | :raises CannotCopy: if there is a problem copying. |
114 | """ |
115 | |
116 | + @call_with(person=REQUEST_USER) |
117 | + @operation_parameters( |
118 | + # Really ICIBuild, patched in _schema_circular_imports. |
119 | + ci_build=Reference(schema=Interface), |
120 | + to_series=TextLine(title=_("Target distroseries name")), |
121 | + to_pocket=TextLine(title=_("Target pocket name")), |
122 | + to_channel=TextLine(title=_("Target channel"), required=False)) |
123 | + @export_write_operation() |
124 | + @operation_for_version('devel') |
125 | + def uploadCIBuild(ci_build, person, to_series, to_pocket, to_channel=None): |
126 | + """Upload the output of a CI build to this archive.""" |
127 | + |
128 | |
129 | class IArchiveAppend(Interface): |
130 | """Archive interface for operations restricted by append privilege.""" |
131 | diff --git a/lib/lp/soyuz/interfaces/archivejob.py b/lib/lp/soyuz/interfaces/archivejob.py |
132 | index 779bc0b..ae25b08 100644 |
133 | --- a/lib/lp/soyuz/interfaces/archivejob.py |
134 | +++ b/lib/lp/soyuz/interfaces/archivejob.py |
135 | @@ -6,21 +6,29 @@ |
136 | __all__ = [ |
137 | 'IArchiveJob', |
138 | 'IArchiveJobSource', |
139 | + 'ICIBuildUploadJob', |
140 | + 'ICIBuildUploadJobSource', |
141 | 'IPackageUploadNotificationJob', |
142 | 'IPackageUploadNotificationJobSource', |
143 | ] |
144 | |
145 | |
146 | +from lazr.restful.fields import Reference |
147 | from zope.interface import ( |
148 | Attribute, |
149 | Interface, |
150 | ) |
151 | from zope.schema import ( |
152 | + Choice, |
153 | Int, |
154 | Object, |
155 | + TextLine, |
156 | ) |
157 | |
158 | from lp import _ |
159 | +from lp.code.interfaces.cibuild import ICIBuild |
160 | +from lp.registry.interfaces.distroseries import IDistroSeries |
161 | +from lp.registry.interfaces.pocket import PackagePublishingPocket |
162 | from lp.services.job.interfaces.job import ( |
163 | IJob, |
164 | IJobSource, |
165 | @@ -62,3 +70,26 @@ class IPackageUploadNotificationJob(IRunnableJob): |
166 | |
167 | class IPackageUploadNotificationJobSource(IArchiveJobSource): |
168 | """Interface for acquiring PackageUploadNotificationJobs.""" |
169 | + |
170 | + |
171 | +class ICIBuildUploadJob(IRunnableJob): |
172 | + """A Job to upload a CI build to an archive.""" |
173 | + |
174 | + ci_build = Reference( |
175 | + schema=ICIBuild, title=_("CI build to copy"), |
176 | + required=True, readonly=True) |
177 | + |
178 | + target_distroseries = Reference( |
179 | + schema=IDistroSeries, title=_("Target distroseries"), |
180 | + required=True, readonly=True) |
181 | + |
182 | + target_pocket = Choice( |
183 | + title=_("Target pocket"), vocabulary=PackagePublishingPocket, |
184 | + required=True, readonly=True) |
185 | + |
186 | + target_channel = TextLine( |
187 | + title=_("Target channel"), required=False, readonly=True) |
188 | + |
189 | + |
190 | +class ICIBuildUploadJobSource(IArchiveJobSource): |
191 | + """Interface for acquiring `CIBuildUploadJob`s.""" |
192 | diff --git a/lib/lp/soyuz/model/archive.py b/lib/lp/soyuz/model/archive.py |
193 | index 727d8b2..4d1ccb1 100644 |
194 | --- a/lib/lp/soyuz/model/archive.py |
195 | +++ b/lib/lp/soyuz/model/archive.py |
196 | @@ -187,6 +187,7 @@ from lp.soyuz.interfaces.archive import ( |
197 | VersionRequiresName, |
198 | ) |
199 | from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthTokenSet |
200 | +from lp.soyuz.interfaces.archivejob import ICIBuildUploadJobSource |
201 | from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet |
202 | from lp.soyuz.interfaces.archivesubscriber import ( |
203 | ArchiveSubscriptionError, |
204 | @@ -1990,6 +1991,34 @@ class Archive(SQLBase): |
205 | sources, self, series, pocket, include_binaries, person=person, |
206 | check_permissions=False, unembargo=True) |
207 | |
208 | + def uploadCIBuild(self, ci_build, person, to_series, to_pocket, |
209 | + to_channel=None): |
210 | + """See `IArchive`.""" |
211 | + series = self._text_to_series(to_series) |
212 | + pocket = self._text_to_pocket(to_pocket) |
213 | + if self.publishing_method != ArchivePublishingMethod.ARTIFACTORY: |
214 | + raise CannotCopy( |
215 | + "CI builds may only be uploaded to archives published using " |
216 | + "Artifactory.") |
217 | + if ci_build.status != BuildStatus.FULLYBUILT: |
218 | + raise CannotCopy( |
219 | + "%r has status '%s', not '%s'." % |
220 | + (ci_build, ci_build.status.title, BuildStatus.FULLYBUILT)) |
221 | + # Check upload permissions. We don't know the package name until we |
222 | + # actually run the job; however, per-package upload permissions are |
223 | + # by source package name, so don't necessarily make sense for CI |
224 | + # builds anyway. For now, just ignore per-package upload |
225 | + # permissions. |
226 | + reason = self.checkUpload( |
227 | + person=person, distroseries=series, sourcepackagename=None, |
228 | + component=None, pocket=pocket) |
229 | + if reason is not None: |
230 | + raise CannotCopy(reason) |
231 | + getUtility(ICIBuildUploadJobSource).create( |
232 | + ci_build=ci_build, requester=person, target_archive=self, |
233 | + target_distroseries=series, target_pocket=pocket, |
234 | + target_channel=to_channel) |
235 | + |
236 | def getAuthToken(self, person): |
237 | """See `IArchive`.""" |
238 | |
239 | diff --git a/lib/lp/soyuz/model/archivejob.py b/lib/lp/soyuz/model/archivejob.py |
240 | index e6785c6..eb918c1 100644 |
241 | --- a/lib/lp/soyuz/model/archivejob.py |
242 | +++ b/lib/lp/soyuz/model/archivejob.py |
243 | @@ -3,20 +3,29 @@ |
244 | |
245 | import io |
246 | import logging |
247 | +import os.path |
248 | +import tempfile |
249 | |
250 | from lazr.delegates import delegate_to |
251 | +from pkginfo import Wheel |
252 | from storm.expr import And |
253 | from storm.locals import ( |
254 | Int, |
255 | JSON, |
256 | Reference, |
257 | ) |
258 | +from wheel_filename import parse_wheel_filename |
259 | from zope.component import getUtility |
260 | from zope.interface import ( |
261 | implementer, |
262 | provider, |
263 | ) |
264 | |
265 | +from lp.code.enums import RevisionStatusArtifactType |
266 | +from lp.code.interfaces.cibuild import ICIBuildSet |
267 | +from lp.code.interfaces.revisionstatus import IRevisionStatusArtifactSet |
268 | +from lp.registry.interfaces.distroseries import IDistroSeriesSet |
269 | +from lp.registry.interfaces.pocket import PackagePublishingPocket |
270 | from lp.services.config import config |
271 | from lp.services.database.enumcol import DBEnum |
272 | from lp.services.database.interfaces import IMasterStore |
273 | @@ -26,16 +35,22 @@ from lp.services.job.model.job import ( |
274 | Job, |
275 | ) |
276 | from lp.services.job.runner import BaseRunnableJob |
277 | +from lp.services.librarian.utils import copy_and_close |
278 | from lp.soyuz.enums import ( |
279 | ArchiveJobType, |
280 | + BinaryPackageFormat, |
281 | PackageUploadStatus, |
282 | ) |
283 | from lp.soyuz.interfaces.archivejob import ( |
284 | IArchiveJob, |
285 | IArchiveJobSource, |
286 | + ICIBuildUploadJob, |
287 | + ICIBuildUploadJobSource, |
288 | IPackageUploadNotificationJob, |
289 | IPackageUploadNotificationJobSource, |
290 | ) |
291 | +from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet |
292 | +from lp.soyuz.interfaces.publishing import IPublishingSet |
293 | from lp.soyuz.interfaces.queue import IPackageUploadSet |
294 | from lp.soyuz.model.archive import Archive |
295 | |
296 | @@ -165,3 +180,115 @@ class PackageUploadNotificationJob(ArchiveJobDerived): |
297 | packageupload.notify( |
298 | status=self.packageupload_status, summary_text=self.summary_text, |
299 | changes_file_object=changes_file_object, logger=logger) |
300 | + |
301 | + |
302 | +class ScanException(Exception): |
303 | + """A CI build upload job failed to scan a file.""" |
304 | + |
305 | + |
306 | +@implementer(ICIBuildUploadJob) |
307 | +@provider(ICIBuildUploadJobSource) |
308 | +class CIBuildUploadJob(ArchiveJobDerived): |
309 | + |
310 | + class_job_type = ArchiveJobType.CI_BUILD_UPLOAD |
311 | + |
312 | + config = config.ICIBuildUploadJobSource |
313 | + |
314 | + @classmethod |
315 | + def create(cls, ci_build, requester, target_archive, target_distroseries, |
316 | + target_pocket, target_channel=None): |
317 | + """See `ICIBuildUploadJobSource`.""" |
318 | + metadata = { |
319 | + "ci_build_id": ci_build.id, |
320 | + "target_distroseries_id": target_distroseries.id, |
321 | + "target_pocket": target_pocket.title, |
322 | + "target_channel": target_channel, |
323 | + } |
324 | + derived = super().create(target_archive, metadata) |
325 | + derived.job.requester = requester |
326 | + return derived |
327 | + |
328 | + def getOopsVars(self): |
329 | + vars = super().getOopsVars() |
330 | + vars.extend([ |
331 | + (key, self.metadata[key]) |
332 | + for key in ( |
333 | + "ci_build_id", |
334 | + "target_distroseries_id", |
335 | + "target_pocket", |
336 | + "target_channel", |
337 | + )]) |
338 | + return vars |
339 | + |
340 | + @property |
341 | + def ci_build(self): |
342 | + return getUtility(ICIBuildSet).getByID(self.metadata["ci_build_id"]) |
343 | + |
344 | + @property |
345 | + def target_distroseries(self): |
346 | + return getUtility(IDistroSeriesSet).get( |
347 | + self.metadata["target_distroseries_id"]) |
348 | + |
349 | + @property |
350 | + def target_pocket(self): |
351 | + return PackagePublishingPocket.getTermByToken( |
352 | + self.metadata["target_pocket"]).value |
353 | + |
354 | + @property |
355 | + def target_channel(self): |
356 | + return self.metadata["target_channel"] |
357 | + |
358 | + def _scanFile(self, path): |
359 | + if path.endswith(".whl"): |
360 | + try: |
361 | + parsed_path = parse_wheel_filename(path) |
362 | + wheel = Wheel(path) |
363 | + except Exception as e: |
364 | + raise ScanException("Failed to scan %s" % path) from e |
365 | + return { |
366 | + "name": wheel.name, |
367 | + "version": wheel.version, |
368 | + "summary": wheel.summary or "", |
369 | + "description": wheel.description, |
370 | + "binpackageformat": BinaryPackageFormat.WHL, |
371 | + "architecturespecific": "any" not in parsed_path.platform_tags, |
372 | + "homepage": wheel.home_page or "", |
373 | + } |
374 | + else: |
375 | + return None |
376 | + |
377 | + def run(self): |
378 | + """See `IRunnableJob`.""" |
379 | + logger = logging.getLogger() |
380 | + with tempfile.TemporaryDirectory(prefix="ci-build-copy-job") as tmpdir: |
381 | + binaries = {} |
382 | + for artifact in getUtility( |
383 | + IRevisionStatusArtifactSet).findByCIBuild(self.ci_build): |
384 | + if artifact.artifact_type == RevisionStatusArtifactType.LOG: |
385 | + continue |
386 | + name = artifact.library_file.filename |
387 | + contents = os.path.join(tmpdir, name) |
388 | + artifact.library_file.open() |
389 | + copy_and_close(artifact.library_file, open(contents, "wb")) |
390 | + metadata = self._scanFile(contents) |
391 | + if metadata is None: |
392 | + logger.info("No upload handler for %s" % name) |
393 | + continue |
394 | + logger.info( |
395 | + "Uploading %s to %s %s (%s)" % ( |
396 | + name, self.archive.reference, |
397 | + self.target_distroseries.getSuite(self.target_pocket), |
398 | + self.target_channel)) |
399 | + metadata["binarypackagename"] = ( |
400 | + getUtility(IBinaryPackageNameSet).ensure(metadata["name"])) |
401 | + del metadata["name"] |
402 | + bpr = self.ci_build.createBinaryPackageRelease(**metadata) |
403 | + bpr.addFile(artifact.library_file) |
404 | + # The publishBinaries interface was designed for .debs, |
405 | + # which need extra per-binary "override" information |
406 | + # (component, etc.). None of this is relevant here. |
407 | + binaries[bpr] = (None, None, None, None) |
408 | + if binaries: |
409 | + getUtility(IPublishingSet).publishBinaries( |
410 | + self.archive, self.target_distroseries, self.target_pocket, |
411 | + binaries, channel=self.target_channel) |
412 | diff --git a/lib/lp/soyuz/model/publishing.py b/lib/lp/soyuz/model/publishing.py |
413 | index 8a397b6..b229c47 100644 |
414 | --- a/lib/lp/soyuz/model/publishing.py |
415 | +++ b/lib/lp/soyuz/model/publishing.py |
416 | @@ -1097,7 +1097,7 @@ def expand_binary_requests(distroseries, binaries): |
417 | # Find the DAS in this series corresponding to the original |
418 | # build arch tag. If it does not exist or is disabled, we should |
419 | # not publish. |
420 | - target_arch = arch_map.get(bpr.build.arch_tag) |
421 | + target_arch = arch_map.get((bpr.build or bpr.ci_build).arch_tag) |
422 | target_archs = [target_arch] if target_arch is not None else [] |
423 | else: |
424 | target_archs = archs |
425 | diff --git a/lib/lp/soyuz/tests/__init__.py b/lib/lp/soyuz/tests/__init__.py |
426 | index e69de29..7ffe3f9 100644 |
427 | --- a/lib/lp/soyuz/tests/__init__.py |
428 | +++ b/lib/lp/soyuz/tests/__init__.py |
429 | @@ -0,0 +1,14 @@ |
430 | +# Copyright 2022 Canonical Ltd. This software is licensed under the |
431 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
432 | + |
433 | +import os |
434 | + |
435 | + |
436 | +here = os.path.dirname(os.path.realpath(__file__)) |
437 | + |
438 | + |
439 | +def datadir(path): |
440 | + """Return fully-qualified path inside the test data directory.""" |
441 | + if path.startswith("/"): |
442 | + raise ValueError("Path is not relative: %s" % path) |
443 | + return os.path.join(here, "data", path) |
444 | diff --git a/lib/lp/soyuz/tests/data/.gitignore b/lib/lp/soyuz/tests/data/.gitignore |
445 | new file mode 100644 |
446 | index 0000000..2955230 |
447 | --- /dev/null |
448 | +++ b/lib/lp/soyuz/tests/data/.gitignore |
449 | @@ -0,0 +1,2 @@ |
450 | +!dist |
451 | +*.egg-info |
452 | diff --git a/lib/lp/soyuz/tests/data/wheel-arch/README.md b/lib/lp/soyuz/tests/data/wheel-arch/README.md |
453 | new file mode 100644 |
454 | index 0000000..3135dc3 |
455 | --- /dev/null |
456 | +++ b/lib/lp/soyuz/tests/data/wheel-arch/README.md |
457 | @@ -0,0 +1 @@ |
458 | +An example package. Build a wheel from this with `pyproject-build`. |
459 | diff --git a/lib/lp/soyuz/tests/data/wheel-arch/dist/wheel-arch-0.0.1.tar.gz b/lib/lp/soyuz/tests/data/wheel-arch/dist/wheel-arch-0.0.1.tar.gz |
460 | new file mode 100644 |
461 | index 0000000..5b61840 |
462 | Binary files /dev/null and b/lib/lp/soyuz/tests/data/wheel-arch/dist/wheel-arch-0.0.1.tar.gz differ |
463 | diff --git a/lib/lp/soyuz/tests/data/wheel-arch/dist/wheel_arch-0.0.1-cp310-cp310-linux_x86_64.whl b/lib/lp/soyuz/tests/data/wheel-arch/dist/wheel_arch-0.0.1-cp310-cp310-linux_x86_64.whl |
464 | new file mode 100644 |
465 | index 0000000..2131f4d |
466 | Binary files /dev/null and b/lib/lp/soyuz/tests/data/wheel-arch/dist/wheel_arch-0.0.1-cp310-cp310-linux_x86_64.whl differ |
467 | diff --git a/lib/lp/soyuz/tests/data/wheel-arch/pyproject.toml b/lib/lp/soyuz/tests/data/wheel-arch/pyproject.toml |
468 | new file mode 100644 |
469 | index 0000000..b0f0765 |
470 | --- /dev/null |
471 | +++ b/lib/lp/soyuz/tests/data/wheel-arch/pyproject.toml |
472 | @@ -0,0 +1,3 @@ |
473 | +[build-system] |
474 | +requires = ["setuptools>=42"] |
475 | +build-backend = "setuptools.build_meta" |
476 | diff --git a/lib/lp/soyuz/tests/data/wheel-arch/setup.py b/lib/lp/soyuz/tests/data/wheel-arch/setup.py |
477 | new file mode 100755 |
478 | index 0000000..d1431d7 |
479 | --- /dev/null |
480 | +++ b/lib/lp/soyuz/tests/data/wheel-arch/setup.py |
481 | @@ -0,0 +1,14 @@ |
482 | +from setuptools import ( |
483 | + Extension, |
484 | + setup, |
485 | + ) |
486 | + |
487 | + |
488 | +setup( |
489 | + name="wheel-arch", |
490 | + version="0.0.1", |
491 | + description="Example description", |
492 | + long_description="Example long description", |
493 | + url="http://example.com/", |
494 | + ext_modules=[Extension("_test", sources=["test.c"])], |
495 | + ) |
496 | diff --git a/lib/lp/soyuz/tests/data/wheel-arch/test.c b/lib/lp/soyuz/tests/data/wheel-arch/test.c |
497 | new file mode 100644 |
498 | index 0000000..576fc6d |
499 | --- /dev/null |
500 | +++ b/lib/lp/soyuz/tests/data/wheel-arch/test.c |
501 | @@ -0,0 +1 @@ |
502 | +#include <Python.h> |
503 | diff --git a/lib/lp/soyuz/tests/data/wheel-indep/README.md b/lib/lp/soyuz/tests/data/wheel-indep/README.md |
504 | new file mode 100644 |
505 | index 0000000..3135dc3 |
506 | --- /dev/null |
507 | +++ b/lib/lp/soyuz/tests/data/wheel-indep/README.md |
508 | @@ -0,0 +1 @@ |
509 | +An example package. Build a wheel from this with `pyproject-build`. |
510 | diff --git a/lib/lp/soyuz/tests/data/wheel-indep/dist/wheel-indep-0.0.1.tar.gz b/lib/lp/soyuz/tests/data/wheel-indep/dist/wheel-indep-0.0.1.tar.gz |
511 | new file mode 100644 |
512 | index 0000000..16fc042 |
513 | Binary files /dev/null and b/lib/lp/soyuz/tests/data/wheel-indep/dist/wheel-indep-0.0.1.tar.gz differ |
514 | diff --git a/lib/lp/soyuz/tests/data/wheel-indep/dist/wheel_indep-0.0.1-py3-none-any.whl b/lib/lp/soyuz/tests/data/wheel-indep/dist/wheel_indep-0.0.1-py3-none-any.whl |
515 | new file mode 100644 |
516 | index 0000000..93b54bd |
517 | Binary files /dev/null and b/lib/lp/soyuz/tests/data/wheel-indep/dist/wheel_indep-0.0.1-py3-none-any.whl differ |
518 | diff --git a/lib/lp/soyuz/tests/data/wheel-indep/pyproject.toml b/lib/lp/soyuz/tests/data/wheel-indep/pyproject.toml |
519 | new file mode 100644 |
520 | index 0000000..b0f0765 |
521 | --- /dev/null |
522 | +++ b/lib/lp/soyuz/tests/data/wheel-indep/pyproject.toml |
523 | @@ -0,0 +1,3 @@ |
524 | +[build-system] |
525 | +requires = ["setuptools>=42"] |
526 | +build-backend = "setuptools.build_meta" |
527 | diff --git a/lib/lp/soyuz/tests/data/wheel-indep/setup.cfg b/lib/lp/soyuz/tests/data/wheel-indep/setup.cfg |
528 | new file mode 100644 |
529 | index 0000000..ae136d3 |
530 | --- /dev/null |
531 | +++ b/lib/lp/soyuz/tests/data/wheel-indep/setup.cfg |
532 | @@ -0,0 +1,5 @@ |
533 | +[metadata] |
534 | +name = wheel-indep |
535 | +version = 0.0.1 |
536 | +description = Example description |
537 | +long_description = Example long description |
538 | diff --git a/lib/lp/soyuz/tests/test_archive.py b/lib/lp/soyuz/tests/test_archive.py |
539 | index a02d75d..fd3d254 100644 |
540 | --- a/lib/lp/soyuz/tests/test_archive.py |
541 | +++ b/lib/lp/soyuz/tests/test_archive.py |
542 | @@ -88,6 +88,7 @@ from lp.soyuz.adapters.overrides import ( |
543 | ) |
544 | from lp.soyuz.enums import ( |
545 | ArchivePermissionType, |
546 | + ArchivePublishingMethod, |
547 | ArchivePurpose, |
548 | ArchiveStatus, |
549 | PackageCopyPolicy, |
550 | @@ -116,6 +117,7 @@ from lp.soyuz.interfaces.archive import ( |
551 | RedirectedPocket, |
552 | VersionRequiresName, |
553 | ) |
554 | +from lp.soyuz.interfaces.archivejob import ICIBuildUploadJobSource |
555 | from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet |
556 | from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus |
557 | from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet |
558 | @@ -3446,6 +3448,71 @@ class TestCopyPackage(TestCaseWithFactory): |
559 | person=source_archive.owner, move=True) |
560 | |
561 | |
562 | +class TestUploadCIBuild(TestCaseWithFactory): |
563 | + |
564 | + layer = DatabaseFunctionalLayer |
565 | + |
566 | + def test_creates_job(self): |
567 | + # The uploadCIBuild method creates a CIBuildUploadJob with the |
568 | + # appropriate parameters. |
569 | + archive = self.factory.makeArchive( |
570 | + publishing_method=ArchivePublishingMethod.ARTIFACTORY) |
571 | + series = self.factory.makeDistroSeries( |
572 | + distribution=archive.distribution) |
573 | + build = self.factory.makeCIBuild(status=BuildStatus.FULLYBUILT) |
574 | + with person_logged_in(archive.owner): |
575 | + archive.uploadCIBuild( |
576 | + build, archive.owner, series.name, "Release", |
577 | + to_channel="edge") |
578 | + [job] = getUtility(ICIBuildUploadJobSource).iterReady() |
579 | + self.assertThat(job, MatchesStructure.byEquality( |
580 | + ci_build=build, |
581 | + target_distroseries=series, |
582 | + target_pocket=PackagePublishingPocket.RELEASE, |
583 | + target_channel="edge")) |
584 | + |
585 | + def test_disallows_non_artifactory_publishing(self): |
586 | + # CI builds may only be copied into archives published using |
587 | + # Artifactory. |
588 | + archive = self.factory.makeArchive() |
589 | + series = self.factory.makeDistroSeries( |
590 | + distribution=archive.distribution) |
591 | + build = self.factory.makeCIBuild(status=BuildStatus.FULLYBUILT) |
592 | + with person_logged_in(archive.owner): |
593 | + self.assertRaisesWithContent( |
594 | + CannotCopy, |
595 | + "CI builds may only be uploaded to archives published using " |
596 | + "Artifactory.", |
597 | + archive.uploadCIBuild, |
598 | + build, archive.owner, series.name, "Release") |
599 | + |
600 | + def test_disallows_incomplete_builds(self): |
601 | + # CI builds with statuses other than FULLYBUILT may not be copied. |
602 | + archive = self.factory.makeArchive( |
603 | + publishing_method=ArchivePublishingMethod.ARTIFACTORY) |
604 | + series = self.factory.makeDistroSeries( |
605 | + distribution=archive.distribution) |
606 | + build = self.factory.makeCIBuild(status=BuildStatus.FAILEDTOBUILD) |
607 | + person = self.factory.makePerson() |
608 | + self.assertRaisesWithContent( |
609 | + CannotCopy, |
610 | + "%r has status 'Failed to build', not 'Successfully built'." % ( |
611 | + build), |
612 | + archive.uploadCIBuild, build, person, series.name, "Release") |
613 | + |
614 | + def test_disallows_non_uploaders(self): |
615 | + # Only people with upload permission may call uploadCIBuild. |
616 | + archive = self.factory.makeArchive( |
617 | + publishing_method=ArchivePublishingMethod.ARTIFACTORY) |
618 | + series = self.factory.makeDistroSeries( |
619 | + distribution=archive.distribution) |
620 | + build = self.factory.makeCIBuild(status=BuildStatus.FULLYBUILT) |
621 | + person = self.factory.makePerson() |
622 | + self.assertRaisesWithContent( |
623 | + CannotCopy, "Signer has no upload rights to this PPA.", |
624 | + archive.uploadCIBuild, build, person, series.name, "Release") |
625 | + |
626 | + |
627 | class TestgetAllPublishedBinaries(TestCaseWithFactory): |
628 | |
629 | layer = DatabaseFunctionalLayer |
630 | diff --git a/lib/lp/soyuz/tests/test_archivejob.py b/lib/lp/soyuz/tests/test_archivejob.py |
631 | index f00e606..e6c1c28 100644 |
632 | --- a/lib/lp/soyuz/tests/test_archivejob.py |
633 | +++ b/lib/lp/soyuz/tests/test_archivejob.py |
634 | @@ -1,19 +1,36 @@ |
635 | # Copyright 2010-2018 Canonical Ltd. This software is licensed under the |
636 | # GNU Affero General Public License version 3 (see the file LICENSE). |
637 | |
638 | +import os.path |
639 | + |
640 | from debian.deb822 import Changes |
641 | +from testtools.matchers import ( |
642 | + Equals, |
643 | + Is, |
644 | + MatchesSetwise, |
645 | + MatchesStructure, |
646 | + ) |
647 | +import transaction |
648 | |
649 | +from lp.code.enums import RevisionStatusArtifactType |
650 | +from lp.code.model.revisionstatus import RevisionStatusArtifact |
651 | +from lp.registry.interfaces.pocket import PackagePublishingPocket |
652 | +from lp.services.database.interfaces import IStore |
653 | from lp.services.job.runner import JobRunner |
654 | from lp.services.mail.sendmail import format_address_for_person |
655 | from lp.soyuz.enums import ( |
656 | ArchiveJobType, |
657 | + BinaryPackageFileType, |
658 | + BinaryPackageFormat, |
659 | PackageUploadStatus, |
660 | ) |
661 | from lp.soyuz.model.archivejob import ( |
662 | ArchiveJob, |
663 | ArchiveJobDerived, |
664 | + CIBuildUploadJob, |
665 | PackageUploadNotificationJob, |
666 | ) |
667 | +from lp.soyuz.tests import datadir |
668 | from lp.testing import TestCaseWithFactory |
669 | from lp.testing.dbuser import dbuser |
670 | from lp.testing.layers import ( |
671 | @@ -117,3 +134,181 @@ class TestPackageUploadNotificationJob(TestCaseWithFactory): |
672 | self.assertEqual(format_address_for_person(creator), email['To']) |
673 | self.assertIn('(Accepted)', email['Subject']) |
674 | self.assertIn('Fake summary', email.get_payload()[0].get_payload()) |
675 | + |
676 | + |
677 | +class TestCIBuildUploadJob(TestCaseWithFactory): |
678 | + |
679 | + layer = LaunchpadZopelessLayer |
680 | + |
681 | + def test_getOopsVars(self): |
682 | + archive = self.factory.makeArchive() |
683 | + distroseries = self.factory.makeDistroSeries( |
684 | + distribution=archive.distribution) |
685 | + build = self.factory.makeCIBuild() |
686 | + job = CIBuildUploadJob.create( |
687 | + build, build.git_repository.owner, archive, distroseries, |
688 | + PackagePublishingPocket.RELEASE, target_channel="edge") |
689 | + expected = [ |
690 | + ("job_id", job.context.job.id), |
691 | + ("archive_id", archive.id), |
692 | + ("archive_job_id", job.context.id), |
693 | + ("archive_job_type", "CI build upload"), |
694 | + ("ci_build_id", build.id), |
695 | + ("target_distroseries_id", distroseries.id), |
696 | + ("target_pocket", "Release"), |
697 | + ("target_channel", "edge"), |
698 | + ] |
699 | + self.assertEqual(expected, job.getOopsVars()) |
700 | + |
701 | + def test_metadata(self): |
702 | + archive = self.factory.makeArchive() |
703 | + distroseries = self.factory.makeDistroSeries( |
704 | + distribution=archive.distribution) |
705 | + build = self.factory.makeCIBuild() |
706 | + job = CIBuildUploadJob.create( |
707 | + build, build.git_repository.owner, archive, distroseries, |
708 | + PackagePublishingPocket.RELEASE, target_channel="edge") |
709 | + expected = { |
710 | + "ci_build_id": build.id, |
711 | + "target_distroseries_id": distroseries.id, |
712 | + "target_pocket": "Release", |
713 | + "target_channel": "edge", |
714 | + } |
715 | + self.assertEqual(expected, job.metadata) |
716 | + self.assertEqual(build, job.ci_build) |
717 | + self.assertEqual(distroseries, job.target_distroseries) |
718 | + self.assertEqual(PackagePublishingPocket.RELEASE, job.target_pocket) |
719 | + self.assertEqual("edge", job.target_channel) |
720 | + |
721 | + def test__scanFile_wheel_indep(self): |
722 | + archive = self.factory.makeArchive() |
723 | + distroseries = self.factory.makeDistroSeries( |
724 | + distribution=archive.distribution) |
725 | + build = self.factory.makeCIBuild() |
726 | + job = CIBuildUploadJob.create( |
727 | + build, build.git_repository.owner, archive, distroseries, |
728 | + PackagePublishingPocket.RELEASE, target_channel="edge") |
729 | + path = "wheel-indep/dist/wheel_indep-0.0.1-py3-none-any.whl" |
730 | + expected = { |
731 | + "name": "wheel-indep", |
732 | + "version": "0.0.1", |
733 | + "summary": "Example description", |
734 | + "description": "Example long description\n", |
735 | + "binpackageformat": BinaryPackageFormat.WHL, |
736 | + "architecturespecific": False, |
737 | + "homepage": "", |
738 | + } |
739 | + self.assertEqual(expected, job._scanFile(datadir(path))) |
740 | + |
741 | + def test__scanFile_wheel_arch(self): |
742 | + archive = self.factory.makeArchive() |
743 | + distroseries = self.factory.makeDistroSeries( |
744 | + distribution=archive.distribution) |
745 | + build = self.factory.makeCIBuild() |
746 | + job = CIBuildUploadJob.create( |
747 | + build, build.git_repository.owner, archive, distroseries, |
748 | + PackagePublishingPocket.RELEASE, target_channel="edge") |
749 | + path = "wheel-arch/dist/wheel_arch-0.0.1-cp310-cp310-linux_x86_64.whl" |
750 | + expected = { |
751 | + "name": "wheel-arch", |
752 | + "version": "0.0.1", |
753 | + "summary": "Example description", |
754 | + "description": "Example long description\n", |
755 | + "binpackageformat": BinaryPackageFormat.WHL, |
756 | + "architecturespecific": True, |
757 | + "homepage": "http://example.com/", |
758 | + } |
759 | + self.assertEqual(expected, job._scanFile(datadir(path))) |
760 | + |
761 | + def test_run_indep(self): |
762 | + archive = self.factory.makeArchive() |
763 | + distroseries = self.factory.makeDistroSeries( |
764 | + distribution=archive.distribution) |
765 | + dases = [ |
766 | + self.factory.makeDistroArchSeries(distroseries=distroseries) |
767 | + for _ in range(2)] |
768 | + build = self.factory.makeCIBuild(distro_arch_series=dases[0]) |
769 | + report = build.getOrCreateRevisionStatusReport("build:0") |
770 | + report.setLog(b"log data") |
771 | + path = "wheel-indep/dist/wheel_indep-0.0.1-py3-none-any.whl" |
772 | + with open(datadir(path), mode="rb") as f: |
773 | + report.attach(name=os.path.basename(path), data=f.read()) |
774 | + artifact = IStore(RevisionStatusArtifact).find( |
775 | + RevisionStatusArtifact, |
776 | + report=report, |
777 | + artifact_type=RevisionStatusArtifactType.BINARY).one() |
778 | + job = CIBuildUploadJob.create( |
779 | + build, build.git_repository.owner, archive, distroseries, |
780 | + PackagePublishingPocket.RELEASE, target_channel="edge") |
781 | + transaction.commit() |
782 | + |
783 | + with dbuser(job.config.dbuser): |
784 | + JobRunner([job]).runAll() |
785 | + |
786 | + self.assertThat(archive.getAllPublishedBinaries(), MatchesSetwise(*( |
787 | + MatchesStructure( |
788 | + binarypackagename=MatchesStructure.byEquality( |
789 | + name="wheel-indep"), |
790 | + binarypackagerelease=MatchesStructure( |
791 | + ci_build=Equals(build), |
792 | + binarypackagename=MatchesStructure.byEquality( |
793 | + name="wheel-indep"), |
794 | + version=Equals("0.0.1"), |
795 | + summary=Equals("Example description"), |
796 | + description=Equals("Example long description\n"), |
797 | + binpackageformat=Equals(BinaryPackageFormat.WHL), |
798 | + architecturespecific=Is(False), |
799 | + homepage=Equals(""), |
800 | + files=MatchesSetwise( |
801 | + MatchesStructure.byEquality( |
802 | + libraryfile=artifact.library_file, |
803 | + filetype=BinaryPackageFileType.WHL))), |
804 | + binarypackageformat=Equals(BinaryPackageFormat.WHL), |
805 | + distroarchseries=Equals(das)) |
806 | + for das in dases))) |
807 | + |
808 | + def test_run_arch(self): |
809 | + archive = self.factory.makeArchive() |
810 | + distroseries = self.factory.makeDistroSeries( |
811 | + distribution=archive.distribution) |
812 | + dases = [ |
813 | + self.factory.makeDistroArchSeries(distroseries=distroseries) |
814 | + for _ in range(2)] |
815 | + build = self.factory.makeCIBuild(distro_arch_series=dases[0]) |
816 | + report = build.getOrCreateRevisionStatusReport("build:0") |
817 | + report.setLog(b"log data") |
818 | + path = "wheel-arch/dist/wheel_arch-0.0.1-cp310-cp310-linux_x86_64.whl" |
819 | + with open(datadir(path), mode="rb") as f: |
820 | + report.attach(name=os.path.basename(path), data=f.read()) |
821 | + artifact = IStore(RevisionStatusArtifact).find( |
822 | + RevisionStatusArtifact, |
823 | + report=report, |
824 | + artifact_type=RevisionStatusArtifactType.BINARY).one() |
825 | + job = CIBuildUploadJob.create( |
826 | + build, build.git_repository.owner, archive, distroseries, |
827 | + PackagePublishingPocket.RELEASE, target_channel="edge") |
828 | + transaction.commit() |
829 | + |
830 | + with dbuser(job.config.dbuser): |
831 | + JobRunner([job]).runAll() |
832 | + |
833 | + self.assertThat(archive.getAllPublishedBinaries(), MatchesSetwise( |
834 | + MatchesStructure( |
835 | + binarypackagename=MatchesStructure.byEquality( |
836 | + name="wheel-arch"), |
837 | + binarypackagerelease=MatchesStructure( |
838 | + ci_build=Equals(build), |
839 | + binarypackagename=MatchesStructure.byEquality( |
840 | + name="wheel-arch"), |
841 | + version=Equals("0.0.1"), |
842 | + summary=Equals("Example description"), |
843 | + description=Equals("Example long description\n"), |
844 | + binpackageformat=Equals(BinaryPackageFormat.WHL), |
845 | + architecturespecific=Is(True), |
846 | + homepage=Equals("http://example.com/"), |
847 | + files=MatchesSetwise( |
848 | + MatchesStructure.byEquality( |
849 | + libraryfile=artifact.library_file, |
850 | + filetype=BinaryPackageFileType.WHL))), |
851 | + binarypackageformat=Equals(BinaryPackageFormat.WHL), |
852 | + distroarchseries=Equals(dases[0])))) |
853 | diff --git a/requirements/launchpad.txt b/requirements/launchpad.txt |
854 | index 981acfb..c2081c1 100644 |
855 | --- a/requirements/launchpad.txt |
856 | +++ b/requirements/launchpad.txt |
857 | @@ -109,6 +109,7 @@ PasteDeploy==2.1.0 |
858 | pathlib2==2.3.2 |
859 | patiencediff==0.2.2 |
860 | pgbouncer==0.0.9 |
861 | +pkginfo==1.8.2 |
862 | prettytable==0.7.2 |
863 | psutil==5.4.2 |
864 | psycopg2==2.8.6 |
865 | @@ -174,6 +175,7 @@ webencodings==0.5.1 |
866 | WebOb==1.8.5 |
867 | WebTest==2.0.35 |
868 | Werkzeug==1.0.1 |
869 | +wheel-filename==1.1.0 |
870 | wrapt==1.12.1 |
871 | wsgi-intercept==1.9.2 |
872 | WSGIProxy2==0.4.6 |
873 | diff --git a/setup.cfg b/setup.cfg |
874 | index db4af9b..145a832 100644 |
875 | --- a/setup.cfg |
876 | +++ b/setup.cfg |
877 | @@ -66,6 +66,7 @@ install_requires = |
878 | oops_twisted |
879 | oops_wsgi |
880 | paramiko |
881 | + pkginfo |
882 | psutil |
883 | pgbouncer |
884 | psycopg2 |
885 | @@ -110,6 +111,7 @@ install_requires = |
886 | WebOb |
887 | WebTest |
888 | Werkzeug |
889 | + wheel-filename |
890 | WSGIProxy2 |
891 | z3c.ptcompat |
892 | zope.app.http |