← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:ci-build-duplicate-upload into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:ci-build-duplicate-upload into launchpad:master.

Commit message:
Allow CI builds to be uploaded multiple times to different targets

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/424593

It's quite confusing that only one `CIBuildUploadJob` can ever be run for a given CI build, even if you want to release it to multiple channels or multiple archives or similar; this is because there's a unique constraint on `BinaryPackageRelease (ci_build, binarypackagename)`.  Lift this restriction by just creating new `BinaryPackagePublishingHistory` rows for the relevant `BinaryPackageRelease` in that case.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:ci-build-duplicate-upload into launchpad:master.
diff --git a/lib/lp/code/interfaces/cibuild.py b/lib/lp/code/interfaces/cibuild.py
index fc7a197..2d75ae1 100644
--- a/lib/lp/code/interfaces/cibuild.py
+++ b/lib/lp/code/interfaces/cibuild.py
@@ -23,6 +23,7 @@ from lazr.restful.declarations import (
     operation_for_version,
     )
 from lazr.restful.fields import Reference
+from zope.interface import Attribute
 from zope.schema import (
     Bool,
     Datetime,
@@ -132,6 +133,10 @@ class ICIBuildView(IPackageBuildView, IPrivacy):
             "A mapping from job IDs to result tokens, retrieved from the "
             "builder.")))
 
+    binarypackages = Attribute(
+        "A list of binary packages that resulted from this build, ordered by "
+        "name.")
+
     def getConfiguration(logger=None):
         """Fetch a CI build's .launchpad.yaml from code hosting, if possible.
 
diff --git a/lib/lp/code/model/cibuild.py b/lib/lp/code/model/cibuild.py
index 4e1ddbd..1982a44 100644
--- a/lib/lp/code/model/cibuild.py
+++ b/lib/lp/code/model/cibuild.py
@@ -8,7 +8,10 @@ __all__ = [
     ]
 
 from datetime import timedelta
-from operator import attrgetter
+from operator import (
+    attrgetter,
+    itemgetter,
+    )
 
 from lazr.lifecycle.event import ObjectCreatedEvent
 import pytz
@@ -85,6 +88,7 @@ from lp.services.macaroons.interfaces import (
     )
 from lp.services.macaroons.model import MacaroonIssuerBase
 from lp.services.propertycache import cachedproperty
+from lp.soyuz.model.binarypackagename import BinaryPackageName
 from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
 from lp.soyuz.model.distroarchseries import DistroArchSeries
 
@@ -466,6 +470,17 @@ class CIBuild(PackageBuildMixin, StormBase):
         """See `IPackageBuild`."""
         # We don't currently send any notifications.
 
+    @property
+    def binarypackages(self):
+        """See `ICIBuild`."""
+        releases = IStore(BinaryPackageRelease).find(
+            (BinaryPackageRelease, BinaryPackageName),
+            BinaryPackageRelease.ci_build == self,
+            BinaryPackageRelease.binarypackagename == BinaryPackageName.id)
+        releases = releases.order_by(
+            BinaryPackageName.name, BinaryPackageRelease.id)
+        return DecoratedResultSet(releases, result_decorator=itemgetter(0))
+
     def createBinaryPackageRelease(
             self, binarypackagename, version, summary, description,
             binpackageformat, architecturespecific, installedsize=None,
diff --git a/lib/lp/code/model/tests/test_cibuild.py b/lib/lp/code/model/tests/test_cibuild.py
index b7e0ca5..89a432c 100644
--- a/lib/lp/code/model/tests/test_cibuild.py
+++ b/lib/lp/code/model/tests/test_cibuild.py
@@ -417,6 +417,7 @@ class TestCIBuild(TestCaseWithFactory):
             installedsize=Equals(1024),
             homepage=Equals("https://example.com/";),
             ))
+        self.assertContentEqual([bpr], build.binarypackages)
 
 
 class TestCIBuildSet(TestCaseWithFactory):
diff --git a/lib/lp/soyuz/model/archivejob.py b/lib/lp/soyuz/model/archivejob.py
index cc9b75b..36eb855 100644
--- a/lib/lp/soyuz/model/archivejob.py
+++ b/lib/lp/soyuz/model/archivejob.py
@@ -324,6 +324,9 @@ class CIBuildUploadJob(ArchiveJobDerived):
         """See `IRunnableJob`."""
         logger = logging.getLogger()
         with tempfile.TemporaryDirectory(prefix="ci-build-copy-job") as tmpdir:
+            releases_by_name = {
+                release.binarypackagename: release
+                for release in self.ci_build.binarypackages}
             binaries = {}
             for artifact in getUtility(
                     IRevisionStatusArtifactSet).findByCIBuild(self.ci_build):
@@ -342,13 +345,20 @@ class CIBuildUploadJob(ArchiveJobDerived):
                         name, self.archive.reference,
                         self.target_distroseries.getSuite(self.target_pocket),
                         self.target_channel))
-                metadata["binarypackagename"] = (
+                metadata["binarypackagename"] = bpn = (
                     getUtility(IBinaryPackageNameSet).ensure(metadata["name"]))
                 del metadata["name"]
                 filetype = self.filetype_by_format[
                     metadata["binpackageformat"]]
-                bpr = self.ci_build.createBinaryPackageRelease(**metadata)
-                bpr.addFile(artifact.library_file, filetype=filetype)
+                bpr = releases_by_name.get(bpn)
+                if bpr is None:
+                    bpr = self.ci_build.createBinaryPackageRelease(**metadata)
+                for bpf in bpr.files:
+                    if (bpf.libraryfile == artifact.library_file and
+                            bpf.filetype == filetype):
+                        break
+                else:
+                    bpr.addFile(artifact.library_file, filetype=filetype)
                 # The publishBinaries interface was designed for .debs,
                 # which need extra per-binary "override" information
                 # (component, etc.).  None of this is relevant here.
diff --git a/lib/lp/soyuz/tests/test_archivejob.py b/lib/lp/soyuz/tests/test_archivejob.py
index d599c48..294fe89 100644
--- a/lib/lp/soyuz/tests/test_archivejob.py
+++ b/lib/lp/soyuz/tests/test_archivejob.py
@@ -496,6 +496,57 @@ class TestCIBuildUploadJob(TestCaseWithFactory):
                 binarypackageformat=Equals(BinaryPackageFormat.CONDA_V2),
                 distroarchseries=Equals(dases[0]))))
 
+    def test_existing_release(self):
+        # A `CIBuildUploadJob` can be run even if the build in question was
+        # already uploaded somewhere, and in that case may add publications
+        # in other locations for the same package.
+        archive = self.factory.makeArchive()
+        distroseries = self.factory.makeDistroSeries(
+            distribution=archive.distribution)
+        das = self.factory.makeDistroArchSeries(distroseries=distroseries)
+        build = self.factory.makeCIBuild(distro_arch_series=das)
+        report = build.getOrCreateRevisionStatusReport("build:0")
+        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()
+        job = CIBuildUploadJob.create(
+            build, build.git_repository.owner, archive, distroseries,
+            PackagePublishingPocket.RELEASE, target_channel="0.0.1/edge")
+        transaction.commit()
+
+        with dbuser(job.config.dbuser):
+            JobRunner([job]).runAll()
+
+        bpphs = archive.getAllPublishedBinaries()
+        # The publications are for the same binary package release, which
+        # has a single file attached to it.
+        self.assertEqual(1, len({bpph.binarypackagename for bpph in bpphs}))
+        self.assertEqual(1, len({bpph.binarypackagerelease for bpph in bpphs}))
+        self.assertThat(archive.getAllPublishedBinaries(), MatchesSetwise(*(
+            MatchesStructure(
+                binarypackagename=MatchesStructure.byEquality(
+                    name="wheel-indep"),
+                binarypackagerelease=MatchesStructure(
+                    ci_build=Equals(build),
+                    files=MatchesSetwise(
+                        MatchesStructure.byEquality(
+                            libraryfile=artifact.library_file,
+                            filetype=BinaryPackageFileType.WHL))),
+                binarypackageformat=Equals(BinaryPackageFormat.WHL),
+                distroarchseries=Equals(das),
+                channel=Equals(channel))
+            for channel in ("edge", "0.0.1/edge"))))
+
 
 class TestViaCelery(TestCaseWithFactory):