← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:artifactory-publish-model into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:artifactory-publish-model into launchpad:master.

Commit message:
Model new columns for Artifactory publishing

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

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

DB patch 2210-44-0 (https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/416727) added a number of new columns to various database tables, in preparation for publishing archives to Artifactory.  This patch makes the corresponding ORM changes, and also arranges to backfill several of the new columns using garbo jobs.

The changes roughly break down into these categories:

 * Denormalize format columns from `{Source,Binary}PackageRelease` to `{Source,Binary}PackagePublishingHistory` (backfilled), in order that they can be used by database constraints.

 * Make some columns nullable that are specific to Debian-format archives, and instead enforce non-null only for Debian-format releases and publications.

 * Add a `channel` column to `{Source,Binary}PackagePublishingHistory`, constrained to be set only for non-Debian-format publications.

 * Add the possibility for a `BinaryPackageRelease` to come from a `CIBuild` rather than a `BinaryPackageBuild`.

 * Add `publishing_method` and `repository_format` columns to `Archive` (backfilled).
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:artifactory-publish-model into launchpad:master.
diff --git a/database/schema/security.cfg b/database/schema/security.cfg
index ada34e5..a200d97 100644
--- a/database/schema/security.cfg
+++ b/database/schema/security.cfg
@@ -2475,6 +2475,7 @@ public.accesspolicygrant                = SELECT, DELETE
 public.accesstoken                      = SELECT, DELETE
 public.account                          = SELECT, DELETE
 public.answercontact                    = SELECT, DELETE
+public.archive                          = SELECT, UPDATE
 public.archiveauthtoken                 = SELECT, UPDATE
 public.archivesubscriber                = SELECT, UPDATE
 public.branch                           = SELECT, UPDATE
diff --git a/lib/lp/archiveuploader/dscfile.py b/lib/lp/archiveuploader/dscfile.py
index 8554832..ee18c03 100644
--- a/lib/lp/archiveuploader/dscfile.py
+++ b/lib/lp/archiveuploader/dscfile.py
@@ -61,7 +61,10 @@ from lp.registry.interfaces.person import (
     IPersonSet,
     PersonCreationRationale,
     )
-from lp.registry.interfaces.sourcepackage import SourcePackageFileType
+from lp.registry.interfaces.sourcepackage import (
+    SourcePackageFileType,
+    SourcePackageType,
+    )
 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
 from lp.services.encoding import guess as guess_encoding
 from lp.services.gpg.interfaces import (
@@ -694,6 +697,7 @@ class DSCFile(SourceUploadFile, SignableTagFile):
         release = self.policy.distroseries.createUploadedSourcePackageRelease(
             sourcepackagename=source_name,
             version=self.dsc_version,
+            format=SourcePackageType.DPKG,
             maintainer=self.maintainer['person'],
             builddepends=encoded.get('Build-Depends', ''),
             builddependsindep=encoded.get('Build-Depends-Indep', ''),
diff --git a/lib/lp/registry/interfaces/distroseries.py b/lib/lp/registry/interfaces/distroseries.py
index ac2959c..d8547d9 100644
--- a/lib/lp/registry/interfaces/distroseries.py
+++ b/lib/lp/registry/interfaces/distroseries.py
@@ -668,7 +668,7 @@ class IDistroSeriesPublic(
         """
 
     def createUploadedSourcePackageRelease(
-        sourcepackagename, version, maintainer, builddepends,
+        sourcepackagename, version, format, maintainer, builddepends,
         builddependsindep, architecturehintlist, component, creator, urgency,
         changelog, changelog_entry, dsc, dscsigningkey, section,
         dsc_maintainer_rfc822, dsc_standards_version, dsc_format,
@@ -686,6 +686,7 @@ class IDistroSeriesPublic(
          :param dateuploaded: timestamp, if not provided will be UTC_NOW
          :param sourcepackagename: `ISourcePackageName`
          :param version: string, a debian valid version
+         :param format: `SourcePackageType`
          :param maintainer: IPerson designed as package maintainer
          :param creator: IPerson, package uploader
          :param component: IComponent
diff --git a/lib/lp/registry/model/distroseries.py b/lib/lp/registry/model/distroseries.py
index 8c3d76c..78b3606 100644
--- a/lib/lp/registry/model/distroseries.py
+++ b/lib/lp/registry/model/distroseries.py
@@ -1060,7 +1060,7 @@ class DistroSeries(SQLBase, BugTargetBase, HasSpecificationsMixin,
             self, build_state, name, pocket, arch_tag)
 
     def createUploadedSourcePackageRelease(
-        self, sourcepackagename, version, maintainer, builddepends,
+        self, sourcepackagename, version, format, maintainer, builddepends,
         builddependsindep, architecturehintlist, component, creator,
         urgency, changelog, changelog_entry, dsc, dscsigningkey, section,
         dsc_maintainer_rfc822, dsc_standards_version, dsc_format,
@@ -1071,7 +1071,8 @@ class DistroSeries(SQLBase, BugTargetBase, HasSpecificationsMixin,
         """See `IDistroSeries`."""
         return SourcePackageRelease(
             upload_distroseries=self, sourcepackagename=sourcepackagename,
-            version=version, maintainer=maintainer, dateuploaded=dateuploaded,
+            version=version, format=format,
+            maintainer=maintainer, dateuploaded=dateuploaded,
             builddepends=builddepends, builddependsindep=builddependsindep,
             architecturehintlist=architecturehintlist, component=component,
             creator=creator, urgency=urgency, changelog=changelog,
diff --git a/lib/lp/scripts/garbo.py b/lib/lp/scripts/garbo.py
index 24c4d61..ff17b49 100644
--- a/lib/lp/scripts/garbo.py
+++ b/lib/lp/scripts/garbo.py
@@ -33,6 +33,7 @@ import six
 from storm.expr import (
     And,
     Cast,
+    Coalesce,
     Except,
     In,
     Join,
@@ -43,6 +44,7 @@ from storm.expr import (
     Row,
     Select,
     SQL,
+    Update,
     )
 from storm.info import ClassAlias
 from storm.store import EmptyResultSet
@@ -147,16 +149,24 @@ from lp.snappy.model.snapbuildjob import (
     SnapBuildJob,
     SnapBuildJobType,
     )
-from lp.soyuz.enums import ArchiveSubscriberStatus
+from lp.soyuz.enums import (
+    ArchivePublishingMethod,
+    ArchiveRepositoryFormat,
+    ArchiveSubscriberStatus,
+    )
 from lp.soyuz.interfaces.publishing import active_publishing_status
 from lp.soyuz.model.archive import Archive
 from lp.soyuz.model.archiveauthtoken import ArchiveAuthToken
 from lp.soyuz.model.archivesubscriber import ArchiveSubscriber
+from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
 from lp.soyuz.model.distributionsourcepackagecache import (
     DistributionSourcePackageCache,
     )
 from lp.soyuz.model.livefsbuild import LiveFSFile
-from lp.soyuz.model.publishing import SourcePackagePublishingHistory
+from lp.soyuz.model.publishing import (
+    BinaryPackagePublishingHistory,
+    SourcePackagePublishingHistory,
+    )
 from lp.soyuz.model.reporting import LatestPersonSourcePackageReleaseCache
 from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
 from lp.translations.interfaces.potemplate import IPOTemplateSet
@@ -1811,6 +1821,116 @@ class RevisionStatusReportPruner(BulkPruner):
         RevisionStatusArtifactType.BINARY.value)
 
 
+class ArchiveArtifactoryColumnsPopulator(TunableLoop):
+    """Populate new `Archive` columns used for Artifactory publishing."""
+
+    maximum_chunk_size = 5000
+
+    def __init__(self, log, abort_time=None):
+        super().__init__(log, abort_time)
+        self.start_at = 1
+        self.store = IMasterStore(Archive)
+
+    def findArchives(self):
+        return self.store.find(
+            Archive,
+            Archive.id >= self.start_at,
+            Or(Archive._publishing_method == None,
+               Archive._repository_format == None),
+            ).order_by(Archive.id)
+
+    def isDone(self):
+        return self.findArchives().is_empty()
+
+    def __call__(self, chunk_size):
+        archives = list(self.findArchives()[:chunk_size])
+        ids = [archive.id for archive in archives]
+        self.store.execute(Update(
+            {
+                Archive._publishing_method: Coalesce(
+                    Archive._publishing_method,
+                    ArchivePublishingMethod.LOCAL.value),
+                Archive._repository_format: Coalesce(
+                    Archive._repository_format,
+                    ArchiveRepositoryFormat.DEBIAN.value),
+                },
+            where=Archive.id.is_in(ids), table=Archive))
+        self.start_at = archives[-1].id + 1
+        transaction.commit()
+
+
+class SourcePackagePublishingHistoryFormatPopulator(TunableLoop):
+    """Populate new `SPPH.format` column."""
+
+    maximum_chunk_size = 5000
+
+    def __init__(self, log, abort_time=None):
+        super().__init__(log, abort_time)
+        self.start_at = 1
+        self.store = IMasterStore(SourcePackagePublishingHistory)
+
+    def findPublications(self):
+        return self.store.find(
+            SourcePackagePublishingHistory,
+            SourcePackagePublishingHistory.id >= self.start_at,
+            SourcePackagePublishingHistory._format == None,
+            ).order_by(SourcePackagePublishingHistory.id)
+
+    def isDone(self):
+        return self.findPublications().is_empty()
+
+    def __call__(self, chunk_size):
+        spphs = list(self.findPublications()[:chunk_size])
+        ids = [spph.id for spph in spphs]
+        self.store.execute(BulkUpdate(
+            {SourcePackagePublishingHistory._format:
+                SourcePackageRelease.format},
+            table=SourcePackagePublishingHistory,
+            values=SourcePackageRelease,
+            where=And(
+                SourcePackagePublishingHistory.sourcepackagerelease ==
+                    SourcePackageRelease.id,
+                SourcePackagePublishingHistory.id.is_in(ids))))
+        self.start_at = spphs[-1].id + 1
+        transaction.commit()
+
+
+class BinaryPackagePublishingHistoryFormatPopulator(TunableLoop):
+    """Populate new `BPPH.binarypackageformat` column."""
+
+    maximum_chunk_size = 5000
+
+    def __init__(self, log, abort_time=None):
+        super().__init__(log, abort_time)
+        self.start_at = 1
+        self.store = IMasterStore(BinaryPackagePublishingHistory)
+
+    def findPublications(self):
+        return self.store.find(
+            BinaryPackagePublishingHistory,
+            BinaryPackagePublishingHistory.id >= self.start_at,
+            BinaryPackagePublishingHistory._binarypackageformat == None,
+            ).order_by(BinaryPackagePublishingHistory.id)
+
+    def isDone(self):
+        return self.findPublications().is_empty()
+
+    def __call__(self, chunk_size):
+        bpphs = list(self.findPublications()[:chunk_size])
+        ids = [bpph.id for bpph in bpphs]
+        self.store.execute(BulkUpdate(
+            {BinaryPackagePublishingHistory._binarypackageformat:
+                BinaryPackageRelease.binpackageformat},
+            table=BinaryPackagePublishingHistory,
+            values=BinaryPackageRelease,
+            where=And(
+                BinaryPackagePublishingHistory.binarypackagerelease ==
+                    BinaryPackageRelease.id,
+                BinaryPackagePublishingHistory.id.is_in(ids))))
+        self.start_at = bpphs[-1].id + 1
+        transaction.commit()
+
+
 class BaseDatabaseGarbageCollector(LaunchpadCronScript):
     """Abstract base class to run a collection of TunableLoops."""
     script_name = None  # Script name for locking and database user. Override.
@@ -2090,6 +2210,8 @@ class DailyDatabaseGarbageCollector(BaseDatabaseGarbageCollector):
     script_name = 'garbo-daily'
     tunable_loops = [
         AnswerContactPruner,
+        ArchiveArtifactoryColumnsPopulator,
+        BinaryPackagePublishingHistoryFormatPopulator,
         BranchJobPruner,
         BugNotificationPruner,
         BugWatchActivityPruner,
@@ -2111,6 +2233,7 @@ class DailyDatabaseGarbageCollector(BaseDatabaseGarbageCollector):
         ScrubPOFileTranslator,
         SnapBuildJobPruner,
         SnapFilePruner,
+        SourcePackagePublishingHistoryFormatPopulator,
         SuggestiveTemplatesCacheUpdater,
         TeamMembershipPruner,
         UnlinkedAccountPruner,
diff --git a/lib/lp/scripts/tests/test_garbo.py b/lib/lp/scripts/tests/test_garbo.py
index 3df3b58..2f6ff8e 100644
--- a/lib/lp/scripts/tests/test_garbo.py
+++ b/lib/lp/scripts/tests/test_garbo.py
@@ -16,6 +16,7 @@ import re
 from textwrap import dedent
 import time
 
+from psycopg2 import IntegrityError
 from pytz import UTC
 import six
 from storm.exceptions import LostObjectError
@@ -88,6 +89,7 @@ from lp.registry.enums import (
     )
 from lp.registry.interfaces.accesspolicy import IAccessPolicySource
 from lp.registry.interfaces.person import IPersonSet
+from lp.registry.interfaces.sourcepackage import SourcePackageType
 from lp.registry.interfaces.teammembership import TeamMembershipStatus
 from lp.registry.model.commercialsubscription import CommercialSubscription
 from lp.registry.model.teammembership import TeamMembership
@@ -147,7 +149,10 @@ from lp.snappy.tests.test_snapbuildjob import (
     run_isolated_jobs,
     )
 from lp.soyuz.enums import (
+    ArchivePublishingMethod,
+    ArchiveRepositoryFormat,
     ArchiveSubscriberStatus,
+    BinaryPackageFormat,
     PackagePublishingStatus,
     )
 from lp.soyuz.interfaces.archive import NAMED_AUTH_TOKEN_FEATURE_FLAG
@@ -2143,6 +2148,132 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
             list(getUtility(
                 IRevisionStatusReportSet).findByRepository(repo2)))
 
+    def test_ArchiveArtifactoryColumnsPopulator(self):
+        switch_dbuser('testadmin')
+        old_archives = [self.factory.makeArchive() for _ in range(2)]
+        for archive in old_archives:
+            removeSecurityProxy(archive)._publishing_method = None
+            removeSecurityProxy(archive)._repository_format = None
+        try:
+            Store.of(old_archives[0]).flush()
+        except IntegrityError:
+            # Now enforced by DB NOT NULL constraint; backfilling is no
+            # longer necessary.
+            return
+        local_debian_archives = [
+            self.factory.makeArchive(
+                publishing_method=ArchivePublishingMethod.LOCAL,
+                repository_format=ArchiveRepositoryFormat.DEBIAN)
+            for _ in range(2)]
+        artifactory_python_archives = [
+            self.factory.makeArchive(
+                publishing_method=ArchivePublishingMethod.ARTIFACTORY,
+                repository_format=ArchiveRepositoryFormat.PYTHON)
+            for _ in range(2)]
+        transaction.commit()
+
+        self.runDaily()
+
+        # Old archives are backfilled.
+        for archive in old_archives:
+            self.assertThat(
+                removeSecurityProxy(archive),
+                MatchesStructure.byEquality(
+                    _publishing_method=ArchivePublishingMethod.LOCAL,
+                    _repository_format=ArchiveRepositoryFormat.DEBIAN))
+        # Other archives are left alone.
+        for archive in local_debian_archives:
+            self.assertThat(
+                removeSecurityProxy(archive),
+                MatchesStructure.byEquality(
+                    _publishing_method=ArchivePublishingMethod.LOCAL,
+                    _repository_format=ArchiveRepositoryFormat.DEBIAN))
+        for archive in artifactory_python_archives:
+            self.assertThat(
+                removeSecurityProxy(archive),
+                MatchesStructure.byEquality(
+                    _publishing_method=ArchivePublishingMethod.ARTIFACTORY,
+                    _repository_format=ArchiveRepositoryFormat.PYTHON))
+
+    def test_SourcePackagePublishingHistoryFormatPopulator(self):
+        switch_dbuser('testadmin')
+        old_spphs = [
+            self.factory.makeSourcePackagePublishingHistory(
+                format=SourcePackageType.DPKG)
+            for _ in range(2)]
+        for spph in old_spphs:
+            removeSecurityProxy(spph)._format = None
+        try:
+            Store.of(old_spphs[0]).flush()
+        except IntegrityError:
+            # Now enforced by DB NOT NULL constraint; backfilling is no
+            # longer necessary.
+            return
+        dpkg_spphs = [
+            self.factory.makeSourcePackagePublishingHistory(
+                format=SourcePackageType.DPKG)
+            for _ in range(2)]
+        rpm_spphs = [
+            self.factory.makeSourcePackagePublishingHistory(
+                format=SourcePackageType.RPM)
+            for _ in range(2)]
+        transaction.commit()
+
+        self.runDaily()
+
+        # Old publications are backfilled.
+        for spph in old_spphs:
+            self.assertEqual(
+                SourcePackageType.DPKG, removeSecurityProxy(spph)._format)
+        # Other publications are left alone.
+        for spph in dpkg_spphs:
+            self.assertEqual(
+                SourcePackageType.DPKG, removeSecurityProxy(spph)._format)
+        for spph in rpm_spphs:
+            self.assertEqual(
+                SourcePackageType.RPM, removeSecurityProxy(spph)._format)
+
+    def test_BinaryPackagePublishingHistoryFormatPopulator(self):
+        switch_dbuser('testadmin')
+        old_bpphs = [
+            self.factory.makeBinaryPackagePublishingHistory(
+                binpackageformat=BinaryPackageFormat.DEB)
+            for _ in range(2)]
+        for bpph in old_bpphs:
+            removeSecurityProxy(bpph)._format = None
+        try:
+            Store.of(old_bpphs[0]).flush()
+        except IntegrityError:
+            # Now enforced by DB NOT NULL constraint; backfilling is no
+            # longer necessary.
+            return
+        deb_bpphs = [
+            self.factory.makeBinaryPackagePublishingHistory(
+                binpackageformat=BinaryPackageFormat.DEB)
+            for _ in range(2)]
+        rpm_bpphs = [
+            self.factory.makeBinaryPackagePublishingHistory(
+                binpackageformat=BinaryPackageFormat.RPM)
+            for _ in range(2)]
+        transaction.commit()
+
+        self.runDaily()
+
+        # Old publications are backfilled.
+        for bpph in old_bpphs:
+            self.assertEqual(
+                BinaryPackageFormat.DEB,
+                removeSecurityProxy(bpph)._binarypackageformat)
+        # Other publications are left alone.
+        for bpph in deb_bpphs:
+            self.assertEqual(
+                BinaryPackageFormat.DEB,
+                removeSecurityProxy(bpph)._binarypackageformat)
+        for bpph in rpm_bpphs:
+            self.assertEqual(
+                BinaryPackageFormat.RPM,
+                removeSecurityProxy(bpph)._binarypackageformat)
+
 
 class TestGarboTasks(TestCaseWithFactory):
     layer = LaunchpadZopelessLayer
diff --git a/lib/lp/soyuz/doc/distroarchseriesbinarypackage.txt b/lib/lp/soyuz/doc/distroarchseriesbinarypackage.txt
index ff8f915..e23d5f9 100644
--- a/lib/lp/soyuz/doc/distroarchseriesbinarypackage.txt
+++ b/lib/lp/soyuz/doc/distroarchseriesbinarypackage.txt
@@ -71,6 +71,7 @@ needs to be removed.
     >>> pe = BinaryPackagePublishingHistory(
     ...      binarypackagerelease=bpr.id,
     ...      binarypackagename=bpr.binarypackagename,
+    ...      _binarypackageformat=bpr.binpackageformat,
     ...      component=main_component.id,
     ...      section=misc_section.id,
     ...      priority=priority,
@@ -116,6 +117,7 @@ needs to be removed.
     >>> pe = BinaryPackagePublishingHistory(
     ...      binarypackagerelease=bpr.id,
     ...      binarypackagename=bpr.binarypackagename,
+    ...      _binarypackageformat=bpr.binpackageformat,
     ...      component=main_component.id,
     ...      section=misc_section.id,
     ...      priority=priority,
diff --git a/lib/lp/soyuz/doc/distroseriesqueue-translations.txt b/lib/lp/soyuz/doc/distroseriesqueue-translations.txt
index 97b7cdf..b9b51bd 100644
--- a/lib/lp/soyuz/doc/distroseriesqueue-translations.txt
+++ b/lib/lp/soyuz/doc/distroseriesqueue-translations.txt
@@ -4,10 +4,7 @@ Upload processing queue with translations
 This test covers the use case when a package includes translations and is
 uploaded into the system.
 
-    >>> from lp.registry.model.gpgkey import GPGKey
-    >>> from lp.soyuz.model.component import Component
     >>> from lp.buildmaster.interfaces.processor import IProcessorSet
-    >>> from lp.soyuz.model.section import Section
     >>> from lp.soyuz.model.publishing import (
     ...     SourcePackagePublishingHistory)
     >>> from lp.registry.interfaces.distribution import IDistributionSet
@@ -28,7 +25,6 @@ uploaded into the system.
     >>> import_public_test_keys()
 
     >>> from lp.services.database.constants import UTC_NOW
-    >>> from lp.registry.interfaces.sourcepackage import SourcePackageUrgency
 
     >>> from lp.soyuz.model.packagetranslationsuploadjob import (
     ...     PackageTranslationsUploadJob)
@@ -54,17 +50,18 @@ queue:
 
 # We are going to import the pmount build into RELEASE pocket.
     >>> pmount_sourcepackagename = getUtility(ISourcePackageNameSet)['pmount']
-    >>> source_package_release = dapper.createUploadedSourcePackageRelease(
-    ...     pmount_sourcepackagename, "0.9.7-2ubuntu2", dapper.owner,
-    ...     None, None, 'i386', Component.get(1), dapper.owner,
-    ...     SourcePackageUrgency.LOW, None, None, None, GPGKey.get(1),
-    ...     Section.get(1), '', '', '', '', dapper.main_archive,
-    ...     'copyright ?!', '', '')
+    >>> source_package_release = factory.makeSourcePackageRelease(
+    ...     distroseries=dapper, sourcepackagename=pmount_sourcepackagename,
+    ...     version="0.9.7-2ubuntu2",
+    ...     maintainer=dapper.owner, creator=dapper.owner,
+    ...     component="main", section_name="base", urgency="low",
+    ...     architecturehintlist="i386")
 
     >>> publishing_history = SourcePackagePublishingHistory(
     ...     distroseries=dapper.id,
     ...     sourcepackagerelease=source_package_release.id,
     ...     sourcepackagename=source_package_release.sourcepackagename,
+    ...     _format=source_package_release.format,
     ...     component=source_package_release.component.id,
     ...     section=source_package_release.section.id,
     ...     status=PackagePublishingStatus.PUBLISHED,
diff --git a/lib/lp/soyuz/doc/sourcepackagerelease.txt b/lib/lp/soyuz/doc/sourcepackagerelease.txt
index b4dd9e3..c3f4ef0 100644
--- a/lib/lp/soyuz/doc/sourcepackagerelease.txt
+++ b/lib/lp/soyuz/doc/sourcepackagerelease.txt
@@ -122,7 +122,10 @@ IDistroSeries API:
 
     >>> from lp.registry.interfaces.distribution import IDistributionSet
     >>> from lp.registry.interfaces.gpg import IGPGKeySet
-    >>> from lp.registry.interfaces.sourcepackage import SourcePackageUrgency
+    >>> from lp.registry.interfaces.sourcepackage import (
+    ...     SourcePackageType,
+    ...     SourcePackageUrgency,
+    ...     )
     >>> from lp.registry.interfaces.sourcepackagename import (
     ...     ISourcePackageNameSet,
     ...     )
@@ -164,7 +167,7 @@ ISourcePackageRelease, it will automatically set the
 'upload_distroseries' to the API entry point, in this case Hoary.
 
     >>> new_spr = hoary.createUploadedSourcePackageRelease(
-    ...     arg_name, version, arg_maintainer,
+    ...     arg_name, version, SourcePackageType.DPKG, arg_maintainer,
     ...     builddepends, builddependsindep, archhintlist, arg_comp,
     ...     arg_creator, arg_urgency, changelog, changelog_entry, dsc,
     ...     arg_key, arg_sect, dsc_maintainer_rfc822, dsc_standards_version,
diff --git a/lib/lp/soyuz/enums.py b/lib/lp/soyuz/enums.py
index 177a87f..d60ea48 100644
--- a/lib/lp/soyuz/enums.py
+++ b/lib/lp/soyuz/enums.py
@@ -6,7 +6,9 @@
 __all__ = [
     'ArchiveJobType',
     'ArchivePermissionType',
+    'ArchivePublishingMethod',
     'ArchivePurpose',
+    'ArchiveRepositoryFormat',
     'ArchiveStatus',
     'ArchiveSubscriberStatus',
     'archive_suffixes',
@@ -626,3 +628,37 @@ class BinarySourceReferenceType(DBEnumeratedType):
         source package, and so the referenced source package needs to remain
         in the archive for as long as the referencing binary package does.
         """)
+
+
+class ArchivePublishingMethod(DBEnumeratedType):
+    """The method used to publish an archive."""
+
+    LOCAL = DBItem(0, """
+        Local
+
+        Publish locally, either using apt-ftparchive or writing indexes
+        directly from the database depending on the archive purpose.
+        """)
+
+    ARTIFACTORY = DBItem(1, """
+        Artifactory
+
+        Publish via JFrog Artifactory.
+        """)
+
+
+class ArchiveRepositoryFormat(DBEnumeratedType):
+    """The repository format used by an archive."""
+
+    DEBIAN = DBItem(0, """
+        Debian
+
+        A Debian-format apt archive
+        (https://wiki.debian.org/DebianRepository/Format).
+        """)
+
+    PYTHON = DBItem(1, """
+        Python
+
+        A Python package index (https://www.python.org/dev/peps/pep-0301/).
+        """)
diff --git a/lib/lp/soyuz/interfaces/archive.py b/lib/lp/soyuz/interfaces/archive.py
index fc7db31..c7f3523 100644
--- a/lib/lp/soyuz/interfaces/archive.py
+++ b/lib/lp/soyuz/interfaces/archive.py
@@ -111,7 +111,9 @@ from lp.services.fields import (
     StrippedTextLine,
     )
 from lp.soyuz.enums import (
+    ArchivePublishingMethod,
     ArchivePurpose,
+    ArchiveRepositoryFormat,
     PackagePublishingStatus,
     )
 from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
@@ -806,6 +808,14 @@ class IArchiveView(IHasBuildRecords):
         "The architectures that are available to be enabled or disabled for "
         "this archive.")
 
+    publishing_method = Choice(
+        title=_("Publishing method"), vocabulary=ArchivePublishingMethod,
+        required=True, readonly=True)
+
+    repository_format = Choice(
+        title=_("Repository format"), vocabulary=ArchiveRepositoryFormat,
+        required=True, readonly=True)
+
     @call_with(check_permissions=True, user=REQUEST_USER)
     @operation_parameters(
         processors=List(
@@ -2321,7 +2331,7 @@ class IArchiveSet(Interface):
     def new(purpose, owner, name=None, displayname=None, distribution=None,
             description=None, enabled=True, require_virtualized=True,
             private=False, suppress_subscription_notifications=False,
-            processors=None):
+            processors=None, publishing_method=None, repository_format=None):
         """Create a new archive.
 
         On named-ppa creation, the signing key for the default PPA for the
@@ -2348,6 +2358,10 @@ class IArchiveSet(Interface):
             emails to subscribers about new subscriptions.
         :param processors: list of `IProcessors` for which the archive should
             build. If omitted, processors with `build_by_default` will be used.
+        :param publishing_method: `ArchivePublishingMethod` for this archive
+            (defaults to `LOCAL`).
+        :param repository_format: `ArchiveRepositoryFormat` for this archive
+            (defaults to `DEBIAN`).
 
         :return: an `IArchive` object.
         :raises AssertionError if name is already taken within distribution.
diff --git a/lib/lp/soyuz/interfaces/binarypackagerelease.py b/lib/lp/soyuz/interfaces/binarypackagerelease.py
index d518ed8..a4c9e4b 100644
--- a/lib/lp/soyuz/interfaces/binarypackagerelease.py
+++ b/lib/lp/soyuz/interfaces/binarypackagerelease.py
@@ -44,10 +44,15 @@ class IBinaryPackageRelease(Interface):
     version = TextLine(required=True, constraint=valid_debian_version)
     summary = Text(required=True)
     description = Text(required=True)
-    build = Int(required=True)
+    build = Reference(
+        # Really IBinaryPackageBuild.
+        Interface, required=False)
+    ci_build = Reference(
+        # Really ICIBuild.
+        Interface, required=False)
     binpackageformat = Int(required=True)
-    component = Int(required=True)
-    section = Int(required=True)
+    component = Int(required=False)
+    section = Int(required=False)
     priority = Int(required=False)
     shlibdeps = TextLine(required=False)
     depends = TextLine(required=False)
diff --git a/lib/lp/soyuz/interfaces/publishing.py b/lib/lp/soyuz/interfaces/publishing.py
index 874d1a1..f98cdb0 100644
--- a/lib/lp/soyuz/interfaces/publishing.py
+++ b/lib/lp/soyuz/interfaces/publishing.py
@@ -50,6 +50,7 @@ from zope.schema import (
     Date,
     Datetime,
     Int,
+    List,
     Text,
     TextLine,
     )
@@ -58,7 +59,9 @@ from lp import _
 from lp.registry.interfaces.distroseries import IDistroSeries
 from lp.registry.interfaces.person import IPerson
 from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.registry.interfaces.sourcepackage import SourcePackageType
 from lp.soyuz.enums import (
+    BinaryPackageFormat,
     PackagePublishingPriority,
     PackagePublishingStatus,
     )
@@ -219,6 +222,9 @@ class ISourcePackagePublishingHistoryPublic(IPublishingView):
         required=False, readonly=False)
     sourcepackagerelease = Attribute(
         'The source package release being published')
+    format = Choice(
+        title=_("Source package format"), vocabulary=SourcePackageType,
+        required=True, readonly=True)
     status = exported(
         Choice(
             title=_('Package Publishing Status'),
@@ -263,6 +269,12 @@ class ISourcePackagePublishingHistoryPublic(IPublishingView):
             vocabulary=PackagePublishingPocket,
             required=True, readonly=True,
             ))
+    channel = List(
+        value_type=TextLine(), title=_("Channel"),
+        required=False, readonly=False,
+        description=_(
+            "The channel into which this entry is published "
+            "(only for archives published using Artifactory)"))
     archive = exported(
         Reference(
             # Really IArchive (fixed in _schema_circular_imports.py).
@@ -609,6 +621,9 @@ class IBinaryPackagePublishingHistoryPublic(IPublishingView):
         required=False, readonly=False)
     binarypackagerelease = Attribute(
         "The binary package release being published")
+    binarypackageformat = Choice(
+        title=_("Binary package format"), vocabulary=BinaryPackageFormat,
+        required=True, readonly=True)
     # This and source_package_version are exported here to
     # avoid clients needing to indirectly look this up via a build.
     # This can cause security errors due to the differing levels of access.
@@ -685,6 +700,12 @@ class IBinaryPackagePublishingHistoryPublic(IPublishingView):
             vocabulary=PackagePublishingPocket,
             required=True, readonly=True,
             ))
+    channel = List(
+        value_type=TextLine(), title=_("Channel"),
+        required=False, readonly=False,
+        description=_(
+            "The channel into which this entry is published "
+            "(only for archives published using Artifactory)"))
     supersededby = Int(
             title=_('The build which superseded this one'),
             required=False, readonly=False,
diff --git a/lib/lp/soyuz/interfaces/sourcepackagerelease.py b/lib/lp/soyuz/interfaces/sourcepackagerelease.py
index 8147761..8e6cee9 100644
--- a/lib/lp/soyuz/interfaces/sourcepackagerelease.py
+++ b/lib/lp/soyuz/interfaces/sourcepackagerelease.py
@@ -84,7 +84,7 @@ class ISourcePackageRelease(Interface):
         title=_("DSC format"),
         description=_(
         "DSC file format used to upload this source"),
-        required=True)
+        required=False)
     dsc_binaries = TextLine(
         title=_("DSC proposed binaries"),
         description=_(
diff --git a/lib/lp/soyuz/model/archive.py b/lib/lp/soyuz/model/archive.py
index 2ab3a4a..727d8b2 100644
--- a/lib/lp/soyuz/model/archive.py
+++ b/lib/lp/soyuz/model/archive.py
@@ -140,7 +140,9 @@ from lp.soyuz.adapters.packagelocation import PackageLocation
 from lp.soyuz.enums import (
     archive_suffixes,
     ArchivePermissionType,
+    ArchivePublishingMethod,
     ArchivePurpose,
+    ArchiveRepositoryFormat,
     ArchiveStatus,
     ArchiveSubscriberStatus,
     PackageCopyPolicy,
@@ -369,6 +371,14 @@ class Archive(SQLBase):
 
     dirty_suites = JSON(name='dirty_suites', allow_none=True)
 
+    _publishing_method = DBEnum(
+        name='publishing_method', allow_none=True,
+        enum=ArchivePublishingMethod)
+
+    _repository_format = DBEnum(
+        name='repository_format', allow_none=True,
+        enum=ArchiveRepositoryFormat)
+
     def _init(self, *args, **kw):
         """Provide the right interface for URL traversal."""
         SQLBase._init(self, *args, **kw)
@@ -2481,6 +2491,28 @@ class Archive(SQLBase):
         elif suite not in self.dirty_suites:
             self.dirty_suites.append(suite)
 
+    @property
+    def publishing_method(self):
+        # XXX cjwatson 2022-04-04: Remove once this column has been backfilled.
+        return (
+            ArchivePublishingMethod.LOCAL if self._publishing_method is None
+            else self._publishing_method)
+
+    @publishing_method.setter
+    def publishing_method(self, value):
+        self._publishing_method = value
+
+    @property
+    def repository_format(self):
+        # XXX cjwatson 2022-04-04: Remove once this column has been backfilled.
+        return (
+            ArchiveRepositoryFormat.DEBIAN if self._repository_format is None
+            else self._repository_format)
+
+    @repository_format.setter
+    def repository_format(self, value):
+        self._repository_format = value
+
 
 def validate_ppa(owner, distribution, proposed_name, private=False):
     """Can 'person' create a PPA called 'proposed_name'?
@@ -2655,7 +2687,9 @@ class ArchiveSet:
     def new(self, purpose, owner, name=None, displayname=None,
             distribution=None, description=None, enabled=True,
             require_virtualized=True, private=False,
-            suppress_subscription_notifications=False, processors=None):
+            suppress_subscription_notifications=False, processors=None,
+            publishing_method=ArchivePublishingMethod.LOCAL,
+            repository_format=ArchiveRepositoryFormat.DEBIAN):
         """See `IArchiveSet`."""
         if distribution is None:
             distribution = getUtility(ILaunchpadCelebrities).ubuntu
@@ -2717,7 +2751,9 @@ class ArchiveSet:
             purpose=purpose, publish=publish,
             signing_key_owner=signing_key_owner,
             signing_key_fingerprint=signing_key_fingerprint,
-            require_virtualized=require_virtualized)
+            require_virtualized=require_virtualized,
+            _publishing_method=publishing_method,
+            _repository_format=repository_format)
 
         # Upon creation archives are enabled by default.
         if enabled == False:
diff --git a/lib/lp/soyuz/model/binarypackagerelease.py b/lib/lp/soyuz/model/binarypackagerelease.py
index 7baebb0..25cae48 100644
--- a/lib/lp/soyuz/model/binarypackagerelease.py
+++ b/lib/lp/soyuz/model/binarypackagerelease.py
@@ -57,14 +57,20 @@ class BinaryPackageRelease(SQLBase):
     version = StringCol(dbName='version', notNull=True)
     summary = StringCol(dbName='summary', notNull=True, default="")
     description = StringCol(dbName='description', notNull=True)
+    # DB constraint: exactly one of build and ci_build is non-NULL.
     build = ForeignKey(
-        dbName='build', foreignKey='BinaryPackageBuild', notNull=True)
+        dbName='build', foreignKey='BinaryPackageBuild', notNull=False)
+    ci_build_id = Int(name='ci_build', allow_none=True)
+    ci_build = Reference(ci_build_id, 'CIBuild.id')
     binpackageformat = DBEnum(name='binpackageformat', allow_none=False,
                               enum=BinaryPackageFormat)
+    # DB constraint: non-nullable for BinaryPackageFormat.{DEB,UDEB,DDEB}.
     component = ForeignKey(dbName='component', foreignKey='Component',
-                           notNull=True)
-    section = ForeignKey(dbName='section', foreignKey='Section', notNull=True)
-    priority = DBEnum(name='priority', allow_none=False,
+                           notNull=False)
+    # DB constraint: non-nullable for BinaryPackageFormat.{DEB,UDEB,DDEB}.
+    section = ForeignKey(dbName='section', foreignKey='Section', notNull=False)
+    # DB constraint: non-nullable for BinaryPackageFormat.{DEB,UDEB,DDEB}.
+    priority = DBEnum(name='priority', allow_none=True,
                       enum=PackagePublishingPriority)
     shlibdeps = StringCol(dbName='shlibdeps')
     depends = StringCol(dbName='depends')
@@ -122,12 +128,18 @@ class BinaryPackageRelease(SQLBase):
     @property
     def sourcepackagename(self):
         """See `IBinaryPackageRelease`."""
-        return self.build.source_package_release.sourcepackagename.name
+        if self.build is not None:
+            return self.build.source_package_release.sourcepackagename.name
+        else:
+            return None
 
     @property
     def sourcepackageversion(self):
         """See `IBinaryPackageRelease`."""
-        return self.build.source_package_release.version
+        if self.build is not None:
+            return self.build.source_package_release.version
+        else:
+            return None
 
     @cachedproperty
     def files(self):
diff --git a/lib/lp/soyuz/model/publishing.py b/lib/lp/soyuz/model/publishing.py
index 810475d..1912ab1 100644
--- a/lib/lp/soyuz/model/publishing.py
+++ b/lib/lp/soyuz/model/publishing.py
@@ -31,6 +31,10 @@ from storm.expr import (
     Sum,
     )
 from storm.info import ClassAlias
+from storm.properties import (
+    List,
+    Unicode,
+    )
 from storm.store import Store
 from storm.zope import IResultSet
 from storm.zope.interfaces import ISQLObjectResultSet
@@ -45,6 +49,7 @@ from lp.app.errors import NotFoundError
 from lp.buildmaster.enums import BuildStatus
 from lp.registry.interfaces.person import validate_public_person
 from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.registry.interfaces.sourcepackage import SourcePackageType
 from lp.registry.model.sourcepackagename import SourcePackageName
 from lp.services.database import bulk
 from lp.services.database.constants import UTC_NOW
@@ -241,8 +246,13 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
         foreignKey='SourcePackageName', dbName='sourcepackagename')
     sourcepackagerelease = ForeignKey(
         foreignKey='SourcePackageRelease', dbName='sourcepackagerelease')
+    _format = DBEnum(
+        name='format', enum=SourcePackageType,
+        default=SourcePackageType.DPKG, allow_none=True)
     distroseries = ForeignKey(foreignKey='DistroSeries', dbName='distroseries')
+    # DB constraint: non-nullable for SourcePackageType.DPKG.
     component = ForeignKey(foreignKey='Component', dbName='component')
+    # DB constraint: non-nullable for SourcePackageType.DPKG.
     section = ForeignKey(foreignKey='Section', dbName='section')
     status = DBEnum(enum=PackagePublishingStatus)
     scheduleddeletiondate = UtcDateTimeCol(default=None)
@@ -256,6 +266,7 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
     pocket = DBEnum(name='pocket', enum=PackagePublishingPocket,
                     default=PackagePublishingPocket.RELEASE,
                     allow_none=False)
+    channel = List(name="channel", type=Unicode(), allow_none=True)
     archive = ForeignKey(dbName="archive", foreignKey="Archive", notNull=True)
     copied_from_archive = ForeignKey(
         dbName="copied_from_archive", foreignKey="Archive", notNull=False)
@@ -276,6 +287,13 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
         dbName='packageupload', foreignKey='PackageUpload', default=None)
 
     @property
+    def format(self):
+        # XXX cjwatson 2022-04-04: Remove once this column has been backfilled.
+        return (
+            self._format if self._format is not None
+            else self.sourcepackagerelease.format)
+
+    @property
     def package_creator(self):
         """See `ISourcePackagePublishingHistory`."""
         return self.sourcepackagerelease.creator
@@ -638,11 +656,19 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
         foreignKey='BinaryPackageName', dbName='binarypackagename')
     binarypackagerelease = ForeignKey(
         foreignKey='BinaryPackageRelease', dbName='binarypackagerelease')
+    _binarypackageformat = DBEnum(
+        name='binarypackageformat', enum=BinaryPackageFormat, allow_none=True)
     distroarchseries = ForeignKey(
         foreignKey='DistroArchSeries', dbName='distroarchseries')
-    component = ForeignKey(foreignKey='Component', dbName='component')
-    section = ForeignKey(foreignKey='Section', dbName='section')
-    priority = DBEnum(name='priority', enum=PackagePublishingPriority)
+    # DB constraint: non-nullable for BinaryPackageFormat.{DEB,UDEB,DDEB}.
+    component = ForeignKey(
+        foreignKey='Component', dbName='component', notNull=False)
+    # DB constraint: non-nullable for BinaryPackageFormat.{DEB,UDEB,DDEB}.
+    section = ForeignKey(
+        foreignKey='Section', dbName='section', notNull=False)
+    # DB constraint: non-nullable for BinaryPackageFormat.{DEB,UDEB,DDEB}.
+    priority = DBEnum(
+        name='priority', enum=PackagePublishingPriority, allow_none=True)
     status = DBEnum(name='status', enum=PackagePublishingStatus)
     phased_update_percentage = IntCol(
         dbName='phased_update_percentage', notNull=False, default=None)
@@ -658,6 +684,7 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
     datemadepending = UtcDateTimeCol(default=None)
     dateremoved = UtcDateTimeCol(default=None)
     pocket = DBEnum(name='pocket', enum=PackagePublishingPocket)
+    channel = List(name="channel", type=Unicode(), allow_none=True)
     archive = ForeignKey(dbName="archive", foreignKey="Archive", notNull=True)
     copied_from_archive = ForeignKey(
         dbName="copied_from_archive", foreignKey="Archive", notNull=False)
@@ -667,6 +694,13 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
     removal_comment = StringCol(dbName="removal_comment", default=None)
 
     @property
+    def binarypackageformat(self):
+        # XXX cjwatson 2022-04-04: Remove once this column has been backfilled.
+        return (
+            self._binarypackageformat if self._binarypackageformat is not None
+            else self.binarypackagerelease.binpackageformat)
+
+    @property
     def distroarchseriesbinarypackagerelease(self):
         """See `IBinaryPackagePublishingHistory`."""
         # Import here to avoid circular import.
@@ -896,6 +930,7 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
             BinaryPackagePublishingHistory(
                 binarypackagename=debug.binarypackagename,
                 binarypackagerelease=debug.binarypackagerelease,
+                _binarypackageformat=debug.binarypackageformat,
                 distroarchseries=debug.distroarchseries,
                 status=PackagePublishingStatus.PENDING,
                 datecreated=UTC_NOW,
@@ -911,6 +946,7 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
         return BinaryPackagePublishingHistory(
             binarypackagename=bpr.binarypackagename,
             binarypackagerelease=bpr,
+            _binarypackageformat=bpr.binpackageformat,
             distroarchseries=self.distroarchseries,
             status=PackagePublishingStatus.PENDING,
             datecreated=UTC_NOW,
@@ -1216,6 +1252,7 @@ class PublishingSet:
             archive=archive,
             sourcepackagename=sourcepackagerelease.sourcepackagename,
             sourcepackagerelease=sourcepackagerelease,
+            _format=sourcepackagerelease.format,
             component=get_component(archive, distroseries, component),
             section=section,
             status=PackagePublishingStatus.PENDING,
diff --git a/lib/lp/soyuz/model/sourcepackagerelease.py b/lib/lp/soyuz/model/sourcepackagerelease.py
index 64086c3..eaa5533 100644
--- a/lib/lp/soyuz/model/sourcepackagerelease.py
+++ b/lib/lp/soyuz/model/sourcepackagerelease.py
@@ -79,22 +79,25 @@ from lp.soyuz.model.queue import (
 class SourcePackageRelease(SQLBase):
     _table = 'SourcePackageRelease'
 
+    # DB constraint: non-nullable for SourcePackageType.DPKG.
     section = ForeignKey(foreignKey='Section', dbName='section')
     creator = ForeignKey(
         dbName='creator', foreignKey='Person',
         storm_validator=validate_public_person, notNull=True)
+    # DB constraint: non-nullable for SourcePackageType.DPKG.
     component = ForeignKey(foreignKey='Component', dbName='component')
     sourcepackagename = ForeignKey(foreignKey='SourcePackageName',
         dbName='sourcepackagename', notNull=True)
     maintainer = ForeignKey(
         dbName='maintainer', foreignKey='Person',
-        storm_validator=validate_public_person, notNull=True)
+        storm_validator=validate_public_person, notNull=False)
     signing_key_owner_id = Int(name="signing_key_owner")
     signing_key_owner = Reference(signing_key_owner_id, 'Person.id')
     signing_key_fingerprint = Unicode()
+    # DB constraint: non-nullable for SourcePackageType.DPKG.
     urgency = DBEnum(
         name='urgency', enum=SourcePackageUrgency,
-        default=SourcePackageUrgency.LOW, allow_none=False)
+        default=SourcePackageUrgency.LOW, allow_none=True)
     dateuploaded = UtcDateTimeCol(dbName='dateuploaded', notNull=True,
         default=UTC_NOW)
     dsc = StringCol(dbName='dsc')
@@ -127,6 +130,7 @@ class SourcePackageRelease(SQLBase):
     # (primary target) we don't need to populate old records.
     dsc_maintainer_rfc822 = StringCol(dbName='dsc_maintainer_rfc822')
     dsc_standards_version = StringCol(dbName='dsc_standards_version')
+    # DB constraint: non-nullable for SourcePackageType.DPKG.
     dsc_format = StringCol(dbName='dsc_format')
     dsc_binaries = StringCol(dbName='dsc_binaries')
 
diff --git a/lib/lp/soyuz/scripts/gina/handlers.py b/lib/lp/soyuz/scripts/gina/handlers.py
index 4a17e60..5466979 100644
--- a/lib/lp/soyuz/scripts/gina/handlers.py
+++ b/lib/lp/soyuz/scripts/gina/handlers.py
@@ -931,6 +931,7 @@ class BinaryPackagePublisher:
         BinaryPackagePublishingHistory(
             binarypackagerelease=binarypackage.id,
             binarypackagename=binarypackage.binarypackagename,
+            _binarypackageformat=binarypackage.binpackageformat,
             component=component.id,
             section=section.id,
             priority=priority,
diff --git a/lib/lp/soyuz/scripts/tests/test_copypackage.py b/lib/lp/soyuz/scripts/tests/test_copypackage.py
index 223f697..dbd68e3 100644
--- a/lib/lp/soyuz/scripts/tests/test_copypackage.py
+++ b/lib/lp/soyuz/scripts/tests/test_copypackage.py
@@ -1230,6 +1230,7 @@ class TestDoDirectCopy(BaseDoCopyTests, TestCaseWithFactory):
             archive=target_archive,
             binarypackagename=bin_i386.binarypackagename,
             binarypackagerelease=bin_i386.binarypackagerelease,
+            _binarypackageformat=bin_i386.binarypackageformat,
             distroarchseries=nobby['i386'], pocket=bin_i386.pocket,
             component=bin_i386.component, section=bin_i386.section,
             priority=bin_i386.priority,
diff --git a/lib/lp/soyuz/tests/test_publishing.py b/lib/lp/soyuz/tests/test_publishing.py
index d645ef8..c153c8e 100644
--- a/lib/lp/soyuz/tests/test_publishing.py
+++ b/lib/lp/soyuz/tests/test_publishing.py
@@ -31,7 +31,10 @@ from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.series import SeriesStatus
-from lp.registry.interfaces.sourcepackage import SourcePackageUrgency
+from lp.registry.interfaces.sourcepackage import (
+    SourcePackageType,
+    SourcePackageUrgency,
+    )
 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
 from lp.services.config import config
 from lp.services.database.constants import UTC_NOW
@@ -235,6 +238,7 @@ class SoyuzTestPublisher:
 
         spr = distroseries.createUploadedSourcePackageRelease(
             sourcepackagename=spn,
+            format=SourcePackageType.DPKG,
             maintainer=maintainer,
             creator=creator,
             component=component,
@@ -290,6 +294,7 @@ class SoyuzTestPublisher:
             distroseries=distroseries,
             sourcepackagerelease=spr,
             sourcepackagename=spr.sourcepackagename,
+            _format=spr.format,
             component=spr.component,
             section=spr.section,
             status=status,
@@ -487,6 +492,7 @@ class SoyuzTestPublisher:
                 distroarchseries=arch,
                 binarypackagerelease=binarypackagerelease,
                 binarypackagename=binarypackagerelease.binarypackagename,
+                _binarypackageformat=binarypackagerelease.binpackageformat,
                 component=binarypackagerelease.component,
                 section=binarypackagerelease.section,
                 priority=binarypackagerelease.priority,
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 7c79147..e72b78f 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -239,6 +239,7 @@ from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.interfaces.sourcepackage import (
     ISourcePackage,
     SourcePackageFileType,
+    SourcePackageType,
     SourcePackageUrgency,
     )
 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
@@ -313,7 +314,9 @@ from lp.snappy.model.snapbuild import SnapFile
 from lp.soyuz.adapters.overrides import SourceOverride
 from lp.soyuz.adapters.packagelocation import PackageLocation
 from lp.soyuz.enums import (
+    ArchivePublishingMethod,
     ArchivePurpose,
+    ArchiveRepositoryFormat,
     BinaryPackageFileType,
     BinaryPackageFormat,
     DistroArchSeriesFilterSense,
@@ -2992,7 +2995,9 @@ class BareLaunchpadObjectFactory(ObjectFactory):
                     purpose=None, enabled=True, private=False,
                     virtualized=True, description=None, displayname=None,
                     suppress_subscription_notifications=False,
-                    processors=None):
+                    processors=None,
+                    publishing_method=ArchivePublishingMethod.LOCAL,
+                    repository_format=ArchiveRepositoryFormat.DEBIAN):
         """Create and return a new arbitrary archive.
 
         :param distribution: Supply IDistribution, defaults to a new one
@@ -3008,6 +3013,10 @@ class BareLaunchpadObjectFactory(ObjectFactory):
         :param suppress_subscription_notifications: Whether to suppress
             subscription notifications, defaults to False.  Only useful
             for private archives.
+        :param publishing_method: `ArchivePublishingMethod` for this archive
+            (defaults to `LOCAL`).
+        :param repository_format: `ArchiveRepositoryFormat` for this archive
+            (defaults to `DEBIAN`).
         """
         if purpose is None:
             purpose = ArchivePurpose.PPA
@@ -3039,7 +3048,9 @@ class BareLaunchpadObjectFactory(ObjectFactory):
                 owner=owner, purpose=purpose,
                 distribution=distribution, name=name, displayname=displayname,
                 enabled=enabled, require_virtualized=virtualized,
-                description=description, processors=processors)
+                description=description, processors=processors,
+                publishing_method=publishing_method,
+                repository_format=repository_format)
 
         if private:
             naked_archive = removeSecurityProxy(archive)
@@ -3834,7 +3845,8 @@ class BareLaunchpadObjectFactory(ObjectFactory):
                                  changelog_entry=None,
                                  homepage=None,
                                  changelog=None,
-                                 copyright=None):
+                                 copyright=None,
+                                 format=None):
         """Make a `SourcePackageRelease`."""
         if distroseries is None:
             if source_package_recipe_build is not None:
@@ -3878,11 +3890,15 @@ class BareLaunchpadObjectFactory(ObjectFactory):
         if version is None:
             version = str(self.getUniqueInteger()) + 'version'
 
+        if format is None:
+            format = SourcePackageType.DPKG
+
         if copyright is None:
             copyright = self.getUniqueString()
 
         return distroseries.createUploadedSourcePackageRelease(
             sourcepackagename=sourcepackagename,
+            format=format,
             maintainer=maintainer,
             creator=creator,
             component=component,