Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package openSUSE-release-tools for 
openSUSE:Factory checked in at 2025-10-22 12:16:13
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/openSUSE-release-tools (Old)
 and      /work/SRC/openSUSE:Factory/.openSUSE-release-tools.new.18484 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "openSUSE-release-tools"

Wed Oct 22 12:16:13 2025 rev:541 rq:1312862 version:20251014.2d75aced

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/openSUSE-release-tools/openSUSE-release-tools.changes
    2025-10-08 18:20:21.105087755 +0200
+++ 
/work/SRC/openSUSE:Factory/.openSUSE-release-tools.new.18484/openSUSE-release-tools.changes
 2025-10-22 12:20:50.811002964 +0200
@@ -1,0 +2,19 @@
+Tue Oct 14 15:34:04 UTC 2025 - [email protected]
+
+- Update to version 20251014.2d75aced:
+  * gocd: sles.target: 16.1: release transactional images
+
+-------------------------------------------------------------------
+Thu Oct 09 08:54:21 UTC 2025 - [email protected]
+
+- Update to version 20251009.414a66f7:
+  * Update .editorconfig-checker.json
+  * .editorconfig: Allow trailing whitespace in .md files
+  * ttm: Simplify version_from_project
+  * ttm: Refactor core logic with per-product attributes
+  * ttm: Drop workaround for NonOSS trees not containing the product version
+  * ttm: Unify methods to get build version of a product
+  * ttm: Drop need_same_build_number, not used anywhere anymore
+  * ttm: Complain about unknown config options
+
+-------------------------------------------------------------------

Old:
----
  openSUSE-release-tools-20251001.0ed4bc8b.obscpio

New:
----
  openSUSE-release-tools-20251014.2d75aced.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ openSUSE-release-tools.spec ++++++
--- /var/tmp/diff_new_pack.E5bS0S/_old  2025-10-22 12:20:51.999053023 +0200
+++ /var/tmp/diff_new_pack.E5bS0S/_new  2025-10-22 12:20:52.003053191 +0200
@@ -21,7 +21,7 @@
 %define announcer_filename factory-package-news
 %define services osrt-slsa.target [email protected] 
[email protected] [email protected] [email protected]
 Name:           openSUSE-release-tools
-Version:        20251001.0ed4bc8b
+Version:        20251014.2d75aced
 Release:        0
 Summary:        Tools to aid in staging and release work for openSUSE/SUSE
 License:        GPL-2.0-or-later AND MIT
@@ -61,9 +61,11 @@
 Requires:       python3-python-dateutil
 Requires:       python3-pyxdg
 Requires:       python3-requests
-# typing extensions are needed on SLE & Leap
 %if 0%{?suse_version} <= 1500
+# typing extensions are needed on SLE & Leap
 Requires:       python3-typing_extensions
+# Backport for py 3.6
+Requires:       python3-dataclasses
 %endif
 
 # Spec related requirements.

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.E5bS0S/_old  2025-10-22 12:20:52.055055383 +0200
+++ /var/tmp/diff_new_pack.E5bS0S/_new  2025-10-22 12:20:52.059055551 +0200
@@ -1,7 +1,7 @@
 <servicedata>
   <service name="tar_scm">
     <param 
name="url">https://github.com/openSUSE/openSUSE-release-tools.git</param>
-    <param 
name="changesrevision">0ed4bc8bcafea4303ea5d5fef6d960d3e3f95155</param>
+    <param 
name="changesrevision">2d75aced96a43eafd9d1a411e7d2f06501a2575c</param>
   </service>
 </servicedata>
 

++++++ openSUSE-release-tools-20251001.0ed4bc8b.obscpio -> 
openSUSE-release-tools-20251014.2d75aced.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openSUSE-release-tools-20251001.0ed4bc8b/.ecrc 
new/openSUSE-release-tools-20251014.2d75aced/.ecrc
--- old/openSUSE-release-tools-20251001.0ed4bc8b/.ecrc  2025-10-01 
10:58:08.000000000 +0200
+++ new/openSUSE-release-tools-20251014.2d75aced/.ecrc  1970-01-01 
01:00:00.000000000 +0100
@@ -1,14 +0,0 @@
-{
-  "Verbose": false,
-  "IgnoreDefaults": false,
-  "Exclude": ["tests/fixtures", "LICENSE", "\\.py$", "bs_copy"],
-  "SpacesAfterTabs": true,
-  "Disable": {
-    "EndOfLine": false,
-    "Indentation": false,
-    "IndentSize": false,
-    "InsertFinalNewline": false,
-    "TrimTrailingWhitespace": false,
-    "MaxLineLength": false
-  }
-}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20251001.0ed4bc8b/.editorconfig 
new/openSUSE-release-tools-20251014.2d75aced/.editorconfig
--- old/openSUSE-release-tools-20251001.0ed4bc8b/.editorconfig  2025-10-01 
10:58:08.000000000 +0200
+++ new/openSUSE-release-tools-20251014.2d75aced/.editorconfig  2025-10-14 
17:33:14.000000000 +0200
@@ -42,3 +42,6 @@
 
 [data/repos.json]
 indent_size = 3
+
+[**.md]
+trim_trailing_whitespace = false
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20251001.0ed4bc8b/.editorconfig-checker.json 
new/openSUSE-release-tools-20251014.2d75aced/.editorconfig-checker.json
--- old/openSUSE-release-tools-20251001.0ed4bc8b/.editorconfig-checker.json     
1970-01-01 01:00:00.000000000 +0100
+++ new/openSUSE-release-tools-20251014.2d75aced/.editorconfig-checker.json     
2025-10-14 17:33:14.000000000 +0200
@@ -0,0 +1,14 @@
+{
+  "Verbose": false,
+  "IgnoreDefaults": false,
+  "Exclude": ["tests/fixtures", "LICENSE", "\\.py$"],
+  "SpacesAfterTabs": true,
+  "Disable": {
+    "EndOfLine": false,
+    "Indentation": false,
+    "IndentSize": false,
+    "InsertFinalNewline": false,
+    "TrimTrailingWhitespace": false,
+    "MaxLineLength": false
+  }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20251001.0ed4bc8b/.github/workflows/ci-test.yml 
new/openSUSE-release-tools-20251014.2d75aced/.github/workflows/ci-test.yml
--- old/openSUSE-release-tools-20251001.0ed4bc8b/.github/workflows/ci-test.yml  
2025-10-01 10:58:08.000000000 +0200
+++ new/openSUSE-release-tools-20251014.2d75aced/.github/workflows/ci-test.yml  
2025-10-14 17:33:14.000000000 +0200
@@ -87,6 +87,9 @@
           if test -e metrics.py; then
               zypper -n in python3-influxdb-client python3-GitPython
           fi
+          if test -e /usr/bin/python3.6; then
+              zypper -n in python3-dataclasses
+          fi
 
       - name: run a simple smoke test whether --help actually works
         run: |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20251001.0ed4bc8b/dist/package/openSUSE-release-tools.spec
 
new/openSUSE-release-tools-20251014.2d75aced/dist/package/openSUSE-release-tools.spec
--- 
old/openSUSE-release-tools-20251001.0ed4bc8b/dist/package/openSUSE-release-tools.spec
       2025-10-01 10:58:08.000000000 +0200
+++ 
new/openSUSE-release-tools-20251014.2d75aced/dist/package/openSUSE-release-tools.spec
       2025-10-14 17:33:14.000000000 +0200
@@ -61,9 +61,11 @@
 Requires:       python3-python-dateutil
 Requires:       python3-pyxdg
 Requires:       python3-requests
-# typing extensions are needed on SLE & Leap
 %if 0%{?suse_version} <= 1500
+# typing extensions are needed on SLE & Leap
 Requires:       python3-typing_extensions
+# Backport for py 3.6
+Requires:       python3-dataclasses
 %endif
 
 # Spec related requirements.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20251001.0ed4bc8b/docs/totest.asciidoc 
new/openSUSE-release-tools-20251014.2d75aced/docs/totest.asciidoc
--- old/openSUSE-release-tools-20251001.0ed4bc8b/docs/totest.asciidoc   
2025-10-01 10:58:08.000000000 +0200
+++ new/openSUSE-release-tools-20251014.2d75aced/docs/totest.asciidoc   
2025-10-14 17:33:14.000000000 +0200
@@ -45,10 +45,10 @@
 -------------------------------------------------------------------------------
 $ osc meta prjconf MyProject:ToTest
 %if "%_repository" == "images"
-%Type: kiwi
-%Repotype: staticlinks
-%Patterntype: none
-%Prefer: openSUSE-release
+Type: kiwi
+Repotype: staticlinks
+Patterntype: none
+Prefer: openSUSE-release
 %endif
 -------------------------------------------------------------------------------
 
@@ -81,7 +81,6 @@
   - MyProduct-Live:
       - x86_64
 take_source_from_product: true
-need_same_build_number: true
 set_snapshot_number: true
 test_subproject: ToTest
 EOF
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20251001.0ed4bc8b/gocd/sles.target.gocd.yaml 
new/openSUSE-release-tools-20251014.2d75aced/gocd/sles.target.gocd.yaml
--- old/openSUSE-release-tools-20251001.0ed4bc8b/gocd/sles.target.gocd.yaml     
2025-10-01 10:58:08.000000000 +0200
+++ new/openSUSE-release-tools-20251014.2d75aced/gocd/sles.target.gocd.yaml     
2025-10-14 17:33:14.000000000 +0200
@@ -38,7 +38,7 @@
         tasks:
         - script: |-
             set -e
-            for image in agama-installer-SLES kiwi-templates-Minimal; do
+            for image in agama-installer-SLES kiwi-templates-Minimal 
SLES_transactional; do
               osc -A https://api.suse.de release 
--target-project=SUSE:SLFO:Products:SLES:16.1:TEST --target-repository=images 
-r images SUSE:SLFO:Products:SLES:16.1 $image
             done
             osc -A https://api.suse.de release  
--target-project=SUSE:SLFO:Products:SLES:16.1:TEST --target-repository=product 
-r product SUSE:SLFO:Products:SLES:16.1 000productcompose
@@ -83,7 +83,7 @@
         tasks:
         - script: |-
             set -e
-            for image in agama-installer-SLES kiwi-templates-Minimal; do
+            for image in agama-installer-SLES kiwi-templates-Minimal 
SLES_transactional; do
               osc -A https://api.suse.de release 
--target-project=SUSE:SLFO:Products:SLES:16.1:PUBLISH 
--target-repository=images -r images SUSE:SLFO:Products:SLES:16.1:TEST $image
             done
             osc -A https://api.suse.de release  
--target-project=SUSE:SLFO:Products:SLES:16.1:PUBLISH 
--target-repository=product -r product SUSE:SLFO:Products:SLES:16.1:TEST 
000productcompose
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20251001.0ed4bc8b/ttm/README.md 
new/openSUSE-release-tools-20251014.2d75aced/ttm/README.md
--- old/openSUSE-release-tools-20251001.0ed4bc8b/ttm/README.md  2025-10-01 
10:58:08.000000000 +0200
+++ new/openSUSE-release-tools-20251014.2d75aced/ttm/README.md  2025-10-14 
17:33:14.000000000 +0200
@@ -8,68 +8,133 @@
 
 Both stages can run independently, but they communicate using the 
`ToTestManagerStatus` attribute to avoid races, like releasing a product while 
it's being published or overwriting a product while it's being tested. The 
releaser publish disables the test subproject before releasing a product.
 
-Products
+Project Configuration
 --------
-
-There are various kinds of product types:
-
-* main: Built in the main project in the `product_repo` for the given 
architectures and released into the `product_repo` in the test subproject. 
Optionally, it uses the OBS `set_release` option to set the build number of the 
products on release. On publish, the `product_repo` in the test subproject is 
publish enabled.
-* livecd: Like main, but uses the `:Live` subproject's `livecd_repo` as source.
-* ftp: Like main, but does not use `set_release`.
-* image: Like main, but released into the `totest_images_repo` in the test 
subproject. On publishing, the `totest_images_repo` is publish enabled.
-* container: Like main, but released into the `totest_container_repo` instead, 
which is always publish enabled so that they can be fetched from the OBS 
registry during testing. For publishing, those products are released into the 
releasetarget of `totest_container_repo`. This is best combined with a 
`kind="maintenance_release"` as the target project, to keep older builds 
instead of overwriting them. For long-living projects, `container-cleaner.py` 
is run which deletes older images.
-* containerfile: Like container, but taken from a repo called `containerfile` 
instead, where container images are built using `Dockerfile` recipes.
-
-Every product can have multiple architectures defined, those are only used to 
check for build success before doing a release. If any of the listed 
architectures failed to build, a release is blocked.
-
-OBS does not allow to release a multibuild container without all of its 
flavors, so mentioning a multibuild container itself can be used instead of 
listing all flavors explicitly. In that case, there is no check for build 
success for the individual flavors, unless they are listed in addition.
-
-There is a check to ensure that every successful build in the `product_repo` 
maps to a product. If this is not the case, an error is printed and the release 
is blocked.
-
-set_release
------------
-
-The `set_release` mechanism can be enabled for main, livecd and image 
products, where their build number is overwritten by `snapshot_number_prefix` + 
a number. If `take_source_from_product` is set, that number is taken from the 
first main product (`product_repo` + `product_arch`) or if that doesn't exist, 
the first image product (first arch). Otherwise, the 
`000release-packages:(base)-release` package from the main project's `standard` 
repo and `arch` is looked at.
-
-Configuration
--------------
-
 The configuration is stored in the `ToTestManagerConfig` attribute in YAML 
format.
 
+Available options and their defaults are:
+
 ```
 base: openSUSE # Defaults to the toplevel project name
 test_subproject: ToTest
+test_project: <project>:ToTest # Defaults to <project>:<test_subproject>
 do_not_release: False # If set, publishing is a noop (and the releaser doesn't 
publish disable!)
-need_same_build_number: False # See set_release above
-set_snapshot_number: False # See set_release above
-snapshot_number_prefix: Snapshot # See set_release above
-take_source_from_product: False # See set_release above
-arch: x86_64 # See set_release above
+set_snapshot_number: False
+snapshot_number_prefix: Snapshot
+take_source_from_product: False
+arch: x86_64
+
+# openQA settings
+openqa_server: None # URL to the openQA server, e.g. 
"https://openqa.opensuse.org";
+openqa_group: None # Name of the job group in openQA, e.g. "openSUSE 
Tumbleweed"
 jobs_num: 42 # Minimum number of openQA jobs before publishing is possible
 
+# Global defaults for products
 product_repo: images
-product_arch: local # See set_release above
+product_arch: local
 livecd_repo: images
 totest_container_repo: containers
-totest_images_repo: images # Repo for image_products. If not set, uses 
product_repo.
+totest_images_repo: images # Default: Same as product_repo.
+
+products:
+  (see below)
+```
+
+Product Configuration
+--------
+
+Every ttm managed project has a list of products which are defined by 
following attributes:
 
+* `package`: The package name, optionally with multibuild flavor, e.g. 
"opensuse-tumbleweed-image:docker"
+* `archs`: List of architectures the product is built for. Default: 
`[product_arch]`
+* `build_prj`: The project the package is built in. Default: main project
+* `build_repo`: The repository the package is built in. Default: `product_repo`
+* `needs_to_contain_product_version`: If true, the *.report binary needs to 
contain the product version in its name. Default: false.
+* `max_size`: If set, maximum allowed size of the *.iso binary in bytes. 
Default: None.
+* `release_prj`: The project the product is released into. Default: 
`test_project` resp. defined by `test_subproject`.
+* `release_repo`: The repository the product is released into. Default: 
`product_repo`
+* `release_set_version`: If true, the "setrelease" mechanism (see below) is 
used.
+* `publish_using_release`: Defines the publishing method:  
+  True: The package is released from release_prj/release_repo according
+        to the releasetarget in the prj meta.  
+  False: release_prj/release_repo is publish disabled on release and publish 
enabled on publish.
+
+To allow a simpler configuration for most common product types and backwards 
compatibility, there are various kinds of pre-defined product types:
+
+* main: Built in the main project in the `product_repo` for the given 
architectures and released into the `product_repo` in the test subproject. 
Optionally, it uses the OBS `set_release` option to set the build number of the 
products on release. On publish, the `product_repo` in the test subproject is 
publish enabled.
+* livecd: Like main, but uses the `:Live` subproject's `livecd_repo` as source.
+* ftp: Like main, but does not use `set_release`.
+* image: Like main, but released into the `totest_images_repo` in the test 
subproject. On publishing, the `totest_images_repo` is publish enabled.
+* container: Like main, but released into the `totest_container_repo` instead, 
which is always publish enabled so that they can be fetched from the OBS 
registry during testing. For publishing, those products are released into the 
releasetarget of `totest_container_repo`. This is best combined with a 
`kind="maintenance_release"` as the target project, to keep older builds 
instead of overwriting them. For long-living projects, `container-cleaner.py` 
is run which deletes older images.
+* containerfile: Like container, but taken from a repo called `containerfile` 
instead, where container images are built using `Dockerfile` recipes.
+
+The following product definitions are equivalent:
+
+```
+totest_images_repo: appliances
 products:
   main:
   - foo:dvd
-  livecd:
-  - livecd-foo:
-    - x86_64
   ftp:
   - foo:ftp
+  livecds:
+  - livecd-foo:
+    - x86_64
   image:
   - foo:kvm:
     - x86_64
-  - foo-container-image:lxc:
-    - x86_64
   container:
   - foo-container-image:docker:
     - x86_64
   containerfile:
   - some-dockerfile:
     - x86_64
+---
+products:
+  custom:
+    # Implicit defaults for each custom product:
+    # archs: [local]
+    # build_prj: openSUSE:Factory
+    # build_repo: images
+    # needs_to_contain_product_version: false
+    # max_size: None
+    # release_prj: openSUSE:Factory:ToTest
+    # release_repo: images
+    # release_set_version: false
+    # publish_using_release: false
+    foo:dvd:
+      max_size: 4700372992
+      release_set_version: true
+    foo:ftp:
+      needs_to_contain_product_version: true
+    livecd-foo:
+      archs: [x86_64]
+      release_set_version: true
+      build_prj: openSUSE:Factory:Live
+    foo:kvm:
+      archs: [x86_64]
+      release_set_version: true
+      release_repo: appliances
+    foo-container-image:docker:
+      archs: [x86_64]
+      release_repo: containers
+      publish_using_release: true
+    some-dockerfile:
+      archs: [x86_64]
+      build_repo: containerfile
+      release_repo: containers
+      publish_using_release: true
 ```
+
+Every product can have multiple architectures defined, those are only used to 
check for build success before doing a release. If any of the listed 
architectures failed to build, a release is blocked.
+
+OBS does not allow to release a multibuild container without all of its 
flavors, so mentioning a multibuild container itself can be used instead of 
listing all flavors explicitly. In that case, there is no check for build 
success for the individual flavors, unless they are listed in addition.
+
+There is a check to ensure that every successful build in the `product_repo` 
maps to a product. If this is not the case, an error is printed and the release 
is blocked.
+
+set_release
+-----------
+
+The `set_release` mechanism can be enabled products, in which case their build 
number is overwritten by `snapshot_number_prefix` + a number.
+If `take_source_from_product` is enabled, that number is taken from the first 
"main" product, or if there is none, the first "custom" product.
+If `take_source_from_product` is disabled, the 
`000release-packages:(base)-release` package from the main project's `standard` 
repo and `arch` is looked at.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20251001.0ed4bc8b/ttm/manager.py 
new/openSUSE-release-tools-20251014.2d75aced/ttm/manager.py
--- old/openSUSE-release-tools-20251001.0ed4bc8b/ttm/manager.py 2025-10-01 
10:58:08.000000000 +0200
+++ new/openSUSE-release-tools-20251014.2d75aced/ttm/manager.py 2025-10-14 
17:33:14.000000000 +0200
@@ -64,35 +64,20 @@
     def current_qa_version(self):
         return self.api.pseudometa_file_load(self.version_file('totest'))
 
-    def iso_build_version(self, project, tree, repo=None, arch=None):
-        for binary in self.binaries_of_product(project, tree, repo=repo, 
arch=arch):
+    def build_version(self, project, tree, repo, arch):
+        for binary in self.binaries_of_product(project, tree, repo, arch):
             result = re.match(
-                
r'.*-(?:Build|Snapshot)([0-9.]+)(?:-Media.*\.iso|\.docker\.tar\.xz|\.tar\.xz|\.raw\.xz|\.appx)',
 binary)
+                
r'.*-(?:Build|Snapshot)([0-9.]+)(?:-Media.*\.iso|\.docker\.tar\.xz|\.tar\.xz|\.raw\.xz|\.appx|\.report)',
 binary)
             if result:
                 return result.group(1)
-        raise NotFoundException(f"can't find {project} iso version")
-
-    def productcompose_build_version(self, project, tree, repo=None, 
arch=None):
-        for binary in self.binaries_of_product(project, tree, repo=repo, 
arch=arch):
-            result = re.match(
-                r'.*-(?:Build|Snapshot)([0-9.]+)(.report)', binary)
-            if result:
-                return result.group(1)
-        raise NotFoundException(f"can't find {project} productcompose version")
+        raise NotFoundException(f"can't find version in 
{project}/{tree}/{repo}/{arch}")
 
     def version_from_totest_project(self):
-        if len(self.project.main_products):
-            return self.iso_build_version(self.project.test_project, 
self.project.main_products[0])
-
-        return self.iso_build_version(self.project.test_project, 
self.project.image_products[0].package,
-                                      
arch=self.project.image_products[0].archs[0])
-
-    def binaries_of_product(self, project, product, repo=None, arch=None):
-        if repo is None:
-            repo = self.project.product_repo
-        if arch is None:
-            arch = self.project.product_arch
+        first_product = self.project.products[0]
+        return self.build_version(first_product.release_prj, 
first_product.package,
+                                  first_product.release_repo, 
first_product.archs[0])
 
+    def binaries_of_product(self, project, product, repo, arch):
         url = self.api.makeurl(['build', project, repo, arch, product])
         try:
             f = self.api.retried_GET(url)
@@ -106,13 +91,6 @@
 
         return ret
 
-    def ftp_build_version(self, project, tree):
-        for binary in self.binaries_of_product(project, tree):
-            result = re.match(r'.*-Build(.*)-Media1.report', binary)
-            if result:
-                return result.group(1)
-        raise NotFoundException(f"can't find {project} ftp version")
-
     # make sure to update the attribute as atomic as possible - as such
     # only update the snapshot and don't erase anything else. The snapshots
     # have very different update times within the pipeline, so there is
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20251001.0ed4bc8b/ttm/publisher.py 
new/openSUSE-release-tools-20251014.2d75aced/ttm/publisher.py
--- old/openSUSE-release-tools-20251001.0ed4bc8b/ttm/publisher.py       
2025-10-01 10:58:08.000000000 +0200
+++ new/openSUSE-release-tools-20251014.2d75aced/ttm/publisher.py       
2025-10-14 17:33:14.000000000 +0200
@@ -220,14 +220,15 @@
 
         if not force:
             wait_time = 20
-            while not self.all_repos_done(self.project.test_project):
-                if self.dryrun:
-                    self.logger.info('{} is still not published, do not wait 
as dryrun.'.format(
-                        self.project.test_project))
-                    return
-                self.logger.info('{} is still not published, waiting {} 
seconds'.format(
-                    self.project.test_project, wait_time))
-                time.sleep(wait_time)
+            for prj in set([p.release_prj for p in self.project.products if 
not p.publish_using_release]):
+                while not self.all_repos_done(prj):
+                    if self.dryrun:
+                        self.logger.info('{} is still not published, do not 
wait as dryrun.'.format(
+                            prj))
+                        return
+                    self.logger.info('{} is still not published, waiting {} 
seconds'.format(
+                        prj, wait_time))
+                    time.sleep(wait_time)
 
         current_snapshot = self.get_status('publishing')
         if self.dryrun:
@@ -296,21 +297,15 @@
         self.logger.info('Publish test project content')
         if self.dryrun or self.project.do_not_release:
             return
-        if self.project.container_products or 
self.project.containerfile_products:
-            self.logger.info('Releasing container products from ToTest')
-            for container in self.project.container_products + 
self.project.containerfile_products:
-                self.release_package(self.project.test_project, 
container.package,
-                                     
repository=self.project.totest_container_repo)
-
-        self.api.switch_flag_in_prj(
-            self.project.test_project, flag='publish', state='enable',
-            repository=self.project.product_repo)
-        if self.project.publish_multiple_product_repo and 
len(self.project.product_repo_overrides):
-            for key, value in self.project.product_repo_overrides.items():
-                self.api.switch_flag_in_prj(self.project.test_project, 
flag='publish',
-                                            state='enable', repository=value)
-
-        if self.project.totest_images_repo != self.project.product_repo:
-            self.logger.info('Publish test project content (image_products)')
-            self.api.switch_flag_in_prj(self.project.test_project, 
flag='publish', state='enable',
-                                        
repository=self.project.totest_images_repo)
+
+        prj_repo_to_publish_enable = set()
+
+        self.logger.info('Releasing container products from ToTest')
+        for p in self.project.products:
+            if p.publish_using_release:
+                self.release_package(p.release_prj, p.package, 
repository=p.release_repo)
+            else:
+                prj_repo_to_publish_enable |= set([(p.release_prj, 
p.release_repo)])
+
+        for prj, repo in prj_repo_to_publish_enable:
+            self.api.switch_flag_in_prj(prj, flag='publish', state='enable', 
repository=repo)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20251001.0ed4bc8b/ttm/releaser.py 
new/openSUSE-release-tools-20251014.2d75aced/ttm/releaser.py
--- old/openSUSE-release-tools-20251001.0ed4bc8b/ttm/releaser.py        
2025-10-01 10:58:08.000000000 +0200
+++ new/openSUSE-release-tools-20251014.2d75aced/ttm/releaser.py        
2025-10-14 17:33:14.000000000 +0200
@@ -9,6 +9,7 @@
 # Distribute under GPLv2 or GPLv3
 
 import re
+from collections import defaultdict
 from lxml import etree as ET
 
 from ttm.manager import ToTestManager, NotFoundException, QAResult
@@ -58,81 +59,51 @@
         self.write_version_to_dashboard('totest', new_snapshot)
         return QAResult.passed
 
-    def release_version(self):
-        url = self.api.makeurl(['build', self.project.name, 'standard', 
self.project.arch,
-                                
f'000release-packages:{self.project.base}-release'])
-        f = self.api.retried_GET(url)
-        root = ET.parse(f).getroot()
-        for binary in root.findall('binary'):
-            binary = binary.get('filename', '')
+    def version_from_project(self):
+        if self.project.take_source_from_product:
+            first_product = self.project.products[0]
+            return self.build_version(first_product.build_prj, 
first_product.package,
+                                      first_product.build_repo, 
first_product.archs[0])
+
+        for binary in self.binaries_of_product(self.project.name,
+                                               
f'000release-packages:{self.project.base}-release',
+                                               repo='standard', 
arch=self.project.arch):
             result = re.match(r'.*-([^-]*)-[^-]*.src.rpm', binary)
             if result:
                 return result.group(1)
 
         raise NotFoundException(f"can't find {self.project.name} version")
 
-    def version_from_project(self):
-        if not self.project.take_source_from_product:
-            return self.release_version()
-
-        if len(self.project.main_products):
-            # 000productcompose has ftp built only and the build number
-            # offline installer carry over build number from ftp product
-            # as well as agama-installer
-            if 'productcompose' in self.project.main_products[0] and\
-                    'productcompose' in self.project.ftp_products[0]:
-                return self.productcompose_build_version(self.project.name, 
self.project.ftp_products[0],
-                                                         
repo=self.project.product_repo_overrides.get(self.project.ftp_products[0],
-                                                                               
                       self.project.product_repo))
-            return self.iso_build_version(self.project.name, 
self.project.main_products[0])
-
-        return self.iso_build_version(self.project.name, 
self.project.image_products[0].package,
-                                      
arch=self.project.image_products[0].archs[0])
-
-    def maxsize_for_package(self, package, arch):
-        if re.match(r'.*[-_]mini[-_].*', package):
-            return 737280000  # a CD needs to match
-
-        if re.match(r'.*[-_]dvd5[-_].*', package):
-            return 4700372992  # a DVD needs to match
-
-        if re.match(r'.*[-_](dvd9[-_]dvd|cd[-_]DVD)[-_].*', package):
-            return 8539996159
+    def package_ok(self, prjresult, product, arch):
+        """Checks one product/arch in a project and returns True if it's 
succeeded"""
 
-        # Other types don't have a fixed size limit
-        return None
-
-    def package_ok(self, prjresult, project, package, repository, arch):
-        """Checks one package in a project and returns True if it's 
succeeded"""
-
-        status = 
prjresult.xpath(f'result[@repository="{repository}"][@arch="{arch}"]/'
-                                 f'status[@package="{package}"]')
+        status = 
prjresult.xpath(f'result[@repository="{product.build_repo}"][@arch="{arch}"]/'
+                                 f'status[@package="{product.package}"]')
 
         failed = [s for s in status if s.get('code') != 'succeeded']
         if len(failed):
             self.logger.info(
-                f"{project} {package} {repository} {arch} -> 
{failed[0].get('code')}")
+                f"{product.build_prj} {product.package} {product.build_repo} 
{arch} -> {failed[0].get('code')}")
             return False
 
         succeeded = [s for s in status if s.get('code') == 'succeeded']
         if not len(succeeded):
-            self.logger.info(f'No "succeeded" for {project} {package} 
{repository} {arch}')
+            self.logger.info(f'No "succeeded" for {product.build_prj} 
{product.package} {product.build_repo} {arch}')
             return False
 
-        maxsize = self.maxsize_for_package(package, arch)
-        if not maxsize:
+        if product.max_size is None:
             return True
 
-        url = self.api.makeurl(['build', project, repository, arch, package])
+        url = self.api.makeurl(['build', product.build_prj, 
product.build_repo, arch, product.package])
         f = self.api.retried_GET(url)
         root = ET.parse(f).getroot()
         for binary in root.findall('binary'):
             if not binary.get('filename', '').endswith('.iso'):
                 continue
             isosize = int(binary.get('size', 0))
-            if isosize > maxsize:
+            if isosize > product.max_size:
                 self.logger.error('%s %s %s %s: %s' % (
-                    project, package, repository, arch, 'too large by %s 
bytes' % (isosize - maxsize)))
+                    product.build_prj, product.package, product.build_repo, 
arch, 'too large by %s bytes' % (isosize - product.max_size)))
                 return False
 
         return True
@@ -140,29 +111,19 @@
     def all_built_products_in_config(self):
         """Verify that all succeeded products are mentioned in the ttm 
config"""
 
-        # First for all products in product_repo
-        products = {}
-        for simple_product in self.project.ftp_products + 
self.project.main_products:
-            products[simple_product] = [self.project.product_arch]
-        for image_product in self.project.image_products + 
self.project.container_products:
-            products[image_product.package] = image_product.archs
-
-        all_found = 
self.verify_package_list_complete(self.project.product_repo, products)
-        if len(self.project.product_repo_overrides):
-            for key, value in self.project.product_repo_overrides.items():
-                all_found = self.verify_package_list_complete(value, products) 
and all_found
-
-        # Then for containerfile_products
-        if self.project.containerfile_products:
-            products = {}
-            for image_product in self.project.containerfile_products:
-                products[image_product.package] = image_product.archs
+        # Dict of products per prj/repo, e.g.
+        # {('openSUSE:Leap:15.6:Images', 'images'): {'livecd-leap-gnome': 
['aarch64', 'x86_64'], ...}}
+        products_for_prj_repo = defaultdict(dict)
+        for p in self.project.products:
+            products_for_prj_repo[(p.build_prj, p.build_repo)][p.package] = 
p.archs
 
-            all_found = self.verify_package_list_complete('containerfile', 
products) and all_found
+        all_found = True
+        for (prj, repo), products in products_for_prj_repo.items():
+            all_found = self.verify_package_list_complete(prj, repo, products) 
and all_found
 
         return all_found
 
-    def verify_package_list_complete(self, repository, product_archs):
+    def verify_package_list_complete(self, project, repository, product_archs):
         """Loop through all successfully built products and check whether they
            are part of product_archs (e.g. {'foo:ftp': ['local'], some-image': 
['x86_64'], ...})"""
 
@@ -170,7 +131,7 @@
         all_found = True
 
         # Get all results for the product repo from OBS
-        url = self.api.makeurl(['build', self.project.name, "_result"],
+        url = self.api.makeurl(['build', project, "_result"],
                                {'repository': repository,
                                 'multibuild': 1})
         f = self.api.retried_GET(url)
@@ -204,49 +165,26 @@
         return all_found
 
     def is_snapshotable(self):
-        """Check various conditions required for factory to be snapshotable
-
-        """
-
-        if not self.all_repos_done(self.project.name):
-            return False
+        """Check various conditions required for factory to be snapshotable"""
 
         all_ok = True
 
-        resultxml = self.api.retried_GET(self.api.makeurl(['build', 
self.project.name, '_result']))
-        prjresult = ET.parse(resultxml).getroot()
-
-        for product in self.project.ftp_products + self.project.main_products:
-            if not self.package_ok(prjresult, self.project.name, product,
-                                   
self.project.product_repo_overrides.get(product, self.project.product_repo),
-                                   self.project.product_arch):
+        # Collect a list of projects to check
+        projects = set([p.build_prj for p in self.project.products])
+        for prj in projects:
+            if not self.all_repos_done(prj):
                 all_ok = False
+                continue
 
-        # agama-installer in Leap uses images repo as source repo as well as 
target repo
-        source_repo = self.project.product_repo
-        if self.project.same_target_images_repo_for_source_repo:
-            source_repo = self.project.totest_images_repo
-        for product in self.project.image_products + 
self.project.container_products:
-            for arch in product.archs:
-                if not self.package_ok(prjresult, self.project.name, 
product.package, source_repo, arch):
-                    all_ok = False
-
-        for product in self.project.containerfile_products:
-            for arch in product.archs:
-                if not self.package_ok(prjresult, self.project.name, 
product.package, 'containerfile', arch):
-                    all_ok = False
-
-        if len(self.project.livecd_products):
-            liveprjname = f'{self.project.name}:Live'
-            if not self.all_repos_done(liveprjname):
-                return False
+            resultxml = self.api.retried_GET(self.api.makeurl(['build', prj, 
'_result']))
+            prjresult = ET.parse(resultxml).getroot()
+
+            for product in self.project.products:
+                if product.build_prj != prj:
+                    continue
 
-            liveresultxml = self.api.retried_GET(self.api.makeurl(['build', 
liveprjname, '_result']))
-            liveprjresult = ET.parse(liveresultxml).getroot()
-            for product in self.project.livecd_products:
                 for arch in product.archs:
-                    if not self.package_ok(liveprjresult, liveprjname, 
product.package,
-                                           self.project.product_repo, arch):
+                    if not self.package_ok(prjresult, product, arch):
                         all_ok = False
 
         if not all_ok:
@@ -256,74 +194,15 @@
         # the product version already.
         product_version = self.get_product_version()
         if product_version is not None:
-            for product in self.project.ftp_products:
-                for binary in self.binaries_of_product(self.project.name, 
product,
-                                                       
repo=self.project.product_repo_overrides.get(
-                                                           product, 
self.project.product_repo)):
-                    # The NonOSS tree doesn't include the version...
-                    if binary.endswith('.report') and 'NonOss' not in binary 
and product_version not in binary:
+            for product in [p for p in self.project.products if 
p.needs_to_contain_product_version]:
+                for binary in self.binaries_of_product(product.build_prj, 
product.package,
+                                                       
repo=product.build_repo, arch=product.archs[0]):
+                    if binary.endswith('.report') and product_version not in 
binary:
                         self.logger.debug(f'{binary} in {product} does not 
include {product_version}')
                         return False
 
-        if self.project.need_same_build_number:
-            # make sure all medias have the same build number
-            builds = set()
-            for p in self.project.ftp_products:
-                if 'Addon-NonOss' in p:
-                    # XXX: don't care about nonoss atm.
-                    continue
-                builds.add(self.ftp_build_version(self.project.name, p))
-            for p in self.project.main_products:
-                builds.add(self.iso_build_version(self.project.name, p))
-            for p in self.project.livecd_products + 
self.project.image_products:
-                for arch in p.archs:
-                    builds.add(self.iso_build_version(self.project.name, 
p.package,
-                                                      arch=arch))
-            if len(builds) != 1:
-                self.logger.debug('not all medias have the same build number')
-                return False
-
         return True
 
-    def _release(self, set_release=None):
-        for container in self.project.container_products:
-            # Containers are built in the same repo as other image products,
-            # but released into a different repo in :ToTest
-            self.release_package(self.project.name, container.package, 
repository=self.project.product_repo,
-                                 target_project=self.project.test_project,
-                                 
target_repository=self.project.totest_container_repo)
-
-        for container in self.project.containerfile_products:
-            # Dockerfile builds are done in a separate repo, but released into 
the same location
-            # as container_products
-            self.release_package(self.project.name, container.package, 
repository='containerfile',
-                                 target_project=self.project.test_project,
-                                 
target_repository=self.project.totest_container_repo)
-
-        if len(self.project.main_products):
-            for product in self.project.ftp_products:
-                self.release_package(self.project.name, product,
-                                     
repository=self.project.product_repo_overrides.get(
-                                         product, self.project.product_repo))
-
-            for cd in self.project.main_products:
-                self.release_package(self.project.name, cd, 
set_release=set_release,
-                                     repository=self.project.product_repo)
-
-        for cd in self.project.livecd_products:
-            self.release_package('%s:Live' %
-                                 self.project.name, cd.package, 
set_release=set_release,
-                                 repository=self.project.livecd_repo)
-
-        for image in self.project.image_products:
-            source_repo = self.project.product_repo
-            if self.project.same_target_images_repo_for_source_repo:
-                source_repo = self.project.totest_images_repo
-            self.release_package(self.project.name, image.package, 
set_release=set_release,
-                                 repository=source_repo,
-                                 target_project=self.project.test_project,
-                                 
target_repository=self.project.totest_images_repo)
-
     def update_totest(self, snapshot=None):
         # omit snapshot, we don't want to rename on release
         if not self.project.set_snapshot_number:
@@ -334,11 +213,14 @@
         else:
             release = None
         if not (self.dryrun or self.project.do_not_release):
-            self.api.switch_flag_in_prj(self.project.test_project, 
flag='publish', state='disable',
-                                        repository=self.project.product_repo)
-
-            if self.project.totest_images_repo != self.project.product_repo:
-                self.api.switch_flag_in_prj(self.project.test_project, 
flag='publish', state='disable',
-                                            
repository=self.project.totest_images_repo)
-
-        self._release(set_release=release)
+            prj_repo_to_publish_disable = \
+                set([(p.release_prj, p.release_repo) for p in 
self.project.products if not p.publish_using_release])
+            for prj, repo in prj_repo_to_publish_disable:
+                self.api.switch_flag_in_prj(prj, flag='publish', 
state='disable', repository=repo)
+
+        for p in self.project.products:
+            self.release_package(p.build_prj, p.package,
+                                 set_release=release if p.release_set_version 
else None,
+                                 repository=p.build_repo,
+                                 target_project=p.release_prj,
+                                 target_repository=p.release_repo)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20251001.0ed4bc8b/ttm/totest.py 
new/openSUSE-release-tools-20251014.2d75aced/ttm/totest.py
--- old/openSUSE-release-tools-20251001.0ed4bc8b/ttm/totest.py  2025-10-01 
10:58:08.000000000 +0200
+++ new/openSUSE-release-tools-20251014.2d75aced/ttm/totest.py  2025-10-14 
17:33:14.000000000 +0200
@@ -8,15 +8,120 @@
 # (C) 2018 [email protected], openSUSE.org
 # Distribute under GPLv2 or GPLv3
 
+import dataclasses
 import yaml
 import re
 from osclib.core import attribute_value_load
+from typing import List, Optional
 
 
-class ImageProduct(object):
-    def __init__(self, package, archs):
-        self.package = package
-        self.archs = archs
[email protected]
+class Product(object):
+    """Attributes documented in README.md"""
+    package: str
+    archs: List[str]  # [totest.product_arch]
+    build_prj: str  # Main prj, e.g. openSUSE:Factory
+    build_repo: str  # totest.product_repo
+    needs_to_contain_product_version: bool  # False
+    max_size: Optional[int]  # In B, e.g. 737280000 for CDs
+    release_prj: str  # totest.test_project
+    release_repo: str  # totest.product_repo
+    release_set_version: bool  # False
+    publish_using_release: bool  # False
+
+    @staticmethod
+    def custom_product(totest, name, **options):
+        # Start with defaults
+        p = Product(package=name, archs=[totest.product_arch],
+                    build_prj=totest.name,
+                    build_repo=totest.product_repo,
+                    needs_to_contain_product_version=False,
+                    max_size=None,
+                    release_prj=totest.test_project,
+                    release_repo=totest.product_repo,
+                    release_set_version=False,
+                    publish_using_release=False)
+
+        # Override options
+        return Product(**{**dataclasses.asdict(p), **options})
+
+    @staticmethod
+    def max_size_default(package):
+        """Used by ftp_product and main_product. livecd size is checked
+           during build."""
+        if re.match(r'.*[-_]mini[-_].*', package):
+            return 737280000  # a CD needs to match
+
+        if re.match(r'.*[-_]dvd5[-_].*', package):
+            return 4700372992  # a DVD needs to match
+
+        if re.match(r'.*[-_](dvd9[-_]dvd|cd[-_]DVD)[-_].*', package):
+            return 8539996159
+
+        # Other types don't have a fixed size limit
+        return None
+
+    @staticmethod
+    def ftp_product(totest, package):
+        """ FTP Repo """
+        build_repo = totest.product_repo
+        extract_product = re.search(r"(.+)/product_repo:(.+)", package)
+        if extract_product:
+            package = extract_product.group(1)
+            build_repo = extract_product.group(2)
+
+        return Product.custom_product(totest, package,
+                                      build_repo=build_repo,
+                                      needs_to_contain_product_version=True)
+
+    @staticmethod
+    def main_product(totest, package):
+        """ Installation DVD """
+        return Product.custom_product(totest, package,
+                                      
max_size=Product.max_size_default(package),
+                                      release_set_version=True)
+
+    @staticmethod
+    def livecd_product(totest, package, archs):
+        """ LiveCD products are like main products, but built in a different
+            project. """
+        p = Product.main_product(totest, package)
+        p.archs = archs
+        p.build_prj = f'{totest.name}:Live'
+        return p
+
+    @staticmethod
+    def image_product(totest, package, archs):
+        if totest.same_target_images_repo_for_source_repo:
+            build_repo = totest.totest_images_repo
+        else:
+            build_repo = totest.product_repo
+
+        release_repo = totest.totest_images_repo
+        if release_repo is None:
+            release_repo = totest.product_repo
+
+        return Product.custom_product(totest, package,
+                                      archs=archs,
+                                      build_repo=build_repo,
+                                      release_repo=release_repo,
+                                      release_set_version=True)
+
+    @staticmethod
+    def container_product(totest, package, archs):
+        """ Containers are built in the same repo as other image products,
+            but released into a different repo in :ToTest """
+        return Product.custom_product(totest, package,
+                                      archs=archs,
+                                      
release_repo=totest.totest_container_repo,
+                                      publish_using_release=True)
+
+    @staticmethod
+    def containerfile_product(totest, package, archs):
+        """ Containerfile builds are like containers but built in a separate 
repo"""
+        p = Product.container_product(totest, package, archs)
+        p.build_repo = 'containerfile'
+        return p
 
 
 class ToTest(object):
@@ -28,76 +133,72 @@
 
         # set the defaults
         self.do_not_release = False
-        self.need_same_build_number = False
         self.set_snapshot_number = False
         self.snapshot_number_prefix = "Snapshot"
         self.take_source_from_product = False
-        self.same_target_images_repo_for_source_repo = False
         self.arch = 'x86_64'
+        self.openqa_group = None
         self.openqa_server = None
+        self.jobs_num = 42
+        self.test_subproject = 'ToTest'
+        self.base = project.split(':')[0]
+        self.products = []
 
+        # Defaults for products
         self.product_repo = 'images'
         self.product_arch = 'local'
+        self.test_project = None
+
+        # Defaults for fixed product types
         self.livecd_repo = 'images'
         self.totest_container_repo = 'containers'
-        # Repo for image_products. If not set, uses product_repo.
+        self.same_target_images_repo_for_source_repo = False
         self.totest_images_repo = None
 
-        self.main_products = []
-        self.ftp_products = []
-        self.container_products = []
-        self.containerfile_products = []
-        self.livecd_products = []
-        self.image_products = []
-        self.product_repo_overrides = {}
-        # publish the default product_repo, ignore product_repo_overrides
-        self.publish_multiple_product_repo = False
+        self.load_config(apiurl)
 
-        self.test_subproject = 'ToTest'
-        self.base = project.split(':')[0]
+    def parse_products(self, products, factory):
+        parsed = []
+        for package in products:
+            for key, value in package.items():
+                parsed.append(factory(self, key, value))
 
-        self.jobs_num = 42
-        self.load_config(apiurl)
-        if not hasattr(self, 'test_project'):
-            self.test_project = f'{project}:{self.test_subproject}'
+        return parsed
 
     def load_config(self, apiurl):
-        config = yaml.safe_load(attribute_value_load(apiurl, self.name, 
'ToTestManagerConfig'))
+        config_yaml = attribute_value_load(apiurl, self.name, 
'ToTestManagerConfig')
+        if not config_yaml:
+            raise Exception('Failed to read ToTestManagerConfig')
+
+        config = yaml.safe_load(config_yaml)
+        raw_products = {}
         for key, value in config.items():
             if key == 'products':
-                self.set_products(value)
-            else:
+                raw_products = value
+            elif hasattr(self, key):
                 setattr(self, key, value)
+            else:
+                raise Exception(f'Unknown config option {key}={value}')
 
-        # Set default for totest_images_repo
+        # Some config migrations
         if self.totest_images_repo is None:
             self.totest_images_repo = self.product_repo
 
-        # do allow to override repository for ftp product
-        ftp_products_copy = self.ftp_products.copy()
-        for product in ftp_products_copy:
-            extract_product = re.search(r"(.+)/product_repo:(.+)", product)
-            if extract_product:
-                self.ftp_products.remove(product)
-                self.ftp_products.append(extract_product.group(1))
-                self.product_repo_overrides[extract_product.group(1)] = 
extract_product.group(2)
-
-    def parse_images(self, products):
-        parsed = []
-        for package in products:
-            # there is only one
-            for key, value in package.items():
-                parsed.append(ImageProduct(key, value))
-
-        return parsed
+        if self.test_project is None:
+            self.test_project = f'{self.name}:{self.test_subproject}'
 
-    def set_products(self, products):
-        # plain arrays
-        setattr(self, 'main_products', products.get('main', []))
-        setattr(self, 'ftp_products', products.get('ftp', []))
-
-        # image products
-        setattr(self, 'livecd_products', 
self.parse_images(products.get('livecds', [])))
-        setattr(self, 'image_products', 
self.parse_images(products.get('images', [])))
-        setattr(self, 'container_products', 
self.parse_images(products.get('container', [])))
-        setattr(self, 'containerfile_products', 
self.parse_images(products.get('containerfile', [])))
+        # Fill self.products. Order is important: The first product is used for
+        # getting the setrelease number for take_source_from_product == True.
+        for mp in raw_products.get('main', []):
+            self.products += [Product.main_product(self, mp)]
+
+        for key, value in raw_products.get('custom', {}).items():
+            self.products += [Product.custom_product(self, key, **value)]
+
+        for mp in raw_products.get('ftp', []):
+            self.products += [Product.ftp_product(self, mp)]
+
+        self.products += self.parse_products(raw_products.get('livecds', []), 
Product.livecd_product)
+        self.products += self.parse_products(raw_products.get('images', []), 
Product.image_product)
+        self.products += self.parse_products(raw_products.get('container', 
[]), Product.container_product)
+        self.products += self.parse_products(raw_products.get('containerfile', 
[]), Product.containerfile_product)

++++++ openSUSE-release-tools.obsinfo ++++++
--- /var/tmp/diff_new_pack.E5bS0S/_old  2025-10-22 12:20:53.443113869 +0200
+++ /var/tmp/diff_new_pack.E5bS0S/_new  2025-10-22 12:20:53.455114374 +0200
@@ -1,5 +1,5 @@
 name: openSUSE-release-tools
-version: 20251001.0ed4bc8b
-mtime: 1759309088
-commit: 0ed4bc8bcafea4303ea5d5fef6d960d3e3f95155
+version: 20251014.2d75aced
+mtime: 1760455994
+commit: 2d75aced96a43eafd9d1a411e7d2f06501a2575c
 

Reply via email to