Compare commits
47 Commits
19d13711d2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 25ba33d43a | |||
| 896a7413fe | |||
| 1c572a30a0 | |||
| e5cd76c0ba | |||
| 484412b34e | |||
| 9409707329 | |||
| aafc74d876 | |||
| 408411117c | |||
| 22f19a2ced | |||
| ac2174d365 | |||
| 4ef74b38eb | |||
| 214aec2e73 | |||
| f431f1c5dc | |||
| 0550e4a431 | |||
| 2264b76d9c | |||
|
|
4ab3ca0642 | ||
|
|
beeb58c07a | ||
|
|
5d1f518733 | ||
|
|
d2f16e65b2 | ||
|
|
0fe2569131 | ||
|
|
33a629f47d | ||
|
|
e30ff9b178 | ||
|
|
79bfdf78c1 | ||
|
|
e40ca64536 | ||
| 295accd825 | |||
| 87b846b4ae | |||
| 330c2a5364 | |||
| 512c1cea7b | |||
| 5845fed88f | |||
| 938fd8170c | |||
| 1f0c10b458 | |||
| b15630c7ea | |||
| bc044a10c9 | |||
| 558a3198f4 | |||
| 8901941e9f | |||
| c6780b53fc | |||
| d49e756c21 | |||
| 12817a7e82 | |||
| a9baf6da95 | |||
| a2f21e1286 | |||
| 7de0f434c3 | |||
| c74cc19d1b | |||
| 1191ef9f1f | |||
| 26128feb7e | |||
| 48dd10ea05 | |||
| 93fdce1d6e | |||
| b5ace86a03 |
7
.env.example
Normal file
7
.env.example
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
BACKEND_TAG=latest
|
||||||
|
FRONTEND_TAG=latest
|
||||||
|
VITE_API_BASE=/taller/api
|
||||||
|
JENKINS_BASE_URL=http://host.docker.internal:8080
|
||||||
|
JENKINS_JOB_NAME=TallerCiCd
|
||||||
|
JENKINS_USER=
|
||||||
|
JENKINS_TOKEN=
|
||||||
@@ -2,54 +2,45 @@ pipeline {
|
|||||||
agent none
|
agent none
|
||||||
|
|
||||||
options {
|
options {
|
||||||
|
disableConcurrentBuilds()
|
||||||
timestamps()
|
timestamps()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parameters {
|
||||||
|
string(
|
||||||
|
name: 'JENKINS_BASE_URL',
|
||||||
|
defaultValue: 'https://openbokeron.org/jenkins',
|
||||||
|
description: 'Base URL del Jenkins objetivo'
|
||||||
|
)
|
||||||
|
string(
|
||||||
|
name: 'JENKINS_JOB_NAME',
|
||||||
|
defaultValue: 'CD',
|
||||||
|
description: 'Nombre del job que se consulta en Jenkins'
|
||||||
|
)
|
||||||
|
string(
|
||||||
|
name: 'VITE_API_BASE',
|
||||||
|
defaultValue: '/taller/api',
|
||||||
|
description: 'Base path/API para el frontend (build time)'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
environment {
|
environment {
|
||||||
NODE_OPTIONS = '--max_old_space_size=2048'
|
NODE_OPTIONS = '--max_old_space_size=2048'
|
||||||
APP_VERSION = "1.0.${BUILD_NUMBER}"
|
APP_VERSION = "1.0.${BUILD_NUMBER}"
|
||||||
DOCKER_BUILDKIT = '1'
|
DOCKER_BUILDKIT = '1'
|
||||||
|
JENKINS_BASE_URL = "${params.JENKINS_BASE_URL}"
|
||||||
|
JENKINS_JOB_NAME = "${params.JENKINS_JOB_NAME}"
|
||||||
|
VITE_API_BASE = "${params.VITE_API_BASE}"
|
||||||
|
PYTHONDONTWRITEBYTECODE = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
stages {
|
stages {
|
||||||
|
|
||||||
/* =========================
|
|
||||||
TESTS
|
|
||||||
========================= */
|
|
||||||
|
|
||||||
stage('Backend: test (main)') {
|
|
||||||
when {
|
|
||||||
branch '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
|
DOCKER BUILD
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
stage('Docker: build images') {
|
stage('Docker: build images') {
|
||||||
when {
|
|
||||||
branch 'main'
|
|
||||||
}
|
|
||||||
agent any
|
agent any
|
||||||
|
|
||||||
steps {
|
steps {
|
||||||
@@ -72,13 +63,12 @@ pipeline {
|
|||||||
--build-arg GIT_COMMIT=${COMMIT_SHORT} \
|
--build-arg GIT_COMMIT=${COMMIT_SHORT} \
|
||||||
--build-arg COMMIT_AUTHOR="${COMMIT_AUTHOR}" \
|
--build-arg COMMIT_AUTHOR="${COMMIT_AUTHOR}" \
|
||||||
--build-arg BUILD_NUMBER=${BUILD_NUMBER} \
|
--build-arg BUILD_NUMBER=${BUILD_NUMBER} \
|
||||||
-t cafeteria-backend:${BUILD_NUMBER} \
|
-t cafeteria-backend:${APP_VERSION} \
|
||||||
-t cafeteria-backend:latest \
|
|
||||||
./backend
|
./backend
|
||||||
|
|
||||||
docker build \
|
docker build \
|
||||||
-t cafeteria-frontend:${BUILD_NUMBER} \
|
--build-arg VITE_API_BASE=${VITE_API_BASE} \
|
||||||
-t cafeteria-frontend:latest \
|
-t cafeteria-frontend:${APP_VERSION} \
|
||||||
./frontend
|
./frontend
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
@@ -89,14 +79,7 @@ pipeline {
|
|||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
stage('Deploy (docker compose)') {
|
stage('Deploy (docker compose)') {
|
||||||
when {
|
|
||||||
branch 'main'
|
|
||||||
}
|
|
||||||
agent any
|
agent any
|
||||||
environment {
|
|
||||||
JENKINS_BASE_URL = 'http://jenkins:8080'
|
|
||||||
JENKINS_JOB_NAME = 'Espetos'
|
|
||||||
}
|
|
||||||
steps {
|
steps {
|
||||||
withCredentials([
|
withCredentials([
|
||||||
usernamePassword(
|
usernamePassword(
|
||||||
@@ -108,17 +91,16 @@ pipeline {
|
|||||||
sh '''
|
sh '''
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "Deploying backend ${BUILD_NUMBER}"
|
echo "Deploying ${APP_VERSION}"
|
||||||
|
|
||||||
echo "BACKEND_TAG=${BUILD_NUMBER}" > .env
|
BACKEND_TAG=${APP_VERSION} FRONTEND_TAG=${APP_VERSION} docker compose up -d
|
||||||
echo "FRONTEND_TAG=${BUILD_NUMBER}" >> .env
|
|
||||||
|
|
||||||
docker-compose up -d
|
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
stage('Cleanup') {
|
stage('Cleanup') {
|
||||||
agent any
|
agent any
|
||||||
steps {
|
steps {
|
||||||
|
|||||||
@@ -38,19 +38,26 @@ pipeline {
|
|||||||
agent {
|
agent {
|
||||||
docker {
|
docker {
|
||||||
image 'python:3.11-slim'
|
image 'python:3.11-slim'
|
||||||
args '-u root'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
HOME = "${WORKSPACE}"
|
||||||
|
PIP_CACHE_DIR = "${WORKSPACE}/.cache/pip"
|
||||||
|
PYTHONDONTWRITEBYTECODE = 1
|
||||||
|
}
|
||||||
|
|
||||||
steps {
|
steps {
|
||||||
dir('backend') {
|
dir('backend') {
|
||||||
sh '''
|
sh '''
|
||||||
set -e
|
set -e
|
||||||
|
mkdir -p "$PIP_CACHE_DIR" "$WORKSPACE/.cache/pytest"
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
pip install --upgrade pip
|
pip install --upgrade pip
|
||||||
pip install -r requirements-dev.txt
|
pip install -r requirements-dev.txt
|
||||||
ruff check app tests
|
ruff check app tests
|
||||||
pytest
|
pytest -o cache_dir="$WORKSPACE/.cache/pytest" --junitxml=pytest.xml
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,23 +73,95 @@ pipeline {
|
|||||||
image 'node:20-slim'
|
image 'node:20-slim'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
environment {
|
||||||
|
HOME = "${WORKSPACE}"
|
||||||
|
NPM_CONFIG_CACHE = "${WORKSPACE}/.cache/npm"
|
||||||
|
}
|
||||||
steps {
|
steps {
|
||||||
dir('frontend') {
|
dir('frontend') {
|
||||||
sh '''
|
sh '''
|
||||||
set -e
|
set -e
|
||||||
|
mkdir -p "$NPM_CONFIG_CACHE"
|
||||||
npm install --no-progress --no-audit --prefer-offline
|
npm install --no-progress --no-audit --prefer-offline
|
||||||
npm run check
|
npm run check
|
||||||
|
mkdir -p test-results
|
||||||
|
npm test
|
||||||
npm run build
|
npm run build
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Cleanup') {
|
}
|
||||||
agent any
|
|
||||||
steps {
|
post {
|
||||||
cleanWs()
|
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.org'
|
||||||
|
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 = """
|
||||||
|
⚠️ Dudoso…
|
||||||
|
|
||||||
|
- 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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -7,6 +7,16 @@
|
|||||||
- Python 3.11+ y `pip`
|
- Python 3.11+ y `pip`
|
||||||
- Node 18+ y `npm`
|
- Node 18+ y `npm`
|
||||||
|
|
||||||
|
## Configuracion de entorno (local/prod)
|
||||||
|
Variables que usa `docker-compose` y el frontend:
|
||||||
|
```bash
|
||||||
|
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`.
|
||||||
|
|
||||||
## Backend (FastAPI)
|
## Backend (FastAPI)
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
@@ -42,6 +52,7 @@ cd frontend
|
|||||||
npm install
|
npm install
|
||||||
npm run dev -- --host --port 5173
|
npm run dev -- --host --port 5173
|
||||||
```
|
```
|
||||||
|
Abrir `http://localhost:5173/taller/`.
|
||||||
Tests, lint/check:
|
Tests, lint/check:
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11-slim AS builder
|
FROM docker.io/library/python:3.11-slim AS builder
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
@@ -9,7 +9,7 @@ COPY requirements.txt .
|
|||||||
RUN pip wheel --no-cache-dir --no-deps -r requirements.txt -w /build/wheels
|
RUN pip wheel --no-cache-dir --no-deps -r requirements.txt -w /build/wheels
|
||||||
|
|
||||||
|
|
||||||
FROM python:3.11-slim
|
FROM docker.io/library/python:3.11-slim
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# ---- Build args (desde Jenkins) ----
|
# ---- Build args (desde Jenkins) ----
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"builds": [
|
|
||||||
{
|
|
||||||
"number": 205,
|
|
||||||
"status": "success",
|
|
||||||
"branch": "main",
|
|
||||||
"commit": "9ac3f91",
|
|
||||||
"author": "Miau",
|
|
||||||
"finished_at": "2024-05-04T10:20:00Z",
|
|
||||||
"duration_seconds": 312
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"number": 204,
|
|
||||||
"status": "failed",
|
|
||||||
"branch": "feature/nosetioestoesunmock",
|
|
||||||
"commit": "75c4ba2",
|
|
||||||
"author": "Miau",
|
|
||||||
"finished_at": "2024-05-04T09:50:00Z",
|
|
||||||
"duration_seconds": 188,
|
|
||||||
"failed_stage": "tests",
|
|
||||||
"fun_message": "woops"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"number": 203,
|
|
||||||
"status": "failed",
|
|
||||||
"branch": "main",
|
|
||||||
"commit": "512ca7e",
|
|
||||||
"author": "Miau",
|
|
||||||
"finished_at": "2024-05-04T09:10:00Z",
|
|
||||||
"duration_seconds": 140,
|
|
||||||
"failed_stage": "lint",
|
|
||||||
"fun_message": "Nadie pasa en local el linter"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"number": 202,
|
|
||||||
"status": "success",
|
|
||||||
"branch": "hotfix/tehedichoqueestoesunmock?",
|
|
||||||
"commit": "c73d8ab",
|
|
||||||
"author": "Miau",
|
|
||||||
"finished_at": "2024-05-03T18:30:00Z",
|
|
||||||
"duration_seconds": 276
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -16,8 +16,10 @@
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from fastapi import FastAPI
|
import requests
|
||||||
|
from fastapi import FastAPI, status
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from app.services.builds import build_history
|
from app.services.builds import build_history
|
||||||
from app.services.menu import build_menu
|
from app.services.menu import build_menu
|
||||||
@@ -76,4 +78,10 @@ def price_for_item(item: str):
|
|||||||
|
|
||||||
@app.get("/builds")
|
@app.get("/builds")
|
||||||
def builds():
|
def builds():
|
||||||
return build_history()
|
try:
|
||||||
|
return build_history()
|
||||||
|
except (requests.RequestException, ValueError):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
content={"builds": [], "error": "jenkins_unavailable"},
|
||||||
|
)
|
||||||
|
|||||||
@@ -15,26 +15,17 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from app.settings import settings
|
from app.settings import settings
|
||||||
|
|
||||||
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
|
|
||||||
|
|
||||||
|
|
||||||
def _load_json(filename: str) -> Dict:
|
|
||||||
path = DATA_DIR / filename
|
|
||||||
with open(path, encoding="utf-8") as file:
|
|
||||||
return json.load(file)
|
|
||||||
|
|
||||||
|
|
||||||
def _sort_builds(builds: List[Dict]) -> List[Dict]:
|
def _sort_builds(builds: List[Dict]) -> List[Dict]:
|
||||||
return sorted(builds, key=lambda build: build.get("number", 0), reverse=True)
|
return sorted(builds, key=lambda build: build.get("number", 0), reverse=True)
|
||||||
|
|
||||||
|
|
||||||
def normalize_build(build: Dict) -> Dict:
|
def normalize_build(build: Dict) -> Dict:
|
||||||
changes = build.get("changeSets", [])
|
changes = build.get("changeSets", [])
|
||||||
commits = []
|
commits = []
|
||||||
@@ -58,11 +49,16 @@ def normalize_build(build: Dict) -> Dict:
|
|||||||
|
|
||||||
|
|
||||||
def _auth_header() -> Dict[str, str]:
|
def _auth_header() -> Dict[str, str]:
|
||||||
|
if not settings.jenkins_user or not settings.jenkins_token:
|
||||||
|
return {}
|
||||||
token = f"{settings.jenkins_user}:{settings.jenkins_token}"
|
token = f"{settings.jenkins_user}:{settings.jenkins_token}"
|
||||||
encoded = base64.b64encode(token.encode()).decode()
|
encoded = base64.b64encode(token.encode()).decode()
|
||||||
return {"Authorization": f"Basic {encoded}"}
|
return {"Authorization": f"Basic {encoded}"}
|
||||||
|
|
||||||
|
|
||||||
def fetch_builds(limit: int = 5) -> List[Dict]:
|
def fetch_builds(limit: int = 5) -> List[Dict]:
|
||||||
|
if not settings.jenkins_job_name:
|
||||||
|
raise ValueError("JENKINS_JOB_NAME not configured")
|
||||||
url = (
|
url = (
|
||||||
f"{settings.jenkins_base_url}/job/{settings.jenkins_job_name}/api/json"
|
f"{settings.jenkins_base_url}/job/{settings.jenkins_job_name}/api/json"
|
||||||
"?tree=builds[number,url,result,timestamp,duration,"
|
"?tree=builds[number,url,result,timestamp,duration,"
|
||||||
@@ -78,6 +74,4 @@ def fetch_builds(limit: int = 5) -> List[Dict]:
|
|||||||
def build_history() -> Dict:
|
def build_history() -> Dict:
|
||||||
"""Return Jenkins build history data."""
|
"""Return Jenkins build history data."""
|
||||||
builds = fetch_builds()
|
builds = fetch_builds()
|
||||||
return {
|
return {"builds": [normalize_build(b) for b in builds]}
|
||||||
"builds": [normalize_build(b) for b in builds]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ class RuntimeConfig:
|
|||||||
git_commit: str = os.getenv("GIT_COMMIT", "local")
|
git_commit: str = os.getenv("GIT_COMMIT", "local")
|
||||||
build_number: str = os.getenv("BUILD_NUMBER", "-")
|
build_number: str = os.getenv("BUILD_NUMBER", "-")
|
||||||
commit_author: str = os.getenv("COMMIT_AUTHOR", "local")
|
commit_author: str = os.getenv("COMMIT_AUTHOR", "local")
|
||||||
jenkins_base_url: str = os.getenv("JENKINS_BASE_URL", "localhost:8080")
|
jenkins_base_url: str = os.getenv(
|
||||||
|
"JENKINS_BASE_URL",
|
||||||
|
"http://localhost:8080"
|
||||||
|
).rstrip("/")
|
||||||
jenkins_job_name: str = os.getenv("JENKINS_JOB_NAME", "")
|
jenkins_job_name: str = os.getenv("JENKINS_JOB_NAME", "")
|
||||||
jenkins_user: str = os.getenv("JENKINS_USER", "")
|
jenkins_user: str = os.getenv("JENKINS_USER", "")
|
||||||
jenkins_token: str = os.getenv("JENKINS_TOKEN", "")
|
jenkins_token: str = os.getenv("JENKINS_TOKEN", "")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import requests
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from app.main import app
|
from app.main import app
|
||||||
@@ -106,3 +107,16 @@ def test_build_history(monkeypatch):
|
|||||||
assert second["status"] == "running"
|
assert second["status"] == "running"
|
||||||
assert second["duration_seconds"] == 1
|
assert second["duration_seconds"] == 1
|
||||||
assert second["commits"] == []
|
assert second["commits"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_history_error_returns_empty(monkeypatch):
|
||||||
|
def raise_error(limit=5):
|
||||||
|
raise requests.RequestException("boom")
|
||||||
|
|
||||||
|
monkeypatch.setattr("app.services.builds.fetch_builds", raise_error)
|
||||||
|
|
||||||
|
response = client.get("/builds")
|
||||||
|
assert response.status_code == 503
|
||||||
|
body = response.json()
|
||||||
|
assert body["builds"] == []
|
||||||
|
assert body["error"] == "jenkins_unavailable"
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ services:
|
|||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
image: cafeteria-backend:${BACKEND_TAG}
|
image: cafeteria-backend:${BACKEND_TAG:-latest}
|
||||||
|
networks:
|
||||||
|
- cafeteria
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
environment:
|
environment:
|
||||||
JENKINS_BASE_URL: ${JENKINS_BASE_URL}
|
JENKINS_BASE_URL: ${JENKINS_BASE_URL:-http://jenkins:8080}
|
||||||
JENKINS_JOB_NAME: ${JENKINS_JOB_NAME}
|
JENKINS_JOB_NAME: ${JENKINS_JOB_NAME:-TallerCiCd}
|
||||||
JENKINS_USER: ${JENKINS_USER}
|
JENKINS_USER: ${JENKINS_USER:-}
|
||||||
JENKINS_TOKEN: ${JENKINS_TOKEN}
|
JENKINS_TOKEN: ${JENKINS_TOKEN:-}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
@@ -26,10 +28,21 @@ services:
|
|||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
image: cafeteria-frontend:${FRONTEND_TAG}
|
args:
|
||||||
|
VITE_API_BASE: ${VITE_API_BASE:-/taller/api}
|
||||||
|
image: cafeteria-frontend:${FRONTEND_TAG:-latest}
|
||||||
|
networks:
|
||||||
|
- cafeteria
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "8081:8081"
|
||||||
depends_on:
|
depends_on:
|
||||||
backend:
|
backend:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
cafeteria:
|
||||||
|
enable_ipv6: true
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 2001:db8::/64
|
||||||
|
|||||||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@@ -6,3 +6,4 @@ dist
|
|||||||
*.log
|
*.log
|
||||||
.svelte-kit
|
.svelte-kit
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
TESTS.md
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
FROM node:20-slim AS build
|
FROM docker.io/library/node:20-slim AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
ARG VITE_API_BASE
|
||||||
|
ENV VITE_API_BASE=$VITE_API_BASE
|
||||||
RUN npm install --no-progress
|
RUN npm install --no-progress
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM nginx:1.27-alpine AS final
|
FROM docker.io/library/nginx:1.27-alpine AS final
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
COPY --from=build /app/dist /usr/share/nginx/html/taller
|
||||||
EXPOSE 80
|
EXPOSE 8081
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 8081;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
location / {
|
location = /taller {
|
||||||
try_files $uri /index.html;
|
return 301 /taller/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /taller/api/ {
|
||||||
|
proxy_pass http://backend:8000/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /taller/ {
|
||||||
|
try_files $uri $uri/ /taller/index.html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-check",
|
"check": "svelte-check",
|
||||||
"test": "vitest"
|
"test": "vitest run --reporter=default --reporter=junit --outputFile=./test-results/junit.xml"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.1",
|
"@sveltejs/vite-plugin-svelte": "^3.0.1",
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
if (!ENABLE_POLLING) return;
|
if (!ENABLE_POLLING) return;
|
||||||
|
|
||||||
const pricesInterval = setInterval(fetchPrices, 8000);
|
const pricesInterval = setInterval(fetchPrices, 180000);
|
||||||
const ciInterval = setInterval(fetchCiStatus, 10000);
|
const ciInterval = setInterval(fetchCiStatus, 300000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(pricesInterval);
|
clearInterval(pricesInterval);
|
||||||
@@ -467,7 +467,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
<div class="openbokeron-logo">
|
<div class="openbokeron-logo">
|
||||||
<div class="logo-bubble">
|
<div class="logo-bubble">
|
||||||
<img
|
<img
|
||||||
src="/open-bokeron-logo.png"
|
src={`${import.meta.env.BASE_URL}/open-bokeron-logo.png`}
|
||||||
alt="Logo de Open Bokeron"
|
alt="Logo de Open Bokeron"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
|||||||
127
frontend/src/App.test.js
Normal file
127
frontend/src/App.test.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
CI/CD Workshop
|
||||||
|
Copyright (C) 2025 OpenBokeron
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { render, screen, waitFor } from '@testing-library/svelte';
|
||||||
|
import App from './App.svelte';
|
||||||
|
|
||||||
|
const mockMenu = {
|
||||||
|
starters: ['Sopa de cocido', 'Ensalada mixta'],
|
||||||
|
mains: ['Pescado al vapor', 'Pollo asado'],
|
||||||
|
garnish: ['Patatas fritas'],
|
||||||
|
desserts: ['Fruta del tiempo', 'Yogur'],
|
||||||
|
notes: ['Pan y bebida incluidos'],
|
||||||
|
menu_price: 5.5,
|
||||||
|
university_deal: {
|
||||||
|
old_price: 4.5,
|
||||||
|
current_price: 5.5,
|
||||||
|
note: 'Gracias al convenio con la Universidad',
|
||||||
|
},
|
||||||
|
alternative: { title: 'Plato alternativo', items: [], price: 5.0 },
|
||||||
|
availability: { last_updated: '12:00' },
|
||||||
|
espetos_tip: 'Hoy toca sardinas',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPrices = {
|
||||||
|
items: [
|
||||||
|
{ item: 'cafe', price: 1.2, currency: 'EUR', generated_at: '10:00' },
|
||||||
|
{ item: 'tostada', price: 2.0, currency: 'EUR', generated_at: '10:00' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockHealth = {
|
||||||
|
status: 'ok',
|
||||||
|
build: 42,
|
||||||
|
commit: 'abc1234def5678',
|
||||||
|
author: 'Test Author',
|
||||||
|
uptime_seconds: 3600,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockBuilds = {
|
||||||
|
builds: [
|
||||||
|
{
|
||||||
|
number: 42,
|
||||||
|
status: 'success',
|
||||||
|
finished_at: Date.now(),
|
||||||
|
duration_seconds: 60,
|
||||||
|
commits: [{ commit: 'abc1234', message: 'Fix bug', author: 'Dev' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function createMockFetch(url) {
|
||||||
|
if (url.includes('/menu')) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockMenu) });
|
||||||
|
}
|
||||||
|
if (url.includes('/prices')) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockPrices) });
|
||||||
|
}
|
||||||
|
if (url.includes('/health')) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockHealth) });
|
||||||
|
}
|
||||||
|
if (url.includes('/builds')) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockBuilds) });
|
||||||
|
}
|
||||||
|
return Promise.resolve({ ok: false, status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('App.svelte', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
globalThis.fetch = vi.fn(createMockFetch);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renderiza el titulo principal', async () => {
|
||||||
|
render(App);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renderiza la seccion hero con eyebrow', async () => {
|
||||||
|
render(App);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Taller CI/CD con Jenkins')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renderiza los botones de accion', async () => {
|
||||||
|
render(App);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Refrescar menú')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Recalcular desayunos')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renderiza la tarjeta de Open Bokeron', async () => {
|
||||||
|
render(App);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Open Bokeron')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText('Somos Open Bokeron, la asociación de software libre de la ETSII.')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -16,5 +16,9 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
|
const baseUrl = import.meta.env.BASE_URL || '/';
|
||||||
|
const defaultApiBase = `${baseUrl.replace(/\/$/, '')}/api`;
|
||||||
|
const rawApiBase = import.meta.env.VITE_API_BASE || defaultApiBase;
|
||||||
|
|
||||||
|
export const API_BASE = rawApiBase.replace(/\/$/, '');
|
||||||
export const ENABLE_POLLING = import.meta.env.MODE !== 'test';
|
export const ENABLE_POLLING = import.meta.env.MODE !== 'test';
|
||||||
|
|||||||
123
frontend/src/services/api.test.js
Normal file
123
frontend/src/services/api.test.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
CI/CD Workshop
|
||||||
|
Copyright (C) 2025 OpenBokeron
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { getMenu, getPrices, getCiStatus, getBuildHistory } from './api';
|
||||||
|
|
||||||
|
describe('API services', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubGlobal('fetch', vi.fn());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMenu', () => {
|
||||||
|
it('devuelve datos del menu cuando la API responde correctamente', async () => {
|
||||||
|
const mockMenu = {
|
||||||
|
starters: ['Sopa'],
|
||||||
|
mains: ['Pescado al vapor'],
|
||||||
|
desserts: ['Fruta'],
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockMenu),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getMenu();
|
||||||
|
|
||||||
|
expect(fetch).toHaveBeenCalledWith('/taller/api/menu');
|
||||||
|
expect(result).toEqual(mockMenu);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lanza error cuando la API falla', async () => {
|
||||||
|
fetch.mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(getMenu()).rejects.toThrow('Respuesta no valida del servidor');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getPrices', () => {
|
||||||
|
it('devuelve array de items cuando la API responde', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
items: [
|
||||||
|
{ item: 'cafe', price: 1.2 },
|
||||||
|
{ item: 'tostada', price: 2.0 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockResponse),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getPrices();
|
||||||
|
|
||||||
|
expect(fetch).toHaveBeenCalledWith('/taller/api/prices');
|
||||||
|
expect(result).toEqual(mockResponse.items);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('devuelve array vacio si no hay items', async () => {
|
||||||
|
fetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getPrices();
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCiStatus', () => {
|
||||||
|
it('llama al endpoint /health', async () => {
|
||||||
|
const mockHealth = { status: 'ok', build: 42 };
|
||||||
|
|
||||||
|
fetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockHealth),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getCiStatus();
|
||||||
|
|
||||||
|
expect(fetch).toHaveBeenCalledWith('/taller/api/health');
|
||||||
|
expect(result).toEqual(mockHealth);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBuildHistory', () => {
|
||||||
|
it('llama al endpoint /builds', async () => {
|
||||||
|
const mockBuilds = { builds: [{ number: 1 }, { number: 2 }] };
|
||||||
|
|
||||||
|
fetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockBuilds),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getBuildHistory();
|
||||||
|
|
||||||
|
expect(fetch).toHaveBeenCalledWith('/taller/api/builds');
|
||||||
|
expect(result).toEqual(mockBuilds);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
19
frontend/src/setupTests.js
Normal file
19
frontend/src/setupTests.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
CI/CD Workshop
|
||||||
|
Copyright (C) 2025 OpenBokeron
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
38
frontend/src/utils/text.test.js
Normal file
38
frontend/src/utils/text.test.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
CI/CD Workshop
|
||||||
|
Copyright (C) 2025 OpenBokeron
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { prettify } from './text';
|
||||||
|
|
||||||
|
describe('prettify', () => {
|
||||||
|
it('convierte snake_case a Title Case', () => {
|
||||||
|
expect(prettify('hello_world')).toBe('Hello World');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maneja palabras multiples con underscore', () => {
|
||||||
|
expect(prettify('cafe_con_leche')).toBe('Cafe Con Leche');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maneja una sola palabra sin underscore', () => {
|
||||||
|
expect(prettify('bocadillo')).toBe('Bocadillo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maneja cadena vacia', () => {
|
||||||
|
expect(prettify('')).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,13 +1,35 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
plugins: [svelte()],
|
const env = loadEnv(mode, process.cwd(), 'VITE_');
|
||||||
test: {
|
const basePath = '/taller/';
|
||||||
environment: 'jsdom',
|
const defaultApiBase = `${basePath.replace(/\/$/, '')}/api`;
|
||||||
globals: true,
|
const apiBase = (env.VITE_API_BASE || defaultApiBase).replace(/\/$/, '');
|
||||||
},
|
const apiProxyPath = apiBase.startsWith('http') ? null : apiBase;
|
||||||
server: {
|
|
||||||
port: 5173,
|
return {
|
||||||
},
|
base: basePath,
|
||||||
|
plugins: [svelte()],
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
globals: true,
|
||||||
|
setupFiles: ['./src/setupTests.js'],
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
proxy: apiProxyPath
|
||||||
|
? {
|
||||||
|
[apiProxyPath]: {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) =>
|
||||||
|
path.startsWith(apiProxyPath)
|
||||||
|
? path.slice(apiProxyPath.length) || '/'
|
||||||
|
: path,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,10 +3,15 @@ FROM jenkins/jenkins:lts
|
|||||||
USER root
|
USER root
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y docker.io curl \
|
&& apt-get install -y ca-certificates curl gnupg \
|
||||||
&& curl -L https://github.com/docker/compose/releases/download/v2.27.0/docker-compose-linux-x86_64 \
|
&& install -m 0755 -d /etc/apt/keyrings \
|
||||||
-o /usr/local/bin/docker-compose \
|
&& curl -fsSL https://download.docker.com/linux/debian/gpg \
|
||||||
&& chmod +x /usr/local/bin/docker-compose \
|
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
|
||||||
|
&& chmod a+r /etc/apt/keyrings/docker.gpg \
|
||||||
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" \
|
||||||
|
> /etc/apt/sources.list.d/docker.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y docker-ce-cli docker-compose-plugin \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
USER jenkins
|
USER jenkins
|
||||||
|
|||||||
Reference in New Issue
Block a user