Merge pull request 'Better use of Jenkins API in frontend' (#39) from feature/main-38-JenkinsAPIUsage into main

Reviewed-on: #39
This commit was merged in pull request #39.
This commit is contained in:
2026-02-22 13:20:08 +01:00
4 changed files with 66 additions and 9 deletions

View File

@@ -15,6 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import base64
import html
from typing import Dict, List
import requests
@@ -26,11 +27,9 @@ def _sort_builds(builds: List[Dict]) -> List[Dict]:
return sorted(builds, key=lambda build: build.get("number", 0), reverse=True)
def normalize_build(build: Dict) -> Dict:
changes = build.get("changeSets", [])
def _extract_commits(build: Dict) -> List[Dict]:
commits = []
for cs in changes:
for cs in build.get("changeSets", []):
for item in cs.get("items", []):
commits.append(
{
@@ -39,14 +38,33 @@ def normalize_build(build: Dict) -> Dict:
"author": item.get("author", {}).get("fullName", "unknown"),
}
)
return commits
def _extract_trigger(build: Dict) -> str:
for action in build.get("actions", []):
for cause in action.get("causes", []):
description = cause.get("shortDescription")
if description:
return html.unescape(description).strip()
return ""
def normalize_build(build: Dict) -> Dict:
commits = _extract_commits(build)
trigger = _extract_trigger(build)
status = (build.get("result") or "RUNNING").lower()
if build.get("building"):
status = "running"
return {
"number": build.get("number"),
"status": (build.get("result") or "RUNNING").lower(),
"status": status,
"finished_at": build.get("timestamp"),
"duration_seconds": build.get("duration", 0) // 1000,
"url": build.get("url"),
"commits": commits,
"trigger": trigger,
}
@@ -63,15 +81,16 @@ def fetch_builds(limit: int = 5) -> List[Dict]:
raise ValueError("JENKINS_JOB_NAME not configured")
url = (
f"{settings.jenkins_base_url}/job/{settings.jenkins_job_name}/api/json"
"?tree=builds[number,url,result,timestamp,duration,"
"changesets[items[commitId,msg,author[fullName]]]]"
"?tree=builds[number,url,result,timestamp,duration,building,"
"changeSets[items[commitId,msg,author[fullName]]],"
"actions[causes[shortDescription]]]"
)
resp = requests.get(url, headers=_auth_header(), timeout=5)
resp.raise_for_status()
builds = resp.json().get("builds", [])
return builds[:limit]
return _sort_builds(builds)[:limit]
def build_history() -> Dict:

View File

@@ -61,6 +61,15 @@ def test_build_history(monkeypatch):
"timestamp": 1719992400000,
"duration": 75000,
"url": "http://jenkins.local/job/demo/205",
"actions": [
{
"causes": [
{
"shortDescription": "Triggered by Merge pull request #37",
}
]
}
],
"changeSets": [
{
"items": [
@@ -79,6 +88,7 @@ def test_build_history(monkeypatch):
"timestamp": 1719988800000,
"duration": 1500,
"url": "http://jenkins.local/job/demo/204",
"actions": [],
"changeSets": [],
},
]
@@ -101,12 +111,14 @@ def test_build_history(monkeypatch):
assert first["commits"] == [
{"commit": "9ac3f91", "message": "Anade la API de Jenkins", "author": "Dev One"}
]
assert first["trigger"] == "Triggered by Merge pull request #37"
second = builds[1]
assert second["number"] == 204
assert second["status"] == "running"
assert second["duration_seconds"] == 1
assert second["commits"] == []
assert second["trigger"] == ""
def test_build_history_error_returns_empty(monkeypatch):

View File

@@ -378,7 +378,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<span class="summary-title">#{build.number}</span>
{#if build.commits.length === 0}
<span class="muted">Ejecución manual</span>
<span class="muted">
{build.trigger || 'Ejecución manual'}
</span>
{:else}
<span class="summary-commit">
{build.commits[0].commit} · {build.commits[0].author}
@@ -401,6 +403,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</p>
{/if}
{#if build.trigger}
<p class="history-row">
<span>Disparo</span>
<span>{build.trigger}</span>
</p>
{/if}
<p class="history-row">
<span>Duración</span>
<span>{build.duration_seconds} s</span>
@@ -408,6 +417,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{#if build.status === 'failure'}
<p class="history-message danger">Build fallida</p>
{:else if build.status === 'running'}
<p class="history-message running">En curso</p>
{:else}
<p class="history-message success">Correcto</p>
{/if}

View File

@@ -723,6 +723,11 @@ li {
box-shadow: 0 0 8px rgba(248, 113, 113, 0.5);
}
.status-dot.running {
background: #facc15;
box-shadow: 0 0 8px rgba(250, 204, 21, 0.5);
}
.history-body {
padding: 0.75rem 0.9rem 0.9rem;
border-top: 1px solid rgba(255, 255, 255, 0.05);
@@ -758,6 +763,12 @@ li {
color: #bbf7d0;
}
.history-message.running {
background: rgba(250, 204, 21, 0.16);
border-color: rgba(250, 204, 21, 0.5);
color: #fef9c3;
}
.history-item[open] {
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28);
}
@@ -770,6 +781,10 @@ li {
border-color: rgba(248, 113, 113, 0.35);
}
.history-item.running {
border-color: rgba(250, 204, 21, 0.35);
}
.chip.danger {
background: rgba(248, 113, 113, 0.2);
color: #fecdd3;