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
 

Reply via email to