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-12-09 12:51:39
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/openSUSE-release-tools (Old)
and /work/SRC/openSUSE:Factory/.openSUSE-release-tools.new.1939 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "openSUSE-release-tools"
Tue Dec 9 12:51:39 2025 rev:544 rq:1321575 version:20251201.8e5d0523
Changes:
--------
---
/work/SRC/openSUSE:Factory/openSUSE-release-tools/openSUSE-release-tools.changes
2025-11-27 15:23:18.533402064 +0100
+++
/work/SRC/openSUSE:Factory/.openSUSE-release-tools.new.1939/openSUSE-release-tools.changes
2025-12-09 12:57:13.833298484 +0100
@@ -1,0 +2,55 @@
+Mon Dec 01 11:48:46 UTC 2025 - [email protected]
+
+- Update to version 20251201.8e5d0523:
+ * Fix package build for git-openqa-maintenance.py
+
+-------------------------------------------------------------------
+Mon Dec 01 10:41:33 UTC 2025 - [email protected]
+
+- Update to version 20251201.383d62da:
+ * Fix template for openSUSE/Leap
+ * Properly fail if branches are different
+ * Read GITEA_TOKEN either from environment or from GITEA_TOKEN_FILE
+ * Run Leap 16 maintenance bot for all 3 projects
+ * Add error handling if getting the version from branch fails
+ * Set OS_TESTS_TEMPLATE when triggering openqa jobs
+ * Fix undefined variable error if QA is still in progress
+ * Properly take the version string from branch
+ * Set GITEA_TOKEN as an environment variable
+ * Use same pattern tests in osado use for openQA flavor
+ * Add configuration for Leap 16 maintenance bot
+ * Populate OS_TEST_ISSUES
+ * Update bot defaults
+ * Adjust user agent
+ * Format to 140 line length
+ * Rename script to reflect what it does
+ * Set dry_run to false
+ * Fix query to openQA test results
+ * Make the build parameter more unique
+ * Refactor process_pull_request
+ * Add missing methods to git history
+ * Query comments for openqa tests and review status
+ * Allow post requests to gitea to have dry runs
+ * Handle updates with patchinfo and multiple source packages
+ * Minor polishing
+ * Add is_build_finished to query status of build in obs/ibs
+ * Add take_action to handle approval of a PR
+ * Refactor pr processing
+ * Query openQA build to know overall status of tests
+ * Allow processing multiple pull requests in batch
+ * Add configuration data for other projects
+ * Remove code for storing PR data
+ * Add cmdline parameters needed for the execution
+ * Allow querying status of the review
+ * Remove dead code
+ * Make style checks pass
+ * Improve logging messages
+ * Refactor openqa test settings generation
+ * Remove dead code
+ * Refactor gitea parameters
+ * Implement get_incident_name
+ * Prepare settings for the current update
+ * Add parameter for repository prefix
+ * Add script for manual maintenance update scheduling
+
+-------------------------------------------------------------------
Old:
----
openSUSE-release-tools-20251120.4f44a622.obscpio
New:
----
openSUSE-release-tools-20251201.8e5d0523.obscpio
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ openSUSE-release-tools.spec ++++++
--- /var/tmp/diff_new_pack.0QY0qs/_old 2025-12-09 12:57:14.973346540 +0100
+++ /var/tmp/diff_new_pack.0QY0qs/_new 2025-12-09 12:57:14.977346708 +0100
@@ -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: 20251120.4f44a622
+Version: 20251201.8e5d0523
Release: 0
Summary: Tools to aid in staging and release work for openSUSE/SUSE
License: GPL-2.0-or-later AND MIT
@@ -414,6 +414,7 @@
%{_bindir}/osrt-issue-diff
%{_bindir}/osrt-legal-auto
%{_bindir}/osrt-openqa-maintenance
+%{_bindir}/osrt-git-openqa-maintenance
%{_bindir}/osrt-repo2fileprovides
%{_bindir}/osrt-requestfinder
%{_bindir}/osrt-totest-manager
++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.0QY0qs/_old 2025-12-09 12:57:15.025348732 +0100
+++ /var/tmp/diff_new_pack.0QY0qs/_new 2025-12-09 12:57:15.033349069 +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">4f44a62294dc014b07548333a66bf7b26655af38</param>
+ <param
name="changesrevision">8e5d0523360aa7f339ffbf3c2ef3be803fb75043</param>
</service>
</servicedata>
++++++ openSUSE-release-tools-20251120.4f44a622.obscpio ->
openSUSE-release-tools-20251201.8e5d0523.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20251120.4f44a622/dist/package/openSUSE-release-tools.spec
new/openSUSE-release-tools-20251201.8e5d0523/dist/package/openSUSE-release-tools.spec
---
old/openSUSE-release-tools-20251120.4f44a622/dist/package/openSUSE-release-tools.spec
2025-11-20 08:55:24.000000000 +0100
+++
new/openSUSE-release-tools-20251201.8e5d0523/dist/package/openSUSE-release-tools.spec
2025-12-01 12:47:54.000000000 +0100
@@ -414,6 +414,7 @@
%{_bindir}/osrt-issue-diff
%{_bindir}/osrt-legal-auto
%{_bindir}/osrt-openqa-maintenance
+%{_bindir}/osrt-git-openqa-maintenance
%{_bindir}/osrt-repo2fileprovides
%{_bindir}/osrt-requestfinder
%{_bindir}/osrt-totest-manager
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20251120.4f44a622/git-openqa-maintenance.py
new/openSUSE-release-tools-20251201.8e5d0523/git-openqa-maintenance.py
--- old/openSUSE-release-tools-20251120.4f44a622/git-openqa-maintenance.py
1970-01-01 01:00:00.000000000 +0100
+++ new/openSUSE-release-tools-20251201.8e5d0523/git-openqa-maintenance.py
2025-12-01 12:47:54.000000000 +0100
@@ -0,0 +1,603 @@
+#!/usr/bin/env python3
+import sys
+import os
+import re
+import argparse
+import subprocess
+import requests
+import logging
+from openqa_client.client import OpenQA_Client
+from urllib.parse import urlencode, urlunparse, urlparse
+from lxml import etree as ET
+from collections import namedtuple
+import osc.core
+
+USER_AGENT = "git-openqa-maintenance
(https://github.com/openSUSE/openSUSE-release-tools"
+dry_run = False
+openqa_dry_run = False
+
+log = logging.getLogger(sys.argv[0] if __name__ == "__main__" else __name__)
+log.setLevel(logging.DEBUG)
+handler = logging.StreamHandler()
+formatter = logging.Formatter("%(name)-2s %(levelname)-2s
%(funcName)s:%(lineno)d: %(message)s")
+handler.setFormatter(formatter)
+log.addHandler(handler)
+
+CONFIG_DATA = {
+ "products/PackageHub": {
+ "repo_template":
"openSUSE:Backports:SLE-{version}:PullRequest:{pr_id}",
+ "OS_TEST_TEMPLATE":
"openSUSE:/Backports:/SLE-{version}:/PullRequest:/@INCIDENTNR@/standard"
+ },
+ "openSUSE/Leap": {
+ "repo_template": "openSUSE:Leap:{version}:PullRequest:{pr_id}",
+ "OS_TEST_TEMPLATE":
"openSUSE:/Leap:/{version}:/PullRequest:/@INCIDENTNR@/standard"
+ },
+ "openSUSE/LeapNonFree": {
+ "repo_template": "openSUSE:Leap:{version}:NonFree:PullRequest:{pr_id}",
+ "OS_TEST_TEMPLATE":
"openSUSE:/Leap:/{version}:/NonFree:/PullRequest:/@INCIDENTNR@/standard"
+ },
+}
+
+GITEA_HOST = None
+GITEA_TOKEN = None
+BS_HOST = None
+REPO_PREFIX = None
+REVIEW_GROUP = None
+openqa = None
+
+# Variables to know status of QA
+QA_UNKNOWN = 0
+QA_INPROGRESS = 1
+QA_FAILED = 2
+QA_PASSED = 3
+
+
+def parse_args():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--myself", help="Username of bot",
default="openqa-maintenance")
+ parser.add_argument(
+ "--review-group",
+ help="Group to be used for approval",
+ default="@qam-openqa-review",
+ )
+ parser.add_argument("--openqa-host", help="OpenQA instance url",
default="http://localhost:9526")
+ parser.add_argument("--verbose", help="Verbosity", default="1", type=int,
choices=[0, 1, 2, 3])
+ parser.add_argument("--branch", help="Target branch, eg. leap-16.0")
+ parser.add_argument("--project", help="Target project")
+ parser.add_argument("--pr-id", help="PR to trigger tests for")
+ parser.add_argument("--gitea", help="Gitea instance to use",
default="https://src.opensuse.org")
+ parser.add_argument("--bs", help="Build service api",
default="https://api.opensuse.org")
+ parser.add_argument("--bs-bot", help="Build service bot",
default="autogits_obs_staging_bot")
+ parser.add_argument(
+ "--repo-prefix",
+ help="Build service repository",
+ default="http://download.opensuse.org/repositories",
+ )
+
+ args = parser.parse_args()
+ return args
+
+
+def process_project(args):
+ pull_requests = get_open_prs_for_project_branch(args.project, args.branch)
+ for req in pull_requests:
+ process_pull_request(req, args)
+
+ log.info("Finished, processed %d pull requests", len(pull_requests))
+
+
+def get_open_prs_for_project_branch(project, branch):
+ pull_requests_url = GITEA_HOST +
f"/api/v1/repos/{project}/pulls?state=open&base_branch={branch}"
+
+ try:
+ pull_requests = request_get(pull_requests_url)
+ except requests.exceptions.HTTPError as e:
+ log.error(f"Project '{project}' doesn't exist: {e}")
+ return []
+
+ if not pull_requests:
+ log.warning(f"No pull requests found for '{project}' on'{branch}'")
+ return []
+
+ pr_numbers = [req["number"] for req in pull_requests]
+ num_prs = len(pr_numbers)
+ log.debug(f"Found {num_prs} pull requests for '{project}' on'{branch}'")
+ return pr_numbers
+
+
+def process_pull_request(pr_id, args):
+ data = gitea_query_pr(args.project, pr_id)
+
+ pr = data["number"]
+ project = data["base"]["repo"]["full_name"]
+ branch = data["base"]["label"]
+ log.info(f"working on {project}#{pr}")
+
+ if branch != args.branch or project != args.project:
+ log.error(f"PR {project}#{pr} does not match target {args.branch},
skipping")
+ return
+
+ pr_events = get_events_by_timeline(project, pr)
+ if not is_build_finished(project, pr, pr_events, args.bs_bot):
+ log.info(f"Build for {project}#{pr} is not ready or is broken,
skipping.")
+ return
+
+ obs_project, bs_repo_url, os_test_template = get_obs_values(project,
branch, pr)
+
+ if not obs_project:
+ log.error(f"Could not compute branch version for {project}#{pr}.")
+ return
+
+ # We need to query every package in the staged update
+ packages_in_project = get_packages_from_obs_project(obs_project)
+ openqa_build_overview = None
+
+ if not packages_in_project:
+ log.warning(f"No packages found in {obs_project}, skipping.")
+ return
+
+ settings = prepare_update_settings(project, obs_project, os_test_template,
bs_repo_url, pr, packages_in_project)
+ openqa_job_params = prepare_openqa_job_params(args, obs_project, data,
settings)
+ openqa_build_overview, previous_review = check_openqa_comment(pr_events,
args.myself)
+ # if there's a comment by us, tests have been triggered, so lets check the
status
+ if openqa_build_overview:
+ log.info(f"Build for {project}#{pr} has openQA tests")
+ log.debug(f"openQA tests are at {openqa_build_overview}")
+ if not previous_review:
+ qa_state = compute_openqa_tests_status(openqa_job_params)
+ take_action(project, pr, qa_state, openqa_build_overview)
+ else:
+ log.info(f"Build for {project}#{pr} has a review already by us:
{previous_review}")
+ else:
+ openqa_build_overview = openqa_schedule(args, openqa_job_params)
+ # instead of using the statuses api, we will have to use the comments
api
+ # to report that tests have been triggered, and approve
+ # gitea_post_status(openqa_job_params["GITEA_STATUSES_URL"],
openqa_build_overview)
+ gitea_post_build_overview(project, pr, openqa_build_overview)
+ log.info(f"Build triggered, results at {openqa_build_overview}")
+
+
+def take_action(project, pr, qa_state, openqa_build_overview):
+ if qa_state == QA_UNKNOWN:
+ log.error(f"QA state is QA_UNKNOWN for {project}#{pr}")
+
+ elif qa_state == QA_INPROGRESS:
+ log.info(f"Tests are still running for {project}#{pr}, not taking any
action")
+
+ elif qa_state == QA_FAILED or qa_state == QA_PASSED:
+ if qa_state == QA_PASSED:
+ msg = f"openQA tests passed: {openqa_build_overview}\n"
+ msg += f"{REVIEW_GROUP}: approve"
+
+ else:
+ msg = f"openQA tests failed: {openqa_build_overview}\n"
+ msg += f"{REVIEW_GROUP}: decline"
+
+ gitea_post_openqa_review(project, pr, msg)
+
+
+def compute_openqa_tests_status(openqa_job_params):
+ values = {
+ "distri": openqa_job_params["DISTRI"],
+ "version": openqa_job_params["VERSION"],
+ "arch": openqa_job_params["ARCH"],
+ "flavor": openqa_job_params["FLAVOR"],
+ "build": openqa_job_params["BUILD"],
+ "scope": "relevant",
+ "latest": "1",
+ }
+ jobs = openqa.openqa_request("GET", "jobs", values)["jobs"]
+ # this comes from openqabot.py#calculate_qa_status
+ if not jobs:
+ return QA_UNKNOWN
+
+ j = {}
+ has_failed = False
+ in_progress = False
+
+ for job in jobs:
+ if job["clone_id"]:
+ continue
+ name = job["name"]
+
+ if name in j and int(job["id"]) < int(j[name]["id"]):
+ continue
+ j[name] = job
+
+ if job["state"] not in ("cancelled", "done"):
+ in_progress = True
+ else:
+ if job["result"] != "passed" and job["result"] != "softfailed":
+ has_failed = True
+
+ if not j:
+ return QA_UNKNOWN
+ if in_progress:
+ return QA_INPROGRESS
+ if has_failed:
+ return QA_FAILED
+
+ return QA_PASSED
+
+
+def is_build_finished(project, pr, pr_events, bs_bot):
+ try:
+ review_id = pr_events[bs_bot]["review"]["review_id"]
+ except KeyError as e:
+ log.warning(f"Could not find key {e} in pr_events for {project}#{pr}.
Assuming build is not finished.")
+ return False
+
+ review = get_build_review_status(project, pr, review_id)
+ if review["state"] == "APPROVED":
+ log.info(f"Build is finished for {project}#{pr}")
+ return True
+ else:
+ log.warning(f"Build is in state {review['state']} for {project}#{pr}")
+ return False
+
+
+def get_build_review_status(project, pr, review_id):
+ return gitea_get_review(project, pr, review_id)
+
+
+def check_openqa_comment(pr_events, myself):
+ openqa_comment = pr_events.get(myself)
+ openqa_build_overview = None
+ previous_review = None
+ if not openqa_comment or "comment" not in openqa_comment:
+ return openqa_build_overview, previous_review
+
+ openqa_url_pattern = re.compile(r"https?://[^\s]+/tests/overview\?[^\s]+")
+ match = openqa_url_pattern.search(openqa_comment["comment"]["body"])
+
+ if match:
+ log.info(f"openQA build url found {match.group(0)}")
+ log.debug(f"openQA build url found
'{openqa_comment['comment']['body']}'")
+ openqa_build_overview = match.group(0)
+
+ # If we find a match for the openQA url, try looking into the comment's
+ # body to search for a review:
+ qam_review_pattern = re.compile(f"{REVIEW_GROUP}:\\s*(.*)")
+ previous_review =
qam_review_pattern.search(openqa_comment["comment"]["body"])
+ if previous_review:
+ previous_review = openqa_comment["comment"]["body"]
+
+ return openqa_build_overview, previous_review
+
+
+def prepare_update_settings(project, obs_project, os_test_template,
bs_repo_url, pr, packages):
+ settings = {}
+ staged_update_name = get_staged_update_name(obs_project)
+ build_project = project.replace("/", "_")
+ # this could also be: obs_project.split(':')[-1]
+ # start with a colon so it looks cool behind 'Build' :/
+ settings["BUILD"] = f":{build_project}:{pr}:{staged_update_name}"
+ settings["INCIDENT_REPO"] = bs_repo_url
+ # so tests can do zypper in -t patch $INCIDENT_PATCH
+ patch_id = obs_project.replace(":", "_")
+ settings["INCIDENT_PATCH"] = patch_id
+ settings["OS_TEST_ISSUES"] = pr
+ settings["OS_TEST_TEMPLATE"] = os_test_template
+ # openSUSE:Maintenance key
+ settings["IMPORT_GPG_KEYS"] = "gpg-pubkey-b3fd7e48-5549fd0f"
+ settings["ZYPPER_ADD_REPO_PREFIX"] = "staged-updates"
+
+ settings["INSTALL_PACKAGES"] = " ".join(packages.keys())
+ settings["VERIFY_PACKAGE_VERSIONS"] = " ".join([f"{p.name}
{p.version}-{p.release}" for p in packages.values()])
+
+ return settings
+
+
+def get_staged_update_name(obs_project):
+ query = {"deleted": 0}
+ url = osc.core.makeurl(BS_HOST, ("source", obs_project), query=query)
+ root = ET.parse(osc.core.http_GET(url)).getroot()
+ source_packages = [n.attrib["name"] for n in root.findall("entry")]
+ packages = []
+ for package in source_packages:
+ if package.startswith("patchinfo"):
+ continue
+ else:
+ packages.append(package)
+
+ # In theory every staged update, has a single package
+ if len(packages) > 1:
+ shortest = min((s for s in packages if ":" not in s), key=len)
+ return shortest
+ elif len(packages) == 0:
+ raise NoSourcePackagesError("No packages detected")
+ else:
+ # this is in case we need to look for the package with the
+ # shortest name in a given update
+ return packages[0]
+
+
+def get_obs_values(project, branch, pr_id):
+ log.debug("Prepare obs url")
+ project_template = CONFIG_DATA[project]["repo_template"]
+ os_test_template = CONFIG_DATA[project]["OS_TEST_TEMPLATE"]
+
+ # Version string has to be extracted from branch name
+ branch_version = _extract_version_from_branch(branch)
+ if not branch_version:
+ log.error(f"Could not get version from {branch}")
+ return None, None, None
+
+ obs_project = project_template.format(version=branch_version,
project=project, pr_id=pr_id)
+ target_repo = REPO_PREFIX + "/"
+ target_repo += obs_project.replace(":", ":/")
+
+ os_test_template_setting = REPO_PREFIX + "/"
+ os_test_template_setting += os_test_template.format(version=branch_version)
+
+ log.info(f"Target project {obs_project}, {target_repo},
{os_test_template_setting}")
+ return obs_project, target_repo, os_test_template_setting
+
+
+VERSION_PATTERN = re.compile(r'(\d+\.\d+)')
+
+
+def _extract_version_from_branch(branch):
+ matches = VERSION_PATTERN.search(branch)
+ if matches:
+ return matches.group(0)
+
+ # Return None to signal that no version was found
+ return None
+
+
+def get_packages_from_obs_project(obs_project):
+ log.debug("Query packages in obs")
+ packages = dict()
+ # repository = osc api /build/{obs_project}
+ # arches = osc api /build/{obs_project}/standard
+ # arch = osc api /build/{obs_project}/standard/{arch}
+ # for arch in arches:
+ # packages = osc api
/build/{obs_project}/{repo}/{arch}/_repository?nosource=1
+ # for package in packages:
+ # get_package_deails = osc api
/build/{obs_project}/standard/aarch64/_repository/opi.rpm?view=fileinfo
+
+ repo = "standard"
+ # osc api /build/{obs_project}/standard
+ url = osc.core.makeurl(BS_HOST, ("build", obs_project, repo))
+ root = ET.parse(osc.core.http_GET(url)).getroot()
+ for arch in [n.attrib["name"] for n in root.findall("entry")]:
+ query = {"nosource": 1}
+ # packages/binary = osc api
/build/{obs_project}/{repo}/{arch}/_repository?nosource=1
+ url = osc.core.makeurl(BS_HOST, ("build", obs_project, repo, arch,
"_repository"), query=query)
+ root = ET.parse(osc.core.http_GET(url)).getroot()
+
+ for binary in root.findall("binary"):
+ b = binary.attrib["filename"]
+ if b.endswith(".rpm"):
+ # get_package_deails = osc api
/build/{obs_project}/standard/aarch64/_repository/opi.rpm?view=fileinfo
+ p = get_package_details(obs_project, repo, arch, b)
+ packages[p.name] = p
+
+ return packages
+
+
+Package = namedtuple("Package", ("name", "version", "release"))
+
+
+def get_package_details(prj, repo, arch, binary):
+ url = osc.core.makeurl(
+ BS_HOST,
+ ("build", prj, repo, arch, "_repository", binary),
+ query={"view": "fileinfo"},
+ )
+ root = ET.parse(osc.core.http_GET(url)).getroot()
+ return Package(
+ root.find(".//name").text,
+ root.find(".//version").text,
+ root.find(".//release").text,
+ )
+
+
+def gitea_query_pr(project, pr_id):
+ log.debug("============== gitea_query_pr")
+ pull_request_url = GITEA_HOST + f"/api/v1/repos/{project}/pulls/{pr_id}"
+ return request_get(pull_request_url)
+
+
+def gitea_post_status(statuses_url, job_url):
+ log.debug("============== gitea_post_status")
+ payload = {
+ "context": "qam-openqa",
+ "description": "openQA check",
+ "state": "pending",
+ "target_url": job_url,
+ }
+ request_post(statuses_url, payload)
+
+
+def gitea_post_build_overview(project, pr_id, job_url):
+ log.debug("============== gitea_post_build_overview")
+ comment_url = GITEA_HOST +
f"/api/v1/repos/{project}/issues/{pr_id}/comments"
+ payload = {
+ "body": f"openQA tests triggered: {job_url}",
+ }
+ request_post(comment_url, payload)
+
+
+def gitea_post_openqa_review(project, pr_id, msg):
+ log.debug("============== gitea_post_openqa_review")
+ comment_url = GITEA_HOST +
f"/api/v1/repos/{project}/issues/{pr_id}/comments"
+ payload = {
+ "body": msg,
+ }
+ request_post(comment_url, payload)
+
+
+def gitea_get_review(project, pr_id, review_id):
+ log.debug("============== gitea_get_review")
+ review_url = GITEA_HOST +
f"/api/v1/repos/{project}/pulls/{pr_id}/reviews/{review_id}"
+ return request_get(review_url)
+
+
+def get_events_by_timeline(project, pr_id):
+ log.debug("============== get_events_by_timeline")
+ url = GITEA_HOST + f"/api/v1/repos/{project}/issues/{pr_id}/timeline"
+ request = request_get(url)
+
+ # if request.status_code == 404:
+ # self.logger.error(f"'{self}' does not have a timeline")
+ # # this should throw an exception
+ # return
+
+ timeline = request
+ timeline.reverse()
+
+ events = {}
+ # reset the timeline every time a pull_push event happens
+ for event in timeline:
+ if event["type"] == "pull_push":
+ log.debug(f"*** All events since last push ({event['body']}) have
been processed for {project}#{pr_id}")
+ break
+
+ user_login = event["user"]["login"]
+ event_type = event["type"]
+
+ if user_login not in events:
+ events[user_login] = {}
+
+ if event_type not in events[user_login]:
+ log.debug(f"Storing most recent '{event_type}' for '{user_login}'
(ID: {event['id']})")
+ events[user_login][event_type] = event
+ else:
+ log.debug(f"Skipping older '{event_type}' for '{user_login}' (ID:
{event['id']})")
+
+ return events
+
+
+def request_post(url, payload):
+ log.debug(f"Posting request to gitea for {url}")
+ log.debug(payload)
+ headers = {
+ "User-Agent": USER_AGENT,
+ "Accept": "application/json",
+ "Authorization": "token " + GITEA_TOKEN,
+ }
+ if dry_run:
+ log.debug(f"would send request to {url} with {payload}")
+ else:
+ try:
+ content = requests.post(url, headers=headers, data=payload)
+ content.raise_for_status()
+ except requests.exceptions.RequestException as e:
+ log.error("Error while fetching %s: %s" % (url, str(e)))
+ raise (e)
+
+
+def request_get(url):
+ log.debug(f"Sending request to gitea for {url}")
+ headers = {
+ "User-Agent": USER_AGENT,
+ "Accept": "application/json",
+ "Authorization": "token " + GITEA_TOKEN,
+ }
+
+ try:
+ content = requests.get(url, headers=headers)
+ content.raise_for_status()
+ except requests.exceptions.RequestException as e:
+ log.error("Error while fetching %s: %s" % (url, str(e)))
+ raise (e)
+ json_data = content.json()
+ return json_data
+
+
+def prepare_openqa_job_params(args, obs_project, data, settings):
+ log.debug("create_openqa_job_params")
+ statuses_url = GITEA_HOST +
f"/api/v1/repos/{data['head']['repo']['full_name']}/statuses/{data['head']['sha']}"
+ params = {
+ "PRIO": "100",
+ # add "target URL" for the "Details" button of the CI status
+ "CI_TARGET_URL": args.openqa_host,
+ # set Gitea parameters so the Minion job will be able to report the
status back to Gitea
+ "GITEA_REPO": data["head"]["repo"]["full_name"],
+ "GITEA_SHA": data["head"]["sha"],
+ "GITEA_STATUSES_URL": statuses_url,
+ "GITEA_PR_URL": data["html_url"],
+ "webhook_id": "gitea:pr:" + str(data["number"]),
+ "VERSION": _extract_version_from_branch(data["base"]["label"]),
+ "DISTRI": "opensuse", # there must be a better way than to hardcode
+ "FLAVOR": "staged-Updates",
+ "ARCH": "x86_64",
+ }
+ return params | settings
+
+
+def openqa_cli(host, subcommand, cmds, dry_run=False):
+ log.debug("============== openqa_cli")
+ client_args = [
+ "openqa-cli",
+ subcommand,
+ "--host",
+ host,
+ ] + cmds
+ log.debug("openqa_cli: %s %s" % (subcommand, client_args))
+ res = subprocess.run(
+ (["echo", "Simulating: "] if dry_run else []) + client_args,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ if len(res.stderr):
+ log.warning(f"openqa_cli() {subcommand} stderr: {res.stderr}")
+ res.check_returncode()
+ return res.stdout.decode("utf-8")
+
+
+def openqa_schedule(args, params):
+ log.debug("============== openqa_schedule")
+
+ cmd_args = []
+ for key in params:
+ cmd_args.append(f"{key}={params[key]}")
+ openqa_cli(args.openqa_host, "schedule", cmd_args, openqa_dry_run)
+
+ query_parameters = {
+ "build": params["BUILD"],
+ "distri": params["DISTRI"],
+ "version": params["VERSION"],
+ }
+
+ base_url = urlparse(args.openqa_host + "/tests/overview")
+ query_string = urlencode(query_parameters)
+ test_overview_url = urlunparse(base_url._replace(query=query_string))
+ return test_overview_url
+
+
+class NoSourcePackagesError(Exception):
+ pass
+
+
+if __name__ == "__main__":
+ args = parse_args()
+
+ token_file_path = os.environ.get("GITEA_TOKEN_FILE")
+ gitea_token = None
+
+ if token_file_path:
+ try:
+ with open(token_file_path, 'r') as f:
+ gitea_token = f.read().strip()
+ except (IOError, FileNotFoundError) as e:
+ raise RuntimeError(f"Error reading GITEA_TOKEN_FILE
'{token_file_path}': {e}")
+ else:
+ gitea_token = os.environ.get("GITEA_TOKEN")
+
+ if not gitea_token:
+ raise RuntimeError("Environment variable GITEA_TOKEN or
GITEA_TOKEN_FILE must be set")
+
+ GITEA_TOKEN = gitea_token
+ GITEA_HOST = args.gitea
+ BS_HOST = args.bs
+ REPO_PREFIX = args.repo_prefix
+ REVIEW_GROUP = args.review_group
+ osc.conf.get_config()
+ openqa = OpenQA_Client(server=args.openqa_host)
+ if args.pr_id:
+ process_pull_request(args.pr_id, args)
+ else:
+ process_project(args)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20251120.4f44a622/gocd/checkers.opensuse.gocd.yaml
new/openSUSE-release-tools-20251201.8e5d0523/gocd/checkers.opensuse.gocd.yaml
---
old/openSUSE-release-tools-20251120.4f44a622/gocd/checkers.opensuse.gocd.yaml
2025-11-20 08:55:24.000000000 +0100
+++
new/openSUSE-release-tools-20251201.8e5d0523/gocd/checkers.opensuse.gocd.yaml
2025-12-01 12:47:54.000000000 +0100
@@ -160,6 +160,27 @@
tasks:
- script: |-
./openqa-maintenance.py --group qam-openqa
--review-mode=accept-onpass --debug --openqa https://openqa.opensuse.org
--apiurl https://api.opensuse.org review
+ openSUSE.QA.Leap16.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 --project openSUSE/Leap
--branch leap-16.0
+ - script: ./git-openqa-maintenance.py --project products/PackageHub
--branch leap-16.0
+ - script: ./git-openqa-maintenance.py --project openSUSE/LeapNonFree
--branch leap-16.0-nonfree
openSUSE.Devel.Reminder:
group: openSUSE.Checkers
lock_behavior: unlockWhenFinished
++++++ openSUSE-release-tools.obsinfo ++++++
--- /var/tmp/diff_new_pack.0QY0qs/_old 2025-12-09 12:57:16.621416009 +0100
+++ /var/tmp/diff_new_pack.0QY0qs/_new 2025-12-09 12:57:16.629416346 +0100
@@ -1,5 +1,5 @@
name: openSUSE-release-tools
-version: 20251120.4f44a622
-mtime: 1763625324
-commit: 4f44a62294dc014b07548333a66bf7b26655af38
+version: 20251201.8e5d0523
+mtime: 1764589674
+commit: 8e5d0523360aa7f339ffbf3c2ef3be803fb75043