08/08
Deployment final y defensa oral en vivo
Última semana del curso: no se agrega código nuevo, se congela lo que ya construiste en las 7 semanas anteriores. Se hace una auditoría final, se etiqueta la versión definitiva en GitHub, y cada alumno sustenta su proyecto completo frente al docente en una defensa oral de 3-5 minutos.
u4s8-apellidopaterno-apellidomaterno-nombres con su Release v1.0 y su Web Service en https://u4s8-apellidopaterno-apellidomaterno-nombres.onrender.com — sistema completo, público, con login, CRUD y datos reales, defendido en vivo ante el docente.
Auditoría final: variables de entorno, backup del .sql, README actualizado
Antes de "cerrar" el proyecto, tres verificaciones rápidas para no perder nada y dejar todo documentado.
Repasar el checklist de seguridad de la Semana 6
El mismo checklist de la Semana 6, ahora como cierre. En el dashboard de Render abre tu servicio u4s7-… → pestaña Environment y confirma los 5 puntos: DEBUG=False; SECRET_KEY propia de producción (distinta a la de tu .env); ALLOWED_HOSTS con tu dominio exacto (sin https://); CSRF_TRUSTED_ORIGINS con tu dirección completa (con https://); y el candado de HTTPS en el navegador. Deben estar las 9 variables (las 5 DB_ más esas 4); si dudas qué lleva cada una, revisa la tabla de la Semana 6, paso 05.6. Las mismas 9 las cargarás en el servicio nuevo u4s8-… del paso 02.
Backup de la base de datos
El respaldo se hace desde tu computadora, que está conectada a la misma base de Aiven que usa Render. Opción A (recomendada, no instala nada): Django exporta todos tus usuarios y servicios a un archivo backup_final.json. En la terminal de VS Code, con (venv) activo y en la carpeta que tiene manage.py:
# dumpdata: exporta los datos de tu base (la de Aiven, la misma que usa Render) a un archivo JSON
# -X utf8: obliga a Python a escribir en UTF-8; sin esto, en Windows los acentos y la ñ dañan el archivo y no se puede restaurar
# --natural-foreign / --natural-primary: guarda a los usuarios por su nombre, para que el respaldo funcione en cualquier base
# --exclude ...: deja fuera tablas internas que Django vuelve a crear solo (tipos de contenido, permisos, bitácora del admin y sesiones)
# --output ..\backup_final.json: guarda el archivo en la carpeta de ARRIBA, fuera del repositorio, para que nunca se suba a GitHub
python -X utf8 manage.py dumpdata --natural-foreign --natural-primary --exclude contenttypes --exclude auth.permission --exclude admin.logentry --exclude sessions --indent 2 --output ..\backup_final.json
# Muestra el archivo creado: revisa que la columna Length (tamaño) no sea 0
Get-Item ..\backup_final.json
# Confirma que el respaldo NO quedó dentro del proyecto: backup_final.json no debe aparecer en la lista
git status
El archivo queda en la carpeta que está arriba de tu proyecto (por eso ..\), así git add . nunca lo toma. Cópialo también a una memoria USB o a tu Drive personal. Si algún día necesitas restaurarlo en una base vacía:
# SOLO si algún día se pierden los datos: primero crea las tablas vacías en la base nueva
python manage.py migrate
# loaddata: vuelve a cargar usuarios y servicios desde el respaldo (-X utf8 para leer bien los acentos)
python -X utf8 manage.py loaddata ..\backup_final.json
Opción B (archivo .sql con mysqldump): mysqldump no viene con Python ni con Windows. Descarga de https://dev.mysql.com/downloads/mysql/ el paquete «Windows (x86, 64-bit), ZIP Archive», descomprímelo en C:\mysql (no hace falta instalar ni encender un servidor MySQL) y usa la herramienta que está en C:\mysql\bin. Si al correrla no imprime nada o dice que falta un archivo .dll, instala el «Microsoft Visual C++ Redistributable» (x64) y vuelve a intentar.
# Confirma que mysqldump funciona: debe imprimir su versión (si descomprimiste en otra carpeta, cambia C:\mysql)
C:\mysql\bin\mysqldump.exe --version
# --single-transaction: copia los datos como estaban en un solo instante, sin bloquear la base (lo recomienda Aiven)
# --set-gtid-purged=OFF: no incluye datos internos de replicación de Aiven (lo recomienda Aiven)
# -h / -P / -u: host, puerto y usuario, los mismos de tu .env · -p: al dar Enter pide la contraseña sin que se vea
# --result-file: escribe el archivo directo; con ">" en PowerShell el archivo sale en otra codificación y no se puede restaurar
# defaultdb: el nombre de tu base (DB_NAME); el archivo queda en la carpeta de arriba, fuera del repositorio
C:\mysql\bin\mysqldump.exe --single-transaction --set-gtid-purged=OFF -h tu-host.aivencloud.com -P 12345 -u avnadmin -p --result-file=..\backup_final.sql defaultdb
Al dar Enter te pide la contraseña: pega la de Aiven (DB_PASSWORD) y da Enter; no se ve mientras la escribes. Si sale Access denied … PROCESS privilege, agrega --no-tablespaces antes de --result-file.
Completar el README definitivo
Abre README.md (en la carpeta que tiene manage.py), selecciona todo con Ctrl + A, bórralo, pega esto y completa lo que está entre corchetes. Se sube a GitHub en el paso 02.
<!-- título del repo: cambia por tus nombres reales -->
# U4S8 — [Apellido Paterno] [Apellido Materno] [Nombre(s)]
<!-- sección con la URL pública de Render -->
## Demo en vivo (producción)
<!-- tu dirección real de Render de esta práctica -->
https://u4s8-hernandez-nunez-maria-jose.onrender.com
<!-- sección que explica el proyecto a quien lo visite -->
## Qué hace este proyecto
<!-- reemplaza el texto entre corchetes por tu descripción -->
[1-2 párrafos describiendo tu vitrina de servicios]
<!-- sección con las tecnologías usadas -->
## Stack técnico
<!-- las 4 herramientas del curso -->
Django + MySQL (Aiven) + Tailwind CSS + Render
<!-- sección con la prueba de seguridad de la Semana 6 -->
## Cómo se prueba el aislamiento por usuario
<!-- pega aquí la explicación que escribiste en el README de la Semana 6 -->
[Copiar la explicación que ya escribiste en la Semana 6]
<!-- sección con el recorrido del curso -->
## Historial de unidades
<!-- Unidad 1 -->
- Unidad 1: portafolio estático con HTML5 + Tailwind
<!-- Unidad 2 -->
- Unidad 2: modelo Servicio migrado a MySQL, deploy dinámico en Render
<!-- Unidad 3 -->
- Unidad 3: CRUD completo con ModelForms + autenticación y sesiones
<!-- Unidad 4 -->
- Unidad 4: paginación, búsqueda, UI review y defensa final
Nunca subas backup_final.json ni backup_final.sql a un repositorio público: suele contener datos reales de usuarios (aunque sean de prueba) y credenciales indirectas.
Última migración en producción y congelamiento de versión
Un tag de Git marca un punto exacto del historial como "la versión que se entrega" — así siempre puedes volver a ver (o restaurar) exactamente el código que se defendió hoy.
Confirmar que no queda ninguna migración pendiente
# Lista las migraciones de core en la base de Aiven: todas deben tener [X] (aplicadas)
python manage.py showmigrations core
# migrate sin cambios pendientes no rompe nada; debe responder "No migrations to apply"
python manage.py migrate
En el plan Free de Render no hay pestaña Shell: en producción migrate corre solo, al final del Build Command. Para confirmarlo, abre tu servicio en Render → Logs del último deploy y busca No migrations to apply. Tu compu y Render usan la misma base de Aiven, así que lo que ves aquí es lo mismo que hay en producción.
Crear el repositorio u4s8-… y subir los cambios
Cada práctica va en su propio repositorio y en su propio Web Service de Render, con el nombre u4s8-apellidopaterno-apellidomaterno-nombres (minúsculas, sin acentos, ñ → n, espacios → guion). Ej.: u4s8-hernandez-nunez-maria-jose. El repo de la Semana 7 (u4s7-…) se queda intacto como evidencia.
La regla del nombre · siempre igual (repo de GitHub y servicio de Render)
- Prefijo de la práctica:
u+ número de unidad +s+ número de semana. Esta práctica esu4s8. - Todo en minúsculas, sin acentos (
á → a), lañse escribeny los espacios se cambian por guion-. - Orden fijo: prefijo → apellido paterno → apellido materno → nombre(s). Si tienes dos nombres, van los dos.
- El mismo nombre se usa en dos lugares: como Repository name en GitHub y como Name del servicio en Render (paso 02.3). Así tu URL queda
https://u4s8-….onrender.com. - GitHub solo acepta letras sin acento, números,
-,_y.(máximo 100 caracteres). Por eso la regla quita acentos y espacios.
| Alumno | Nombre del repositorio y del servicio de Render |
|---|---|
| María José Hernández Núñez | u4s8-hernandez-nunez-maria-jose |
| José Ángel De la Cruz Peña | u4s8-de-la-cruz-pena-jose-angel |
| Ana López (un solo apellido) | u4s8-lopez-ana |
| Prefijo | Práctica del curso |
|---|---|
u1s1 | Unidad 1 · Semana 1 — Sitio estático en Render |
u1s2 | Unidad 1 · Semana 2 — Tailwind CSS |
u2s3 | Unidad 2 · Semana 3 — Django + MySQL en Aiven |
u2s4 | Unidad 2 · Semana 4 — Modelos y deploy en Render |
u3s5 | Unidad 3 · Semana 5 — CRUD con ModelForms |
u3s6 | Unidad 3 · Semana 6 — Autenticación y sesiones |
u4s7 | Unidad 4 · Semana 7 — Pulido y defensa |
u4s8 | Unidad 4 · Semana 8 — Deploy final (esta guía) |
Genera tu nombre sin errores
u4s8-hernandez-nunez-maria-joseTu repo quedará en https://github.com/TU_USUARIO/u4s8-hernandez-nunez-maria-jose y, con el mismo nombre en Render (paso 02.3), tu sitio en https://u4s8-hernandez-nunez-maria-jose.onrender.com. Los datos de ejemplo están precargados: escribe los tuyos y usa «Copiar nombre».
- Entra a
https://github.comy confirma arriba a la derecha que es tu cuenta. - Clic en + → New repository. En Repository name pega el nombre que copiaste del generador de arriba (
u4s8-…). - Déjalo en Public y no marques «Add a README», «.gitignore» ni «license» (el repo debe nacer vacío). Clic en Create repository y copia la URL que termina en
.git.
# Muestra a qué repo apunta hoy tu proyecto (debe salir el u4s7-… de la Semana 7)
git remote -v
# Cambia "origin" al repo nuevo de esta práctica; pega TU URL (set-url porque origin ya existe)
git remote set-url origin https://github.com/TU_USUARIO/u4s8-hernandez-nunez-maria-jose.git
# Confirma que ahora apunta al u4s8-… (sale dos veces: fetch y push)
git remote -v
# Lista lo que cambió: NO deben aparecer .env, venv/ ni staticfiles/
git status
# Agrega todos los archivos nuevos y modificados (incluye core/migrations/ si hubo migración)
git add .
# Crea el commit con un mensaje que describe el cambio
git commit -m "U4S8: version final del curso (README definitivo)"
# Sube la rama main al repo nuevo; -u la deja vinculada para que después baste con "git push"
git push -u origin main
Recarga la página del repo en GitHub: deben verse manage.py, requirements.txt, manage.py, core/views.py y tus plantillas. Si ves .env, avisa al profesor antes de seguir (hay que cambiar la contraseña de Aiven).
Crear el Web Service en Render, campo por campo
- Abre
https://dashboard.render.come inicia sesión con GitHub. - Arriba a la derecha: + New → Web Service. No elijas «Static Site».
- En «Source Code» elige Git Provider, busca tu repo
u4s8-…(no elu4s7-…) y clic en Connect. - Si el repo no aparece: clic en Configure GitHub, en «Repository access» agrega
u4s8-…, guarda y regresa a Render.
Llena solo estos campos; el resto déjalo como viene:
| Campo | Qué escribir | Por qué |
|---|---|---|
| Name | El mismo nombre del repo, el que copiaste del generador (ej. u4s8-hernandez-nunez-maria-jose) | Se vuelve tu dirección: https://u4s8-hernandez-nunez-maria-jose.onrender.com. Anótala; la usas en 02.4. |
| Language | Python 3 | Render lo detecta por requirements.txt; si no, elígelo. |
| Branch | main | Cada git push a esta rama redespliega solo. |
| Region | La que venga (ej. Oregon (US West)) | Dónde está el servidor. Cualquiera sirve. |
| Root Directory | Vacío | Solo se llena si manage.py está en una subcarpeta (ej. backend). |
| Build Command | El del bloque de abajo | Se corre una vez en cada deploy: instala, junta estáticos y migra. |
| Start Command | El del bloque de abajo | Se queda corriendo siempre para atender visitas. |
| Instance Type | Free | Gratis; alcanza para el curso. |
# 1) instala requirements.txt 2) junta los estáticos para whitenoise 3) aplica migraciones en Aiven; "&&" = "si el anterior salió bien, sigue"
pip install -r requirements.txt && python manage.py collectstatic --noinput && python manage.py migrate
# arranca gunicorn con miproyecto/wsgi.py; cambia "miproyecto" por la carpeta que tiene settings.py
gunicorn miproyecto.wsgi
¿Qué va en lugar de miproyecto? En VS Code abre settings.py, busca con Ctrl + F WSGI_APPLICATION y copia lo que está antes de .application. Ej.: WSGI_APPLICATION = 'miproyecto.wsgi.application' → gunicorn miproyecto.wsgi. Es el mismo Start Command que usaste desde la Semana 4.
Las 9 variables de entorno: nombre, valor y de dónde sale
Render no tiene tu .env (no se sube a GitHub). En el mismo formulario baja a «Environment Variables». Los nombres (Key) deben escribirse exactamente así, en mayúsculas, porque settings.py los busca con config('NOMBRE'):
| Key (nombre exacto) | Value (qué debe tener) | De dónde lo sacas |
|---|---|---|
DB_NAME | Nombre de la base, normalmente defaultdb | Aiven → tu servicio MySQL → Overview → «Database name». Igual que en tu .env. |
DB_USER | Usuario de MySQL, normalmente avnadmin | Aiven → Overview → «User». Igual que en tu .env. |
DB_PASSWORD | La contraseña real de Aiven | Aiven → Overview → «Password» (icono del ojo). Igual que en tu .env. |
DB_HOST | El host, termina en .aivencloud.com, sin https:// | Aiven → Overview → «Host». Igual que en tu .env. |
DB_PORT | Un número de 5 dígitos (ej. 12345), no 3306 | Aiven → Overview → «Port». Igual que en tu .env. |
SECRET_KEY | Una clave larga nueva, distinta de la local | Se genera en la terminal de VS Code (bloque de abajo). No la reutilices. |
DEBUG | False (con F mayúscula) | En producción siempre False. En tu .env local va True. |
ALLOWED_HOSTS | u4s8-hernandez-nunez-maria-jose.onrender.com | Tu dominio de Render: sin https:// y sin / al final. |
CSRF_TRUSTED_ORIGINS | https://u4s8-hernandez-nunez-maria-jose.onrender.com | La dirección completa: con https:// y sin / al final. Sin ella, el registro, el login y los formularios del CRUD dan «403 CSRF». |
# Imprime una clave aleatoria de ~67 caracteres; cópiala completa y pégala como valor de SECRET_KEY en Render
python -c "import secrets; print(secrets.token_urlsafe(50))"
Para cargarlas tienes dos formas: rápida, clic en Add from .env, pega el bloque de abajo con tus valores y clic en Add variables; o manual, clic 9 veces en + Add Environment Variable escribiendo Key y Value.
# nombre de la base de Aiven (cópialo de tu .env local)
DB_NAME=defaultdb
# usuario de Aiven (cópialo de tu .env local)
DB_USER=avnadmin
# contraseña real de Aiven (cópiala de tu .env local)
DB_PASSWORD=tu-password-real-de-aiven
# host real de Aiven, sin https:// (cópialo de tu .env local)
DB_HOST=tu-host-real.aivencloud.com
# puerto real de Aiven (cópialo de tu .env local)
DB_PORT=12345
# clave NUEVA generada con el comando de arriba, solo para Render
SECRET_KEY=pega-aqui-la-clave-nueva
# en producción SIEMPRE False
DEBUG=False
# tu dominio de esta práctica: SIN https:// y SIN / al final
ALLOWED_HOSTS=u4s8-hernandez-nunez-maria-jose.onrender.com
# tu dirección completa: CON https:// y SIN / al final
CSRF_TRUSTED_ORIGINS=https://u4s8-hernandez-nunez-maria-jose.onrender.com
En «Add from .env» Render puede tomar las líneas que empiezan con # como comentarios, pero para no arriesgarte pega solo las 9 líneas NOMBRE=valor, sin los comentarios. Sin espacios antes ni después del =, y sin comillas.
Si el nombre ya estaba ocupado, Render le agrega letras (ej. u4s8-...-a1b2.onrender.com). Al terminar, copia la dirección real que aparece debajo del nombre del servicio y corrige ALLOWED_HOSTS y CSRF_TRUSTED_ORIGINS en la pestaña Environment → Edit → Save, rebuild, and deploy.
Desplegar y leer el log
Clic en Deploy Web Service. Tarda de 2 a 5 minutos. En el log debes ver, en este orden:
# 1. Render descarga tu repo u4s8-… de GitHub
==> Cloning from https://github.com/tu-usuario/u4s8-hernandez-nunez-maria-jose
# 2. empieza tu Build Command
==> Running build command 'pip install -r requirements.txt && ...'
# 3. pip instaló Django, PyMySQL, gunicorn, whitenoise, python-decouple...
Successfully installed Django-5.1.4 PyMySQL-1.1.1 gunicorn-23.0.0 ...
# 4. collectstatic juntó tus CSS/JS e imágenes
127 static files copied to '/opt/render/project/src/staticfiles', 381 post-processed.
# 5. migrate se conectó a Aiven; si no cambiaste modelos, no hay nada pendiente (correcto)
No migrations to apply.
# 6. el build terminó bien
==> Build successful 🎉
# 7. arranca tu Start Command
==> Running 'gunicorn miproyecto.wsgi'
# 8. el estado cambia a "Live" en verde: tu sitio ya responde
==> Your service is live 🎉
| Error en el log o en el navegador | Causa | Solución |
|---|---|---|
SECRET_KEY not found (u otra variable) | Falta una variable o el nombre tiene un error de dedo | Pestaña Environment: revisa las 9 y sus nombres exactos. |
ModuleNotFoundError: No module named 'miproyecto' | Start Command con otro nombre de carpeta | Settings → «Start Command»: corrígelo (ver 02.3). |
Can't connect to MySQL server / Access denied | Algún DB_ mal copiado o Aiven apagado | Compara con tu .env y revisa que Aiven diga «Running». |
Bad Request (400) al abrir la página | ALLOWED_HOSTS no coincide con tu dominio | Corrígelo sin https:// y sin /. |
Prohibido (403) · CSRF verification failed al guardar un formulario | Falta o está mal CSRF_TRUSTED_ORIGINS | Debe ser https://tu-dominio.onrender.com, sin / final. |
TemplateDoesNotExist: core/login.html | El template no se subió o tiene otro nombre | Revisa en GitHub la carpeta core/templates/core/. |
Table '...core_servicio' doesn't exist / Unknown column | Cambiaste el modelo y no se subió la migración | Repite 02.1 y sube core/migrations/ con git push. |
Al entrar a /servicios/ sin sesión manda a /accounts/login/ (404) | Falta LOGIN_URL en settings.py | Agrega LOGIN_URL = 'login' (Semana 6, paso 03) y haz git push. |
Después de corregir algo en Environment, Render redespliega solo. Si corregiste código, haz git push; o usa Manual Deploy → Deploy latest commit.
Probar la versión final en producción
- Abre
https://u4s8-tus-apellidos-nombres.onrender.com/servicios/: debe pedirte iniciar sesión y, al entrar, mostrar tus servicios con paginación y búsqueda (Semana 7). - Prueba una vez el ciclo completo: crear, editar y eliminar un servicio; cerrar sesión; registrarte con una cuenta nueva y confirmar que no ve tus datos.
- Confirma el candado de HTTPS y que
/no-existe/muestra un «Not Found» simple, sin la página amarilla de Django. - Esta es la URL que vas a mostrar en la defensa (paso 03). Ábrela un par de minutos antes: en el plan Free, Render «duerme» el sitio tras 15 minutos sin visitas.
Crear un tag anotado con la versión final
Hazlo después de que el push del README esté en GitHub y el deploy diga «Live»: el tag marca exactamente ese último commit.
# Confirma que origin apunta al repo u4s8-… (el de esta práctica): el tag se sube ahí
git remote -v
# -a: crea un tag "anotado" (con mensaje y fecha) · v1.0: nombre estándar de una versión final · -m: mensaje descriptivo
git tag -a v1.0 -m "Version final entregada - Unidad 4, Semana 8"
# Muestra los tags que existen en tu proyecto; debe aparecer v1.0
git tag
# push del tag: por defecto git push NO sube los tags, hay que pedirlo explícitamente
git push origin v1.0
Publicar un Release en GitHub
En tu repositorio u4s8-… de GitHub: en la columna derecha, sección "Releases" → "Draft a new release" → elige el tag v1.0 → completa un título (ej. "Entrega final del curso") y una breve descripción → "Publish release".
Un Release con tag es la forma profesional de decir "esta es la versión oficial 1.0" — es exactamente lo que verías en cualquier proyecto de software real antes de entregarlo a un cliente.
Defensa oral express por alumno (3-5 min): estructura y timing
Cada alumno pasa por turno. Es el mismo guion de 4 bloques que preparaste y ensayaste en la Semana 7 — hoy se presenta ante el docente, con posible live coding y preguntas conceptuales de cualquiera de las 4 Unidades.
Mostrar la URL de producción (u4s8-…)
Abre la vitrina publicada, muestra el candado de HTTPS y recorre el listado (con paginación y búsqueda de la Semana 7).
Recitar el guion de 2 minutos de la Semana 7
Qué hace el sistema, qué decisiones técnicas tomaste y por qué, un desafío real que resolviste.
Live coding o pregunta conceptual (a criterio del docente)
Puede ser cualquiera de los retos ya practicados en el simulacro de la Semana 7, sobre cualquiera de las 4 Unidades.
Cierre
Muestra el Release v1.0 en GitHub como evidencia final de la entrega.
La calificación final aplica la Matriz de Evaluación Anti-IA completa: Live Coding 40%, examen conceptual 25%, commits 20%, entregable publicado 15% — repasa la lista de cotejo de las 3 Unidades técnicas si te queda alguna duda puntual.
Retroalimentación grupal y cierre del curso
Los últimos 30 minutos son para el grupo completo, no para un alumno individual.
Ronda de retroalimentación
El docente comparte patrones observados en las 8 semanas (errores comunes, buenas prácticas destacadas) sin señalar proyectos individuales.
Repaso del camino completo
De un portafolio HTML estático (Semana 1) a una aplicación Django con MySQL, CRUD y autenticación en producción (Semana 8) — el mismo proyecto, creciendo unidad por unidad.
Portafolio final para compartir
Con la plantilla de portafolio (Anexo D), arma una página con los 8 enlaces del curso — lista para compartir en LinkedIn o con reclutadores.
Entregar el portafolio final
Marca cada punto a medida que lo completas — queda guardado en este navegador.
Entrega final del curso
Último push
# Revisa que no haya cambios sin subir (debe decir "nothing to commit, working tree clean")
git status
# Solo si cambiaste algo después del tag: prepara el README modificado
git add README.md
# Guarda el cambio con su mensaje
git commit -m "README final: cierre del curso"
# Sube el commit al repo u4s8-…; Render redespliega solo
git push
Entregar
Pega en la tarea final del curso: el enlace del Release v1.0 (ej. https://github.com/tu-usuario/u4s8-hernandez-nunez-maria-jose/releases/tag/v1.0), la URL pública de Render (ej. https://u4s8-hernandez-nunez-maria-jose.onrender.com) y el enlace de tu portafolio final.
Tabla de referencias — de dónde sale toda la información
Todo lo de esta guía viene de documentación oficial de cada herramienta, no de blogs de terceros.
| Herramienta | Para qué | Dónde |
|---|---|---|
| MySQL — mysqldump | Exportar un respaldo completo de la base de datos | dev.mysql.com — mysqldump |
| Aiven — Backup y restore | Respaldos automáticos ya incluidos en el servicio gestionado | aiven.io — Backup/restore |
| Django — Deployment checklist | Repaso final de configuración segura de producción | docs.djangoproject.com — Deployment checklist |
| Git — Tagging | Marcar un punto exacto del historial como versión final | git-scm.com — Tagging |
| GitHub — Releases | Publicar una versión oficial del proyecto | docs.github.com — Releases |