From b9447994ca480a584947401518e58515178ea81b Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 8 Oct 2025 11:49:48 +0200 Subject: [PATCH 01/30] Add script for manual maintenance update scheduling --- manual-maintenance-tests-trigger.py | 409 ++++++++++++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 manual-maintenance-tests-trigger.py diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py new file mode 100644 index 00000000..b8247e85 --- /dev/null +++ b/manual-maintenance-tests-trigger.py @@ -0,0 +1,409 @@ +#!/usr/bin/env python3 +import pika +import sys +import os +import argparse +import json +import subprocess +import requests +import re +import logging +import datetime +from urllib.parse import urlencode, urlunparse, urlparse +from lxml import etree as ET +from collections import namedtuple +import osc.core + +USER_AGENT = "manual-trigger.py (https://github.com/os-autoinst/scripts)" +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": "openSUSE:Backports:SLE-{version}:PullRequest:{pr_id}" +} +GITEA_HOST = None +BS_HOST = None + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--myself", help="Username of bot", default="qam-openqa") + 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( + "--build-bot", help="Username of bot that approves when build is finished" + ) + parser.add_argument("--branch", help="Target branch, eg. leap-16.0") + parser.add_argument("--project", help="Target project") + parser.add_argument( + "--store-pr", + help="Stores pull request data and exits", + action="store_true", + default=False, + ) + parser.add_argument("--pr-data", help="File to use for simulation", default=False) + 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 instance", default="https://api.opensuse.org" + ) + + args = parser.parse_args() + return args + + +def trigger_tests_for_pr(args): + if not args.pr_data: + data = gitea_query_pr(args.project, args.pr_id) + else: + log.info("Loading pr data from file") + json_file = args.pr_data + with open(json_file, "r") as f: + content = f.read() + data = json.loads(content) + + pr = data["number"] + by = data["user"]["login"] + project = data["base"]["repo"]["full_name"] + branch = data["base"]["label"] + log.info(f"working on {project}#{pr}") + + if args.store_pr and not args.pr_data: + timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + filename = f"tests/data/opensuse-maintenance/by-{by}-{pr}-{timestamp}.json" + try: + with open(filename, "w") as file_object: + json.dump(data, file_object, indent=4) + log.info(f"Storing review-requested file to {filename}") + log.debug( + f"Captured pr {args.project}#{args.branch} PR: {args.pr_id}, saved to {filename}. Exiting" + ) + exit() + + except IOError as e: + log.error(f"Error saving file: {e}") + + if branch == args.branch and project == args.project: + obs_project = get_obs_project(project, branch, pr) + packages_in_project = get_packages_from_obs_project(obs_project) + if packages_in_project: + settings = prepare_openqa_settings() + openqa_build_overview = openqa_schedule(settings) + else: + log.error(f"PR {project}#{pr} does not target {args.branch}") + + +def get_obs_project(project, branch, pr_id): + log.debug("Prepare obs url") + template = CONFIG_DATA[project] + # Version string has to be extracted from branch name + branch_version = branch.split("-")[-1] + obs_project = template.format(version=branch_version, project=project, pr_id=pr_id) + log.debug(f"Target project {obs_project}") + return obs_project + + +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 + ) + # breakpoint() + 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 = package_details(obs_project, repo, arch, b) + packages[p.name] = p + + return packages + + +Package = namedtuple("Package", ("name", "version", "release")) + + +def 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 handle_build_finished(data, args): + log.debug("============== handle_build_finished") + build_bot = args.build_bot + myself = args.myself + if build_bot == data["sender"]["username"]: + log.debug(f"Build marked as finished by ({build_bot})") + else: + if args.verbose >= 1: + log.debug( + f"Aborting: PR approval is by {data['sender']['username']}, not by our bot {build_bot}" + ) + return + + if ( + data["pull_request"]["base"]["label"] == args.branch + and data["pull_request"]["base"]["repo"]["full_name"] == args.project + ): + pull_request = data["pull_request"] + job_params = { + "id": pull_request["id"], + "label": pull_request["head"]["label"], + "branch": pull_request["head"]["ref"], + "sha": pull_request["head"]["sha"], + "pr_html_url": pull_request["html_url"], + "clone_url": pull_request["head"]["repo"]["clone_url"], + "repo_name": pull_request["head"]["repo"][ + "name" + ], # this should be full_name but openQA cli complains + "repo_api_url": data["repository"]["url"], + "repo_html_url": data["repository"]["html_url"], + } + packages_in_testing = get_packages_from_obs_project(job_params) + job_params["packages"] = packages_in_testing + params = create_openqa_job_params(args, job_params) + job_url = openqa_schedule(args, params) + log.debug(job_url) + gitea_post_status(job_params, job_url) + + else: + if args.verbose >= 1: + log.debug(f"Project and branch don't match {args.project}#{args.branch}") + return + + +def gitea_query_pr(project, pr_id): + log.debug("============== gitea_query_pr") + # sha = job_params['sha'] + # statuses_url = job_params['repo_api_url'] + '/statuses/' + job_params['sha']; + pull_request_url = GITEA_HOST + f"/api/v1/repos/{project}/pulls/{pr_id}" + token = os.environ.get("GITEA_TOKEN") + headers = { + "User-Agent": USER_AGENT, + "Accept": "application/json", + "Authorization": "token " + token, + } + # payload = { + # 'context': 'qam-openqa', + # 'description': "openQA check", + # 'state': "pending", + # 'target_url': job_url, + # } + return request_get(pull_request_url, headers) + + +def gitea_post_status(job_params, job_url): + log.debug("============== gitea_post_status") + sha = job_params["sha"] + statuses_url = job_params["repo_api_url"] + "/statuses/" + job_params["sha"] + token = os.environ.get("GITEA_TOKEN") + headers = { + "User-Agent": USER_AGENT, + "Accept": "application/json", + "Authorization": "token " + token, + } + payload = { + "context": "qam-openqa", + "description": "openQA check", + "state": "pending", + "target_url": job_url, + } + request_post(statuses_url, headers, payload) + + +def request_post(url, headers, payload): + log.debug("============== request_post") + log.debug(payload) + 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, headers): + log.debug("============== request_get") + + 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 create_openqa_job_params(args, job_params): + log.debug("============== create_openqa_job_params") + raw_url = job_params["repo_html_url"] + "/raw/branch/" + job_params["sha"] + statuses_url = job_params["repo_api_url"] + "/statuses/" + job_params["sha"] + params = { + "BUILD": job_params["repo_name"] + "#" + job_params["sha"], + "CASEDIR": job_params["clone_url"] + "#" + job_params["sha"], + "_GROUP_ID": "0", + "PRIO": "100", + "NEEDLES_DIR": "%%CASEDIR%%/needles", + # set the URL for the scenario definitions YAML file so the Minion job will download it from GitHub + "SCENARIO_DEFINITIONS_YAML_FILE": raw_url + "/" + "scenario-definitions.yaml", + # 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": job_params["repo_name"], + "GITEA_SHA": job_params["sha"], + "GITEA_STATUSES_URL": statuses_url, + "GITEA_PR_URL": job_params["pr_html_url"], + "webhook_id": "gitea:pr:" + str(job_params["id"]), + } + return params + + +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 osc_cli(command, args, dry_run=False): + log.debug("============== osc_cli") + client_args = ["osc", command] + args + log.debug("osc_cli: %s %s" % (command, 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() {command} stderr: {res.stderr}") + res.check_returncode() + return res.stdout.decode("utf-8") + + +def openqa_schedule(args, params): + log.debug("============== openqa_schedule") + scenario_url = "https://raw.githubusercontent.com/os-autoinst/os-autoinst-distri-openQA/refs/heads/master/scenario-definitions.yaml" + scenario_yaml = fetch_url(scenario_url, request_type="text") + yaml_file = "/tmp/distri-openqa-scenario.yaml" + with open(yaml_file, "w") as f: + f.write(scenario_yaml.decode("utf-8")) + cmd_args = [ + "--param-file", + "SCENARIO_DEFINITIONS_YAML=" + yaml_file, + "VERSION=Tumbleweed", + "DISTRI=openqa", + "FLAVOR=dev", + "ARCH=x86_64", + "HDD_1=opensuse-Tumbleweed-x86_64-20250920-minimalx@uefi.qcow2", + ] + for key in params: + cmd_args.append(key + "=" + params[key]) + output = openqa_cli(args.openqa_host, "schedule", cmd_args, dry_run) + pattern = re.compile(r".*?(?Phttps?://\S+)", re.DOTALL) + + query_parameters = { + "build": params["BUILD"], + "distri": "openqa", + "version": "Tumbleweed", + } + + 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 + + +def fetch_url(url, request_type="text"): + log.debug("============== fetch_url") + try: + content = requests.get(url, headers={"User-Agent": USER_AGENT}) + content.raise_for_status() + except requests.exceptions.RequestException as e: + log.error("Error while fetching %s: %s" % (url, str(e))) + raise (e) + raw = content.content + if request_type == "json": + try: + content = content.json() + except json.decoder.JSONDecodeError as e: + log.error( + "Error while decoding JSON from %s -> >>%s<<: %s" % (url, raw, str(e)) + ) + raise (e) + else: + content = raw + return content + + +if __name__ == "__main__": + args = parse_args() + + ret = os.environ.get("GITEA_TOKEN") + if ret is None: + raise RuntimeError("Environment variable GITEA_TOKEN is not set") + + GITEA_HOST = args.gitea + BS_HOST = args.bs + osc.conf.get_config() + + trigger_tests_for_pr(args) + # if args.simulate_review_requested_event: + # simulate(args) + # elif(args.simulate_build_finished_event and args.build_bot): + # simulate_build_finished_event(args) + # else: + # listen(args) From 0158e91b14773e6a52b1f983ef20031e77cbb0dd Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 8 Oct 2025 14:35:52 +0200 Subject: [PATCH 02/30] Add parameter for repository prefix --- manual-maintenance-tests-trigger.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index b8247e85..a9eaef94 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -31,6 +31,7 @@ } GITEA_HOST = None BS_HOST = None +REPO_PREFIX = None def parse_args(): @@ -59,7 +60,12 @@ def parse_args(): "--gitea", help="Gitea instance to use", default="https://src.opensuse.org" ) parser.add_argument( - "--bs", help="Build service instance", default="https://api.opensuse.org" + "--bs", help="Build service api", default="https://api.opensuse.org" + ) + parser.add_argument( + "--repo-prefix", + help="Build service repository", + default="http://download.opensuse.org/repositories", ) args = parser.parse_args() @@ -398,6 +404,7 @@ def fetch_url(url, request_type="text"): GITEA_HOST = args.gitea BS_HOST = args.bs + REPO_PREFIX = args.repo_prefix osc.conf.get_config() trigger_tests_for_pr(args) From 217d66c181372ffc96ad8afc3419ddf9da209fdc Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 8 Oct 2025 14:42:14 +0200 Subject: [PATCH 03/30] Prepare settings for the current update --- manual-maintenance-tests-trigger.py | 46 ++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index a9eaef94..d91b0264 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -104,23 +104,55 @@ def trigger_tests_for_pr(args): log.error(f"Error saving file: {e}") if branch == args.branch and project == args.project: - obs_project = get_obs_project(project, branch, pr) + obs_project, bs_repo_url = get_obs_values(project, branch, pr) packages_in_project = get_packages_from_obs_project(obs_project) if packages_in_project: - settings = prepare_openqa_settings() + settings = prepare_update_settings( + obs_project, bs_repo_url, pr, packages_in_project + ) + job_params = create_openqa_job_params(obs_project, data) openqa_build_overview = openqa_schedule(settings) else: log.error(f"PR {project}#{pr} does not target {args.branch}") -def get_obs_project(project, branch, pr_id): +def prepare_update_settings(obs_project, bs_repo_url, pr, packages): + settings = {} + staged_update_name = get_incident_name(obs_project) + # this could also be: obs_project.split(':')[-1] + # start with a colon so it looks cool behind 'Build' :/ + settings["BUILD"] = f":{pr}:{staged_update_name}" + patch_id = pr + settings["INCIDENT_REPO"] = bs_repo_url + settings["INCIDENT_PATCH"] = patch_id + + # openSUSE:Maintenance key + settings["IMPORT_GPG_KEYS"] = "gpg-pubkey-b3fd7e48-5549fd0f" + settings["ZYPPER_ADD_REPO_PREFIX"] = "staged_update" + + 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_incident_name(obs_project): + log.error("not implemented") + return "incident_name_here" + + +def get_obs_values(project, branch, pr_id): log.debug("Prepare obs url") template = CONFIG_DATA[project] # Version string has to be extracted from branch name branch_version = branch.split("-")[-1] obs_project = template.format(version=branch_version, project=project, pr_id=pr_id) - log.debug(f"Target project {obs_project}") - return obs_project + target_repo = REPO_PREFIX + "/" + target_repo += obs_project.replace(":", ":/") + log.debug(f"Target project {obs_project}, {target_repo}") + return obs_project, target_repo def get_packages_from_obs_project(obs_project): @@ -151,7 +183,7 @@ def get_packages_from_obs_project(obs_project): b = binary.attrib["filename"] if b.endswith(".rpm"): # get_package_deails = osc api /build/{obs_project}/standard/aarch64/_repository/opi.rpm?view=fileinfo - p = package_details(obs_project, repo, arch, b) + p = get_package_details(obs_project, repo, arch, b) packages[p.name] = p return packages @@ -160,7 +192,7 @@ def get_packages_from_obs_project(obs_project): Package = namedtuple("Package", ("name", "version", "release")) -def package_details(prj, repo, arch, binary): +def get_package_details(prj, repo, arch, binary): url = osc.core.makeurl( BS_HOST, ("build", prj, repo, arch, "_repository", binary), From 9a6db2467b47c4c07a33389287610424806e7f0e Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 8 Oct 2025 16:29:56 +0200 Subject: [PATCH 04/30] Implement get_incident_name We need to query the source packages of the update to be able to figure out the name of the given staged update, for now we're sticking to the promise that staged updates are single units --- manual-maintenance-tests-trigger.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index d91b0264..f0078439 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -105,6 +105,7 @@ def trigger_tests_for_pr(args): if branch == args.branch and project == args.project: obs_project, bs_repo_url = get_obs_values(project, branch, pr) + # We need to query every package in the staged update packages_in_project = get_packages_from_obs_project(obs_project) if packages_in_project: settings = prepare_update_settings( @@ -118,7 +119,7 @@ def trigger_tests_for_pr(args): def prepare_update_settings(obs_project, bs_repo_url, pr, packages): settings = {} - staged_update_name = get_incident_name(obs_project) + staged_update_name = get_staged_update_name(obs_project) # this could also be: obs_project.split(':')[-1] # start with a colon so it looks cool behind 'Build' :/ settings["BUILD"] = f":{pr}:{staged_update_name}" @@ -138,9 +139,19 @@ def prepare_update_settings(obs_project, bs_repo_url, pr, packages): return settings -def get_incident_name(obs_project): - log.error("not implemented") - return "incident_name_here" +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")] + + # In theory every staged update, has a single package + if len(source_packages) > 1: + raise MultipleSourcePackagesError("Multiple packages detected") + elif len(source_packages) == 0: + raise NoSourcePackagesError("No packages detected") + else: + return source_packages[0] def get_obs_values(project, branch, pr_id): @@ -426,6 +437,11 @@ def fetch_url(url, request_type="text"): content = raw return content +class MultipleSourcePackagesError(Exception): + pass + +class NoSourcePackagesError(Exception): + pass if __name__ == "__main__": args = parse_args() From efe0f4932e72c5fe0d4d1a598696b540d1dbf8ef Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 8 Oct 2025 16:31:00 +0200 Subject: [PATCH 05/30] Refactor gitea parameters --- manual-maintenance-tests-trigger.py | 46 +++++++++++++---------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index f0078439..d32f4b01 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -263,46 +263,37 @@ def handle_build_finished(data, args): def gitea_query_pr(project, pr_id): log.debug("============== gitea_query_pr") - # sha = job_params['sha'] - # statuses_url = job_params['repo_api_url'] + '/statuses/' + job_params['sha']; pull_request_url = GITEA_HOST + f"/api/v1/repos/{project}/pulls/{pr_id}" - token = os.environ.get("GITEA_TOKEN") - headers = { - "User-Agent": USER_AGENT, - "Accept": "application/json", - "Authorization": "token " + token, - } - # payload = { - # 'context': 'qam-openqa', - # 'description': "openQA check", - # 'state': "pending", - # 'target_url': job_url, - # } - return request_get(pull_request_url, headers) + return request_get(pull_request_url) + +def gitea_query_files(project, pr_id): + pull_request_url = GITEA_HOST + f"/api/v1/repos/{project}/pulls/{pr_id}/files" + return request_get(pull_request_url) def gitea_post_status(job_params, job_url): log.debug("============== gitea_post_status") sha = job_params["sha"] statuses_url = job_params["repo_api_url"] + "/statuses/" + job_params["sha"] - token = os.environ.get("GITEA_TOKEN") - headers = { - "User-Agent": USER_AGENT, - "Accept": "application/json", - "Authorization": "token " + token, - } + payload = { "context": "qam-openqa", "description": "openQA check", "state": "pending", "target_url": job_url, } - request_post(statuses_url, headers, payload) + request_post(statuses_url, payload) -def request_post(url, headers, payload): +def request_post(url, payload): log.debug("============== request_post") log.debug(payload) + token = os.environ.get("GITEA_TOKEN") + headers = { + "User-Agent": USER_AGENT, + "Accept": "application/json", + "Authorization": "token " + token, + } try: content = requests.post(url, headers=headers, data=payload) content.raise_for_status() @@ -311,9 +302,14 @@ def request_post(url, headers, payload): raise (e) -def request_get(url, headers): +def request_get(url): log.debug("============== request_get") - + token = os.environ.get("GITEA_TOKEN") + headers = { + "User-Agent": USER_AGENT, + "Accept": "application/json", + "Authorization": "token " + token, + } try: content = requests.get(url, headers=headers) content.raise_for_status() From b25199c3c7bfa32341f1acbbb78f3d8f114e0af0 Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Thu, 9 Oct 2025 10:40:44 +0200 Subject: [PATCH 06/30] Remove dead code --- manual-maintenance-tests-trigger.py | 81 ----------------------------- 1 file changed, 81 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index d32f4b01..5a3e87f4 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -217,59 +217,11 @@ def get_package_details(prj, repo, arch, binary): ) -def handle_build_finished(data, args): - log.debug("============== handle_build_finished") - build_bot = args.build_bot - myself = args.myself - if build_bot == data["sender"]["username"]: - log.debug(f"Build marked as finished by ({build_bot})") - else: - if args.verbose >= 1: - log.debug( - f"Aborting: PR approval is by {data['sender']['username']}, not by our bot {build_bot}" - ) - return - - if ( - data["pull_request"]["base"]["label"] == args.branch - and data["pull_request"]["base"]["repo"]["full_name"] == args.project - ): - pull_request = data["pull_request"] - job_params = { - "id": pull_request["id"], - "label": pull_request["head"]["label"], - "branch": pull_request["head"]["ref"], - "sha": pull_request["head"]["sha"], - "pr_html_url": pull_request["html_url"], - "clone_url": pull_request["head"]["repo"]["clone_url"], - "repo_name": pull_request["head"]["repo"][ - "name" - ], # this should be full_name but openQA cli complains - "repo_api_url": data["repository"]["url"], - "repo_html_url": data["repository"]["html_url"], - } - packages_in_testing = get_packages_from_obs_project(job_params) - job_params["packages"] = packages_in_testing - params = create_openqa_job_params(args, job_params) - job_url = openqa_schedule(args, params) - log.debug(job_url) - gitea_post_status(job_params, job_url) - - else: - if args.verbose >= 1: - log.debug(f"Project and branch don't match {args.project}#{args.branch}") - return - - 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_query_files(project, pr_id): - pull_request_url = GITEA_HOST + f"/api/v1/repos/{project}/pulls/{pr_id}/files" - return request_get(pull_request_url) - def gitea_post_status(job_params, job_url): log.debug("============== gitea_post_status") @@ -364,18 +316,6 @@ def openqa_cli(host, subcommand, cmds, dry_run=False): return res.stdout.decode("utf-8") -def osc_cli(command, args, dry_run=False): - log.debug("============== osc_cli") - client_args = ["osc", command] + args - log.debug("osc_cli: %s %s" % (command, 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() {command} stderr: {res.stderr}") - res.check_returncode() return res.stdout.decode("utf-8") @@ -412,27 +352,6 @@ def openqa_schedule(args, params): return test_overview_url -def fetch_url(url, request_type="text"): - log.debug("============== fetch_url") - try: - content = requests.get(url, headers={"User-Agent": USER_AGENT}) - content.raise_for_status() - except requests.exceptions.RequestException as e: - log.error("Error while fetching %s: %s" % (url, str(e))) - raise (e) - raw = content.content - if request_type == "json": - try: - content = content.json() - except json.decoder.JSONDecodeError as e: - log.error( - "Error while decoding JSON from %s -> >>%s<<: %s" % (url, raw, str(e)) - ) - raise (e) - else: - content = raw - return content - class MultipleSourcePackagesError(Exception): pass From a63d3ca7bfb99b67407f2ee4e45d3b187e4652a4 Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Thu, 9 Oct 2025 10:42:08 +0200 Subject: [PATCH 07/30] Refactor openqa test settings generation --- manual-maintenance-tests-trigger.py | 60 ++++++++++++----------------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index 5a3e87f4..4e9ba326 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -111,8 +111,10 @@ def trigger_tests_for_pr(args): settings = prepare_update_settings( obs_project, bs_repo_url, pr, packages_in_project ) - job_params = create_openqa_job_params(obs_project, data) - openqa_build_overview = openqa_schedule(settings) + openqa_job_params = prepare_openqa_job_params( + args, obs_project, data, settings + ) + openqa_build_overview = openqa_schedule(args, openqa_job_params) else: log.error(f"PR {project}#{pr} does not target {args.branch}") @@ -272,28 +274,29 @@ def request_get(url): return json_data -def create_openqa_job_params(args, job_params): - log.debug("============== create_openqa_job_params") - raw_url = job_params["repo_html_url"] + "/raw/branch/" + job_params["sha"] - statuses_url = job_params["repo_api_url"] + "/statuses/" + job_params["sha"] +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 = { - "BUILD": job_params["repo_name"] + "#" + job_params["sha"], - "CASEDIR": job_params["clone_url"] + "#" + job_params["sha"], - "_GROUP_ID": "0", + "_GROUP_ID": "39", "PRIO": "100", - "NEEDLES_DIR": "%%CASEDIR%%/needles", - # set the URL for the scenario definitions YAML file so the Minion job will download it from GitHub - "SCENARIO_DEFINITIONS_YAML_FILE": raw_url + "/" + "scenario-definitions.yaml", # 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": job_params["repo_name"], - "GITEA_SHA": job_params["sha"], + "GITEA_REPO": data["head"]["repo"]["full_name"], + "GITEA_SHA": data["head"]["sha"], "GITEA_STATUSES_URL": statuses_url, - "GITEA_PR_URL": job_params["pr_html_url"], - "webhook_id": "gitea:pr:" + str(job_params["id"]), + "GITEA_PR_URL": data["html_url"], + "webhook_id": "gitea:pr:" + str(data["number"]), + "VERSION": data["base"]["label"].split("-")[-1], + "DISTRI": "opensuse", # there must be a better way than to hardcode + "FLAVOR": "staged-updates", + "ARCH": "x86_64", } - return params + return params | settings def openqa_cli(host, subcommand, cmds, dry_run=False): @@ -321,29 +324,16 @@ def openqa_cli(host, subcommand, cmds, dry_run=False): def openqa_schedule(args, params): log.debug("============== openqa_schedule") - scenario_url = "https://raw.githubusercontent.com/os-autoinst/os-autoinst-distri-openQA/refs/heads/master/scenario-definitions.yaml" - scenario_yaml = fetch_url(scenario_url, request_type="text") - yaml_file = "/tmp/distri-openqa-scenario.yaml" - with open(yaml_file, "w") as f: - f.write(scenario_yaml.decode("utf-8")) - cmd_args = [ - "--param-file", - "SCENARIO_DEFINITIONS_YAML=" + yaml_file, - "VERSION=Tumbleweed", - "DISTRI=openqa", - "FLAVOR=dev", - "ARCH=x86_64", - "HDD_1=opensuse-Tumbleweed-x86_64-20250920-minimalx@uefi.qcow2", - ] + + cmd_args = [] for key in params: - cmd_args.append(key + "=" + params[key]) + cmd_args.append(f"{key}={params[key]}") output = openqa_cli(args.openqa_host, "schedule", cmd_args, dry_run) - pattern = re.compile(r".*?(?Phttps?://\S+)", re.DOTALL) query_parameters = { "build": params["BUILD"], - "distri": "openqa", - "version": "Tumbleweed", + "distri": params["DISTRI"], + "version": params["VERSION"], } base_url = urlparse(args.openqa_host + "/tests/overview") From e01789d34c4237b6a6c6b08ec796fc7c24db6319 Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Thu, 9 Oct 2025 11:08:54 +0200 Subject: [PATCH 08/30] Improve logging messages --- manual-maintenance-tests-trigger.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index 4e9ba326..6463fa48 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -115,6 +115,7 @@ def trigger_tests_for_pr(args): args, obs_project, data, settings ) openqa_build_overview = openqa_schedule(args, openqa_job_params) + log.info(f"Build triggered, results at {openqa_build_overview}") else: log.error(f"PR {project}#{pr} does not target {args.branch}") @@ -131,7 +132,7 @@ def prepare_update_settings(obs_project, bs_repo_url, pr, packages): # openSUSE:Maintenance key settings["IMPORT_GPG_KEYS"] = "gpg-pubkey-b3fd7e48-5549fd0f" - settings["ZYPPER_ADD_REPO_PREFIX"] = "staged_update" + settings["ZYPPER_ADD_REPO_PREFIX"] = "staged-updates" settings["INSTALL_PACKAGES"] = " ".join(packages.keys()) settings["VERIFY_PACKAGE_VERSIONS"] = " ".join( @@ -164,7 +165,7 @@ def get_obs_values(project, branch, pr_id): obs_project = template.format(version=branch_version, project=project, pr_id=pr_id) target_repo = REPO_PREFIX + "/" target_repo += obs_project.replace(":", ":/") - log.debug(f"Target project {obs_project}, {target_repo}") + log.info(f"Target project {obs_project}, {target_repo}") return obs_project, target_repo @@ -240,7 +241,7 @@ def gitea_post_status(job_params, job_url): def request_post(url, payload): - log.debug("============== request_post") + log.debug(f"Posting request to gitea for {url}") log.debug(payload) token = os.environ.get("GITEA_TOKEN") headers = { @@ -257,7 +258,7 @@ def request_post(url, payload): def request_get(url): - log.debug("============== request_get") + log.debug(f"Sending request to gitea for {url}") token = os.environ.get("GITEA_TOKEN") headers = { "User-Agent": USER_AGENT, @@ -281,7 +282,6 @@ def prepare_openqa_job_params(args, obs_project, data, settings): + f"/api/v1/repos/{data['head']['repo']['full_name']}/statuses/{data['head']['sha']}" ) params = { - "_GROUP_ID": "39", "PRIO": "100", # add "target URL" for the "Details" button of the CI status "CI_TARGET_URL": args.openqa_host, @@ -319,9 +319,6 @@ def openqa_cli(host, subcommand, cmds, dry_run=False): return res.stdout.decode("utf-8") - return res.stdout.decode("utf-8") - - def openqa_schedule(args, params): log.debug("============== openqa_schedule") @@ -345,9 +342,11 @@ def openqa_schedule(args, params): class MultipleSourcePackagesError(Exception): pass + class NoSourcePackagesError(Exception): pass + if __name__ == "__main__": args = parse_args() From 65ef5595e0ab0ca1fbf370dad24ec155eec78fbd Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Thu, 9 Oct 2025 11:09:53 +0200 Subject: [PATCH 09/30] Add test pull request 151 data --- ...s_workflow_pr_bot-151-20251007-154142.json | 312 ++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 tests/data/opensuse-maintenance/by-autogits_workflow_pr_bot-151-20251007-154142.json diff --git a/tests/data/opensuse-maintenance/by-autogits_workflow_pr_bot-151-20251007-154142.json b/tests/data/opensuse-maintenance/by-autogits_workflow_pr_bot-151-20251007-154142.json new file mode 100644 index 00000000..206ff375 --- /dev/null +++ b/tests/data/opensuse-maintenance/by-autogits_workflow_pr_bot-151-20251007-154142.json @@ -0,0 +1,312 @@ +{ + "id": 2722, + "url": "https://src.opensuse.org/products/PackageHub/pulls/151", + "number": 151, + "user": { + "id": 1652, + "login": "autogits_workflow_pr_bot", + "login_name": "", + "source_id": 0, + "full_name": "", + "email": "autogits_workflow_pr_bot@noreply.src.opensuse.org", + "avatar_url": "https://src.opensuse.org/avatar/dabd7e8102cdc7b31f1fa67c4bcb3908", + "html_url": "https://src.opensuse.org/autogits_workflow_pr_bot", + "language": "", + "is_admin": false, + "last_login": "0001-01-01T00:00:00Z", + "created": "2025-08-13T08:32:42+02:00", + "restricted": false, + "active": false, + "prohibit_login": false, + "location": "", + "website": "", + "description": "", + "visibility": "public", + "followers_count": 0, + "following_count": 0, + "starred_repos_count": 0, + "username": "autogits_workflow_pr_bot" + }, + "title": "Forwarded PRs: opi", + "body": "This is a forwarded pull request by AutoGits PR Review Bot\nreferencing the following pull request(s):\n\nPR: pool/opi!1\n\n### ManualMergeProject enabled. To merge, 'merge ok' is required by project maintainer in the project PR.", + "labels": [], + "milestone": null, + "assignee": null, + "assignees": [], + "requested_reviewers": [ + { + "id": 1778, + "login": "packagehub-review", + "login_name": "", + "source_id": 0, + "full_name": "", + "email": "packagehub-review@noreply.src.opensuse.org", + "avatar_url": "https://src.opensuse.org/avatar/484734e0814fd8726512df9f3b5b4373", + "html_url": "https://src.opensuse.org/packagehub-review", + "language": "", + "is_admin": false, + "last_login": "0001-01-01T00:00:00Z", + "created": "2025-09-26T15:25:43+02:00", + "restricted": false, + "active": false, + "prohibit_login": false, + "location": "", + "website": "", + "description": "", + "visibility": "public", + "followers_count": 0, + "following_count": 0, + "starred_repos_count": 0, + "username": "packagehub-review" + }, + { + "id": 1008, + "login": "autogits_obs_staging_bot", + "login_name": "", + "source_id": 0, + "full_name": "", + "email": "autogits_obs_staging_bot@noreply.src.opensuse.org", + "avatar_url": "https://src.opensuse.org/avatars/9aa9b21c0beaf80d4af7a0fca8a326862127f8b5fa68e47d0870800441ced967", + "html_url": "https://src.opensuse.org/autogits_obs_staging_bot", + "language": "", + "is_admin": false, + "last_login": "0001-01-01T00:00:00Z", + "created": "2024-07-06T14:31:34+02:00", + "restricted": false, + "active": false, + "prohibit_login": false, + "location": "", + "website": "", + "description": "I stage proposed changes and see if they build.", + "visibility": "public", + "followers_count": 0, + "following_count": 0, + "starred_repos_count": 0, + "username": "autogits_obs_staging_bot" + } + ], + "requested_reviewers_teams": [], + "state": "open", + "draft": false, + "is_locked": false, + "comments": 3, + "review_comments": 1, + "additions": 1, + "deletions": 1, + "changed_files": 1, + "html_url": "https://src.opensuse.org/products/PackageHub/pulls/151", + "diff_url": "https://src.opensuse.org/products/PackageHub/pulls/151.diff", + "patch_url": "https://src.opensuse.org/products/PackageHub/pulls/151.patch", + "mergeable": true, + "merged": false, + "merged_at": null, + "merge_commit_sha": null, + "merged_by": null, + "allow_maintainer_edit": false, + "base": { + "label": "leap-16.0", + "ref": "leap-16.0", + "sha": "5e849a18d0d7166efd6358fe18a35e8c894dc88cffa0dc0e81da6ed1f4159d78", + "repo_id": 91295, + "repo": { + "id": 91295, + "owner": { + "id": 181, + "login": "products", + "login_name": "", + "source_id": 0, + "full_name": "", + "email": "", + "avatar_url": "https://src.opensuse.org/avatars/86024cad1e83101d97359d7351051156", + "html_url": "https://src.opensuse.org/products", + "language": "", + "is_admin": false, + "last_login": "0001-01-01T00:00:00Z", + "created": "2023-09-20T11:20:19+02:00", + "restricted": false, + "active": false, + "prohibit_login": false, + "location": "", + "website": "", + "description": "", + "visibility": "public", + "followers_count": 0, + "following_count": 0, + "starred_repos_count": 0, + "username": "products" + }, + "name": "PackageHub", + "full_name": "products/PackageHub", + "description": "", + "empty": false, + "private": false, + "fork": false, + "template": false, + "mirror": false, + "size": 10985, + "language": "", + "languages_url": "https://src.opensuse.org/api/v1/repos/products/PackageHub/languages", + "html_url": "https://src.opensuse.org/products/PackageHub", + "url": "https://src.opensuse.org/api/v1/repos/products/PackageHub", + "link": "", + "ssh_url": "gitea@src.opensuse.org:products/PackageHub.git", + "clone_url": "https://src.opensuse.org/products/PackageHub.git", + "original_url": "", + "website": "", + "stars_count": 1, + "forks_count": 8, + "watchers_count": 14, + "open_issues_count": 0, + "open_pr_counter": 12, + "release_counter": 0, + "default_branch": "leap-16.0", + "archived": false, + "created_at": "2024-09-17T14:31:57+02:00", + "updated_at": "2025-10-07T13:37:58+02:00", + "archived_at": "1970-01-01T01:00:00+01:00", + "permissions": { + "admin": false, + "push": false, + "pull": true + }, + "has_issues": true, + "internal_tracker": { + "enable_time_tracker": false, + "allow_only_contributors_to_track_time": true, + "enable_issue_dependencies": true + }, + "has_wiki": false, + "has_pull_requests": true, + "has_projects": false, + "projects_mode": "all", + "has_releases": false, + "has_packages": false, + "has_actions": true, + "ignore_whitespace_conflicts": false, + "allow_merge_commits": true, + "allow_rebase": true, + "allow_rebase_explicit": true, + "allow_squash_merge": true, + "allow_fast_forward_only_merge": true, + "allow_rebase_update": true, + "allow_manual_merge": true, + "autodetect_manual_merge": true, + "default_delete_branch_after_merge": false, + "default_merge_style": "merge", + "default_allow_maintainer_edit": false, + "avatar_url": "", + "internal": false, + "mirror_interval": "", + "object_format_name": "sha256", + "mirror_updated": "0001-01-01T00:00:00Z", + "topics": [], + "licenses": [] + } + }, + "head": { + "label": "PR_opi#1", + "ref": "PR_opi#1", + "sha": "429aebe847df795df16558b7ac22e96971e8f8f27a40a0d9b149eb969c452168", + "repo_id": 91295, + "repo": { + "id": 91295, + "owner": { + "id": 181, + "login": "products", + "login_name": "", + "source_id": 0, + "full_name": "", + "email": "", + "avatar_url": "https://src.opensuse.org/avatars/86024cad1e83101d97359d7351051156", + "html_url": "https://src.opensuse.org/products", + "language": "", + "is_admin": false, + "last_login": "0001-01-01T00:00:00Z", + "created": "2023-09-20T11:20:19+02:00", + "restricted": false, + "active": false, + "prohibit_login": false, + "location": "", + "website": "", + "description": "", + "visibility": "public", + "followers_count": 0, + "following_count": 0, + "starred_repos_count": 0, + "username": "products" + }, + "name": "PackageHub", + "full_name": "products/PackageHub", + "description": "", + "empty": false, + "private": false, + "fork": false, + "template": false, + "mirror": false, + "size": 10985, + "language": "", + "languages_url": "https://src.opensuse.org/api/v1/repos/products/PackageHub/languages", + "html_url": "https://src.opensuse.org/products/PackageHub", + "url": "https://src.opensuse.org/api/v1/repos/products/PackageHub", + "link": "", + "ssh_url": "gitea@src.opensuse.org:products/PackageHub.git", + "clone_url": "https://src.opensuse.org/products/PackageHub.git", + "original_url": "", + "website": "", + "stars_count": 1, + "forks_count": 8, + "watchers_count": 14, + "open_issues_count": 0, + "open_pr_counter": 12, + "release_counter": 0, + "default_branch": "leap-16.0", + "archived": false, + "created_at": "2024-09-17T14:31:57+02:00", + "updated_at": "2025-10-07T13:37:58+02:00", + "archived_at": "1970-01-01T01:00:00+01:00", + "permissions": { + "admin": false, + "push": false, + "pull": true + }, + "has_issues": true, + "internal_tracker": { + "enable_time_tracker": false, + "allow_only_contributors_to_track_time": true, + "enable_issue_dependencies": true + }, + "has_wiki": false, + "has_pull_requests": true, + "has_projects": false, + "projects_mode": "all", + "has_releases": false, + "has_packages": false, + "has_actions": true, + "ignore_whitespace_conflicts": false, + "allow_merge_commits": true, + "allow_rebase": true, + "allow_rebase_explicit": true, + "allow_squash_merge": true, + "allow_fast_forward_only_merge": true, + "allow_rebase_update": true, + "allow_manual_merge": true, + "autodetect_manual_merge": true, + "default_delete_branch_after_merge": false, + "default_merge_style": "merge", + "default_allow_maintainer_edit": false, + "avatar_url": "", + "internal": false, + "mirror_interval": "", + "object_format_name": "sha256", + "mirror_updated": "0001-01-01T00:00:00Z", + "topics": [], + "licenses": [] + } + }, + "merge_base": "3d7a75de4e9d586b0f51168efbfc7b91296dd9dfdd6283e5c43f9e1d4063e5ca", + "due_date": null, + "created_at": "2025-09-30T14:35:55+02:00", + "updated_at": "2025-09-30T14:44:41+02:00", + "closed_at": null, + "pin_order": 0 +} \ No newline at end of file From f85c72e3e2ba8398ae1dd730b5a33e95a169cce4 Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Thu, 9 Oct 2025 11:28:53 +0200 Subject: [PATCH 10/30] Make style checks pass --- manual-maintenance-tests-trigger.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index 6463fa48..af748014 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -1,12 +1,10 @@ #!/usr/bin/env python3 -import pika import sys import os import argparse import json import subprocess import requests -import re import logging import datetime from urllib.parse import urlencode, urlunparse, urlparse @@ -228,7 +226,6 @@ def gitea_query_pr(project, pr_id): def gitea_post_status(job_params, job_url): log.debug("============== gitea_post_status") - sha = job_params["sha"] statuses_url = job_params["repo_api_url"] + "/statuses/" + job_params["sha"] payload = { @@ -325,7 +322,7 @@ def openqa_schedule(args, params): cmd_args = [] for key in params: cmd_args.append(f"{key}={params[key]}") - output = openqa_cli(args.openqa_host, "schedule", cmd_args, dry_run) + openqa_cli(args.openqa_host, "schedule", cmd_args, dry_run) query_parameters = { "build": params["BUILD"], From e9a1773e39022e5458b3811511fffb130ccfff7a Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 09:52:01 +0200 Subject: [PATCH 11/30] Remove dead code --- manual-maintenance-tests-trigger.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index af748014..2df9bdc9 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -188,7 +188,6 @@ def get_packages_from_obs_project(obs_project): url = osc.core.makeurl( BS_HOST, ("build", obs_project, repo, arch, "_repository"), query=query ) - # breakpoint() root = ET.parse(osc.core.http_GET(url)).getroot() for binary in root.findall("binary"): @@ -336,10 +335,6 @@ def openqa_schedule(args, params): return test_overview_url -class MultipleSourcePackagesError(Exception): - pass - - class NoSourcePackagesError(Exception): pass From 3760270b2077b580a41a2c0f41c52abf1e504b2e Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 09:54:34 +0200 Subject: [PATCH 12/30] Allow querying status of the review --- manual-maintenance-tests-trigger.py | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index 2df9bdc9..52626ede 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -116,6 +116,8 @@ def trigger_tests_for_pr(args): log.info(f"Build triggered, results at {openqa_build_overview}") else: log.error(f"PR {project}#{pr} does not target {args.branch}") +def get_build_review_status(project, pr, review_id): + return gitea_get_review(project, pr, review_id) def prepare_update_settings(obs_project, bs_repo_url, pr, packages): @@ -236,6 +238,53 @@ def gitea_post_status(job_params, job_url): request_post(statuses_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) From 527f39c67a9a81e68bdfb1136a82c431c53cfe5d Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 09:56:12 +0200 Subject: [PATCH 13/30] Add cmdline parameters needed for the execution --- manual-maintenance-tests-trigger.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index 52626ede..25852e6c 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -36,13 +36,15 @@ def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--myself", help="Username of bot", default="qam-openqa") parser.add_argument( - "--openqa-host", help="OpenQA instance url", default="http://localhost:9526" + "--review-group", + help="Group to be used for approval", + default="@qam-openqa-review", ) parser.add_argument( - "--verbose", help="Verbosity", default="1", type=int, choices=[0, 1, 2, 3] + "--openqa-host", help="OpenQA instance url", default="http://localhost:9526" ) parser.add_argument( - "--build-bot", help="Username of bot that approves when build is finished" + "--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") @@ -60,6 +62,9 @@ def parse_args(): 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", From aaa6f764e17a386011cdac17e6747a1b31741a36 Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 09:58:31 +0200 Subject: [PATCH 14/30] Remove code for storing PR data --- manual-maintenance-tests-trigger.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index 25852e6c..41695947 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -48,13 +48,6 @@ def parse_args(): ) parser.add_argument("--branch", help="Target branch, eg. leap-16.0") parser.add_argument("--project", help="Target project") - parser.add_argument( - "--store-pr", - help="Stores pull request data and exits", - action="store_true", - default=False, - ) - parser.add_argument("--pr-data", help="File to use for simulation", default=False) 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" @@ -86,20 +79,10 @@ def trigger_tests_for_pr(args): data = json.loads(content) pr = data["number"] - by = data["user"]["login"] project = data["base"]["repo"]["full_name"] branch = data["base"]["label"] log.info(f"working on {project}#{pr}") - if args.store_pr and not args.pr_data: - timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") - filename = f"tests/data/opensuse-maintenance/by-{by}-{pr}-{timestamp}.json" - try: - with open(filename, "w") as file_object: - json.dump(data, file_object, indent=4) - log.info(f"Storing review-requested file to {filename}") - log.debug( - f"Captured pr {args.project}#{args.branch} PR: {args.pr_id}, saved to {filename}. Exiting" ) exit() From 839cc5971a5cadda79fc66440e08064cd41333fc Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 09:58:57 +0200 Subject: [PATCH 15/30] Add configuration data for other projects --- manual-maintenance-tests-trigger.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index 41695947..a3ea8971 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -25,7 +25,9 @@ log.addHandler(handler) CONFIG_DATA = { - "products/PackageHub": "openSUSE:Backports:SLE-{version}:PullRequest:{pr_id}" + "products/PackageHub": "openSUSE:Backports:SLE-{version}:PullRequest:{pr_id}", + "openSUSE/Leap": "openSUSE:Leap:{version}:PullRequest:{pr_id}", + "openSUSE/LeapNonFree": "openSUSE:Leap:{version}:NonFree:PullRequest:{pr_id}", } GITEA_HOST = None BS_HOST = None From d8794a4410d0be08a845f8b7c2a84257f1e6ae8c Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 10:01:50 +0200 Subject: [PATCH 16/30] Allow processing multiple pull requests in batch --- manual-maintenance-tests-trigger.py | 44 +++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index a3ea8971..59a21091 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -70,15 +70,37 @@ def parse_args(): return args -def trigger_tests_for_pr(args): - if not args.pr_data: - data = gitea_query_pr(args.project, args.pr_id) - else: - log.info("Loading pr data from file") - json_file = args.pr_data - with open(json_file, "r") as f: - content = f.read() - data = json.loads(content) +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"] @@ -397,3 +419,7 @@ class NoSourcePackagesError(Exception): # simulate_build_finished_event(args) # else: # listen(args) + if args.pr_id: + process_pull_request(args.pr_id, args) + else: + process_project(args) From 80aaf60d70cb0778e44e186ee78f3bda6a71aefb Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 10:04:15 +0200 Subject: [PATCH 17/30] Query openQA build to know overall status of tests --- manual-maintenance-tests-trigger.py | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index 59a21091..2a2c5023 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -7,6 +7,7 @@ import requests import logging import datetime +from openqa_client.client import OpenQA_Client from urllib.parse import urlencode, urlunparse, urlparse from lxml import etree as ET from collections import namedtuple @@ -126,6 +127,47 @@ def process_pull_request(pr_id, args): ) openqa_build_overview = openqa_schedule(args, openqa_job_params) log.info(f"Build triggered, results at {openqa_build_overview}") +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"], + "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 else: log.error(f"PR {project}#{pr} does not target {args.branch}") def get_build_review_status(project, pr, review_id): @@ -419,6 +461,7 @@ class NoSourcePackagesError(Exception): # simulate_build_finished_event(args) # else: # listen(args) + openqa = OpenQA_Client(server=args.openqa_host) if args.pr_id: process_pull_request(args.pr_id, args) else: From 3ce1fd5b38d16a19c1753440d09436c1b94a1779 Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 10:06:01 +0200 Subject: [PATCH 18/30] Refactor pr processing --- manual-maintenance-tests-trigger.py | 53 ++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index 2a2c5023..ce3cc664 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -108,24 +108,45 @@ def process_pull_request(pr_id, args): branch = data["base"]["label"] log.info(f"working on {project}#{pr}") + if branch != args.branch and project != args.project: + log.error(f"PR {project}#{pr} does not 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 = get_obs_values(project, branch, pr) + # 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 packages_in_project: + settings = prepare_update_settings( + obs_project, 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}" ) - exit() - - except IOError as e: - log.error(f"Error saving file: {e}") - - if branch == args.branch and project == args.project: - obs_project, bs_repo_url = get_obs_values(project, branch, pr) - # We need to query every package in the staged update - packages_in_project = get_packages_from_obs_project(obs_project) - if packages_in_project: - settings = prepare_update_settings( - obs_project, bs_repo_url, pr, packages_in_project - ) - openqa_job_params = prepare_openqa_job_params( - args, obs_project, data, settings - ) + 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 compute_openqa_tests_status(openqa_job_params): values = { From 586f4a8a0bebdb22ecd3bd11a8f98984386ca5e6 Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 10:07:10 +0200 Subject: [PATCH 19/30] Add take_action to handle approval of a PR --- manual-maintenance-tests-trigger.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index ce3cc664..90d6205a 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -33,6 +33,14 @@ GITEA_HOST = 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(): @@ -148,6 +156,24 @@ def process_pull_request(pr_id, args): # 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.debug(f"QA state is QA_UNKNOWN for {project}#{pr}") + + 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"], From 72002f99e5ffc63660701e7fd1227af110eb3de7 Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 10:08:20 +0200 Subject: [PATCH 20/30] Add is_build_finished to query status of build in obs/ibs --- manual-maintenance-tests-trigger.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index 90d6205a..ccbb532b 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -215,8 +215,26 @@ def compute_openqa_tests_status(openqa_job_params): 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.error(f"PR {project}#{pr} does not target {args.branch}") + 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) From 7d529ce6fe90ed25e201ba21c09bd90aea8e8e85 Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 10:10:07 +0200 Subject: [PATCH 21/30] Minor polishing --- manual-maintenance-tests-trigger.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index ccbb532b..d52930a6 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 import sys import os +import re import argparse -import json import subprocess import requests import logging -import datetime from openqa_client.client import OpenQA_Client from urllib.parse import urlencode, urlunparse, urlparse from lxml import etree as ET @@ -14,7 +13,8 @@ import osc.core USER_AGENT = "manual-trigger.py (https://github.com/os-autoinst/scripts)" -dry_run = False +dry_run = True +openqa_dry_run = False log = logging.getLogger(sys.argv[0] if __name__ == "__main__" else __name__) log.setLevel(logging.DEBUG) @@ -344,10 +344,8 @@ def gitea_query_pr(project, pr_id): return request_get(pull_request_url) -def gitea_post_status(job_params, job_url): +def gitea_post_status(statuses_url, job_url): log.debug("============== gitea_post_status") - statuses_url = job_params["repo_api_url"] + "/statuses/" + job_params["sha"] - payload = { "context": "qam-openqa", "description": "openQA check", @@ -364,6 +362,7 @@ def gitea_get_review(project, pr_id, 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" @@ -404,6 +403,7 @@ def get_events_by_timeline(project, pr_id): return events + def request_post(url, payload): log.debug(f"Posting request to gitea for {url}") log.debug(payload) @@ -429,6 +429,7 @@ def request_get(url): "Accept": "application/json", "Authorization": "token " + token, } + try: content = requests.get(url, headers=headers) content.raise_for_status() @@ -489,7 +490,7 @@ def openqa_schedule(args, params): cmd_args = [] for key in params: cmd_args.append(f"{key}={params[key]}") - openqa_cli(args.openqa_host, "schedule", cmd_args, dry_run) + openqa_cli(args.openqa_host, "schedule", cmd_args, openqa_dry_run) query_parameters = { "build": params["BUILD"], @@ -517,15 +518,8 @@ class NoSourcePackagesError(Exception): GITEA_HOST = args.gitea BS_HOST = args.bs REPO_PREFIX = args.repo_prefix + REVIEW_GROUP = args.review_group osc.conf.get_config() - - trigger_tests_for_pr(args) - # if args.simulate_review_requested_event: - # simulate(args) - # elif(args.simulate_build_finished_event and args.build_bot): - # simulate_build_finished_event(args) - # else: - # listen(args) openqa = OpenQA_Client(server=args.openqa_host) if args.pr_id: process_pull_request(args.pr_id, args) From 448c4024d9aa88b020dd96d5dd94fc5973f73aa2 Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 10:11:59 +0200 Subject: [PATCH 22/30] Handle updates with patchinfo and multiple source packages --- manual-maintenance-tests-trigger.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index d52930a6..e16e3473 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -266,14 +266,23 @@ def get_staged_update_name(obs_project): 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(source_packages) > 1: - raise MultipleSourcePackagesError("Multiple packages detected") - elif len(source_packages) == 0: + 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: - return source_packages[0] + # 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): From 85b308ceef1f203d10fb34e82d8b0d14759e6b1b Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 10:12:51 +0200 Subject: [PATCH 23/30] Allow post requests to gitea to have dry runs --- manual-maintenance-tests-trigger.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index e16e3473..841fb0fc 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -422,12 +422,15 @@ def request_post(url, payload): "Accept": "application/json", "Authorization": "token " + token, } - 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) + if dry_run: + log.debug(f"would send requst 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): From 6dbe64e4561768da5c314ec5da7d3068a4d24dd8 Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 10:14:44 +0200 Subject: [PATCH 24/30] Query comments for openqa tests and review status --- manual-maintenance-tests-trigger.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index 841fb0fc..0fe46875 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -239,6 +239,31 @@ 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(obs_project, bs_repo_url, pr, packages): settings = {} staged_update_name = get_staged_update_name(obs_project) From 973cb96b174393df0dff8dd739e90ae1f21eddd2 Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 10:20:03 +0200 Subject: [PATCH 25/30] Add missing methods to git history --- manual-maintenance-tests-trigger.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index 0fe46875..90e3b0f4 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -389,6 +389,24 @@ def gitea_post_status(statuses_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 = ( From 5d2e7d5c3c56b5fde3bfa97df927bd716050f3bf Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Wed, 15 Oct 2025 11:10:43 +0200 Subject: [PATCH 26/30] Remove old test file --- ...s_workflow_pr_bot-151-20251007-154142.json | 312 ------------------ 1 file changed, 312 deletions(-) delete mode 100644 tests/data/opensuse-maintenance/by-autogits_workflow_pr_bot-151-20251007-154142.json diff --git a/tests/data/opensuse-maintenance/by-autogits_workflow_pr_bot-151-20251007-154142.json b/tests/data/opensuse-maintenance/by-autogits_workflow_pr_bot-151-20251007-154142.json deleted file mode 100644 index 206ff375..00000000 --- a/tests/data/opensuse-maintenance/by-autogits_workflow_pr_bot-151-20251007-154142.json +++ /dev/null @@ -1,312 +0,0 @@ -{ - "id": 2722, - "url": "https://src.opensuse.org/products/PackageHub/pulls/151", - "number": 151, - "user": { - "id": 1652, - "login": "autogits_workflow_pr_bot", - "login_name": "", - "source_id": 0, - "full_name": "", - "email": "autogits_workflow_pr_bot@noreply.src.opensuse.org", - "avatar_url": "https://src.opensuse.org/avatar/dabd7e8102cdc7b31f1fa67c4bcb3908", - "html_url": "https://src.opensuse.org/autogits_workflow_pr_bot", - "language": "", - "is_admin": false, - "last_login": "0001-01-01T00:00:00Z", - "created": "2025-08-13T08:32:42+02:00", - "restricted": false, - "active": false, - "prohibit_login": false, - "location": "", - "website": "", - "description": "", - "visibility": "public", - "followers_count": 0, - "following_count": 0, - "starred_repos_count": 0, - "username": "autogits_workflow_pr_bot" - }, - "title": "Forwarded PRs: opi", - "body": "This is a forwarded pull request by AutoGits PR Review Bot\nreferencing the following pull request(s):\n\nPR: pool/opi!1\n\n### ManualMergeProject enabled. To merge, 'merge ok' is required by project maintainer in the project PR.", - "labels": [], - "milestone": null, - "assignee": null, - "assignees": [], - "requested_reviewers": [ - { - "id": 1778, - "login": "packagehub-review", - "login_name": "", - "source_id": 0, - "full_name": "", - "email": "packagehub-review@noreply.src.opensuse.org", - "avatar_url": "https://src.opensuse.org/avatar/484734e0814fd8726512df9f3b5b4373", - "html_url": "https://src.opensuse.org/packagehub-review", - "language": "", - "is_admin": false, - "last_login": "0001-01-01T00:00:00Z", - "created": "2025-09-26T15:25:43+02:00", - "restricted": false, - "active": false, - "prohibit_login": false, - "location": "", - "website": "", - "description": "", - "visibility": "public", - "followers_count": 0, - "following_count": 0, - "starred_repos_count": 0, - "username": "packagehub-review" - }, - { - "id": 1008, - "login": "autogits_obs_staging_bot", - "login_name": "", - "source_id": 0, - "full_name": "", - "email": "autogits_obs_staging_bot@noreply.src.opensuse.org", - "avatar_url": "https://src.opensuse.org/avatars/9aa9b21c0beaf80d4af7a0fca8a326862127f8b5fa68e47d0870800441ced967", - "html_url": "https://src.opensuse.org/autogits_obs_staging_bot", - "language": "", - "is_admin": false, - "last_login": "0001-01-01T00:00:00Z", - "created": "2024-07-06T14:31:34+02:00", - "restricted": false, - "active": false, - "prohibit_login": false, - "location": "", - "website": "", - "description": "I stage proposed changes and see if they build.", - "visibility": "public", - "followers_count": 0, - "following_count": 0, - "starred_repos_count": 0, - "username": "autogits_obs_staging_bot" - } - ], - "requested_reviewers_teams": [], - "state": "open", - "draft": false, - "is_locked": false, - "comments": 3, - "review_comments": 1, - "additions": 1, - "deletions": 1, - "changed_files": 1, - "html_url": "https://src.opensuse.org/products/PackageHub/pulls/151", - "diff_url": "https://src.opensuse.org/products/PackageHub/pulls/151.diff", - "patch_url": "https://src.opensuse.org/products/PackageHub/pulls/151.patch", - "mergeable": true, - "merged": false, - "merged_at": null, - "merge_commit_sha": null, - "merged_by": null, - "allow_maintainer_edit": false, - "base": { - "label": "leap-16.0", - "ref": "leap-16.0", - "sha": "5e849a18d0d7166efd6358fe18a35e8c894dc88cffa0dc0e81da6ed1f4159d78", - "repo_id": 91295, - "repo": { - "id": 91295, - "owner": { - "id": 181, - "login": "products", - "login_name": "", - "source_id": 0, - "full_name": "", - "email": "", - "avatar_url": "https://src.opensuse.org/avatars/86024cad1e83101d97359d7351051156", - "html_url": "https://src.opensuse.org/products", - "language": "", - "is_admin": false, - "last_login": "0001-01-01T00:00:00Z", - "created": "2023-09-20T11:20:19+02:00", - "restricted": false, - "active": false, - "prohibit_login": false, - "location": "", - "website": "", - "description": "", - "visibility": "public", - "followers_count": 0, - "following_count": 0, - "starred_repos_count": 0, - "username": "products" - }, - "name": "PackageHub", - "full_name": "products/PackageHub", - "description": "", - "empty": false, - "private": false, - "fork": false, - "template": false, - "mirror": false, - "size": 10985, - "language": "", - "languages_url": "https://src.opensuse.org/api/v1/repos/products/PackageHub/languages", - "html_url": "https://src.opensuse.org/products/PackageHub", - "url": "https://src.opensuse.org/api/v1/repos/products/PackageHub", - "link": "", - "ssh_url": "gitea@src.opensuse.org:products/PackageHub.git", - "clone_url": "https://src.opensuse.org/products/PackageHub.git", - "original_url": "", - "website": "", - "stars_count": 1, - "forks_count": 8, - "watchers_count": 14, - "open_issues_count": 0, - "open_pr_counter": 12, - "release_counter": 0, - "default_branch": "leap-16.0", - "archived": false, - "created_at": "2024-09-17T14:31:57+02:00", - "updated_at": "2025-10-07T13:37:58+02:00", - "archived_at": "1970-01-01T01:00:00+01:00", - "permissions": { - "admin": false, - "push": false, - "pull": true - }, - "has_issues": true, - "internal_tracker": { - "enable_time_tracker": false, - "allow_only_contributors_to_track_time": true, - "enable_issue_dependencies": true - }, - "has_wiki": false, - "has_pull_requests": true, - "has_projects": false, - "projects_mode": "all", - "has_releases": false, - "has_packages": false, - "has_actions": true, - "ignore_whitespace_conflicts": false, - "allow_merge_commits": true, - "allow_rebase": true, - "allow_rebase_explicit": true, - "allow_squash_merge": true, - "allow_fast_forward_only_merge": true, - "allow_rebase_update": true, - "allow_manual_merge": true, - "autodetect_manual_merge": true, - "default_delete_branch_after_merge": false, - "default_merge_style": "merge", - "default_allow_maintainer_edit": false, - "avatar_url": "", - "internal": false, - "mirror_interval": "", - "object_format_name": "sha256", - "mirror_updated": "0001-01-01T00:00:00Z", - "topics": [], - "licenses": [] - } - }, - "head": { - "label": "PR_opi#1", - "ref": "PR_opi#1", - "sha": "429aebe847df795df16558b7ac22e96971e8f8f27a40a0d9b149eb969c452168", - "repo_id": 91295, - "repo": { - "id": 91295, - "owner": { - "id": 181, - "login": "products", - "login_name": "", - "source_id": 0, - "full_name": "", - "email": "", - "avatar_url": "https://src.opensuse.org/avatars/86024cad1e83101d97359d7351051156", - "html_url": "https://src.opensuse.org/products", - "language": "", - "is_admin": false, - "last_login": "0001-01-01T00:00:00Z", - "created": "2023-09-20T11:20:19+02:00", - "restricted": false, - "active": false, - "prohibit_login": false, - "location": "", - "website": "", - "description": "", - "visibility": "public", - "followers_count": 0, - "following_count": 0, - "starred_repos_count": 0, - "username": "products" - }, - "name": "PackageHub", - "full_name": "products/PackageHub", - "description": "", - "empty": false, - "private": false, - "fork": false, - "template": false, - "mirror": false, - "size": 10985, - "language": "", - "languages_url": "https://src.opensuse.org/api/v1/repos/products/PackageHub/languages", - "html_url": "https://src.opensuse.org/products/PackageHub", - "url": "https://src.opensuse.org/api/v1/repos/products/PackageHub", - "link": "", - "ssh_url": "gitea@src.opensuse.org:products/PackageHub.git", - "clone_url": "https://src.opensuse.org/products/PackageHub.git", - "original_url": "", - "website": "", - "stars_count": 1, - "forks_count": 8, - "watchers_count": 14, - "open_issues_count": 0, - "open_pr_counter": 12, - "release_counter": 0, - "default_branch": "leap-16.0", - "archived": false, - "created_at": "2024-09-17T14:31:57+02:00", - "updated_at": "2025-10-07T13:37:58+02:00", - "archived_at": "1970-01-01T01:00:00+01:00", - "permissions": { - "admin": false, - "push": false, - "pull": true - }, - "has_issues": true, - "internal_tracker": { - "enable_time_tracker": false, - "allow_only_contributors_to_track_time": true, - "enable_issue_dependencies": true - }, - "has_wiki": false, - "has_pull_requests": true, - "has_projects": false, - "projects_mode": "all", - "has_releases": false, - "has_packages": false, - "has_actions": true, - "ignore_whitespace_conflicts": false, - "allow_merge_commits": true, - "allow_rebase": true, - "allow_rebase_explicit": true, - "allow_squash_merge": true, - "allow_fast_forward_only_merge": true, - "allow_rebase_update": true, - "allow_manual_merge": true, - "autodetect_manual_merge": true, - "default_delete_branch_after_merge": false, - "default_merge_style": "merge", - "default_allow_maintainer_edit": false, - "avatar_url": "", - "internal": false, - "mirror_interval": "", - "object_format_name": "sha256", - "mirror_updated": "0001-01-01T00:00:00Z", - "topics": [], - "licenses": [] - } - }, - "merge_base": "3d7a75de4e9d586b0f51168efbfc7b91296dd9dfdd6283e5c43f9e1d4063e5ca", - "due_date": null, - "created_at": "2025-09-30T14:35:55+02:00", - "updated_at": "2025-09-30T14:44:41+02:00", - "closed_at": null, - "pin_order": 0 -} \ No newline at end of file From 39ee8d7d6f2ad622406b112918fa84839c46fad7 Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Thu, 16 Oct 2025 10:48:07 +0200 Subject: [PATCH 27/30] Refactor process_pull_request Avoid multiple nested ifs, to the code more readable --- manual-maintenance-tests-trigger.py | 53 +++++++++++++++-------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index 90e3b0f4..0c3ecb5f 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -130,32 +130,35 @@ def process_pull_request(pr_id, args): packages_in_project = get_packages_from_obs_project(obs_project) openqa_build_overview = None - if packages_in_project: - settings = prepare_update_settings( - obs_project, 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}" - ) + if not packages_in_project: + log.warning(f"No packages found in {obs_project}, skipping.") + return + + settings = prepare_update_settings( + project, obs_project, 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: - 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}") + 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): From ac3ae3e8c8051bf85994fe38bea9dc1d3a15f17d Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Thu, 16 Oct 2025 10:49:55 +0200 Subject: [PATCH 28/30] Make the build parameter more unique This is mainly to avoid collisions later on --- manual-maintenance-tests-trigger.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index 0c3ecb5f..8f49d711 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -267,13 +267,14 @@ def check_openqa_comment(pr_events, myself): return openqa_build_overview, previous_review -def prepare_update_settings(obs_project, bs_repo_url, pr, packages): +def prepare_update_settings(project, obs_project, 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":{pr}:{staged_update_name}" patch_id = pr + settings["BUILD"] = f":{build_project}:{pr}:{staged_update_name}" settings["INCIDENT_REPO"] = bs_repo_url settings["INCIDENT_PATCH"] = patch_id From b37346541646904b83acba3043ce0251c0a55ac2 Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Thu, 16 Oct 2025 10:54:42 +0200 Subject: [PATCH 29/30] Fix query to openQA test results Build parameter was missing, so query would return all jobs for given parameters --- manual-maintenance-tests-trigger.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/manual-maintenance-tests-trigger.py b/manual-maintenance-tests-trigger.py index 8f49d711..8ceaf03c 100644 --- a/manual-maintenance-tests-trigger.py +++ b/manual-maintenance-tests-trigger.py @@ -30,6 +30,7 @@ "openSUSE/Leap": "openSUSE:Leap:{version}:PullRequest:{pr_id}", "openSUSE/LeapNonFree": "openSUSE:Leap:{version}:NonFree:PullRequest:{pr_id}", } + GITEA_HOST = None BS_HOST = None REPO_PREFIX = None @@ -183,6 +184,7 @@ def compute_openqa_tests_status(openqa_job_params): "version": openqa_job_params["VERSION"], "arch": openqa_job_params["ARCH"], "flavor": openqa_job_params["FLAVOR"], + "build": openqa_job_params["BUILD"], "scope": "relevant", "latest": "1", } @@ -273,9 +275,10 @@ def prepare_update_settings(project, obs_project, bs_repo_url, pr, packages): build_project = project.replace("/", "_") # this could also be: obs_project.split(':')[-1] # start with a colon so it looks cool behind 'Build' :/ - patch_id = pr 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 # openSUSE:Maintenance key @@ -470,7 +473,7 @@ def request_post(url, payload): "Authorization": "token " + token, } if dry_run: - log.debug(f"would send requst to {url} with {payload}") + log.debug(f"would send request to {url} with {payload}") else: try: content = requests.post(url, headers=headers, data=payload) From 40badf1c8aff1fbc3961ae7ab13f669ce3af9fe0 Mon Sep 17 00:00:00 2001 From: Santiago Zarate Date: Thu, 16 Oct 2025 11:22:53 +0200 Subject: [PATCH 30/30] Rename script --- manual-maintenance-tests-trigger.py => git-openqa-maintenance.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename manual-maintenance-tests-trigger.py => git-openqa-maintenance.py (100%) diff --git a/manual-maintenance-tests-trigger.py b/git-openqa-maintenance.py similarity index 100% rename from manual-maintenance-tests-trigger.py rename to git-openqa-maintenance.py