Compare commits

..

24 Commits

Author SHA1 Message Date
cf6522452d Update README 2026-03-10 10:54:06 +01:00
d367abe451 Merge pull request 'Show author info' (#40) from feature/main-38-JenkinsAPIUsage into main
Reviewed-on: #40
2026-02-22 13:42:04 +01:00
f5910f763b Merge branch 'main' into feature/main-38-JenkinsAPIUsage
All checks were successful
Tests / Declarative: Post Actions passed: 14
CI-Multi/pipeline/pr-main This commit looks good
2026-02-22 13:40:51 +01:00
f2da39f0db Show author info
All checks were successful
Tests / Declarative: Post Actions passed: 14
CI-Multi/pipeline/pr-main This commit looks good
2026-02-22 13:38:30 +01:00
84f4d2eef8 Merge pull request 'Better use of Jenkins API in frontend' (#39) from feature/main-38-JenkinsAPIUsage into main
Reviewed-on: #39
2026-02-22 13:20:08 +01:00
9297bed50e Update front & back
All checks were successful
Tests / Declarative: Post Actions passed: 14
CI-Multi/pipeline/pr-main This commit looks good
2026-02-22 13:16:54 +01:00
6660035fa3 Merge pull request 'Add Trivy stage to CD' (#37) from feature/main-36-AddTrivyJob into main
Reviewed-on: #37
2026-02-22 12:24:21 +01:00
bd769b0eb0 Format files
All checks were successful
Tests / Declarative: Post Actions passed: 14
CI-Multi/pipeline/pr-main This commit looks good
2026-02-22 12:20:30 +01:00
f9185e0f21 Merge pull request 'Replace openbokeron.org with uma.es' (#35) from bugfix/main-34-UpdateOBURL into main
Reviewed-on: #35
2026-02-16 20:36:28 +01:00
3556ac3d76 Update url
All checks were successful
Tests / Declarative: Post Actions passed: 14
CI-Multi/pipeline/pr-main This commit looks good
2026-02-16 20:28:25 +01:00
25ba33d43a Merge pull request 'Update jenkinsfile.ci' (#30) from feature/main-UpdateJenkinsFileCi into main
Reviewed-on: #30
2026-02-06 21:39:23 +01:00
896a7413fe Update Jenkinsfile.ci
All checks were successful
Tests / Declarative: Post Actions passed: 14
CI-Multi/pipeline/pr-main This commit looks good
2026-02-06 21:35:56 +01:00
1c572a30a0 Delete build from CD job 2026-02-06 21:34:34 +01:00
e5cd76c0ba Publish frontend tests
All checks were successful
Tests / Declarative: Post Actions passed: 14
CI-Multi/pipeline/pr-main This commit looks good
2026-02-06 21:13:24 +01:00
484412b34e Generate junit xml in frontend tests 2026-02-06 21:11:28 +01:00
9409707329 Update ci msgs 2026-02-06 21:08:25 +01:00
aafc74d876 Delete illegal option
All checks were successful
Tests / Declarative: Post Actions passed: 5
CI-Multi/pipeline/pr-main This commit looks good
2026-02-06 20:59:19 +01:00
408411117c Update shell
Some checks failed
Tests / Declarative: Post Actions passed: 5
CI-Multi/pipeline/pr-main There was a failure building this commit
2026-02-06 20:57:55 +01:00
22f19a2ced Update Jenkinsfile.ci
Some checks failed
Tests / Declarative: Post Actions passed: 5
CI-Multi/pipeline/pr-main There was a failure building this commit
2026-02-06 20:54:14 +01:00
ac2174d365 Actualizar Jenkinsfile.ci
All checks were successful
Tests / Declarative: Post Actions passed: 5
CI-Multi/pipeline/pr-main This commit looks good
2026-02-06 20:45:24 +01:00
4ef74b38eb Actualizar Jenkinsfile.ci
All checks were successful
Tests / Declarative: Post Actions passed: 5
CI-Multi/pipeline/pr-main This commit looks good
2026-02-06 20:34:23 +01:00
214aec2e73 Actualizar Jenkinsfile.ci
Some checks failed
Tests / Declarative: Post Actions passed: 5
CI-Multi/pipeline/pr-main There was a failure building this commit
2026-02-06 20:31:19 +01:00
f431f1c5dc Actualizar Jenkinsfile.ci
Some checks failed
Tests / Declarative: Post Actions passed: 5
CI-Multi/pipeline/pr-main There was a failure building this commit
2026-02-06 20:27:14 +01:00
0550e4a431 Update jenkinsfile.ci
All checks were successful
Tests / Declarative: Post Actions No test results found
CI-Multi/pipeline/pr-main This commit looks good
2026-02-06 20:22:25 +01:00
11 changed files with 355 additions and 77 deletions

View File

@@ -9,7 +9,7 @@ pipeline {
parameters {
string(
name: 'JENKINS_BASE_URL',
defaultValue: 'https://openbokeron.org/jenkins',
defaultValue: 'https://openbokeron.uma.es/jenkins',
description: 'Base URL del Jenkins objetivo'
)
string(
@@ -36,32 +36,6 @@ pipeline {
stages {
/* =========================
TESTS
========================= */
stage('Backend: test (main)') {
agent {
docker {
image 'python:3.11-slim'
args '-u root'
}
}
steps {
dir('backend') {
sh '''
set -e
python -m venv .venv
. .venv/bin/activate
pip install --upgrade pip
pip install -r requirements-dev.txt
pytest
'''
}
}
}
/* =========================
DOCKER BUILD
========================= */
@@ -100,6 +74,22 @@ pipeline {
}
}
/* =========================
TRIVY
========================= */
stage('Security: Trivy job') {
agent any
steps {
build job: 'Trivy Scanner',
parameters: [
string(name: 'APP_VERSION', value: "${APP_VERSION}")
],
propagate: true,
wait: true
}
}
/* =========================
DEPLOY
========================= */

View File

@@ -57,7 +57,7 @@ pipeline {
pip install --upgrade pip
pip install -r requirements-dev.txt
ruff check app tests
pytest -o cache_dir="$WORKSPACE/.cache/pytest"
pytest -o cache_dir="$WORKSPACE/.cache/pytest" --junitxml=pytest.xml
'''
}
}
@@ -84,6 +84,7 @@ pipeline {
mkdir -p "$NPM_CONFIG_CACHE"
npm install --no-progress --no-audit --prefer-offline
npm run check
mkdir -p test-results
npm test
npm run build
'''
@@ -93,13 +94,74 @@ pipeline {
}
post {
post {
always {
script {
node {
junit testResults: 'backend/pytest.xml', allowEmptyResults: true
junit testResults: 'frontend/test-results/junit.xml', allowEmptyResults: true
if (env.CHANGE_ID) {
def giteaBase = 'https://openbokeron.uma.es'
def owner = 'OpenBokeron'
def repo = 'TallerCiCd'
def pr = env.CHANGE_ID
def result = currentBuild.currentResult
def msg = ""
if (result == "SUCCESS") {
msg = """
✅ Todo en orden, camarada.
- URL: ${env.BUILD_URL}
""".stripIndent().trim()
}
else if (result == "FAILURE") {
msg = """
❌ Alto ahí. ¿Qué clase de crímenes de guerra has cometido en esta PR?
- URL: ${env.BUILD_URL}
""".stripIndent().trim()
}
else {
msg = """
⚠️ Algo ha pasado, no sé si es culpa tuyao mía. Por probabilidad, pensaré que la has pifiado tú.
- URL: ${env.BUILD_URL}
""".stripIndent().trim()
}
def commentsUrl = "${giteaBase}/gitea/api/v1/repos/${owner}/${repo}/issues/${pr}/comments"
withCredentials([usernamePassword(credentialsId: 'jenkins-bot-api',
usernameVariable: 'GITEA_USER',
passwordVariable: 'GITEA_TOKEN')]) {
def body = msg.replace('\\', '\\\\').replace('"','\\"').replace('\n','\\n')
// Avoid interpolation of secret variables (https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#string-interpolation)
withEnv(["GITEA_COMMENTS_URL=${commentsUrl}", "GITEA_BODY=${body}"]) {
sh(label: 'Comentar en PR (Gitea)', script: '''
curl -sS -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @- \
"$GITEA_COMMENTS_URL" <<JSON
{"body":"$GITEA_BODY"}
JSON
''')
}
}
} else {
echo "No es build de PR (CHANGE_ID vacío); no comento."
}
cleanWs()
}
}
}
}
}
}

View File

@@ -4,8 +4,11 @@
- `frontend/`: interfaz en Svelte (Vite).
## Requisitos locales
- Python 3.11+ y `pip`
- Ubuntu/Debian: `python3`, `python3-venv` y `python3-pip`
- Node 18+ y `npm`
- Docker y Docker Compose (solo para la opcion con contenedores)
En muchas distros `python` no existe o apunta a otra versión. Para evitar problemas, en esta guia se usa siempre `python3`.
## Configuracion de entorno (local/prod)
Variables que usa `docker-compose` y el frontend:
@@ -14,59 +17,107 @@ cp .env.example .env
```
Notas:
- `VITE_API_BASE` por defecto apunta a `/taller/api` y el frontend proxya a la API.
- Para Jenkins local en contenedor: `JENKINS_BASE_URL=http://jenkins:8080`. Se hace necesario que back y jenkins estén en la misma red de Docker si se quiere probar en local.
- Para VPS: `JENKINS_BASE_URL=https://openbokeron.org/jenkins`.
- El archivo `.env` se usa para el despliegue con `docker compose` y para inyectar variables en las imágenes/contenedores. No hace falta para arrancar backend y frontend a mano en local.
- `JENKINS_BASE_URL` no suele funcionar en local tal como viene en el ejemplo. Esa variable está pensada para entornos desplegados, donde se sustituye por la URL pública real del Jenkins accesible desde el servidor o VPS.
- Si se quisiera probar Jenkins también en local con contenedores, backend y Jenkins tendrían que compartir red Docker y usar una URL interna como `http://jenkins:8080`.
- Para tener Jenkins local hay dos opciones sencillas: levantar el contenedor incluido en `jenkins/jenkins-compose.yml` o instalar Jenkins directamente en la máquina host.
- La opcion con contenedor puede dar guerra porque aquí se trabaja con un esquema tipo Docker-outside-of-Docker. Si da problemas, suele ser más simple instalar Jenkins en la máquina anfitriona y ajustar `JENKINS_BASE_URL` en el `.env` a esa URL local.
## Backend (FastAPI)
## Arranque local sin Docker
Abre dos terminales: una para el backend y otra para el frontend.
### Backend (FastAPI)
```bash
cd backend
python -m venv .venv
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000
python3 -m pip install -r requirements.txt
python3 -m uvicorn app.main:app --reload --port 8000
```
Si todo va bien, la API queda disponible en `http://127.0.0.1:8000`.
Importante: deja esa terminal abierta y con el entorno virtual activado mientras trabajas con el backend.
Endpoints:
- `GET /health` estado.
- `GET /menu` devuelve el menú del día.
- `GET /prices` lista de precios aleatorios.
- `GET /prices/{item}` precio aleatorio para un item concreto.
Tests y lint:
Tests y lint del backend:
```bash
cd backend
pip install -r requirements-dev.txt
source .venv/bin/activate
python3 -m pip install -r requirements-dev.txt
pytest
ruff check app tests
```
Docker:
```bash
cd backend
docker build -t cafeteria-backend .
docker run -p 8000:8000 cafeteria-backend
```
Si abres otra terminal, tienes que volver a activar el entorno virtual con `source .venv/bin/activate` antes de lanzar `pytest`, `ruff` o cualquier comando del backend.
## Frontend (Svelte)
### Frontend (Svelte)
```bash
cd frontend
npm install
npm run dev -- --host --port 5173
```
Abrir `http://localhost:5173/taller/`.
Tests, lint/check:
Tests y check del frontend:
```bash
cd frontend
npm test # no, no voy a hacer tests de frontend
npm test
npm run check
```
Docker (sirve con nginx):
## Arranque con Docker Compose
Desde la raíz del proyecto:
```bash
cp .env.example .env
docker compose up --build
```
Servicios disponibles:
- Frontend: `http://localhost:8081/taller/`
- Backend: `http://localhost:8000/health`
Para parar los contenedores:
```bash
docker compose down
```
Si quieres reconstruir imágenes despues de cambios grandes:
```bash
docker compose up --build
```
## Arranque con Docker manual (sin Compose)
### Backend
```bash
cd backend
docker build -t cafeteria-backend .
docker run --rm -p 8000:8000 cafeteria-backend
```
### Frontend
```bash
cd frontend
docker build -t cafeteria-frontend .
docker run -p 8080:80 cafeteria-frontend
docker run --rm -p 8081:8081 cafeteria-frontend
```
Abrir `http://localhost:8081/taller/`.
Nota: el contenedor del frontend está preparado para funcionar bien junto a `docker compose`, donde puede resolver el servicio `backend` por red interna. Si lanzas ambos contenedores manualmente por separado, tendrás que conectarlos a una red común y ajustar la resolución entre ellos.
## Jenkinsfile (pipeline)
Incluido `Jenkinsfile` declarativo para ejecutar en un agente cualquiera con Python 3.11+, Node 18+ y Docker:

View File

@@ -57,7 +57,7 @@ def health():
"commit": settings.git_commit,
"build": settings.build_number,
"author": settings.commit_author,
"uptime_seconds": uptime()
"uptime_seconds": uptime(),
}

View File

@@ -15,7 +15,9 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import base64
from typing import Dict, List
import html
import re
from typing import Dict, List, Tuple
import requests
@@ -26,25 +28,74 @@ 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({
commits.append(
{
"commit": item.get("commitId", "")[:7],
"message": item.get("msg", ""),
"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 _summarize_trigger(trigger: str) -> Tuple[str, str, str]:
if not trigger:
return "", "", ""
pr_url = ""
for line in trigger.splitlines():
if line.strip().lower().startswith("reviewed-on:"):
pr_url = line.split(":", 1)[-1].strip()
break
author_match = re.search(r"\(([^)]+)\)\.", trigger)
author = author_match.group(1).strip() if author_match else ""
pr_match = re.search(r"#(\d+)", trigger)
title_match = re.search(r"Merge pull request '([^']+)'", trigger)
if pr_match:
pr_number = pr_match.group(1)
title = title_match.group(1).strip() if title_match else ""
if title:
return f"Merge PR #{pr_number} · {title}", pr_url, author
return f"Merge PR #{pr_number}", pr_url, author
return trigger.splitlines()[0].strip(), pr_url, author
def normalize_build(build: Dict) -> Dict:
commits = _extract_commits(build)
trigger = _extract_trigger(build)
trigger_label, trigger_url, trigger_author = _summarize_trigger(trigger)
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,
"trigger_label": trigger_label,
"trigger_url": trigger_url,
"trigger_author": trigger_author,
}
@@ -61,15 +112,17 @@ 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 = 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:
"""Return Jenkins build history data."""

View File

@@ -54,6 +54,7 @@ def _pick_mains(count: int = ITEMS_PER_SECTION) -> List[str]:
random.shuffle(mains)
return mains
def _pick_garnish() -> List[str]:
garnish_options = MENU_SOURCE["mains"]["garnish"]
@@ -69,6 +70,7 @@ def _build_alternative() -> Dict:
"note": alternative.get("note", ""),
}
def build_menu(items_per_section: int = ITEMS_PER_SECTION) -> Dict:
today = datetime.now()

View File

@@ -24,10 +24,7 @@ class RuntimeConfig:
git_commit: str = os.getenv("GIT_COMMIT", "local")
build_number: str = os.getenv("BUILD_NUMBER", "-")
commit_author: str = os.getenv("COMMIT_AUTHOR", "local")
jenkins_base_url: str = os.getenv(
"JENKINS_BASE_URL",
"http://localhost:8080"
).rstrip("/")
jenkins_base_url: str = os.getenv("JENKINS_BASE_URL", "http://localhost:8080").rstrip("/")
jenkins_job_name: str = os.getenv("JENKINS_JOB_NAME", "")
jenkins_user: str = os.getenv("JENKINS_USER", "")
jenkins_token: str = os.getenv("JENKINS_TOKEN", "")

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,20 @@ 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"
assert first["trigger_label"] == "Merge PR #37"
assert first["trigger_url"] == ""
assert first["trigger_author"] == ""
second = builds[1]
assert second["number"] == 204
assert second["status"] == "running"
assert second["duration_seconds"] == 1
assert second["commits"] == []
assert second["trigger"] == ""
assert second["trigger_label"] == ""
assert second["trigger_url"] == ""
assert second["trigger_author"] == ""
def test_build_history_error_returns_empty(monkeypatch):

View File

@@ -8,7 +8,7 @@
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check",
"test": "vitest"
"test": "vitest run --reporter=default --reporter=junit --outputFile=./test-results/junit.xml"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.1",

View File

@@ -377,12 +377,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<span class={`status-dot ${build.status}`}></span>
<span class="summary-title">#{build.number}</span>
{#if build.commits.length === 0}
<span class="muted">Ejecución manual</span>
{#if build.trigger_label || build.trigger}
<div class="summary-trigger">
{#if build.trigger_url}
<a href={build.trigger_url} target="_blank" rel="noreferrer">
{build.trigger_label || build.trigger}
</a>
{:else}
<span>{build.trigger_label || build.trigger}</span>
{/if}
{#if build.trigger_author}
<span class="trigger-author">{build.trigger_author}</span>
{/if}
</div>
{:else if build.commits.length > 0}
<span class="summary-commit">
{build.commits[0].commit} · {build.commits[0].author}
</span>
{:else}
<span class="muted">Ejecución manual</span>
{/if}
</div>
@@ -394,7 +407,36 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</summary>
<div class="history-body">
{#if build.trigger_label || build.trigger}
<p class="history-row">
<span>Disparo</span>
<span>
{#if build.trigger_url}
<a
class="history-link"
href={build.trigger_url}
target="_blank"
rel="noreferrer"
>
{build.trigger_label || build.trigger}
</a>
{:else}
{build.trigger_label || build.trigger}
{/if}
{#if build.trigger_author}
<span class="trigger-author">{build.trigger_author}</span>
{/if}
</span>
</p>
{/if}
{#if build.commits.length > 0}
<p class="history-row">
<span>Commit</span>
<span class="mono">
{build.commits[0].commit} · {build.commits[0].author}
</span>
</p>
<p class="history-row">
<span>Mensaje</span>
<span>{build.commits[0].message}</span>
@@ -408,6 +450,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

@@ -697,6 +697,25 @@ li {
font-size: 0.95rem;
}
.summary-trigger {
display: flex;
align-items: center;
gap: 0.4rem;
flex-wrap: wrap;
color: #fbbf24;
font-weight: 700;
font-size: 0.95rem;
}
.summary-trigger a {
color: inherit;
text-decoration: none;
}
.summary-trigger a:hover {
text-decoration: underline;
}
.summary-meta {
display: flex;
gap: 0.6rem;
@@ -723,6 +742,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);
@@ -743,6 +767,21 @@ li {
color: #cbd5f5;
}
.history-link {
color: #fbbf24;
text-decoration: none;
font-weight: 700;
}
.history-link:hover {
text-decoration: underline;
}
.muted {
color: #94a3b8;
font-size: 0.95rem;
}
.history-message {
margin: 0.2rem 0 0;
background: rgba(248, 113, 113, 0.12);
@@ -758,6 +797,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 +815,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;
@@ -820,3 +869,15 @@ li {
font-size: 0.8rem;
opacity: 0.85;
}
.trigger-author {
padding: 0.1rem 0.4rem;
border-radius: 999px;
background: rgba(148, 163, 184, 0.16);
color: #cbd5f5;
font-size: 0.78rem;
font-weight: 700;
}
.history-row .trigger-author {
margin-left: 0.35rem;
}