launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28485
[Merge] ~cjwatson/launchpad:ci-build-upload-job into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:ci-build-upload-job into launchpad:master.
Commit message:
Add method to upload a CI build to an archive
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/423198
This new `Archive.uploadCIBuild` method will be used to publish the output of CI builds in archives that publish via Artifactory. It's somewhat similar to `Archive.copyPackage`, and I considered calling it `copyCIBuild` instead, but I decided to go with "upload" since it's only usable for the initial copy into an archive, not for copies between archives after that.
There's a new `CIBuildUploadJob` which fetches the output files from the librarian and works out how to turn them into `BinaryPackageRelease`s. This is inevitably package-type-specific, and for the time being is only implemented for Python wheels, using `pkginfo` and `wheel-filename`.
Dependencies MP: https://code.launchpad.net/~cjwatson/lp-source-dependencies/+git/lp-source-dependencies/+merge/423187
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:ci-build-upload-job into launchpad:master.
diff --git a/database/schema/security.cfg b/database/schema/security.cfg
index 0024832..f04f672 100644
--- a/database/schema/security.cfg
+++ b/database/schema/security.cfg
@@ -1433,7 +1433,7 @@ public.archivesigningkey = SELECT, INSERT
public.binarypackagebuild = SELECT, INSERT, UPDATE
public.binarypackagefile = SELECT, INSERT
public.binarypackagename = SELECT, INSERT
-public.binarypackagepublishinghistory = SELECT
+public.binarypackagepublishinghistory = SELECT, INSERT
public.binarypackagerelease = SELECT, INSERT
public.binarysourcereference = SELECT, INSERT
public.bug = SELECT, UPDATE
diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py
index 1d3cb17..6bf443b 100644
--- a/lib/lp/_schema_circular_imports.py
+++ b/lib/lp/_schema_circular_imports.py
@@ -397,6 +397,7 @@ patch_plain_parameter_type(IArchive, 'syncSource', 'from_archive', IArchive)
patch_plain_parameter_type(IArchive, 'copyPackage', 'from_archive', IArchive)
patch_plain_parameter_type(
IArchive, 'copyPackages', 'from_archive', IArchive)
+patch_plain_parameter_type(IArchive, 'uploadCIBuild', 'ci_build', ICIBuild)
patch_entry_return_type(IArchive, 'newSubscription', IArchiveSubscriber)
patch_plain_parameter_type(
IArchive, 'getArchiveDependency', 'dependency', IArchive)
diff --git a/lib/lp/services/config/schema-lazr.conf b/lib/lp/services/config/schema-lazr.conf
index 45bf05a..4fc8131 100644
--- a/lib/lp/services/config/schema-lazr.conf
+++ b/lib/lp/services/config/schema-lazr.conf
@@ -1914,6 +1914,7 @@ dbuser: process-job-source-groups
# can be loaded from.
job_sources:
IBranchModifiedMailJobSource,
+ ICIBuildUploadJobSource,
ICommercialExpiredJobSource,
IExpiringMembershipNotificationJobSource,
IGitRepositoryModifiedMailJobSource,
@@ -1966,6 +1967,11 @@ module: lp.charms.interfaces.charmrecipejob
dbuser: charm-build-job
crontab_group: MAIN
+[ICIBuildUploadJobSource]
+module: lp.soyuz.interfaces.archivejob
+dbuser: uploader
+crontab_group: FREQUENT
+
[ICommercialExpiredJobSource]
module: lp.registry.interfaces.productjob
dbuser: product-job
diff --git a/lib/lp/soyuz/configure.zcml b/lib/lp/soyuz/configure.zcml
index 29cabca..7236f92 100644
--- a/lib/lp/soyuz/configure.zcml
+++ b/lib/lp/soyuz/configure.zcml
@@ -433,21 +433,32 @@
<class class="lp.soyuz.model.archivejob.ArchiveJob">
<allow interface="lp.soyuz.interfaces.archivejob.IArchiveJob"/>
</class>
- <class class="lp.soyuz.model.archivejob.PackageUploadNotificationJob">
- <allow interface="lp.soyuz.interfaces.archivejob.IArchiveJob"/>
- <allow interface="lp.soyuz.interfaces.archivejob.IPackageUploadNotificationJob"/>
- </class>
<securedutility
component="lp.soyuz.model.archivejob.ArchiveJob"
provides="lp.soyuz.interfaces.archivejob.IArchiveJobSource">
<allow interface="lp.soyuz.interfaces.archivejob.IArchiveJobSource"/>
</securedutility>
+
+ <class class="lp.soyuz.model.archivejob.PackageUploadNotificationJob">
+ <allow interface="lp.soyuz.interfaces.archivejob.IArchiveJob"/>
+ <allow interface="lp.soyuz.interfaces.archivejob.IPackageUploadNotificationJob"/>
+ </class>
<securedutility
component="lp.soyuz.model.archivejob.PackageUploadNotificationJob"
provides="lp.soyuz.interfaces.archivejob.IPackageUploadNotificationJobSource">
<allow interface="lp.soyuz.interfaces.archivejob.IPackageUploadNotificationJobSource"/>
</securedutility>
+ <class class="lp.soyuz.model.archivejob.CIBuildUploadJob">
+ <allow interface="lp.soyuz.interfaces.archivejob.IArchiveJob"/>
+ <allow interface="lp.soyuz.interfaces.archivejob.ICIBuildUploadJob"/>
+ </class>
+ <securedutility
+ component="lp.soyuz.model.archivejob.CIBuildUploadJob"
+ provides="lp.soyuz.interfaces.archivejob.ICIBuildUploadJobSource">
+ <allow interface="lp.soyuz.interfaces.archivejob.ICIBuildUploadJobSource"/>
+ </securedutility>
+
<!-- ArchivePermission -->
<class
diff --git a/lib/lp/soyuz/enums.py b/lib/lp/soyuz/enums.py
index 9953f43..39b8ceb 100644
--- a/lib/lp/soyuz/enums.py
+++ b/lib/lp/soyuz/enums.py
@@ -57,6 +57,12 @@ class ArchiveJobType(DBEnumeratedType):
or held for approval.
""")
+ CI_BUILD_UPLOAD = DBItem(2, """
+ CI build upload
+
+ Upload a CI build to this archive.
+ """)
+
class ArchivePermissionType(DBEnumeratedType):
"""Archive Permission Type.
diff --git a/lib/lp/soyuz/interfaces/archive.py b/lib/lp/soyuz/interfaces/archive.py
index e011591..5ee645a 100644
--- a/lib/lp/soyuz/interfaces/archive.py
+++ b/lib/lp/soyuz/interfaces/archive.py
@@ -1672,6 +1672,18 @@ class IArchiveView(IHasBuildRecords):
:raises CannotCopy: if there is a problem copying.
"""
+ @call_with(person=REQUEST_USER)
+ @operation_parameters(
+ # Really ICIBuild, patched in _schema_circular_imports.
+ ci_build=Reference(schema=Interface),
+ to_series=TextLine(title=_("Target distroseries name")),
+ to_pocket=TextLine(title=_("Target pocket name")),
+ to_channel=TextLine(title=_("Target channel"), required=False))
+ @export_write_operation()
+ @operation_for_version('devel')
+ def uploadCIBuild(ci_build, person, to_series, to_pocket, to_channel=None):
+ """Upload the output of a CI build to this archive."""
+
class IArchiveAppend(Interface):
"""Archive interface for operations restricted by append privilege."""
diff --git a/lib/lp/soyuz/interfaces/archivejob.py b/lib/lp/soyuz/interfaces/archivejob.py
index 779bc0b..ae25b08 100644
--- a/lib/lp/soyuz/interfaces/archivejob.py
+++ b/lib/lp/soyuz/interfaces/archivejob.py
@@ -6,21 +6,29 @@
__all__ = [
'IArchiveJob',
'IArchiveJobSource',
+ 'ICIBuildUploadJob',
+ 'ICIBuildUploadJobSource',
'IPackageUploadNotificationJob',
'IPackageUploadNotificationJobSource',
]
+from lazr.restful.fields import Reference
from zope.interface import (
Attribute,
Interface,
)
from zope.schema import (
+ Choice,
Int,
Object,
+ TextLine,
)
from lp import _
+from lp.code.interfaces.cibuild import ICIBuild
+from lp.registry.interfaces.distroseries import IDistroSeries
+from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.services.job.interfaces.job import (
IJob,
IJobSource,
@@ -62,3 +70,26 @@ class IPackageUploadNotificationJob(IRunnableJob):
class IPackageUploadNotificationJobSource(IArchiveJobSource):
"""Interface for acquiring PackageUploadNotificationJobs."""
+
+
+class ICIBuildUploadJob(IRunnableJob):
+ """A Job to upload a CI build to an archive."""
+
+ ci_build = Reference(
+ schema=ICIBuild, title=_("CI build to copy"),
+ required=True, readonly=True)
+
+ target_distroseries = Reference(
+ schema=IDistroSeries, title=_("Target distroseries"),
+ required=True, readonly=True)
+
+ target_pocket = Choice(
+ title=_("Target pocket"), vocabulary=PackagePublishingPocket,
+ required=True, readonly=True)
+
+ target_channel = TextLine(
+ title=_("Target channel"), required=False, readonly=True)
+
+
+class ICIBuildUploadJobSource(IArchiveJobSource):
+ """Interface for acquiring `CIBuildUploadJob`s."""
diff --git a/lib/lp/soyuz/model/archive.py b/lib/lp/soyuz/model/archive.py
index 727d8b2..4d1ccb1 100644
--- a/lib/lp/soyuz/model/archive.py
+++ b/lib/lp/soyuz/model/archive.py
@@ -187,6 +187,7 @@ from lp.soyuz.interfaces.archive import (
VersionRequiresName,
)
from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthTokenSet
+from lp.soyuz.interfaces.archivejob import ICIBuildUploadJobSource
from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
from lp.soyuz.interfaces.archivesubscriber import (
ArchiveSubscriptionError,
@@ -1990,6 +1991,34 @@ class Archive(SQLBase):
sources, self, series, pocket, include_binaries, person=person,
check_permissions=False, unembargo=True)
+ def uploadCIBuild(self, ci_build, person, to_series, to_pocket,
+ to_channel=None):
+ """See `IArchive`."""
+ series = self._text_to_series(to_series)
+ pocket = self._text_to_pocket(to_pocket)
+ if self.publishing_method != ArchivePublishingMethod.ARTIFACTORY:
+ raise CannotCopy(
+ "CI builds may only be uploaded to archives published using "
+ "Artifactory.")
+ if ci_build.status != BuildStatus.FULLYBUILT:
+ raise CannotCopy(
+ "%r has status '%s', not '%s'." %
+ (ci_build, ci_build.status.title, BuildStatus.FULLYBUILT))
+ # Check upload permissions. We don't know the package name until we
+ # actually run the job; however, per-package upload permissions are
+ # by source package name, so don't necessarily make sense for CI
+ # builds anyway. For now, just ignore per-package upload
+ # permissions.
+ reason = self.checkUpload(
+ person=person, distroseries=series, sourcepackagename=None,
+ component=None, pocket=pocket)
+ if reason is not None:
+ raise CannotCopy(reason)
+ getUtility(ICIBuildUploadJobSource).create(
+ ci_build=ci_build, requester=person, target_archive=self,
+ target_distroseries=series, target_pocket=pocket,
+ target_channel=to_channel)
+
def getAuthToken(self, person):
"""See `IArchive`."""
diff --git a/lib/lp/soyuz/model/archivejob.py b/lib/lp/soyuz/model/archivejob.py
index e6785c6..eb918c1 100644
--- a/lib/lp/soyuz/model/archivejob.py
+++ b/lib/lp/soyuz/model/archivejob.py
@@ -3,20 +3,29 @@
import io
import logging
+import os.path
+import tempfile
from lazr.delegates import delegate_to
+from pkginfo import Wheel
from storm.expr import And
from storm.locals import (
Int,
JSON,
Reference,
)
+from wheel_filename import parse_wheel_filename
from zope.component import getUtility
from zope.interface import (
implementer,
provider,
)
+from lp.code.enums import RevisionStatusArtifactType
+from lp.code.interfaces.cibuild import ICIBuildSet
+from lp.code.interfaces.revisionstatus import IRevisionStatusArtifactSet
+from lp.registry.interfaces.distroseries import IDistroSeriesSet
+from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.services.config import config
from lp.services.database.enumcol import DBEnum
from lp.services.database.interfaces import IMasterStore
@@ -26,16 +35,22 @@ from lp.services.job.model.job import (
Job,
)
from lp.services.job.runner import BaseRunnableJob
+from lp.services.librarian.utils import copy_and_close
from lp.soyuz.enums import (
ArchiveJobType,
+ BinaryPackageFormat,
PackageUploadStatus,
)
from lp.soyuz.interfaces.archivejob import (
IArchiveJob,
IArchiveJobSource,
+ ICIBuildUploadJob,
+ ICIBuildUploadJobSource,
IPackageUploadNotificationJob,
IPackageUploadNotificationJobSource,
)
+from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet
+from lp.soyuz.interfaces.publishing import IPublishingSet
from lp.soyuz.interfaces.queue import IPackageUploadSet
from lp.soyuz.model.archive import Archive
@@ -165,3 +180,115 @@ class PackageUploadNotificationJob(ArchiveJobDerived):
packageupload.notify(
status=self.packageupload_status, summary_text=self.summary_text,
changes_file_object=changes_file_object, logger=logger)
+
+
+class ScanException(Exception):
+ """A CI build upload job failed to scan a file."""
+
+
+@implementer(ICIBuildUploadJob)
+@provider(ICIBuildUploadJobSource)
+class CIBuildUploadJob(ArchiveJobDerived):
+
+ class_job_type = ArchiveJobType.CI_BUILD_UPLOAD
+
+ config = config.ICIBuildUploadJobSource
+
+ @classmethod
+ def create(cls, ci_build, requester, target_archive, target_distroseries,
+ target_pocket, target_channel=None):
+ """See `ICIBuildUploadJobSource`."""
+ metadata = {
+ "ci_build_id": ci_build.id,
+ "target_distroseries_id": target_distroseries.id,
+ "target_pocket": target_pocket.title,
+ "target_channel": target_channel,
+ }
+ derived = super().create(target_archive, metadata)
+ derived.job.requester = requester
+ return derived
+
+ def getOopsVars(self):
+ vars = super().getOopsVars()
+ vars.extend([
+ (key, self.metadata[key])
+ for key in (
+ "ci_build_id",
+ "target_distroseries_id",
+ "target_pocket",
+ "target_channel",
+ )])
+ return vars
+
+ @property
+ def ci_build(self):
+ return getUtility(ICIBuildSet).getByID(self.metadata["ci_build_id"])
+
+ @property
+ def target_distroseries(self):
+ return getUtility(IDistroSeriesSet).get(
+ self.metadata["target_distroseries_id"])
+
+ @property
+ def target_pocket(self):
+ return PackagePublishingPocket.getTermByToken(
+ self.metadata["target_pocket"]).value
+
+ @property
+ def target_channel(self):
+ return self.metadata["target_channel"]
+
+ def _scanFile(self, path):
+ if path.endswith(".whl"):
+ try:
+ parsed_path = parse_wheel_filename(path)
+ wheel = Wheel(path)
+ except Exception as e:
+ raise ScanException("Failed to scan %s" % path) from e
+ return {
+ "name": wheel.name,
+ "version": wheel.version,
+ "summary": wheel.summary or "",
+ "description": wheel.description,
+ "binpackageformat": BinaryPackageFormat.WHL,
+ "architecturespecific": "any" not in parsed_path.platform_tags,
+ "homepage": wheel.home_page or "",
+ }
+ else:
+ return None
+
+ def run(self):
+ """See `IRunnableJob`."""
+ logger = logging.getLogger()
+ with tempfile.TemporaryDirectory(prefix="ci-build-copy-job") as tmpdir:
+ binaries = {}
+ for artifact in getUtility(
+ IRevisionStatusArtifactSet).findByCIBuild(self.ci_build):
+ if artifact.artifact_type == RevisionStatusArtifactType.LOG:
+ continue
+ name = artifact.library_file.filename
+ contents = os.path.join(tmpdir, name)
+ artifact.library_file.open()
+ copy_and_close(artifact.library_file, open(contents, "wb"))
+ metadata = self._scanFile(contents)
+ if metadata is None:
+ logger.info("No upload handler for %s" % name)
+ continue
+ logger.info(
+ "Uploading %s to %s %s (%s)" % (
+ name, self.archive.reference,
+ self.target_distroseries.getSuite(self.target_pocket),
+ self.target_channel))
+ metadata["binarypackagename"] = (
+ getUtility(IBinaryPackageNameSet).ensure(metadata["name"]))
+ del metadata["name"]
+ bpr = self.ci_build.createBinaryPackageRelease(**metadata)
+ bpr.addFile(artifact.library_file)
+ # The publishBinaries interface was designed for .debs,
+ # which need extra per-binary "override" information
+ # (component, etc.). None of this is relevant here.
+ binaries[bpr] = (None, None, None, None)
+ if binaries:
+ getUtility(IPublishingSet).publishBinaries(
+ self.archive, self.target_distroseries, self.target_pocket,
+ binaries, channel=self.target_channel)
diff --git a/lib/lp/soyuz/model/publishing.py b/lib/lp/soyuz/model/publishing.py
index 8a397b6..b229c47 100644
--- a/lib/lp/soyuz/model/publishing.py
+++ b/lib/lp/soyuz/model/publishing.py
@@ -1097,7 +1097,7 @@ def expand_binary_requests(distroseries, binaries):
# Find the DAS in this series corresponding to the original
# build arch tag. If it does not exist or is disabled, we should
# not publish.
- target_arch = arch_map.get(bpr.build.arch_tag)
+ target_arch = arch_map.get((bpr.build or bpr.ci_build).arch_tag)
target_archs = [target_arch] if target_arch is not None else []
else:
target_archs = archs
diff --git a/lib/lp/soyuz/tests/__init__.py b/lib/lp/soyuz/tests/__init__.py
index e69de29..7ffe3f9 100644
--- a/lib/lp/soyuz/tests/__init__.py
+++ b/lib/lp/soyuz/tests/__init__.py
@@ -0,0 +1,14 @@
+# Copyright 2022 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+import os
+
+
+here = os.path.dirname(os.path.realpath(__file__))
+
+
+def datadir(path):
+ """Return fully-qualified path inside the test data directory."""
+ if path.startswith("/"):
+ raise ValueError("Path is not relative: %s" % path)
+ return os.path.join(here, "data", path)
diff --git a/lib/lp/soyuz/tests/data/.gitignore b/lib/lp/soyuz/tests/data/.gitignore
new file mode 100644
index 0000000..2955230
--- /dev/null
+++ b/lib/lp/soyuz/tests/data/.gitignore
@@ -0,0 +1,2 @@
+!dist
+*.egg-info
diff --git a/lib/lp/soyuz/tests/data/wheel-arch/README.md b/lib/lp/soyuz/tests/data/wheel-arch/README.md
new file mode 100644
index 0000000..3135dc3
--- /dev/null
+++ b/lib/lp/soyuz/tests/data/wheel-arch/README.md
@@ -0,0 +1 @@
+An example package. Build a wheel from this with `pyproject-build`.
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
new file mode 100644
index 0000000..5b61840
Binary files /dev/null and b/lib/lp/soyuz/tests/data/wheel-arch/dist/wheel-arch-0.0.1.tar.gz differ
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
new file mode 100644
index 0000000..2131f4d
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
diff --git a/lib/lp/soyuz/tests/data/wheel-arch/pyproject.toml b/lib/lp/soyuz/tests/data/wheel-arch/pyproject.toml
new file mode 100644
index 0000000..b0f0765
--- /dev/null
+++ b/lib/lp/soyuz/tests/data/wheel-arch/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools>=42"]
+build-backend = "setuptools.build_meta"
diff --git a/lib/lp/soyuz/tests/data/wheel-arch/setup.py b/lib/lp/soyuz/tests/data/wheel-arch/setup.py
new file mode 100755
index 0000000..d1431d7
--- /dev/null
+++ b/lib/lp/soyuz/tests/data/wheel-arch/setup.py
@@ -0,0 +1,14 @@
+from setuptools import (
+ Extension,
+ setup,
+ )
+
+
+setup(
+ name="wheel-arch",
+ version="0.0.1",
+ description="Example description",
+ long_description="Example long description",
+ url="http://example.com/",
+ ext_modules=[Extension("_test", sources=["test.c"])],
+ )
diff --git a/lib/lp/soyuz/tests/data/wheel-arch/test.c b/lib/lp/soyuz/tests/data/wheel-arch/test.c
new file mode 100644
index 0000000..576fc6d
--- /dev/null
+++ b/lib/lp/soyuz/tests/data/wheel-arch/test.c
@@ -0,0 +1 @@
+#include <Python.h>
diff --git a/lib/lp/soyuz/tests/data/wheel-indep/README.md b/lib/lp/soyuz/tests/data/wheel-indep/README.md
new file mode 100644
index 0000000..3135dc3
--- /dev/null
+++ b/lib/lp/soyuz/tests/data/wheel-indep/README.md
@@ -0,0 +1 @@
+An example package. Build a wheel from this with `pyproject-build`.
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
new file mode 100644
index 0000000..16fc042
Binary files /dev/null and b/lib/lp/soyuz/tests/data/wheel-indep/dist/wheel-indep-0.0.1.tar.gz differ
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
new file mode 100644
index 0000000..93b54bd
Binary files /dev/null and b/lib/lp/soyuz/tests/data/wheel-indep/dist/wheel_indep-0.0.1-py3-none-any.whl differ
diff --git a/lib/lp/soyuz/tests/data/wheel-indep/pyproject.toml b/lib/lp/soyuz/tests/data/wheel-indep/pyproject.toml
new file mode 100644
index 0000000..b0f0765
--- /dev/null
+++ b/lib/lp/soyuz/tests/data/wheel-indep/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools>=42"]
+build-backend = "setuptools.build_meta"
diff --git a/lib/lp/soyuz/tests/data/wheel-indep/setup.cfg b/lib/lp/soyuz/tests/data/wheel-indep/setup.cfg
new file mode 100644
index 0000000..ae136d3
--- /dev/null
+++ b/lib/lp/soyuz/tests/data/wheel-indep/setup.cfg
@@ -0,0 +1,5 @@
+[metadata]
+name = wheel-indep
+version = 0.0.1
+description = Example description
+long_description = Example long description
diff --git a/lib/lp/soyuz/tests/test_archive.py b/lib/lp/soyuz/tests/test_archive.py
index a02d75d..fd3d254 100644
--- a/lib/lp/soyuz/tests/test_archive.py
+++ b/lib/lp/soyuz/tests/test_archive.py
@@ -88,6 +88,7 @@ from lp.soyuz.adapters.overrides import (
)
from lp.soyuz.enums import (
ArchivePermissionType,
+ ArchivePublishingMethod,
ArchivePurpose,
ArchiveStatus,
PackageCopyPolicy,
@@ -116,6 +117,7 @@ from lp.soyuz.interfaces.archive import (
RedirectedPocket,
VersionRequiresName,
)
+from lp.soyuz.interfaces.archivejob import ICIBuildUploadJobSource
from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus
from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet
@@ -3446,6 +3448,71 @@ class TestCopyPackage(TestCaseWithFactory):
person=source_archive.owner, move=True)
+class TestUploadCIBuild(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_creates_job(self):
+ # The uploadCIBuild method creates a CIBuildUploadJob with the
+ # appropriate parameters.
+ archive = self.factory.makeArchive(
+ publishing_method=ArchivePublishingMethod.ARTIFACTORY)
+ series = self.factory.makeDistroSeries(
+ distribution=archive.distribution)
+ build = self.factory.makeCIBuild(status=BuildStatus.FULLYBUILT)
+ with person_logged_in(archive.owner):
+ archive.uploadCIBuild(
+ build, archive.owner, series.name, "Release",
+ to_channel="edge")
+ [job] = getUtility(ICIBuildUploadJobSource).iterReady()
+ self.assertThat(job, MatchesStructure.byEquality(
+ ci_build=build,
+ target_distroseries=series,
+ target_pocket=PackagePublishingPocket.RELEASE,
+ target_channel="edge"))
+
+ def test_disallows_non_artifactory_publishing(self):
+ # CI builds may only be copied into archives published using
+ # Artifactory.
+ archive = self.factory.makeArchive()
+ series = self.factory.makeDistroSeries(
+ distribution=archive.distribution)
+ build = self.factory.makeCIBuild(status=BuildStatus.FULLYBUILT)
+ with person_logged_in(archive.owner):
+ self.assertRaisesWithContent(
+ CannotCopy,
+ "CI builds may only be uploaded to archives published using "
+ "Artifactory.",
+ archive.uploadCIBuild,
+ build, archive.owner, series.name, "Release")
+
+ def test_disallows_incomplete_builds(self):
+ # CI builds with statuses other than FULLYBUILT may not be copied.
+ archive = self.factory.makeArchive(
+ publishing_method=ArchivePublishingMethod.ARTIFACTORY)
+ series = self.factory.makeDistroSeries(
+ distribution=archive.distribution)
+ build = self.factory.makeCIBuild(status=BuildStatus.FAILEDTOBUILD)
+ person = self.factory.makePerson()
+ self.assertRaisesWithContent(
+ CannotCopy,
+ "%r has status 'Failed to build', not 'Successfully built'." % (
+ build),
+ archive.uploadCIBuild, build, person, series.name, "Release")
+
+ def test_disallows_non_uploaders(self):
+ # Only people with upload permission may call uploadCIBuild.
+ archive = self.factory.makeArchive(
+ publishing_method=ArchivePublishingMethod.ARTIFACTORY)
+ series = self.factory.makeDistroSeries(
+ distribution=archive.distribution)
+ build = self.factory.makeCIBuild(status=BuildStatus.FULLYBUILT)
+ person = self.factory.makePerson()
+ self.assertRaisesWithContent(
+ CannotCopy, "Signer has no upload rights to this PPA.",
+ archive.uploadCIBuild, build, person, series.name, "Release")
+
+
class TestgetAllPublishedBinaries(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
diff --git a/lib/lp/soyuz/tests/test_archivejob.py b/lib/lp/soyuz/tests/test_archivejob.py
index f00e606..e6c1c28 100644
--- a/lib/lp/soyuz/tests/test_archivejob.py
+++ b/lib/lp/soyuz/tests/test_archivejob.py
@@ -1,19 +1,36 @@
# Copyright 2010-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
+import os.path
+
from debian.deb822 import Changes
+from testtools.matchers import (
+ Equals,
+ Is,
+ MatchesSetwise,
+ MatchesStructure,
+ )
+import transaction
+from lp.code.enums import RevisionStatusArtifactType
+from lp.code.model.revisionstatus import RevisionStatusArtifact
+from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.services.database.interfaces import IStore
from lp.services.job.runner import JobRunner
from lp.services.mail.sendmail import format_address_for_person
from lp.soyuz.enums import (
ArchiveJobType,
+ BinaryPackageFileType,
+ BinaryPackageFormat,
PackageUploadStatus,
)
from lp.soyuz.model.archivejob import (
ArchiveJob,
ArchiveJobDerived,
+ CIBuildUploadJob,
PackageUploadNotificationJob,
)
+from lp.soyuz.tests import datadir
from lp.testing import TestCaseWithFactory
from lp.testing.dbuser import dbuser
from lp.testing.layers import (
@@ -117,3 +134,181 @@ class TestPackageUploadNotificationJob(TestCaseWithFactory):
self.assertEqual(format_address_for_person(creator), email['To'])
self.assertIn('(Accepted)', email['Subject'])
self.assertIn('Fake summary', email.get_payload()[0].get_payload())
+
+
+class TestCIBuildUploadJob(TestCaseWithFactory):
+
+ layer = LaunchpadZopelessLayer
+
+ def test_getOopsVars(self):
+ archive = self.factory.makeArchive()
+ distroseries = self.factory.makeDistroSeries(
+ distribution=archive.distribution)
+ build = self.factory.makeCIBuild()
+ job = CIBuildUploadJob.create(
+ build, build.git_repository.owner, archive, distroseries,
+ PackagePublishingPocket.RELEASE, target_channel="edge")
+ expected = [
+ ("job_id", job.context.job.id),
+ ("archive_id", archive.id),
+ ("archive_job_id", job.context.id),
+ ("archive_job_type", "CI build upload"),
+ ("ci_build_id", build.id),
+ ("target_distroseries_id", distroseries.id),
+ ("target_pocket", "Release"),
+ ("target_channel", "edge"),
+ ]
+ self.assertEqual(expected, job.getOopsVars())
+
+ def test_metadata(self):
+ archive = self.factory.makeArchive()
+ distroseries = self.factory.makeDistroSeries(
+ distribution=archive.distribution)
+ build = self.factory.makeCIBuild()
+ job = CIBuildUploadJob.create(
+ build, build.git_repository.owner, archive, distroseries,
+ PackagePublishingPocket.RELEASE, target_channel="edge")
+ expected = {
+ "ci_build_id": build.id,
+ "target_distroseries_id": distroseries.id,
+ "target_pocket": "Release",
+ "target_channel": "edge",
+ }
+ self.assertEqual(expected, job.metadata)
+ self.assertEqual(build, job.ci_build)
+ self.assertEqual(distroseries, job.target_distroseries)
+ self.assertEqual(PackagePublishingPocket.RELEASE, job.target_pocket)
+ self.assertEqual("edge", job.target_channel)
+
+ def test__scanFile_wheel_indep(self):
+ archive = self.factory.makeArchive()
+ distroseries = self.factory.makeDistroSeries(
+ distribution=archive.distribution)
+ build = self.factory.makeCIBuild()
+ job = CIBuildUploadJob.create(
+ build, build.git_repository.owner, archive, distroseries,
+ PackagePublishingPocket.RELEASE, target_channel="edge")
+ path = "wheel-indep/dist/wheel_indep-0.0.1-py3-none-any.whl"
+ expected = {
+ "name": "wheel-indep",
+ "version": "0.0.1",
+ "summary": "Example description",
+ "description": "Example long description\n",
+ "binpackageformat": BinaryPackageFormat.WHL,
+ "architecturespecific": False,
+ "homepage": "",
+ }
+ self.assertEqual(expected, job._scanFile(datadir(path)))
+
+ def test__scanFile_wheel_arch(self):
+ archive = self.factory.makeArchive()
+ distroseries = self.factory.makeDistroSeries(
+ distribution=archive.distribution)
+ build = self.factory.makeCIBuild()
+ job = CIBuildUploadJob.create(
+ build, build.git_repository.owner, archive, distroseries,
+ PackagePublishingPocket.RELEASE, target_channel="edge")
+ path = "wheel-arch/dist/wheel_arch-0.0.1-cp310-cp310-linux_x86_64.whl"
+ expected = {
+ "name": "wheel-arch",
+ "version": "0.0.1",
+ "summary": "Example description",
+ "description": "Example long description\n",
+ "binpackageformat": BinaryPackageFormat.WHL,
+ "architecturespecific": True,
+ "homepage": "http://example.com/",
+ }
+ self.assertEqual(expected, job._scanFile(datadir(path)))
+
+ def test_run_indep(self):
+ archive = self.factory.makeArchive()
+ distroseries = self.factory.makeDistroSeries(
+ distribution=archive.distribution)
+ dases = [
+ self.factory.makeDistroArchSeries(distroseries=distroseries)
+ for _ in range(2)]
+ build = self.factory.makeCIBuild(distro_arch_series=dases[0])
+ report = build.getOrCreateRevisionStatusReport("build:0")
+ report.setLog(b"log data")
+ path = "wheel-indep/dist/wheel_indep-0.0.1-py3-none-any.whl"
+ with open(datadir(path), mode="rb") as f:
+ report.attach(name=os.path.basename(path), data=f.read())
+ artifact = IStore(RevisionStatusArtifact).find(
+ RevisionStatusArtifact,
+ report=report,
+ artifact_type=RevisionStatusArtifactType.BINARY).one()
+ job = CIBuildUploadJob.create(
+ build, build.git_repository.owner, archive, distroseries,
+ PackagePublishingPocket.RELEASE, target_channel="edge")
+ transaction.commit()
+
+ with dbuser(job.config.dbuser):
+ JobRunner([job]).runAll()
+
+ self.assertThat(archive.getAllPublishedBinaries(), MatchesSetwise(*(
+ MatchesStructure(
+ binarypackagename=MatchesStructure.byEquality(
+ name="wheel-indep"),
+ binarypackagerelease=MatchesStructure(
+ ci_build=Equals(build),
+ binarypackagename=MatchesStructure.byEquality(
+ name="wheel-indep"),
+ version=Equals("0.0.1"),
+ summary=Equals("Example description"),
+ description=Equals("Example long description\n"),
+ binpackageformat=Equals(BinaryPackageFormat.WHL),
+ architecturespecific=Is(False),
+ homepage=Equals(""),
+ files=MatchesSetwise(
+ MatchesStructure.byEquality(
+ libraryfile=artifact.library_file,
+ filetype=BinaryPackageFileType.WHL))),
+ binarypackageformat=Equals(BinaryPackageFormat.WHL),
+ distroarchseries=Equals(das))
+ for das in dases)))
+
+ def test_run_arch(self):
+ archive = self.factory.makeArchive()
+ distroseries = self.factory.makeDistroSeries(
+ distribution=archive.distribution)
+ dases = [
+ self.factory.makeDistroArchSeries(distroseries=distroseries)
+ for _ in range(2)]
+ build = self.factory.makeCIBuild(distro_arch_series=dases[0])
+ report = build.getOrCreateRevisionStatusReport("build:0")
+ report.setLog(b"log data")
+ path = "wheel-arch/dist/wheel_arch-0.0.1-cp310-cp310-linux_x86_64.whl"
+ with open(datadir(path), mode="rb") as f:
+ report.attach(name=os.path.basename(path), data=f.read())
+ artifact = IStore(RevisionStatusArtifact).find(
+ RevisionStatusArtifact,
+ report=report,
+ artifact_type=RevisionStatusArtifactType.BINARY).one()
+ job = CIBuildUploadJob.create(
+ build, build.git_repository.owner, archive, distroseries,
+ PackagePublishingPocket.RELEASE, target_channel="edge")
+ transaction.commit()
+
+ with dbuser(job.config.dbuser):
+ JobRunner([job]).runAll()
+
+ self.assertThat(archive.getAllPublishedBinaries(), MatchesSetwise(
+ MatchesStructure(
+ binarypackagename=MatchesStructure.byEquality(
+ name="wheel-arch"),
+ binarypackagerelease=MatchesStructure(
+ ci_build=Equals(build),
+ binarypackagename=MatchesStructure.byEquality(
+ name="wheel-arch"),
+ version=Equals("0.0.1"),
+ summary=Equals("Example description"),
+ description=Equals("Example long description\n"),
+ binpackageformat=Equals(BinaryPackageFormat.WHL),
+ architecturespecific=Is(True),
+ homepage=Equals("http://example.com/"),
+ files=MatchesSetwise(
+ MatchesStructure.byEquality(
+ libraryfile=artifact.library_file,
+ filetype=BinaryPackageFileType.WHL))),
+ binarypackageformat=Equals(BinaryPackageFormat.WHL),
+ distroarchseries=Equals(dases[0]))))
diff --git a/requirements/launchpad.txt b/requirements/launchpad.txt
index 981acfb..c2081c1 100644
--- a/requirements/launchpad.txt
+++ b/requirements/launchpad.txt
@@ -109,6 +109,7 @@ PasteDeploy==2.1.0
pathlib2==2.3.2
patiencediff==0.2.2
pgbouncer==0.0.9
+pkginfo==1.8.2
prettytable==0.7.2
psutil==5.4.2
psycopg2==2.8.6
@@ -174,6 +175,7 @@ webencodings==0.5.1
WebOb==1.8.5
WebTest==2.0.35
Werkzeug==1.0.1
+wheel-filename==1.1.0
wrapt==1.12.1
wsgi-intercept==1.9.2
WSGIProxy2==0.4.6
diff --git a/setup.cfg b/setup.cfg
index db4af9b..145a832 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -66,6 +66,7 @@ install_requires =
oops_twisted
oops_wsgi
paramiko
+ pkginfo
psutil
pgbouncer
psycopg2
@@ -110,6 +111,7 @@ install_requires =
WebOb
WebTest
Werkzeug
+ wheel-filename
WSGIProxy2
z3c.ptcompat
zope.app.http