Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package openSUSE-release-tools for
openSUSE:Factory checked in at 2026-06-22 17:25:36
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/openSUSE-release-tools (Old)
and /work/SRC/openSUSE:Factory/.openSUSE-release-tools.new.1956 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "openSUSE-release-tools"
Mon Jun 22 17:25:36 2026 rev:555 rq:1360633 version:20260612.e4dd77ac
Changes:
--------
---
/work/SRC/openSUSE:Factory/openSUSE-release-tools/openSUSE-release-tools.changes
2026-06-05 17:39:31.900118288 +0200
+++
/work/SRC/openSUSE:Factory/.openSUSE-release-tools.new.1956/openSUSE-release-tools.changes
2026-06-22 17:26:21.155294118 +0200
@@ -1,0 +2,25 @@
+Fri Jun 12 14:58:19 UTC 2026 - [email protected]
+
+- Update to version 20260612.e4dd77ac:
+ * gocd: Fix pipeline name
+ * Add FTBFS dashboard for openSUSE:Factory:Rebuild
+
+-------------------------------------------------------------------
+Fri Jun 12 13:37:14 UTC 2026 - [email protected]
+
+- Update to version 20260612.f6ea0660:
+ * Implement pagination for Gitea API calls
+
+-------------------------------------------------------------------
+Thu Jun 11 12:46:56 UTC 2026 - [email protected]
+
+- Update to version 20260611.466e394c:
+ * docker_publisher.py: Set opensuse/leap:16.0 as latest
+
+-------------------------------------------------------------------
+Wed Jun 10 09:14:24 UTC 2026 - [email protected]
+
+- Update to version 20260610.93957ea7:
+ * gocd: Add TTM.Leap_16.0_Images
+
+-------------------------------------------------------------------
Old:
----
openSUSE-release-tools-20260529.ebc6e9d1.obscpio
New:
----
openSUSE-release-tools-20260612.e4dd77ac.obscpio
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ openSUSE-release-tools.spec ++++++
--- /var/tmp/diff_new_pack.o4fkho/_old 2026-06-22 17:26:23.131362953 +0200
+++ /var/tmp/diff_new_pack.o4fkho/_new 2026-06-22 17:26:23.143363371 +0200
@@ -21,7 +21,7 @@
%define announcer_filename factory-package-news
%define services osrt-slsa.target [email protected]
[email protected] [email protected] [email protected]
[email protected] [email protected]
Name: openSUSE-release-tools
-Version: 20260529.ebc6e9d1
+Version: 20260612.e4dd77ac
Release: 0
Summary: Tools to aid in staging and release work for openSUSE/SUSE
License: GPL-2.0-or-later AND MIT
++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.o4fkho/_old 2026-06-22 17:26:23.223366159 +0200
+++ /var/tmp/diff_new_pack.o4fkho/_new 2026-06-22 17:26:23.231366437 +0200
@@ -1,7 +1,7 @@
<servicedata>
<service name="tar_scm">
<param
name="url">https://github.com/openSUSE/openSUSE-release-tools.git</param>
- <param
name="changesrevision">ebc6e9d15ec31f9fe6b2a7ffdca556e54e043010</param>
+ <param
name="changesrevision">e4dd77acfb891ccbfe04e75faab83e93917e4b02</param>
</service>
</servicedata>
++++++ openSUSE-release-tools-20260529.ebc6e9d1.obscpio ->
openSUSE-release-tools-20260612.e4dd77ac.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20260529.ebc6e9d1/dashboard/ftbfs.py
new/openSUSE-release-tools-20260612.e4dd77ac/dashboard/ftbfs.py
--- old/openSUSE-release-tools-20260529.ebc6e9d1/dashboard/ftbfs.py
1970-01-01 01:00:00.000000000 +0100
+++ new/openSUSE-release-tools-20260612.e4dd77ac/dashboard/ftbfs.py
2026-06-12 16:56:44.000000000 +0200
@@ -0,0 +1,390 @@
+#!/usr/bin/env python3
+"""
+Generates an HTML report of packages failing to build in
openSUSE:Factory:Rebuild.
+For each failing package it shows:
+ - Last build date in openSUSE:Factory:Rebuild
+ - Whether the package builds in openSUSE:Factory
+ - The devel project
+ - Open Requests targeting the devel project
+"""
+
+import sys
+import os
+import json
+import argparse
+import logging
+from datetime import datetime, timezone
+from concurrent.futures import ThreadPoolExecutor
+from xml.etree import ElementTree as ET
+
+# osc imports
+import osc.conf
+import osc.core
+
+from jinja2 import Environment, FileSystemLoader
+
+log = logging.getLogger(__name__)
+
+# --------------------------------------------------------------------------- #
+# Constants
+# --------------------------------------------------------------------------- #
+APIURL = "https://api.opensuse.org"
+REBUILD_PROJECT = "openSUSE:Factory:Rebuild"
+FACTORY_PROJECT = "openSUSE:Factory"
+# Repositories / arches to consider for build results
+REPO = "standard"
+ARCH = "x86_64"
+# Cache file for devel projects data
+DEVEL_CACHE_FILE = "devel_cache.json"
+# Minimum request ID to consider, this should be bumped periodically because
+# we only want to check recent submissions.
+MIN_REQUEST_ID = 1235000
+
+# --------------------------------------------------------------------------- #
+# OBS helpers
+# --------------------------------------------------------------------------- #
+
+
+def obs_get(path: str, query: dict | None = None) -> ET.Element:
+ """Perform a raw GET against the OBS API and return parsed XML."""
+ url = osc.core.makeurl(APIURL, path.split("/"), query=query or {})
+ log.debug(f"obs_get: {url}")
+ try:
+ f = osc.core.http_GET(url)
+ return ET.parse(f).getroot()
+ except Exception as e:
+ log.error(f"obs_get failed for {url}: {type(e).__name__}: {e}")
+ raise
+
+
+def rebuild_status(project: str = REBUILD_PROJECT) -> int:
+ """
+ Check how far along the rebuild project is.
+
+ Returns the percentage (0-100) of packages that have reached a final state.
+ 100 means the rebuild is fully complete.
+ """
+ root = obs_get(
+ f"build/{project}/_result",
+ {"view": "status", "arch": ARCH, "multibuild": "1"},
+ )
+ total = 0
+ pending = 0
+ for result_el in root.findall("result"):
+ for status_el in result_el.findall("status"):
+ code = status_el.get("code", "")
+ if code in ("excluded"):
+ continue
+ total += 1
+ code = status_el.get("code", "")
+ if code in ("blocked", "building", "scheduled", "dispatching",
"signing"):
+ pending += 1
+ return ((total - pending) * 100 // total) if total else 100
+
+
+def get_failed_packages_with_results(project: str = REBUILD_PROJECT) ->
tuple[list[str], dict[str, list[dict]]]:
+ """
+ Return (sorted_fail_list, {package: [results]}) from a single API call.
+
+ Each result dict: {code, detail}
+ """
+ failed: set[str] = set()
+ results_map: dict[str, list[dict]] = {}
+ root = obs_get(
+ f"build/{project}/_result",
+ {"view": "status", "arch": ARCH, "multibuild": "1"},
+ )
+
+ for result_el in root.findall("result"):
+ for status_el in result_el.findall("status"):
+ pkg = status_el.get("package", "")
+ code = status_el.get("code", "")
+ detail_el = status_el.find("details")
+ detail = detail_el.text if detail_el is not None else ""
+ results_map.setdefault(pkg, []).append({"code": code, "detail":
detail})
+ if code in ("failed", "unresolvable", "broken"):
+ failed.add(pkg)
+
+ return sorted(failed), results_map
+
+
+def get_all_devel_projects(project: str = FACTORY_PROJECT) -> dict[str,
tuple[str, str]]:
+ """
+ Fetch all packages and their devel info from a project in a single API
call.
+ Returns dict of package_name -> (devel_project, devel_package).
+ Uses a local cache file if available from the same day.
+ """
+ cache_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
DEVEL_CACHE_FILE)
+
+ if os.path.isfile(cache_path):
+ try:
+ # Check if file is from previous calendar day
+ mtime = os.path.getmtime(cache_path)
+ if datetime.fromtimestamp(mtime, tz=timezone.utc).date() ==
datetime.now(tz=timezone.utc).date():
+ log.info(f"Loading devel projects from cache file:
{cache_path}")
+ with open(cache_path, "r", encoding="utf-8") as f:
+ data = json.load(f)
+ return {k: tuple(v) for k, v in data.items()}
+ else:
+ log.info(f"Cache file {cache_path} is outdated (from previous
day), recreating…")
+ except Exception as e:
+ log.warning(f"Failed to load cache file: {e}, fetching from API
instead")
+
+ log.info(f"Fetching devel projects for all packages in {project} …")
+ try:
+ root = obs_get("search/package", {"match": f'@project="{project}"'})
+ except Exception as e:
+ log.error(f"get_all_devel_projects failed: {type(e).__name__}: {e}")
+ return {}
+
+ result: dict[str, tuple[str, str]] = {}
+ for pkg_el in root.findall("package"):
+ pkg_name = pkg_el.get("name", "")
+ devel = pkg_el.find("devel")
+ if devel is not None:
+ result[pkg_name] = (devel.get("project", ""), devel.get("package",
pkg_name))
+ else:
+ result[pkg_name] = ("", "")
+
+ log.info(f"Got devel info for {len(result)} packages.")
+
+ # Save to cache file
+ try:
+ with open(cache_path, "w", encoding="utf-8") as f:
+ json.dump(result, f, indent=2)
+ log.info(f"Saved devel projects to cache file: {cache_path}")
+ except Exception as e:
+ log.warning(f"Failed to save cache file: {e}")
+
+ return result
+
+
+def fetch_open_requests() -> dict:
+ """
+ Fetch all open/under-review submit requests from OBS and return them
+ indexed by target_project -> package -> list of request dicts.
+
+ Notes:
+ - XPath string literals MUST use single quotes (encoded as %27 by
makeurl).
+ - Double quotes get encoded as %22 and OBS drops them → HTTP 400.
+ """
+ match_expr = "(state/@name='new' or state/@name='review')"
+
+ log.info("Fetching open requests")
+ try:
+ root = obs_get("search/request", {"match": match_expr})
+ except Exception as e:
+ log.error(f"fetch_open_requests failed: {type(e).__name__}: {e}")
+ return {}
+
+ if root.tag == "request":
+ request_els = [root]
+ else:
+ request_els = root.findall("request")
+
+ index: dict[str, dict[str, list[dict]]] = {}
+
+ SKIP_ACTION_TYPES = {"change_devel", "maintenance_incident",
"maintenance_release", "add_role", "set_bugowner"}
+
+ for req_el in request_els:
+ rid = req_el.get("id", "")
+ if int(rid) < MIN_REQUEST_ID:
+ continue
+
+ state_el = req_el.find("state")
+ if state_el is None:
+ log.warning(f"request id={rid!r} has no <state> — skipping")
+ continue
+ state_name = state_el.get("name", "")
+ state_who = state_el.get("who", "")
+
+ for action_el in req_el.findall("action"):
+ action_type = action_el.get("type", "")
+ if action_type in SKIP_ACTION_TYPES:
+ continue
+
+ target_el = action_el.find("target")
+ if target_el is None:
+ log.warning(f"request id={rid!r} has an <action> with no
<target> — skipping")
+ continue
+
+ tgt_prj = target_el.get("project", "")
+ tgt_pkg = target_el.get("package", "")
+ if not tgt_pkg:
+ continue
+
+ record = {
+ "id": rid,
+ "type": action_type,
+ "state": state_name,
+ "creator": state_who,
+ "target_project": tgt_prj,
+ "target_package": tgt_pkg,
+ }
+ index.setdefault(tgt_prj, {}).setdefault(tgt_pkg,
[]).append(record)
+
+ log.info(f"Read {len(index)} projects, with {sum(len(pkgs) for pkgs in
index.values())} total requests")
+
+ return index
+
+
+def fetch_package_history(pkg: str) -> tuple[str, datetime | None]:
+ """Helper to fetch history for a single package."""
+ try:
+ root = obs_get(f"build/{REBUILD_PROJECT}/{REPO}/{ARCH}/{pkg}/_history")
+ entries = root.findall("entry")
+ if entries:
+ t = entries[-1].get("time")
+ if t:
+ return pkg, datetime.fromtimestamp(int(t), tz=timezone.utc)
+ except Exception:
+ pass
+ return pkg, None
+
+
+def collect_data(limit: int | None = None) -> list[dict]:
+ """Collect all required data and return a list of package records."""
+ log.info("Fetching failing packages from %s …", REBUILD_PROJECT)
+ packages, rebuild_results_map =
get_failed_packages_with_results(REBUILD_PROJECT)
+ _, rebuild_results_map_f =
get_failed_packages_with_results(FACTORY_PROJECT)
+
+ if limit:
+ packages = packages[:limit]
+ log.info("Found %d failing packages.", len(packages))
+
+ sr_cache = fetch_open_requests()
+ devel_cache = get_all_devel_projects(FACTORY_PROJECT)
+
+ log.info("Fetching build histories in parallel...")
+ with ThreadPoolExecutor() as executor:
+ history_map = dict(executor.map(fetch_package_history, packages))
+
+ records = []
+ for i, pkg in enumerate(packages, 1):
+ log.info("[%d/%d] Processing %s …", i, len(packages), pkg)
+
+ rebuild_results = rebuild_results_map.get(pkg, [])
+ factory_results = rebuild_results_map_f.get(pkg, [])
+ factory_result = factory_results[0] if factory_results else {"code":
"unknown", "detail": ""}
+
+ last_build = history_map.get(pkg)
+
+ base_name = pkg.split(":", 1)[0]
+ devel_project, devel_package = devel_cache.get(base_name, ("", ""))
+
+ records.append(
+ {
+ "package": pkg,
+ "last_build": last_build,
+ "last_build_ts": int(last_build.timestamp()) if last_build
else None,
+ "build_results": rebuild_results,
+ "factory_status": factory_result["code"],
+ "factory_detail": factory_result["detail"],
+ "devel_project": devel_project,
+ "devel_package": devel_package,
+ "open_requests": [],
+ }
+ )
+
+ # Match open requests to packages using a lookup dict.
+ # Key by base name (multibuild suffix stripped) so that all multibuild
+ # variants (e.g. foo:a and foo:b) receive the same open requests.
+ rec_index: dict[str, list[dict]] = {}
+ for rec in records:
+ rec_index.setdefault(rec["package"].split(":", 1)[0], []).append(rec)
+
+ for project, pkgs in sr_cache.items():
+ for pkg_name, srs in pkgs.items():
+ for rec in rec_index.get(pkg_name, []):
+ rec["open_requests"].extend(srs)
+
+ return records
+
+
+def render_html(records: list[dict], output_path: str, rebuild_pct: int) ->
None:
+
+ # Get the path from where the script is run and look for templates in the
same directory
+ script_dir = os.path.dirname(os.path.abspath(__file__))
+ template_dir = os.path.join(script_dir, 'templates')
+ env = Environment(loader=FileSystemLoader(template_dir), autoescape=True)
+ template = env.get_template("ftbfs.html")
+ now = datetime.now(tz=timezone.utc)
+ html = template.render(
+ packages=records,
+ rebuild_project=REBUILD_PROJECT,
+ factory_project=FACTORY_PROJECT,
+ generated_at_utc=now.strftime("%Y-%m-%d %H:%M UTC"),
+ generated_at_iso=now.isoformat(),
+ rebuild_pct=rebuild_pct,
+ )
+ with open(output_path, "w", encoding="utf-8") as f:
+ f.write(html)
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Generate FTBFS HTML report for openSUSE:Factory:Rebuild"
+ )
+ parser.add_argument(
+ "-o", "--output",
+ default="ftbfs.html",
+ help="Output HTML file path (default: ftbfs.html)",
+ )
+ parser.add_argument(
+ "-n", "--limit",
+ type=int,
+ default=None,
+ help="Limit the number of packages processed (for testing)",
+ )
+ parser.add_argument(
+ "-v", "--verbose",
+ action="store_true",
+ help="Enable verbose logging",
+ )
+ parser.add_argument(
+ "-i", "--incomplete",
+ nargs="?",
+ type=int,
+ const=0,
+ help="Generate report even if the rebuild is still in progress.
Optionally specify minimum completion percentage (0-100).",
+ )
+ args = parser.parse_args()
+
+ logging.basicConfig(
+ level=logging.DEBUG if args.verbose else logging.INFO,
+ format="%(asctime)s %(levelname)s %(message)s",
+ datefmt="%H:%M:%S",
+ )
+
+ # Load osc configuration (reads ~/.config/osc/oscrc)
+ try:
+ osc.conf.get_config()
+ except osc.conf.ConfigError as e:
+ log.error("Could not load osc configuration: %s", e)
+ sys.exit(1)
+
+ rebuild_pct = rebuild_status()
+ log.info("Rebuild completion: %d%%", rebuild_pct)
+ if args.incomplete is None:
+ if rebuild_pct < 100:
+ log.error(
+ "%s still has pending builds – %.1f%% complete. "
+ "Use --incomplete to generate the report anyway.",
+ REBUILD_PROJECT, rebuild_pct,
+ )
+ sys.exit(1)
+ elif rebuild_pct < args.incomplete:
+ log.error(
+ "%s still has pending builds – %.1f%% complete. "
+ "The report will only be generated if completion is >= %d%%.",
+ REBUILD_PROJECT, rebuild_pct, args.incomplete,
+ )
+ sys.exit(1)
+
+ records = collect_data(limit=args.limit)
+ render_html(records, args.output, rebuild_pct)
+ print(f"\nReport saved to: {args.output} with a total of {len(records)}
packages listed")
+
+
+if __name__ == "__main__":
+ main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20260529.ebc6e9d1/dashboard/templates/ftbfs.html
new/openSUSE-release-tools-20260612.e4dd77ac/dashboard/templates/ftbfs.html
--- old/openSUSE-release-tools-20260529.ebc6e9d1/dashboard/templates/ftbfs.html
1970-01-01 01:00:00.000000000 +0100
+++ new/openSUSE-release-tools-20260612.e4dd77ac/dashboard/templates/ftbfs.html
2026-06-12 16:56:44.000000000 +0200
@@ -0,0 +1,298 @@
+<!DOCTYPE html>
+<html lang="en" data-bs-theme="light">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>openSUSE:Factory FTBFS Report</title>
+<link
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
rel="stylesheet">
+<link
href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css"
rel="stylesheet">
+<style>
+ .stat-card {
+ transition: transform 0.15s ease;
+ }
+ .stat-card:hover {
+ transform: translateY(-2px);
+ }
+ thead th {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ background-color: var(--bs-body-bg);
+ }
+</style>
+</head>
+<body>
+
+<!-- Header -->
+<nav class="navbar navbar-expand-lg border-bottom sticky-top bg-body">
+ <div class="container-fluid">
+ <div class="d-flex flex-column">
+ <h1 class="h5 mb-0 fw-bold">FTFBS report at {{ rebuild_pct }}% build</h1>
+
+ <p class="mb-0 text-body-secondary small">Packages failing to build in
<code>{{ rebuild_project }}</code></p>
+ </div>
+ <div class="d-flex align-items-center gap-3">
+ <div class="text-end d-none d-sm-block">
+ <span class="small text-body-secondary">Generated </span><span
id="generatedAt" datetime="{{ generated_at_iso }}" title="{{ generated_at_utc
}}"></span>
+ </div>
+ <button class="btn btn-outline-secondary btn-sm" id="themeToggle"
title="Toggle theme">
+ <i class="bi bi-moon-fill" id="themeIcon"></i>
+ </button>
+
+ </div>
+ </div>
+</nav>
+
+<!-- Stats bar -->
+{% set factory_ok = packages | selectattr('factory_status', 'eq',
'succeeded') | list | length %}
+{% set factory_fail = packages | selectattr('factory_status', 'in',
['failed','broken','unresolvable']) | list | length %}
+{% set with_sr = packages | selectattr('open_requests') | list | length %}
+{% set no_devel = packages | rejectattr('devel_project') | list | length %}
+
+{% set stats = [
+ (packages|length, "failing in Rebuild"),
+ (factory_ok, "OK in Factory"),
+ (factory_fail, "failing in Factory too"),
+ (with_sr, "have open SRs"),
+ (no_devel, "without devel project")
+] %}
+
+<div class="container-fluid py-3">
+ <div class="row g-2">
+ {% for val, label in stats %}
+ <div class="col-auto">
+ <div class="stat-card card border-info border-opacity-50 bg-body">
+ <div class="card-body py-2 px-3 d-flex align-items-center gap-2">
+ <span class="badge bg-info rounded-circle"
style="width:8px;height:8px;padding:0"></span>
+ <span class="small">{{ val }} {{ label }}</span>
+ </div>
+ </div>
+ </div>
+ {% endfor %}
+ </div>
+</div>
+
+<!-- Controls -->
+<div class="container-fluid pb-3">
+ <div class="row g-2 align-items-center">
+ <div class="col-md-4">
+ <div class="input-group">
+ <span class="input-group-text"><i class="bi bi-search"></i></span>
+ <input type="text" class="form-control" id="searchBox"
placeholder="Filter packages…" oninput="applySearch()">
+ </div>
+ </div>
+ <div class="col-auto ms-auto">
+ <div class="btn-group" role="group">
+ <button class="btn btn-outline-secondary btn-sm"
onclick="applySort('name')">Name</button>
+ <button class="btn btn-outline-secondary btn-sm"
onclick="applySort('date_desc')">Newest</button>
+ <button class="btn btn-outline-secondary btn-sm"
onclick="applySort('date_asc')">Oldest</button>
+ <button class="btn btn-outline-secondary btn-sm"
onclick="applySort('factory')">Factory</button>
+ <button class="btn btn-outline-secondary btn-sm"
onclick="applySort('devel')">Devel</button>
+ <button class="btn btn-outline-secondary btn-sm"
onclick="applySort('sr')">SRs</button>
+ </div>
+ </div>
+ </div>
+</div>
+
+<!-- Table -->
+<div class="container-fluid pb-4">
+ <div class="table-responsive">
+
+ <table class="table table-striped table-hover table-bordered align-middle
mb-0" id="pkgTable">
+ <thead class="table-light">
+ <tr>
+ <th>Package name</th>
+ <th>Last successful build in oS:F:Rebuild</th>
+ <th>Build Status oS:F:Rebuild</th>
+ <th>Build Status oS:Factory</th>
+ <th>Devel Project</th>
+ <th>Open Requests</th>
+ </tr>
+ </thead>
+ <tbody id="tableBody">
+ {% for pkg in packages %}
+ <tr
+ data-pkg="{{ pkg.package }}"
+ data-factory="{{ pkg.factory_status }}"
+ data-devel="{{ pkg.devel_project }}"
+ data-sr="{{ pkg.open_requests | length }}"
+ data-date="{{ pkg.last_build.timestamp() if pkg.last_build else 0 }}"
+ >
+ <!-- Package -->
+ <td class="font-monospace fw-semibold">
+ <a href="https://build.opensuse.org/package/show/{{ rebuild_project
}}/{{ pkg.package }}"
+ target="_blank" rel="noopener" class="text-decoration-none">{{
pkg.package }}</a>
+ </td>
+
+
+ <!-- Last build date -->
+ <td class="text-nowrap small text-body-secondary relative-time">
+ {% if pkg.last_build_ts %}
+ <span class="relative-time-label" datetime="{{
pkg.last_build.isoformat() }}" title="{{ pkg.last_build.strftime('%Y-%m-%d
%H:%M') }} UTC"></span>
+ {% else %}
+ <span class="text-body-secondary">—</span>
+ {% endif %}
+ </td>
+
+
+ <!-- Build status in Rebuild -->
+ <td>
+ {% for res in pkg.build_results %}
+ {% if res.code == 'failed' or res.code == 'broken' %}
+ {% set badge_cls = 'bg-danger' %}
+ {% elif res.code == 'succeeded' %}
+ {% set badge_cls = 'bg-success' %}
+ {% elif res.code == 'building' %}
+ {% set badge_cls = 'bg-primary' %}
+ {% elif res.code == 'unresolvable' %}
+ {% set badge_cls = 'bg-warning text-dark' %}
+ {% elif res.code == 'disabled' %}
+ {% set badge_cls = 'bg-secondary opacity-75' %}
+ {% else %}
+ {% set badge_cls = 'bg-secondary' %}
+ {% endif %}
+ <a href="https://build.opensuse.org/package/show/{{
rebuild_project }}/{{ pkg.package }}" target="_blank" rel="noopener"
class="text-decoration-none">
+ <span class="badge {{ badge_cls }}">{{ res.code }}</span>
+ </a>
+ {% if res.detail %}
+ <br/><small class="text-body-secondary text-truncate
d-inline-block" style="max-width:360px" data-bs-toggle="tooltip" title="{{
res.detail }}">{{ res.detail }}</small>
+ {% endif %}
+ {% endfor %}
+ </td>
+
+ <!-- Factory status -->
+ <td>
+ {% set fs = pkg.factory_status %}
+ {% if fs == 'failed' or fs == 'broken' %}
+ {% set fbadge = 'bg-danger' %}
+ {% elif fs == 'succeeded' %}
+ {% set fbadge = 'bg-success' %}
+ {% elif fs == 'building' %}
+ {% set fbadge = 'bg-primary' %}
+ {% elif fs == 'unresolvable' %}
+ {% set fbadge = 'bg-warning text-dark' %}
+ {% elif fs == 'disabled' %}
+ {% set fbadge = 'bg-secondary opacity-75' %}
+ {% else %}
+ {% set fbadge = 'bg-secondary' %}
+ {% endif %}
+ <a href="https://build.opensuse.org/package/show/{{ factory_project
}}/{{ pkg.package }}" target="_blank" rel="noopener"
class="text-decoration-none">
+ <span class="badge {{ fbadge }}">{{ fs }}</span>
+ </a>
+ {% if pkg.factory_detail %}
+ <br/><small class="text-body-secondary text-truncate
d-inline-block" style="max-width:360px" data-bs-toggle="tooltip" title="{{
pkg.factory_detail }}">{{ pkg.factory_detail }}</small>
+ {% endif %}
+ </td>
+
+ <!-- Devel project -->
+ <td class="text-success">
+ {% if pkg.devel_project %}
+ <a href="https://build.opensuse.org/project/show/{{
pkg.devel_project }}"
+ target="_blank" rel="noopener" class="text-success
text-decoration-none">{{ pkg.devel_project }}</a>
+ <br/><small><a href="https://build.opensuse.org/package/show/{{
pkg.devel_project }}/{{ pkg.devel_package }}"
+ target="_blank" rel="noopener" class="text-success
text-decoration-none">{{ pkg.devel_package }}</a></small>
+ {% else %}
+ <span class="text-body-secondary">—</span>
+ {% endif %}
+ </td>
+
+
+ <!-- Open Submit Requests -->
+ <td>
+ {% if pkg.open_requests %}
+ <ul class="list-unstyled mb-0">
+ {% for sr in pkg.open_requests %}
+ <li class="d-flex align-items-center gap-2 mb-1"
style="flex-wrap: wrap;">
+ <span class="fw-semibold">
+ <a href="https://build.opensuse.org/request/show/{{ sr.id }}"
+ target="_blank" rel="noopener"
class="text-decoration-none">#{{ sr.id }}</a>
+ </span>
+ <span class="badge {% if sr.type == 'delete' %}bg-danger{%
elif sr.type == 'submit' %}bg-success{% else %}bg-secondary{% endif %}">{{
sr.type }}</span>
+ <span class="small text-body-secondary">→ {{ sr.target_project
}}</span>
+ </li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <span class="text-body-secondary">—</span>
+ {% endif %}
+ </td>
+
+ </tr>
+ {% else %}
+ <tr class="empty-row">
+ <td colspan="6" class="text-center py-5 text-body-secondary">
+ <i class="bi bi-check-circle display-4 d-block mb-2"></i>
+ No failing packages found — great news!
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+</div>
+
+<script
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
+<script
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/timeago.min.js"></script>
+<script>
+ // ── Theme toggle ──────────────────────────────────────────────────
+ const html = document.documentElement;
+ const toggle = document.getElementById('themeToggle');
+ const icon = document.getElementById('themeIcon');
+ function setTheme(theme) {
+ html.setAttribute('data-bs-theme', theme);
+ icon.className = theme === 'dark' ? 'bi bi-sun-fill' : 'bi bi-moon-fill';
+ localStorage.setItem('theme', theme);
+ }
+ const saved = localStorage.getItem('theme');
+ if (saved) setTheme(saved);
+ toggle.addEventListener('click', () => {
+ setTheme(html.getAttribute('data-bs-theme') === 'dark' ? 'light' : 'dark');
+ });
+
+ // ── Search & sort ────────────────────────────────────────────────
+
+ function applySearch() {
+ const q = document.getElementById('searchBox').value.toLowerCase();
+ document.querySelectorAll('#tableBody tr[data-pkg]').forEach(row => {
+ const pkg = row.dataset.pkg.toLowerCase();
+ row.classList.toggle('d-none', !pkg.includes(q));
+
+ });
+ }
+
+ function applySort(val) {
+ const tbody = document.getElementById('tableBody');
+ const rows = Array.from(tbody.querySelectorAll('tr[data-pkg]'));
+
+ document.querySelectorAll('.btn-group .btn').forEach(btn => {
+ btn.classList.toggle('btn-primary',
btn.getAttribute('onclick').includes(`'${val}'`));
+ btn.classList.toggle('btn-outline-secondary',
!btn.getAttribute('onclick').includes(`'${val}'`));
+ });
+
+ rows.sort((a, b) => {
+ if (val === 'name') return
a.dataset.pkg.localeCompare(b.dataset.pkg);
+ if (val === 'date_desc') return parseFloat(b.dataset.date) -
parseFloat(a.dataset.date);
+ if (val === 'date_asc') return parseFloat(a.dataset.date) -
parseFloat(b.dataset.date);
+ if (val === 'factory') return
a.dataset.factory.localeCompare(b.dataset.factory);
+ if (val === 'devel') return
a.dataset.devel.localeCompare(b.dataset.devel);
+ if (val === 'sr') return parseInt(b.dataset.sr || '0') -
parseInt(a.dataset.sr || '0');
+ return 0;
+ });
+
+ rows.forEach(r => tbody.appendChild(r));
+ }
+
+ // ── Relative time formatting ─────────────────────────────────────
+
+ function updateRelativeTimes() {
+ timeago.render(document.querySelectorAll('.relative-time-label,
#generatedAt'));
+ }
+
+ updateRelativeTimes();
+ applySort('date_asc');
+ document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => new
bootstrap.Tooltip(el));
+ setInterval(updateRelativeTimes, 60000);
+
+</script>
+</body>
+</html>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20260529.ebc6e9d1/docker_publisher.py
new/openSUSE-release-tools-20260612.e4dd77ac/docker_publisher.py
--- old/openSUSE-release-tools-20260529.ebc6e9d1/docker_publisher.py
2026-05-29 14:28:31.000000000 +0200
+++ new/openSUSE-release-tools-20260612.e4dd77ac/docker_publisher.py
2026-06-12 16:56:44.000000000 +0200
@@ -403,11 +403,11 @@
'fetchers': {
'x86_64':
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Leap:16.0/containers/x86_64/opensuse-leap-image:docker",
maintenance_release=True), # noqa: E501
'aarch64':
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Leap:16.0/containers/aarch64/opensuse-leap-image:docker",
maintenance_release=True), # noqa: E501
- 'armv7l': None,
+ 'armv7l': None, # Let's finally drop it from
opensuse/leap:latest
'ppc64le':
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Leap:16.0/containers/ppc64le/opensuse-leap-image:docker",
maintenance_release=True), # noqa: E501
's390x':
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Leap:16.0/containers/s390x/opensuse-leap-image:docker",
maintenance_release=True), # noqa: E501
},
- 'publisher': DockerImagePublisherRegistry(drc_leap, "16.0"),
+ 'publisher': DockerImagePublisherRegistry(drc_leap, "latest",
["16", "16.0"]),
},
# Like Leap 15.6, but using the 15.5 image for armv7l
'leap-15': {
@@ -418,7 +418,7 @@
'ppc64le':
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Leap:15.6/containers/ppc64le/opensuse-leap-image:docker",
maintenance_release=True), # noqa: E501
's390x':
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Leap:15.6/containers/s390x/opensuse-leap-image:docker",
maintenance_release=True), # noqa: E501
},
- 'publisher': DockerImagePublisherRegistry(drc_leap, "latest",
["15"]),
+ 'publisher': DockerImagePublisherRegistry(drc_leap, "15"),
},
}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20260529.ebc6e9d1/git-openqa-maintenance.py
new/openSUSE-release-tools-20260612.e4dd77ac/git-openqa-maintenance.py
--- old/openSUSE-release-tools-20260529.ebc6e9d1/git-openqa-maintenance.py
2026-05-29 14:28:31.000000000 +0200
+++ new/openSUSE-release-tools-20260612.e4dd77ac/git-openqa-maintenance.py
2026-06-12 16:56:44.000000000 +0200
@@ -11,9 +11,9 @@
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
+USER_AGENT = "git-openqa-maintenance
(https://github.com/openSUSE/openSUSE-release-tools)"
log = logging.getLogger(sys.argv[0] if __name__ == "__main__" else __name__)
log.setLevel(logging.DEBUG)
@@ -86,13 +86,28 @@
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}"
+ limit = 50
+ page = 1
+ pull_requests = []
- try:
- pull_requests = request_get(pull_requests_url)
- except requests.exceptions.HTTPError as e:
- log.error(f"Project '{project}' doesn't exist: {e}")
- return []
+ while True:
+ pull_requests_url = GITEA_HOST +
f"/api/v1/repos/{project}/pulls?state=open&base_branch={branch}&limit={limit}&page={page}"
+
+ try:
+ request = request_get(pull_requests_url)
+ except requests.exceptions.HTTPError as e:
+ log.error(f"Project '{project}' doesn't exist: {e}")
+ return []
+
+ if not request:
+ break
+
+ pull_requests.extend(request)
+
+ if len(request) < limit:
+ break
+
+ page += 1
if not pull_requests:
log.warning(f"No pull requests found for '{project}' on'{branch}'")
@@ -443,15 +458,24 @@
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)
+ limit = 50
+ page = 1
+ timeline = []
+
+ while True:
+ url = GITEA_HOST +
f"/api/v1/repos/{project}/issues/{pr_id}/timeline?limit={limit}&page={page}"
+ request = request_get(url)
+
+ if not request:
+ break
+
+ timeline.extend(request)
+
+ if len(request) < limit:
+ break
- # if request.status_code == 404:
- # self.logger.error(f"'{self}' does not have a timeline")
- # # this should throw an exception
- # return
+ page += 1
- timeline = request
timeline.reverse()
events = {}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20260529.ebc6e9d1/gocd/dashboard.ftbfs.gocd.yaml
new/openSUSE-release-tools-20260612.e4dd77ac/gocd/dashboard.ftbfs.gocd.yaml
--- old/openSUSE-release-tools-20260529.ebc6e9d1/gocd/dashboard.ftbfs.gocd.yaml
1970-01-01 01:00:00.000000000 +0100
+++ new/openSUSE-release-tools-20260612.e4dd77ac/gocd/dashboard.ftbfs.gocd.yaml
2026-06-12 16:56:44.000000000 +0200
@@ -0,0 +1,24 @@
+format_version: 3
+pipelines:
+ Dashboard.ftbfs:
+ group: openSUSE.Checkers
+ lock_behavior: unlockWhenFinished
+ environment_variables:
+ OSC_CONFIG: /home/go/config/oscrc-totest-manager
+ RSYNC_PASSWORD: '{{SECRET:[opensuse.secrets][RSYNC_FOR_COOLO]}}'
+ materials:
+ script:
+ git: https://github.com/openSUSE/openSUSE-release-tools.git
+ timer:
+ spec: 0 */30 * ? * *
+ only_on_changes: false
+ stages:
+ - Run:
+ approval: manual
+ resources:
+ - staging-bot
+ tasks:
+ - script: |-
+ set -e
+ PYTHONPATH=$PWD python3 ./dashboard/ftbfs.py --incomplete 60 -o
dashboard/output/ftbfs.html
+ rsync -av dashboard/output/
rsync://[email protected]:11873/factory-dashboard.opensuse.org/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20260529.ebc6e9d1/gocd/totestmanager.gocd.yaml
new/openSUSE-release-tools-20260612.e4dd77ac/gocd/totestmanager.gocd.yaml
--- old/openSUSE-release-tools-20260529.ebc6e9d1/gocd/totestmanager.gocd.yaml
2026-05-29 14:28:31.000000000 +0200
+++ new/openSUSE-release-tools-20260612.e4dd77ac/gocd/totestmanager.gocd.yaml
2026-06-12 16:56:44.000000000 +0200
@@ -189,6 +189,27 @@
- script: |-
install -D /home/go/config/openqa-client.conf
/home/go/.config/openqa/client.conf
scripts/totest-manager.py -A https://api.opensuse.org --debug run
openSUSE:Leap:16.0:Products
+ TTM.Leap_16.0_Images:
+ group: openSUSE.Checkers
+ lock_behavior: unlockWhenFinished
+ environment_variables:
+ OSC_CONFIG: /home/go/config/oscrc-totest-manager
+ materials:
+ script:
+ git: https://github.com/openSUSE/openSUSE-release-tools.git
+ destination: scripts
+ timer:
+ spec: 0 */15 * ? * *
+ only_on_changes: false
+ stages:
+ - Run:
+ approval: manual
+ resources:
+ - staging-bot
+ tasks:
+ - script: |-
+ install -D /home/go/config/openqa-client.conf
/home/go/.config/openqa/client.conf
+ scripts/totest-manager.py -A https://api.opensuse.org --debug run
openSUSE:Leap:16.0:Images
TTM.Leap_16.1_Products:
group: openSUSE.Checkers
lock_behavior: unlockWhenFinished
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/openSUSE-release-tools-20260529.ebc6e9d1/gocd/totestmanager.gocd.yaml.erb
new/openSUSE-release-tools-20260612.e4dd77ac/gocd/totestmanager.gocd.yaml.erb
---
old/openSUSE-release-tools-20260529.ebc6e9d1/gocd/totestmanager.gocd.yaml.erb
2026-05-29 14:28:31.000000000 +0200
+++
new/openSUSE-release-tools-20260612.e4dd77ac/gocd/totestmanager.gocd.yaml.erb
2026-06-12 16:56:44.000000000 +0200
@@ -10,6 +10,7 @@
openSUSE:Leap:15.6:Images
openSUSE:Leap:15.6:ARM:Images
openSUSE:Leap:16.0:Products
+ openSUSE:Leap:16.0:Images
openSUSE:Leap:16.1:Products
openSUSE:Leap:Micro:6.0
openSUSE:Leap:Micro:6.0:Images
++++++ openSUSE-release-tools.obsinfo ++++++
--- /var/tmp/diff_new_pack.o4fkho/_old 2026-06-22 17:26:24.563412839 +0200
+++ /var/tmp/diff_new_pack.o4fkho/_new 2026-06-22 17:26:24.571413117 +0200
@@ -1,5 +1,5 @@
name: openSUSE-release-tools
-version: 20260529.ebc6e9d1
-mtime: 1780057711
-commit: ebc6e9d15ec31f9fe6b2a7ffdca556e54e043010
+version: 20260612.e4dd77ac
+mtime: 1781276204
+commit: e4dd77acfb891ccbfe04e75faab83e93917e4b02