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 2026-11-10 14:44:29
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/openSUSE-release-tools (Old)
and /work/SRC/openSUSE:Factory/.openSUSE-release-tools.new.3759290 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "openSUSE-release-tools"
Tue Nov 10 14:44:29 2026 rev:561 rq:1396486 version:20261103.4d61b3c3
Changes:
--------
---
/work/SRC/openSUSE:Factory/openSUSE-release-tools/openSUSE-release-tools.changes
2026-10-20 12:41:59.771715021 +0200
+++
/work/SRC/openSUSE:Factory/.openSUSE-release-tools.new.3759290/openSUSE-release-tools.changes
2026-11-10 14:46:06.021842578 +0100
@@ -1,0 +2,30 @@
+Tue Nov 03 09:56:06 UTC 2026 - [email protected]
+
+- Update to version 20261103.4d61b3c3:
+ * ttm: fix logic bugs in set_openqa_group_id
+ * ttm: throw an exeption if the openQA configuration is incomplete
+ * ttm: deprecate openqa_group setting
+ * ttm: use openqa_group_id for group lookup if it is set
+ * ttm: add job group lookup by id
+
+-------------------------------------------------------------------
+Thu Oct 29 18:51:14 UTC 2026 - [email protected]
+
+- Update to version 20261029.2ea606f7:
+ * gocd: monitors: SUSE.Repo.Monitor: exclude in-development projects
+ * gocd: rabbit-repoid: allow excluding projects
+ * obsrsync: allow specifying openQA configuration from env variables
+ * packaging: ensure we exclude obsrsync.py from the main package
+ * osrt-obsrsync: add packaging and example configuration files for obsrysnc
+ * obsrsync_trigger: add initial obsrsync trigger script that doesn't need
RepoMonitor
+
+-------------------------------------------------------------------
+Thu Oct 29 17:28:32 UTC 2026 - [email protected]
+
+- Update to version 20261029.c705c4f0:
+ * Add also configuration for Package Hub 16.0
+ * Add Package Hub 16.1 for testing
+ * CI: update docker composer command
+ * Setup bot for Leap 16.1
+
+-------------------------------------------------------------------
Old:
----
openSUSE-release-tools-20261012.83f7e5fa.obscpio
New:
----
openSUSE-release-tools-20261103.4d61b3c3.obscpio
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ openSUSE-release-tools.spec ++++++
--- /var/tmp/diff_new_pack.H17USM/_old 2026-11-10 14:46:07.367898969 +0100
+++ /var/tmp/diff_new_pack.H17USM/_new 2026-11-10 14:46:07.369899053 +0100
@@ -21,7 +21,7 @@
%define announcer_filename factory-package-news
%define services osrt-slsa.target [email protected]
[email protected] [email protected] [email protected]
[email protected] [email protected]
Name: openSUSE-release-tools
-Version: 20261012.83f7e5fa
+Version: 20261103.4d61b3c3
Release: 0
Summary: Tools to aid in staging and release work for openSUSE/SUSE
License: GPL-2.0-or-later AND MIT
@@ -121,6 +121,20 @@
%description scm
VCS implementations used by scripts in %{name}.
+%package openqa-trigger-bots
+Summary: openQA trigger bots
+Group: Development/Tools/Other
+Requires: osclib = %{version}
+Requires: %{name} = %{version}
+Requires: python3-pika
+Requires: python3-openqa_client
+Requires: python3-lxml
+BuildArch: noarch
+
+%description openqa-trigger-bots
+Bots to trigger images syncing in openQA, and to get
+back feedback.
+
%package check-bugowner
Summary: Check bugowner review bot
Group: Development/Tools/Other
@@ -289,6 +303,7 @@
Requires: openSUSE-release-tools-check-bugowner
Requires: openSUSE-release-tools-pkglistgen
Requires: openSUSE-release-tools-repo-checker
+Requires: openSUSE-release-tools-openqa-trigger-bots
%sysusers_requires
Recommends: logrotate
BuildArch: noarch
@@ -486,6 +501,7 @@
%exclude %{_datadir}/%{source_dir}/suppkg_rebuild.py
%exclude %{_datadir}/%{source_dir}/skippkg-finder.py
%exclude %{_datadir}/%{source_dir}/slfo-packagelist-uploader.py
+%exclude %{_datadir}/%{source_dir}/obsrsync.py
%exclude %{_datadir}/%{source_dir}/osclib
%exclude %{_datadir}/%{source_dir}/osc-cycle.py
%exclude %{_datadir}/%{source_dir}/osc-origin.py
@@ -522,6 +538,10 @@
%{_bindir}/osrt-check_bugowner
%{_datadir}/%{source_dir}/check_bugowner.py
+%files openqa-trigger-bots
+%{_bindir}/osrt-obsrsync
+%{_datadir}/%{source_dir}/obsrsync.py
+
%files check-source
%{_bindir}/osrt-check_source
%if 0%{?suse_version} > 1500
@@ -548,12 +568,14 @@
%{_sysconfdir}/openSUSE-release-tools/ibsapi
%{_sysconfdir}/openSUSE-release-tools/osrt-check-bugowner-gitea.env.in
%{_sysconfdir}/openSUSE-release-tools/osrt-git-installcheck.env.in
+%{_sysconfdir}/openSUSE-release-tools/osrt-obsrsync.env.in
%{_sysusersdir}/%{name}.conf
%{_sysusersdir}/%{name}-staging.conf
%{_unitdir}/[email protected]
%{_unitdir}/[email protected]
%{_unitdir}/[email protected]
%{_unitdir}/[email protected]
+%{_unitdir}/[email protected]
%{_unitdir}/[email protected]
%{_unitdir}/[email protected]
%{_unitdir}/[email protected]
@@ -565,6 +587,7 @@
%dir %attr(750,osrt-slsa,osrt-slsa) %{_sharedstatedir}/osrt-slsa
%dir %attr(750,osrt-slsa,osrt-slsa) %{_sharedstatedir}/osrt-slsa/pkglistgen
%dir %attr(750,osrt-slsa,osrt-slsa) %{_sharedstatedir}/osrt-slsa/relpkggen
+%dir %attr(750,osrt-slsa,osrt-slsa) %{_sharedstatedir}/osrt-slsa/obsrsync
%dir %attr(750,osrt-staging,osrt-staging) %{_sharedstatedir}/osrt-staging
%dir %attr(750,osrt-staging,osrt-staging)
%{_sharedstatedir}/osrt-staging/check-bugowner
%dir %attr(750,osrt-staging,osrt-staging)
%{_sharedstatedir}/osrt-staging/git-installcheck
++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.H17USM/_old 2026-11-10 14:46:07.419901147 +0100
+++ /var/tmp/diff_new_pack.H17USM/_new 2026-11-10 14:46:07.422901273 +0100
@@ -1,7 +1,7 @@
<servicedata>
<service name="tar_scm">
<param
name="url">https://github.com/openSUSE/openSUSE-release-tools.git</param>
- <param
name="changesrevision">83f7e5fa518790dedaf6a790fcf474e239279349</param>
+ <param
name="changesrevision">4d61b3c33b82d36a2d57458f7f92e079452b74c1</param>
</service>
</servicedata>
++++++ openSUSE-release-tools-20261012.83f7e5fa.obscpio ->
openSUSE-release-tools-20261103.4d61b3c3.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20261012.83f7e5fa/.github/workflows/ci-test.yml
new/openSUSE-release-tools-20261103.4d61b3c3/.github/workflows/ci-test.yml
--- old/openSUSE-release-tools-20261012.83f7e5fa/.github/workflows/ci-test.yml
2026-10-12 12:56:36.000000000 +0200
+++ new/openSUSE-release-tools-20261103.4d61b3c3/.github/workflows/ci-test.yml
2026-11-03 10:55:23.000000000 +0100
@@ -30,7 +30,7 @@
sudo apt-get install -y docker-compose
- name: Run tests
- run: docker-compose -f dist/ci/docker-compose.yml run test
+ run: docker compose -f dist/ci/docker-compose.yml run test
env:
# Specifies how many jobs you would like to run in parallel,
# used for partitioning
@@ -40,7 +40,7 @@
- name: Stop containers
if: always()
- run: docker-compose -f dist/ci/docker-compose.yml down
+ run: docker compose -f dist/ci/docker-compose.yml down
smoke-test:
runs-on: ubuntu-latest
@@ -120,9 +120,9 @@
run: sed -i -e "s,../..:,$PWD:," dist/ci/docker-compose.yml
- name: Run Flaker
- run: docker-compose -f dist/ci/docker-compose.yml run flaker
+ run: docker compose -f dist/ci/docker-compose.yml run flaker
- name: Stop containers
if: always()
- run: docker-compose -f dist/ci/docker-compose.yml down
+ run: docker compose -f dist/ci/docker-compose.yml down
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/openSUSE-release-tools-20261012.83f7e5fa/Makefile
new/openSUSE-release-tools-20261103.4d61b3c3/Makefile
--- old/openSUSE-release-tools-20261012.83f7e5fa/Makefile 2026-10-12
12:56:36.000000000 +0200
+++ new/openSUSE-release-tools-20261103.4d61b3c3/Makefile 2026-11-03
10:55:23.000000000 +0100
@@ -11,7 +11,7 @@
all:
install:
- install -d -m 755 $(DESTDIR)$(bindir) $(DESTDIR)$(pkgdatadir)
$(DESTDIR)$(unitdir) $(DESTDIR)$(oscplugindir)
$(DESTDIR)$(sysconfdir)/$(package_name)
$(DESTDIR)$(grafana_provisioning_dir)/dashboards
$(DESTDIR)$(grafana_provisioning_dir)/datasources
$(DESTDIR)$(logdir)/$(package_name)
$(DESTDIR)$(varlibdir)/osrt-staging/check-bugowner
$(DESTDIR)$(varlibdir)/osrt-staging/git-installcheck
$(DESTDIR)$(varlibdir)/osrt-slsa/pkglistgen
$(DESTDIR)$(varlibdir)/osrt-slsa/relpkggen
+ install -d -m 755 $(DESTDIR)$(bindir) $(DESTDIR)$(pkgdatadir)
$(DESTDIR)$(unitdir) $(DESTDIR)$(oscplugindir)
$(DESTDIR)$(sysconfdir)/$(package_name)
$(DESTDIR)$(grafana_provisioning_dir)/dashboards
$(DESTDIR)$(grafana_provisioning_dir)/datasources
$(DESTDIR)$(logdir)/$(package_name)
$(DESTDIR)$(varlibdir)/osrt-staging/check-bugowner
$(DESTDIR)$(varlibdir)/osrt-staging/git-installcheck
$(DESTDIR)$(varlibdir)/osrt-slsa/obsrsync
$(DESTDIR)$(varlibdir)/osrt-slsa/pkglistgen
$(DESTDIR)$(varlibdir)/osrt-slsa/relpkggen
for i in $(pkgdata_SCRIPTS); do install -m 755 $$i
$(DESTDIR)$(pkgdatadir); done
chmod 644 $(DESTDIR)$(pkgdatadir)/osc-*.py
for i in $(pkgdata_DATA); do cp -a $$i $(DESTDIR)$(pkgdatadir); done
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/openSUSE-release-tools-20261012.83f7e5fa/README.md
new/openSUSE-release-tools-20261103.4d61b3c3/README.md
--- old/openSUSE-release-tools-20261012.83f7e5fa/README.md 2026-10-12
12:56:36.000000000 +0200
+++ new/openSUSE-release-tools-20261103.4d61b3c3/README.md 2026-11-03
10:55:23.000000000 +0100
@@ -62,7 +62,7 @@
Using [Docker Compose](https://docs.docker.com/compose/), a containerized OBS
can be started via one command. The default credentials are `Admin` and
`opensuse` on [0.0.0.0:3000](http://0.0.0.0:3000). You can change the port by
setting the environment variable `OSRT_EXPOSED_OBS_PORT`.
- docker-compose -f dist/ci/docker-compose.yml up api
+ docker compose -f dist/ci/docker-compose.yml up api
To make things easier, you can add an alias to refer to this instance (do not
forget to adjust the port if you are using a different one):
@@ -91,16 +91,16 @@
sed -i -e "s,../..:,$PWD:," dist/ci/docker-compose.yml
# Run the linter
- docker-compose -f dist/ci/docker-compose.yml run flaker
+ docker compose -f dist/ci/docker-compose.yml run flaker
# Run the test full suite (it may take some time)...
- docker-compose -f dist/ci/docker-compose.yml run test
+ docker compose -f dist/ci/docker-compose.yml run test
# .. or just run a single test (i.e., the 'tests/util_tests.py')
- docker-compose -f dist/ci/docker-compose.yml run test run_as_tester pytest
tests/util_tests.py
+ docker compose -f dist/ci/docker-compose.yml run test run_as_tester pytest
tests/util_tests.py
# We are finished. Now you can shut the containers down.
- docker-compose -f dist/ci/docker-compose.yml down
+ docker compose -f dist/ci/docker-compose.yml down
The [docker-compose.yml](dist/ci/docker-compose.yml) mentions two container
images that are built in the
[openSUSE:Tools:Images](https://build.opensuse.org/project/show/openSUSE:Tools:Images)
project:
@@ -115,15 +115,15 @@
This section lists a few tricks to debug problems in the CI. You will use your
local setup so, as a first step, you need to be able to run the tests as
described in the previous section.
To see the logs from all the containers, the following command can be executed:
- docker-compose -f dist/ci/docker-compose.yml logs -f --tail=10
+ docker compose -f dist/ci/docker-compose.yml logs -f --tail=10
You can run commands in any container by using the docker-compose `exec`
command. For instance, you can connect to a container through a shell with the
following command (in this case, it will connect to the container behind the
`api` service):
- docker-compose -f dist/ci/docker-compose.yml exec api sh
+ docker compose -f dist/ci/docker-compose.yml exec api sh
Or you could check the API logs by issuing the following command:
- docker-compose -f dist/ci/docker-compose.yml exec api sh -c 'tail -f
/srv/www/obs/api/log/*.log'
+ docker compose -f dist/ci/docker-compose.yml exec api sh -c 'tail -f
/srv/www/obs/api/log/*.log'
To debug problems in the test suite or in the code, place a `breakpoint()`
call and you will get access to Python's debugger.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20261012.83f7e5fa/config/osrt-obsrsync.env.in
new/openSUSE-release-tools-20261103.4d61b3c3/config/osrt-obsrsync.env.in
--- old/openSUSE-release-tools-20261012.83f7e5fa/config/osrt-obsrsync.env.in
1970-01-01 01:00:00.000000000 +0100
+++ new/openSUSE-release-tools-20261103.4d61b3c3/config/osrt-obsrsync.env.in
2026-11-03 10:55:23.000000000 +0100
@@ -0,0 +1,19 @@
+# Copy this file to /etc/default/osrt-obsrsync.env.<instance_name>
+# where <instance_name> is the name of the unit instance
+
+# API_URL: the Open Build Service apiurl
+API_URL="https://api.suse.de"
+
+# OPENQA_URL: the url of the local openQA instance
+OPENQA_URL="https://openqa.suse.de"
+
+# OSRT_NAMESPACES: the namespaces to look for
+OSRT_NAMESPACES="SUSE:SLFO:"
+
+# OSC_CONFIG: the path to the oscrc configuration file
+# to use to interact with the Open Build Service
+OSC_CONFIG="/etc/openSUSE-release-tools/oscrc.example"
+
+# OPENQA_CONFIG: the path to the openQA configuration
+# file to use to interact with openQA
+OPENQA_CONFIG="/etc/openSUSE-release-tools/openQA.example"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20261012.83f7e5fa/dist/package/openSUSE-release-tools.spec
new/openSUSE-release-tools-20261103.4d61b3c3/dist/package/openSUSE-release-tools.spec
---
old/openSUSE-release-tools-20261012.83f7e5fa/dist/package/openSUSE-release-tools.spec
2026-10-12 12:56:36.000000000 +0200
+++
new/openSUSE-release-tools-20261103.4d61b3c3/dist/package/openSUSE-release-tools.spec
2026-11-03 10:55:23.000000000 +0100
@@ -121,6 +121,20 @@
%description scm
VCS implementations used by scripts in %{name}.
+%package openqa-trigger-bots
+Summary: openQA trigger bots
+Group: Development/Tools/Other
+Requires: osclib = %{version}
+Requires: %{name} = %{version}
+Requires: python3-pika
+Requires: python3-openqa_client
+Requires: python3-lxml
+BuildArch: noarch
+
+%description openqa-trigger-bots
+Bots to trigger images syncing in openQA, and to get
+back feedback.
+
%package check-bugowner
Summary: Check bugowner review bot
Group: Development/Tools/Other
@@ -289,6 +303,7 @@
Requires: openSUSE-release-tools-check-bugowner
Requires: openSUSE-release-tools-pkglistgen
Requires: openSUSE-release-tools-repo-checker
+Requires: openSUSE-release-tools-openqa-trigger-bots
%sysusers_requires
Recommends: logrotate
BuildArch: noarch
@@ -486,6 +501,7 @@
%exclude %{_datadir}/%{source_dir}/suppkg_rebuild.py
%exclude %{_datadir}/%{source_dir}/skippkg-finder.py
%exclude %{_datadir}/%{source_dir}/slfo-packagelist-uploader.py
+%exclude %{_datadir}/%{source_dir}/obsrsync.py
%exclude %{_datadir}/%{source_dir}/osclib
%exclude %{_datadir}/%{source_dir}/osc-cycle.py
%exclude %{_datadir}/%{source_dir}/osc-origin.py
@@ -522,6 +538,10 @@
%{_bindir}/osrt-check_bugowner
%{_datadir}/%{source_dir}/check_bugowner.py
+%files openqa-trigger-bots
+%{_bindir}/osrt-obsrsync
+%{_datadir}/%{source_dir}/obsrsync.py
+
%files check-source
%{_bindir}/osrt-check_source
%if 0%{?suse_version} > 1500
@@ -548,12 +568,14 @@
%{_sysconfdir}/openSUSE-release-tools/ibsapi
%{_sysconfdir}/openSUSE-release-tools/osrt-check-bugowner-gitea.env.in
%{_sysconfdir}/openSUSE-release-tools/osrt-git-installcheck.env.in
+%{_sysconfdir}/openSUSE-release-tools/osrt-obsrsync.env.in
%{_sysusersdir}/%{name}.conf
%{_sysusersdir}/%{name}-staging.conf
%{_unitdir}/[email protected]
%{_unitdir}/[email protected]
%{_unitdir}/[email protected]
%{_unitdir}/[email protected]
+%{_unitdir}/[email protected]
%{_unitdir}/[email protected]
%{_unitdir}/[email protected]
%{_unitdir}/[email protected]
@@ -565,6 +587,7 @@
%dir %attr(750,osrt-slsa,osrt-slsa) %{_sharedstatedir}/osrt-slsa
%dir %attr(750,osrt-slsa,osrt-slsa) %{_sharedstatedir}/osrt-slsa/pkglistgen
%dir %attr(750,osrt-slsa,osrt-slsa) %{_sharedstatedir}/osrt-slsa/relpkggen
+%dir %attr(750,osrt-slsa,osrt-slsa) %{_sharedstatedir}/osrt-slsa/obsrsync
%dir %attr(750,osrt-staging,osrt-staging) %{_sharedstatedir}/osrt-staging
%dir %attr(750,osrt-staging,osrt-staging)
%{_sharedstatedir}/osrt-staging/check-bugowner
%dir %attr(750,osrt-staging,osrt-staging)
%{_sharedstatedir}/osrt-staging/git-installcheck
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20261012.83f7e5fa/docs/totest.asciidoc
new/openSUSE-release-tools-20261103.4d61b3c3/docs/totest.asciidoc
--- old/openSUSE-release-tools-20261012.83f7e5fa/docs/totest.asciidoc
2026-10-12 12:56:36.000000000 +0200
+++ new/openSUSE-release-tools-20261103.4d61b3c3/docs/totest.asciidoc
2026-11-03 10:55:23.000000000 +0100
@@ -68,7 +68,8 @@
-------------------------------------------------------------------------------
$ cat <<EOF > myattribute
jobs_num: 2
-openqa_group: My Product 66.6
+# openqa_group: My Product 66.6 has an id of 666. openqa_group is deprecated
+openqa_group_id: 666
openqa_server: https://openqa.opensuse.org
product_repo: images
product_arch: x86_64
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20261012.83f7e5fa/gocd/checkers.opensuse.gocd.yaml
new/openSUSE-release-tools-20261103.4d61b3c3/gocd/checkers.opensuse.gocd.yaml
---
old/openSUSE-release-tools-20261012.83f7e5fa/gocd/checkers.opensuse.gocd.yaml
2026-10-12 12:56:36.000000000 +0200
+++
new/openSUSE-release-tools-20261103.4d61b3c3/gocd/checkers.opensuse.gocd.yaml
2026-11-03 10:55:23.000000000 +0100
@@ -143,7 +143,7 @@
- staging-bot
tasks:
- script: ./origin-manager.py -A https://api.opensuse.org --debug
review
- openSUSE.QA.Leap16.Maint:
+ openSUSE.QA.Leap16.0.Maint:
group: openSUSE.Checkers
lock_behavior: unlockWhenFinished
environment_variables:
@@ -164,6 +164,65 @@
- script: ./git-openqa-maintenance.py --openqa-host
https://openqa.opensuse.org --project openSUSE/Leap --branch leap-16.0
- script: ./git-openqa-maintenance.py --openqa-host
https://openqa.opensuse.org --project products/PackageHub --branch leap-16.0
- script: ./git-openqa-maintenance.py --openqa-host
https://openqa.opensuse.org --project openSUSE/LeapNonFree --branch
leap-16.0-nonfree
+ openSUSE.QA.Leap16.1.Maint:
+ group: openSUSE.Checkers
+ lock_behavior: unlockWhenFinished
+ environment_variables:
+ OSC_CONFIG: /home/go/config/oscrc-openqa-maintenance
+ GITEA_TOKEN_FILE: /home/go/config/openqa-maintenance-botmaster-token
+ materials:
+ script:
+ git: https://github.com/openSUSE/openSUSE-release-tools.git
+ timer:
+ spec: 0 */10 * ? * *
+ only_on_changes: false
+ stages:
+ - Run:
+ approval: manual
+ resources:
+ - staging-bot
+ tasks:
+ - script: ./git-openqa-maintenance.py --openqa-host
https://openqa.opensuse.org --project openSUSE/Leap --branch leap-16.1
+ - script: ./git-openqa-maintenance.py --openqa-host
https://openqa.opensuse.org --project products/PackageHub --branch leap-16.1
+ - script: ./git-openqa-maintenance.py --openqa-host
https://openqa.opensuse.org --project openSUSE/LeapNonFree --branch
leap-16.1-nonfree
+ openSUSE.QA.PackageHub16.0.Maint:
+ group: openSUSE.Checkers
+ lock_behavior: unlockWhenFinished
+ environment_variables:
+ OSC_CONFIG: /home/go/config/oscrc-openqa-maintenance
+ GITEA_TOKEN_FILE: /home/go/config/openqa-maintenance-botmaster-token
+ materials:
+ script:
+ git: https://github.com/openSUSE/openSUSE-release-tools.git
+ timer:
+ spec: 0 */10 * ? * *
+ only_on_changes: false
+ stages:
+ - Run:
+ approval: manual
+ resources:
+ - staging-bot
+ tasks:
+ - script: ./git-openqa-maintenance.py --openqa-host
https://openqa.suse.de --project products/PackageHub --branch leap-16.0
+ openSUSE.QA.PackageHub16.1.Maint:
+ group: openSUSE.Checkers
+ lock_behavior: unlockWhenFinished
+ environment_variables:
+ OSC_CONFIG: /home/go/config/oscrc-openqa-maintenance
+ GITEA_TOKEN_FILE: /home/go/config/openqa-maintenance-botmaster-token
+ materials:
+ script:
+ git: https://github.com/openSUSE/openSUSE-release-tools.git
+ timer:
+ spec: 0 */10 * ? * *
+ only_on_changes: false
+ stages:
+ - Run:
+ approval: manual
+ resources:
+ - staging-bot
+ tasks:
+ - script: ./git-openqa-maintenance.py --openqa-host
https://openqa.suse.de --project products/PackageHub --branch leap-16.1
openSUSE.Devel.Reminder:
group: openSUSE.Checkers
lock_behavior: unlockWhenFinished
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20261012.83f7e5fa/gocd/monitors.gocd.yaml
new/openSUSE-release-tools-20261103.4d61b3c3/gocd/monitors.gocd.yaml
--- old/openSUSE-release-tools-20261012.83f7e5fa/gocd/monitors.gocd.yaml
2026-10-12 12:56:36.000000000 +0200
+++ new/openSUSE-release-tools-20261103.4d61b3c3/gocd/monitors.gocd.yaml
2026-11-03 10:55:23.000000000 +0100
@@ -89,7 +89,7 @@
git config --global user.email "[email protected]"
git config --global user.name "GoCD Repo Monitor"
cd repos
- ../scripts/gocd/rabbit-repoid.py -A https://api.suse.de
SUSE:SLE SUSE:ALP SUSE:SLFO Devel:YaST
+ ../scripts/gocd/rabbit-repoid.py -A https://api.suse.de
SUSE:SLE SUSE:ALP SUSE:SLFO Devel:YaST --exclude SUSE:SLE-15-SP7:Update:QR:TEST
--exclude SUSE:SLFO:Products:SLES:16.2:TEST
openSUSE.Repo.Monitor:
group: Monitors
lock_behavior: unlockWhenFinished
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20261012.83f7e5fa/gocd/rabbit-repoid.py
new/openSUSE-release-tools-20261103.4d61b3c3/gocd/rabbit-repoid.py
--- old/openSUSE-release-tools-20261012.83f7e5fa/gocd/rabbit-repoid.py
2026-10-12 12:56:36.000000000 +0200
+++ new/openSUSE-release-tools-20261103.4d61b3c3/gocd/rabbit-repoid.py
2026-11-03 10:55:23.000000000 +0100
@@ -18,11 +18,12 @@
class Listener(PubSubConsumer):
- def __init__(self, apiurl, amqp_prefix, namespaces):
+ def __init__(self, apiurl, amqp_prefix, namespaces, exclude=None):
super(Listener, self).__init__(amqp_prefix,
logging.getLogger(__name__))
self.apiurl = apiurl
self.amqp_prefix = amqp_prefix
self.namespaces = namespaces
+ self.exclude = exclude or []
# repos to check on startup
self.repositories_to_check = []
# repos to check periodically that in flux
@@ -68,7 +69,7 @@
return ids
def is_part_of_namespaces(self, project):
- if ":PullRequest:" in project:
+ if ":PullRequest:" in project or project in self.exclude:
return False
for namespace in self.namespaces:
@@ -148,6 +149,8 @@
parser.add_argument('--apiurl', '-A', type=str, help='API URL of OBS')
parser.add_argument('-d', '--debug', action='store_true', default=False,
help='enable debug information')
+ parser.add_argument('--exclude', action='append', default=[],
+ help='projects to exclude (can be specified multiple
times)')
parser.add_argument('namespaces', nargs='*', help='namespaces to wait for')
args = parser.parse_args()
@@ -164,7 +167,7 @@
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
- listener = Listener(apiurl, amqp_prefix, args.namespaces)
+ listener = Listener(apiurl, amqp_prefix, args.namespaces,
exclude=args.exclude)
try:
listener.run(10800)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/openSUSE-release-tools-20261012.83f7e5fa/obsrsync.py
new/openSUSE-release-tools-20261103.4d61b3c3/obsrsync.py
--- old/openSUSE-release-tools-20261012.83f7e5fa/obsrsync.py 1970-01-01
01:00:00.000000000 +0100
+++ new/openSUSE-release-tools-20261103.4d61b3c3/obsrsync.py 2026-11-03
10:55:23.000000000 +0100
@@ -0,0 +1,342 @@
+#!/usr/bin/python3
+
+import argparse
+import glob
+import json
+import logging
+import os
+import random
+import requests
+
+import configparser
+
+import osc
+from osc.core import http_GET, makeurl
+from osclib.core import target_archs
+from osclib.PubSubConsumer import PubSubConsumer
+from lxml import etree as ET
+from urllib.error import HTTPError
+
+from openqa_client.client import OpenQA_Client
+from openqa_client.exceptions import RequestError
+
+from urllib.parse import urlparse, urlunparse
+
+
+class Configurable_OpenQA_Client(OpenQA_Client):
+ # The whole __init__ block comes from openQA-python-client,
+ # with the single change of being able to load a custom configuration
+ # file rather than looking at the hardcoded paths.
+ # Once openQA-python-client gets the same feature, this whole class
+ # can be removed and OpenQA_Client can be used instead.
+ #
+ # openQA-python-client is licensed with the same license as OSRT,
+ # GPL2+, and is copyrighted to Red Hat.
+ #
https://github.com/os-autoinst/openQA-python-client/blob/ed924a8921ecee6a843bd141faac6e41c4739e0a/src/openqa_client/client.py
+ def __init__(
+ self, server: str = "", scheme: str = "", retries: int = 5, wait: int
= 10,
+ config_path: str = ""
+ ) -> None:
+ self.retries = retries
+ self.wait = wait
+ # Read in config files.
+ config = configparser.ConfigParser()
+ if config_path and os.path.exists(config_path):
+ config.read(config_path)
+ else:
+ paths = ("/etc/openqa",
f"{os.path.expanduser('~')}/.config/openqa")
+ config.read(f"{path}/client.conf" for path in paths)
+
+ # If server not specified, default to the first one in the
+ # configuration file. If no configuration file, default to
+ # localhost. NOTE: this is different from the perl client, it
+ # *always* defaults to localhost.
+ if not server:
+ try:
+ server = config.sections()[0]
+ except (configparser.MissingSectionHeaderError, IndexError):
+ server = "localhost"
+
+ if server.startswith("http"):
+ # Handle entries like [http://foo] or [https://foo]. The,
+ # perl client does NOT handle these, so you shouldn't use
+ # them. This client started out supporting this, though,
+ # so it should continue to.
+ if not scheme:
+ scheme = urlparse(server).scheme
+ server = urlparse(server).netloc
+
+ if not scheme:
+ if server in ("localhost", "127.0.0.1", "::1"):
+ # Default to non-TLS for localhost; cert is unlikely to
+ # be valid for 'localhost' and there's no MITM...
+ scheme = "http"
+ else:
+ scheme = "https"
+
+ self.baseurl = urlunparse((scheme, server, "", "", "", ""))
+
+ # Get the API secrets from the config file.
+ try:
+ apikey = config.get(server, "key")
+ self.apisecret = config.get(server, "secret")
+ except configparser.Error:
+ try:
+ apikey = config.get(self.baseurl, "key")
+ self.apisecret = config.get(self.baseurl, "secret")
+ except configparser.Error:
+ raise Exception(f"No API key for {server}: only GET requests
will be allowed")
+
+ # Create a Requests session and ensure some standard headers
+ # will be used for all requests run through the session.
+ self.session: requests.Session = requests.Session()
+ headers = {}
+ headers["Accept"] = "json"
+ if apikey:
+ headers["X-API-Key"] = apikey
+ self.session.headers.update(headers)
+
+
+class Listener(PubSubConsumer):
+ def __init__(
+ self, apiurl, amqp_prefix, namespaces, openqa_url, state_dir,
dry_run=False
+ ):
+ super(Listener, self).__init__(amqp_prefix,
logging.getLogger(__name__))
+ self.apiurl = apiurl
+ self.amqp_prefix = amqp_prefix
+ self.namespaces = namespaces
+ self.state_dir = state_dir
+ self.dry_run = dry_run
+ self.openqa = Configurable_OpenQA_Client(server=openqa_url,
config_path=os.environ.get("OPENQA_CONFIG", ""))
+
+ self.repositories_to_check = []
+ self.repositories_to_monitor = set()
+
+ os.makedirs(self.state_dir, exist_ok=True)
+ self.interesting_repos = set()
+ self.refresh_interesting_repos()
+
+ def refresh_interesting_repos(self):
+ interesting = set()
+ try:
+ repo_list = self.openqa.openqa_request("GET", "obs_rsync")
+ for project, repository in repo_list:
+ interesting.add(f"{project}_-_{repository}")
+ except RequestError as e:
+ self.logger.warning(f"Could not fetch interesting repos from
openQA: {e}")
+ else:
+ self.interesting_repos = interesting
+
+ def interval(self):
+ if len(self.repositories_to_check) or
len(self.repositories_to_monitor):
+ return 5
+ return super(Listener, self).interval()
+
+ def still_alive(self):
+ super(Listener, self).still_alive()
+ self.refresh_interesting_repos()
+ self.check_some_repos()
+
+ def routing_keys(self):
+ return [self.amqp_prefix + ".obs.repo.build_finished"]
+
+ def check_arch(self, project, repository, architecture):
+ url = makeurl(
+ self.apiurl,
+ ["build", project, repository, architecture],
+ {"view": "status"},
+ )
+ root = ET.parse(http_GET(url)).getroot()
+ if root.get("code") == "finished":
+ buildid = root.find("buildid")
+ if buildid is not None:
+ return buildid.text
+
+ self.logger.info(
+ f"{project}/{repository}/{architecture}: code=%s dirty=%s"
+ % (root.get("code"), root.get("dirty", "false"))
+ )
+ if root.get("code") == "scheduling" or root.get("dirty", "false") ==
"true":
+ self.repositories_to_monitor.add(f"{project}/{repository}")
+
+ def check_all_archs(self, project, repository):
+ ids = {}
+ try:
+ archs = target_archs(self.apiurl, project, repository)
+ except HTTPError:
+ return None
+
+ for arch in archs:
+ repoid = self.check_arch(project, repository, arch)
+ if not repoid:
+ self.logger.info(f"{project}/{repository}/{arch} not yet done")
+ return None
+ ids[arch] = repoid
+
+ self.logger.info(f"All of {project}/{repository} finished")
+ return ids
+
+ def is_part_of_namespaces(self, project):
+ if ":PullRequest:" in project:
+ return False
+ for namespace in self.namespaces:
+ if project.startswith(namespace):
+ return True
+ return False
+
+ def start_consuming(self):
+ for namespace in self.namespaces:
+ for state_file in glob.glob(
+ os.path.join(self.state_dir, f"{namespace}*.json")
+ ):
+ state = os.path.basename(state_file).replace(".json", "")
+ project, repository = state.split("_-_")
+ if self.is_part_of_namespaces(project):
+ self.repositories_to_check.append([project, repository])
+ self.check_some_repos()
+ super(Listener, self).start_consuming()
+
+ def check_some_repos(self):
+ count = 0
+ limit = 15
+ while len(self.repositories_to_check):
+ project, repository = self.repositories_to_check.pop()
+ self.logger.debug(f"Check repo {project}/{repository}")
+ self.update_repo(project, repository)
+ count += 1
+ if count >= limit:
+ return
+
+ repos = list(self.repositories_to_monitor)
+ random.shuffle(repos)
+ for entry in repos:
+ self.repositories_to_monitor.discard(entry)
+ project, repository = entry.split("/")
+ self.logger.debug(f"Recheck repo {project}/{repository}")
+ self.update_repo(project, repository)
+ count += 1
+ if count >= limit:
+ return
+
+ def update_repo(self, project, repository):
+ ids = self.check_all_archs(project, repository)
+ if not ids:
+ return
+
+ state_id = f"{project}_-_{repository}"
+ state_file = os.path.join(self.state_dir, f"{state_id}.json")
+ old_ids = {}
+
+ if os.path.exists(state_file):
+ with open(state_file, "r") as f:
+ old_ids = json.load(f)
+
+ if ids == old_ids:
+ self.logger.debug(f"{state_id} did not change")
+ return
+
+ if state_id in self.interesting_repos:
+ if self.dry_run:
+ self.logger.info(
+ f"[DRY-RUN] Would trigger openQA obs_rsync for
{project}/{repository}"
+ )
+ else:
+ try:
+ self.logger.info(
+ f"Triggering openQA obs_rsync for
{project}/{repository}"
+ )
+ self.openqa.openqa_request(
+ "PUT",
+ f"obs_rsync/{project}/runs?repository={repository}",
+ retries=0,
+ )
+ except RequestError as e:
+ self.logger.error(
+ f"Got exception on syncing repository to openQA: {e}"
+ )
+
+ # We don't return here, as we would rather lose the
+ # event than spamming openQA over and over. So after
+ # logging the error, just go ahead and save the state
+ # anyway.
+
+ if self.dry_run:
+ self.logger.info(f"[DRY-RUN] Would save new state to {state_file}")
+ return
+
+ with open(state_file, "w") as f:
+ json.dump(ids, f)
+
+ def on_message(self, unused_channel, method, properties, body):
+ self.logger.debug("on_message")
+ self.acknowledge_message(method.delivery_tag)
+ try:
+ body = json.loads(body)
+ except ValueError:
+ return
+
+ if method.routing_key.endswith(".obs.repo.build_finished"):
+ if not self.is_part_of_namespaces(body["project"]):
+ return
+ self.restart_timer()
+ self.logger.info(
+ f"Repo finished event:
{body['project']}/{body['repo']}/{body['arch']}"
+ )
+ self.update_repo(body["project"], body["repo"])
+ else:
+ self.logger.warning(f"unknown rabbitmq message
{method.routing_key}")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ description="Daemon to monitor OBS via RabbitMQ and trigger openQA
obs_rsync"
+ )
+ parser.add_argument(
+ "--apiurl", "-A", type=str, required=True, help="API URL of OBS"
+ )
+ parser.add_argument("--openqa", type=str, required=True, help="OpenQA URL")
+ parser.add_argument(
+ "--state-dir",
+ type=str,
+ default="/var/lib/obs-openqa-sync",
+ help="Directory to keep track of build states locally",
+ )
+ parser.add_argument(
+ "--dry-run",
+ action="store_true",
+ default=False,
+ help="Print actions without triggering openQA or saving state",
+ )
+ parser.add_argument(
+ "--run-duration",
+ type=int,
+ default=10800,
+ help="Seconds to run before exiting. Set to 0 to run indefinitely
(default: 10800)",
+ )
+ parser.add_argument(
+ "-d",
+ "--debug",
+ action="store_true",
+ default=False,
+ help="enable debug information",
+ )
+ parser.add_argument("namespaces", nargs="*", help="namespaces to wait for")
+
+ args = parser.parse_args()
+
+ osc.conf.get_config(override_apiurl=args.apiurl)
+ osc.conf.config["debug"] = args.debug
+
+ apiurl = osc.conf.config["apiurl"]
+ amqp_prefix = "suse" if apiurl.endswith("suse.de") else "opensuse"
+
+ logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
+
+ listener = Listener(
+ apiurl, amqp_prefix, args.namespaces, args.openqa, args.state_dir,
args.dry_run
+ )
+
+ try:
+ listener.run(args.run_duration)
+ except KeyboardInterrupt:
+ listener.stop()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20261012.83f7e5fa/systemd/[email protected]
new/openSUSE-release-tools-20261103.4d61b3c3/systemd/[email protected]
--- old/openSUSE-release-tools-20261012.83f7e5fa/systemd/[email protected]
1970-01-01 01:00:00.000000000 +0100
+++ new/openSUSE-release-tools-20261103.4d61b3c3/systemd/[email protected]
2026-11-03 10:55:23.000000000 +0100
@@ -0,0 +1,20 @@
+[Unit]
+PartOf=osrt-slsa.target
+Description=service for osrt-obsrsync running for %i
+After=network-online.target
+StartLimitBurst=3
+StartLimitIntervalSec=60
+
+[Service]
+Type=simple
+Restart=always
+RestartSec=5
+User=osrt-slsa
+SyslogIdentifier=osrt-slsa
+EnvironmentFile=/etc/default/osrt-obsrsync.env.%i
+WorkingDirectory=/var/lib/osrt-slsa/obsrsync
+ExecStartPre=/bin/bash -xc '/usr/bin/systemctl is-active --quiet
osrt-obsrsync@%i.service && exit 1 || exit 0'
+ExecStart=/usr/bin/osrt-obsrsync -A "${API_URL}" --openqa "${OPENQA_URL}"
--state-dir /var/lib/osrt-slsa/obsrsync/%i $OSRT_NAMESPACES
+
+[Install]
+WantedBy=osrt-slsa.target
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20261012.83f7e5fa/ttm/README.md
new/openSUSE-release-tools-20261103.4d61b3c3/ttm/README.md
--- old/openSUSE-release-tools-20261012.83f7e5fa/ttm/README.md 2026-10-12
12:56:36.000000000 +0200
+++ new/openSUSE-release-tools-20261103.4d61b3c3/ttm/README.md 2026-11-03
10:55:23.000000000 +0100
@@ -25,8 +25,9 @@
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"
+openqa_server: None # URL to the openQA server, e.g:
https://openqa.opensuse.org
+openqa_group: None # [Deprecated] Name of the job group in openQA, e.g:
openSUSE Tumbleweed
+openqa_group_id: None # id of the job group in openQA, e.g.: 1
jobs_num: 42 # Minimum number of openQA jobs before publishing is possible
# Global defaults for products
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20261012.83f7e5fa/ttm/publisher.py
new/openSUSE-release-tools-20261103.4d61b3c3/ttm/publisher.py
--- old/openSUSE-release-tools-20261012.83f7e5fa/ttm/publisher.py
2026-10-12 12:56:36.000000000 +0200
+++ new/openSUSE-release-tools-20261103.4d61b3c3/ttm/publisher.py
2026-11-03 10:55:23.000000000 +0100
@@ -28,10 +28,18 @@
ToTestManager.__init__(self, tool)
def setup(self, project):
+ """Setup the ToTestPublisher with the given project.
+
+ When openQA is enabled but is missing a setting it will throw an
exception.
+ """
super(ToTestPublisher, self).setup(project)
- self.openqa = OpenQA_Client(server=self.project.openqa_server)
self.load_issues_to_ignore()
self.seen_issues_updated = False
+ self.openqa = OpenQA_Client(server=self.project.openqa_server)
+ if not (self.project.openqa_server and (self.project.openqa_group_id
or self.project.openqa_group)):
+ raise Exception('OSRT:ToTestManagerConfig: openQA configuration is
incomplete')
+
+ self.set_openqa_group_id()
def ignore_issue(self, ref, build_nr):
if self.issues_to_ignore.get(ref) != build_nr:
@@ -236,20 +244,14 @@
return
self.update_status('published', current_snapshot)
- group_id = self.openqa_group_id()
- if not group_id:
- return
-
- self.add_published_tag(group_id, current_snapshot)
+ self.add_published_tag(current_snapshot)
def find_openqa_results(self, snapshot):
- """Return the openqa jobs of a given snapshot and filter out the
- cloned jobs
-
+ """Return the openqa jobs of a given snapshot and filter out the
cloned jobs
"""
- url = makeurl(self.project.openqa_server,
- ['api', 'v1', 'jobs'], {'group':
self.project.openqa_group, 'build': snapshot, 'latest': 1})
+ jobs_lookup = {'build': snapshot, 'latest': 1, 'groupid':
self.project.openqa_group_id}
+ url = makeurl(self.project.openqa_server, ['api', 'v1', 'jobs'],
jobs_lookup)
f = self.api.retried_GET(url)
jobs = []
for job in json.load(f)['jobs']:
@@ -259,24 +261,39 @@
jobs.append(job)
return jobs
- def add_published_tag(self, group_id, snapshot):
+ def add_published_tag(self, snapshot):
if self.dryrun:
return
status_flag = 'published'
data = {'text': f'tag:{snapshot}:{status_flag}:{status_flag}'}
- self.openqa.openqa_request('POST', f'groups/{group_id}/comments',
data=data)
+ self.openqa.openqa_request('POST',
f'groups/{self.project.openqa_group_id}/comments', data=data)
- def openqa_group_id(self):
- url = makeurl(self.project.openqa_server,
- ['api', 'v1', 'job_groups'])
- f = self.api.retried_GET(url)
- job_groups = json.load(f)
- for jg in job_groups:
- if jg['name'] == self.project.openqa_group:
- return jg['id']
+ def set_openqa_group_id(self):
+ """Ensures the group_id is set and exists in the openQA instance,
throws otherwise"""
- self.logger.debug('No openQA group id found for status comment update,
ignoring')
+ def _get_group_id_by_name():
+ url = makeurl(self.project.openqa_server, ['api', 'v1',
'job_groups'])
+ f = self.api.retried_GET(url)
+ job_groups = json.load(f)
+ for jg in job_groups:
+ if jg['name'] == self.project.openqa_group:
+ return jg['id']
+
+ # Kill the run if the group doesn't exist
+ raise Exception('OSRT:ToTestManagerConfig: configured openQA job
group was not found')
+
+ def _validate_group_by_id():
+ url = makeurl(self.project.openqa_server, ['api', 'v1',
'job_groups', f"{self.project.openqa_group_id}"])
+ f = self.api.retried_GET(url)
+ return f.status == 200 # Let the HTTP exception kill the run if
the group_id is invalid
+
+ if not self.project.openqa_group_id and self.project.openqa_group:
+ self.project.openqa_group_id = _get_group_id_by_name()
+ self.logger.warning("[DEPRECATED] openqa_group in
OSRT:ToTestManagerConfig must be changed to"
+ + f" openqa_group_id:
'{self.project.openqa_group_id}'.")
+ else:
+ _validate_group_by_id()
def load_issues_to_ignore(self):
text = self.api.attribute_value_load('IgnoredIssues')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20261012.83f7e5fa/ttm/totest.py
new/openSUSE-release-tools-20261103.4d61b3c3/ttm/totest.py
--- old/openSUSE-release-tools-20261012.83f7e5fa/ttm/totest.py 2026-10-12
12:56:36.000000000 +0200
+++ new/openSUSE-release-tools-20261103.4d61b3c3/ttm/totest.py 2026-11-03
10:55:23.000000000 +0100
@@ -138,6 +138,7 @@
self.take_source_from_product = False
self.arch = 'x86_64'
self.openqa_group = None
+ self.openqa_group_id = None
self.openqa_server = None
self.jobs_num = 42
self.test_subproject = 'ToTest'
++++++ openSUSE-release-tools.obsinfo ++++++
--- /var/tmp/diff_new_pack.H17USM/_old 2026-11-10 14:46:08.177932904 +0100
+++ /var/tmp/diff_new_pack.H17USM/_new 2026-11-10 14:46:08.184933197 +0100
@@ -1,5 +1,5 @@
name: openSUSE-release-tools
-version: 20261012.83f7e5fa
-mtime: 1791802596
-commit: 83f7e5fa518790dedaf6a790fcf474e239279349
+version: 20261103.4d61b3c3
+mtime: 1793699723
+commit: 4d61b3c33b82d36a2d57458f7f92e079452b74c1