Implement automatic versioning and display in the application

- Added a step in the CI/CD pipeline to generate a version based on the branch and commit history.
- Updated Dockerfile to accept the application version as an argument.
- Modified the configuration to retrieve the application version from the environment variable.
- Created a new API endpoint to expose the application version.
- Added a new Svelte component to display the application version in the UI.
This commit is contained in:
2026-02-05 09:58:35 -06:00
parent 276a020f58
commit 27d9625bd0
6 changed files with 196 additions and 5 deletions

View File

@@ -13,6 +13,60 @@ jobs:
steps:
- name: Checkout código
uses: actions/checkout@v4
with:
fetch-depth: 0 # Necesario para obtener el historial completo de git
# ------------------------
# Generar versión automática
# ------------------------
- name: Generar versión automática
id: version
run: |
# Obtener año y mes actual
YEAR=$(date +%y)
MONTH=$(date +%m)
# Detectar rama actual usando variables de Gitea
BRANCH="${GITHUB_REF##*/}"
if [ "$BRANCH" = "development" ]; then
# Para development: YY.MM.1.<short-git-hash>
SHORT_HASH=$(git rev-parse --short=8 HEAD)
VERSION="${YEAR}.${MONTH}.${{ secrets.VERSION_MAYOR }}.${SHORT_HASH}"
elif [ "$BRANCH" = "main" ]; then
# Para main: YY.MM.0.<commit-count>
# Intentar obtener el último tag
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$LAST_TAG" ]; then
# No hay tags, usar commit count total (primera ejecución)
COMMIT_COUNT=$(git rev-list --count HEAD)
echo "⚠️ No se encontraron tags. Usando commit count total: ${COMMIT_COUNT}"
else
# Hay tags, contar commits desde el último tag
COMMIT_COUNT=$(git rev-list ${LAST_TAG}..HEAD --count)
# Si es 0, significa que estamos en el mismo commit del tag, incrementar
if [ "$COMMIT_COUNT" -eq 0 ]; then
COMMIT_COUNT=1
fi
echo "📌 Último tag: ${LAST_TAG}"
echo "🔢 Commits desde el último tag: ${COMMIT_COUNT}"
fi
VERSION="${YEAR}.${MONTH}.${{ secrets.VERSION_MAYOR }}.${COMMIT_COUNT}"
else
# Fallback para otras ramas
SHORT_HASH=$(git rev-parse --short=8 HEAD)
VERSION="${YEAR}.${MONTH}.99.${SHORT_HASH}"
fi
# Guardar versión en output para usarla en steps posteriores
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
echo "BRANCH=${BRANCH}" >> $GITHUB_OUTPUT
# Mostrar en logs
echo "📦 Versión generada: ${VERSION}"
echo "🌿 Rama: ${BRANCH}"
- name: Login a Harbor
run: |
@@ -27,6 +81,7 @@ jobs:
- name: Build backend
run: |
docker build \
--build-arg APP_VERSION=${{ steps.version.outputs.VERSION }} \
-t dev.aduanasoft.com/anexo76/backend:latest \
-f ./backend/Dockerfile \
./backend
@@ -55,6 +110,19 @@ jobs:
run: |
docker push dev.aduanasoft.com/anexo76/frontend:latest
# ------------------------
# Crear tag de versión (solo para main)
# ------------------------
- name: Crear tag de versión
if: github.ref == 'refs/heads/main'
run: |
VERSION=${{ steps.version.outputs.VERSION }}
git config user.name "Gitea Actions"
git config user.email "actions@gitea.local"
git tag -a "v${VERSION}" -m "Release version ${VERSION}"
git push origin "v${VERSION}"
echo "✅ Tag v${VERSION} creado y pusheado"
# ------------------------
# Deploy a Development
# ------------------------

View File

@@ -2,6 +2,12 @@ FROM python:3.11-slim
WORKDIR /app
# ========================================
# ARG para recibir la versión desde CI/CD
# ========================================
ARG APP_VERSION="dev"
ENV APP_VERSION=${APP_VERSION}
# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \
gcc \

View File

@@ -2,6 +2,7 @@
Configuración centralizada de la aplicación usando Pydantic Settings
"""
import os
from typing import List
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -12,7 +13,9 @@ class Settings(BaseSettings):
# Application
APP_NAME: str = "Anexo76"
APP_VERSION: str = "1.0.0"
# La versión se obtiene de la variable de entorno APP_VERSION que se pasa desde Docker
# Si no existe, usa un valor por defecto de desarrollo
APP_VERSION: str = os.getenv("APP_VERSION", "dev-local")
DEBUG: bool = True
ENVIRONMENT: str = "development"

View File

@@ -77,10 +77,8 @@ async def http_exception_handler(request: Request, exc: HTTPException):
def run_migrations():
subprocess.run(
["alembic", "upgrade", "head"],
check=True
)
subprocess.run(["alembic", "upgrade", "head"], check=True)
# Inicializar la base de datos
@app.on_event("startup")
@@ -132,3 +130,24 @@ async def root():
async def health_check():
"""Health check endpoint"""
return {"status": "healthy", "environment": settings.ENVIRONMENT}
@app.get("/api/version")
async def get_version():
"""
Endpoint de versión de la aplicación
Retorna la versión de la aplicación que fue incrustada en la imagen Docker
durante el proceso de CI/CD. La versión se genera automáticamente según la rama:
- development: YY.MM.1.<short-git-hash>
- main: YY.MM.0.<commit-count>
Returns:
dict: Información de versión y entorno
"""
return {
"service": settings.APP_NAME,
"version": settings.APP_VERSION,
"environment": settings.ENVIRONMENT,
"debug": settings.DEBUG,
}

View File

@@ -0,0 +1,90 @@
<script lang="ts">
import { onMount } from 'svelte';
import { api } from '$lib/api';
/**
* Componente para mostrar la versión de la aplicación
* Consume el endpoint /api/version del backend para obtener información de versión
*/
interface VersionInfo {
service: string;
version: string;
environment: string;
debug: boolean;
}
let versionInfo: VersionInfo | null = $state(null);
let loading: boolean = $state(true);
let error: string | null = $state(null);
/**
* Obtener información de versión desde el backend
*/
async function fetchVersion() {
try {
loading = true;
error = null;
const response = await api.get<VersionInfo>('/version');
if (response.error) {
throw new Error(response.error);
}
if (response.data) {
versionInfo = response.data;
}
} catch (err) {
console.error('Error al obtener versión:', err);
error = err instanceof Error ? err.message : 'Error desconocido';
} finally {
loading = false;
}
}
// Cargar versión al montar el componente
onMount(() => {
fetchVersion();
});
/**
* Obtener color del badge según el entorno
*/
function getEnvironmentColor(env: string): string {
switch (env?.toLowerCase()) {
case 'production':
return 'bg-green-600 text-white';
case 'development':
return 'bg-yellow-600 text-white';
case 'staging':
return 'bg-blue-600 text-white';
default:
return 'bg-gray-600 text-white';
}
}
</script>
<!-- Componente de versión -->
<div class="flex items-center app-version">
{#if loading}
<div class="text-xs text-muted-foreground">Cargando versión...</div>
{:else if error}
<div class="text-xs text-destructive">Error: {error}</div>
{:else if versionInfo}
<div class="flex items-center gap-2 text-xs">
<!-- Versión -->
<span class="font-mono font-semibold text-foreground">
v{versionInfo.version}
</span>
<!-- Indicador de debug (solo si está activo) -->
{#if versionInfo.debug}
<span class="rounded bg-orange-600 px-2 py-0.5 text-xs font-medium text-white">
DEBUG
</span>
{/if}
</div>
{/if}
</div>

View File

@@ -17,6 +17,7 @@
import { goto } from "$app/navigation";
import { browser } from "$app/environment";
import { getBackendAssetUrl } from "$lib/utils";
import AppVersion from "$lib/components/app-version.svelte";
let { user }: { user: { name: string; email: string; avatar: string } } = $props();
const sidebar = useSidebar();
@@ -172,6 +173,10 @@
<LogOutIcon />
Log out
</DropdownMenu.Item>
<DropdownMenu.Separator />
<div class="px-2 py-2">
<AppVersion />
</div>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Sidebar.MenuItem>