Módulo 7: Tags, versiones y entrega

Unidad: GIT-09
Público: alumnado de DAW, DAM, ASIR y grados de informática.
Itinerario: tronco, cierre del bloque de corrección y entrega; antes del proyecto final.
Prerrequisitos: módulos 5 y 6; manejar ramas, remotos, push y recuperación.
Objetivo observable: crear, verificar y publicar una versión etiquetada, distinguiendo tag ligero de anotado y justificando por qué una versión publicada no se mueve.
Fuente técnica: documentación oficial de git tag, git describe, git archive y git push.
Duración orientativa: 2,5 horas.

Idea clave

Un tag es un nombre estable para un commit concreto. Nada más, y eso es justo lo que lo hace útil: v1.0.0 no cambia de significado, mientras que main avanza con cada integración.

De ahí salen las tres ideas del módulo:

  • Una rama se mueve; un tag no debería moverse nunca una vez publicado.
  • Los tags no viajan con un git push normal: hay que publicarlos aparte.
  • Etiquetar es marcar, no empaquetar. La entrega —notas de versión, paquete descargable, release— se construye a partir del tag.

Parte 1 — Etiquetar

Partimos de un proyecto con tres commits:

mkdir conversor && cd conversor && git init
printf "# Conversor de unidades\n\nConvierte km a millas.\n" > README.md
git add README.md && git commit -m "docs: crea README"
printf "def km_a_millas(km):\n    return km * 0.621371\n" > conversor.py
git add conversor.py && git commit -m "feat: convierte km a millas"
printf "\n## Uso\n\n    python conversor.py 10\n" >> README.md
git add README.md && git commit -m "docs: documenta el uso"

Antes de etiquetar: el estado importa, y Git no avisa

Comprueba siempre el estado antes de marcar una versión:

git status --short
git log --oneline -3
2ed7be7 docs: documenta el uso
e1b1d83 feat: convierte km a millas
76d03fd docs: crea README

Sin salida en status, el árbol está limpio. Esto no es una formalidad, y conviene entender por qué con una prueba:

printf "basura\n" > temp.txt
git status --short
git tag -a v9.9.9 -m "prueba"
git log --oneline -1 v9.9.9
?? temp.txt
354d3a4 feat: convierte millas a km

Git ha creado el tag sin protestar. El peligro no es que Git te lo impida: es que no dice nada. El tag apunta al último commit e ignora por completo temp.txt. Si ese archivo sin confirmar era parte de lo que acabas de probar, has etiquetado una versión que no es la que funcionaba en tu equipo.

Por eso la comprobación va antes, no después:

git tag -d v9.9.9
rm temp.txt

Tag ligero y tag anotado

Hay dos tipos, y la diferencia es real, no cosmética.

git tag v0.1.0-ligero
git tag -a v1.0.0 -m "Primera versión funcional del conversor"

Un tag ligero es solo un nombre apuntando a un commit. Un tag anotado (-a) es un objeto propio de Git con autor, fecha y mensaje. Compruébalo:

git cat-file -t v0.1.0-ligero
git cat-file -t v1.0.0
commit
tag

El primero es el commit; el segundo es un objeto tag que apunta al commit. Para marcar versiones se usa siempre el anotado: deja constancia de quién publicó la versión y cuándo, que es información que se necesita meses después.

Ver qué hay dentro

git show v1.0.0
tag v1.0.0
Tagger: Alumno Git <alumno@example.test>
Date:   Tue Aug 4 21:50:26 2026 +0200

Primera versión funcional del conversor

commit 2ed7be7d31fd84795c74705f0bc51909692eb34a
Author: Alumno Git <alumno@example.test>
Date:   Tue Aug 4 21:50:26 2026 +0200

    docs: documenta el uso

Primero los datos del tag, después el commit al que apunta. Con un tag ligero esa primera parte no existe.

Etiquetar un commit anterior

No hace falta etiquetar en el momento. Si olvidaste marcar una versión, pásale el hash:

git tag -a v0.9.0 e1b1d83 -m "Versión previa a la documentación"
git log --oneline --decorate -3
2ed7be7 (HEAD -> main, tag: v1.0.0, tag: v0.1.0-ligero) docs: documenta el uso
e1b1d83 (tag: v0.9.0) feat: convierte km a millas
76d03fd docs: crea README

--decorate muestra qué nombres apuntan a cada commit. Un mismo commit puede tener varios tags.

Listar

git tag
git tag -n
v0.1.0-ligero
v1.0.0
v0.1.0-ligero   docs: documenta el uso
v1.0.0          Primera versión funcional del conversor

-n muestra el mensaje. Fíjate en que el ligero no tiene mensaje propio: enseña el del commit, porque no guarda nada más.

Parte 2 — Publicar una versión

git push no publica los tags

Este es el error número uno del módulo. Con un remoto de práctica:

git init --bare ../conversor-remoto.git
git remote add origin ../conversor-remoto.git
git push -u origin main
git ls-remote --tags origin

git ls-remote --tags origin no imprime nada. Los commits están publicados; los tags no. Son referencias distintas y push solo envía la rama que le pides.

Publicar uno, o todos

git push origin v1.0.0
git ls-remote --tags origin
 * [new tag]         v1.0.0 -> v1.0.0
d9a4e6253469e09ac47a0927f13098883f4417cf	refs/tags/v1.0.0
2ed7be7d31fd84795c74705f0bc51909692eb34a	refs/tags/v1.0.0^{}

Esas dos líneas son la mejor demostración de qué es un tag anotado. La primera es el hash del objeto tag; la segunda, con ^{}, es el commit al que apunta. Publica ahora los demás:

git push origin --tags
git ls-remote --tags origin
 * [new tag]         v0.1.0-ligero -> v0.1.0-ligero
 * [new tag]         v0.9.0 -> v0.9.0
2ed7be7d31fd84795c74705f0bc51909692eb34a	refs/tags/v0.1.0-ligero
7d2a050e683946e021bb5afb8c1f449fac3eaa98	refs/tags/v0.9.0
e1b1d83caf1221a805fbeba13fbc44e771238ee6	refs/tags/v0.9.0^{}
d9a4e6253469e09ac47a0927f13098883f4417cf	refs/tags/v1.0.0
2ed7be7d31fd84795c74705f0bc51909692eb34a	refs/tags/v1.0.0^{}

El ligero aparece en una sola línea: no hay objeto intermedio que desreferenciar. Los anotados, en dos.

Publicar tag por tag es más seguro que --tags, que envía también los que solo tenías como prueba local.

Borrar un tag son dos pasos

git tag -d v0.1.0-ligero
git ls-remote --tags origin | grep ligero
Deleted tag 'v0.1.0-ligero' (was 2ed7be7)
2ed7be7d31fd84795c74705f0bc51909692eb34a	refs/tags/v0.1.0-ligero

Borrarlo en local no lo borra del remoto. Hace falta el segundo paso:

git push origin --delete v0.1.0-ligero
 - [deleted]         v0.1.0-ligero

Por qué no se mueve un tag publicado

Esto se repite como norma sin explicar de dónde sale. Vamos a verlo. Un compañero clona el proyecto y ya tiene la versión:

git clone ../conversor-remoto.git ../conversor-compa
cd ../conversor-compa && git log --oneline -1 v1.0.0
2ed7be7 docs: documenta el uso

Mientras tanto, alguien decide «corregir» la versión moviendo el tag a otro commit:

git tag -f -a v1.0.0 -m "Versión inicial (corregida)"
git push -f origin v1.0.0
Updated tag 'v1.0.0' (was d9a4e62)
 + d9a4e62...d28e607 v1.0.0 -> v1.0.0 (forced update)

Ahora el compañero actualiza su copia:

git fetch origin --tags
git log --oneline -1 v1.0.0
   2ed7be7..354d3a4  main       -> origin/main
 ! [rejected]        v1.0.0     -> v1.0.0  (would clobber existing tag)
2ed7be7 docs: documenta el uso

Ahí está el problema, en la palabra clobber. Git se niega a pisar un tag que ya tenía, así que:

  • En el remoto, v1.0.0 apunta a un commit.
  • En el equipo del compañero, v1.0.0 apunta a otro distinto.
  • Ninguno de los dos recibe ningún aviso al trabajar.

Dos personas dicen «tengo la v1.0.0» y tienen código diferente. Un tag solo vale como referencia si significa lo mismo para todo el mundo, y en el momento en que se publica deja de estar bajo tu control.

Solo se actualiza si cada persona fuerza explícitamente:

git fetch origin --tags --force
 t [tag update]      v1.0.0     -> v1.0.0

Pero eso exige avisar a todo el equipo, uno por uno. La salida correcta ante una versión mal publicada es publicar una versión nuevav1.0.1— no reescribir la anterior.

Parte 3 — Nombrar las versiones

Semantic Versioning

Git acepta cualquier nombre de tag. v1.0.0 sigue la convención SemVer, que es la más extendida y la que se usará en el proyecto final:

v  MAJOR . MINOR . PATCH
       1  .   4   .   2
Parte Se incrementa cuando Ejemplo
MAJOR Hay un cambio incompatible con la versión anterior 1.4.22.0.0
MINOR Se añade funcionalidad manteniendo compatibilidad 1.4.21.5.0
PATCH Se corrige un error sin cambiar el comportamiento esperado 1.4.21.4.3

Al subir un nivel, los inferiores vuelven a cero. La v inicial no es parte de SemVer, pero es la costumbre en los tags de Git.

Es una convención, no una regla que Git aplique: nada te impide etiquetar entrega-final-buena. La ventaja de seguirla es que cualquiera entiende, solo mirando el número, si actualizar es arriesgado.

git describe: dónde estoy respecto a la última versión

git describe
v1.0.0-1-g354d3a4

Se lee: 1 commit después de v1.0.0, en el commit 354d3a4 (la g indica git). Si estuvieras justo en el tag, imprimiría solo v1.0.0.

Es lo que se usa para poner un número de versión automático en un paquete o en una compilación, y de paso responde a la pregunta «¿esto que tengo delante es exactamente la versión publicada o lleva cambios encima?».

Parte 4 — Entregar

Situarse en una versión

Para ver el proyecto tal y como estaba en una versión:

git switch --detach v0.9.0
git status -sb
cat conversor.py
HEAD is now at e1b1d83 feat: convierte km a millas
## HEAD (no branch)
def km_a_millas(km):
    return km * 0.621371

## HEAD (no branch) significa HEAD separado: estás en un commit, no en una rama. Es correcto para mirar, no para trabajar — los commits que hicieras aquí no pertenecerían a ninguna rama. Vuelve con:

git switch -

El guion significa «la rama anterior». Si necesitas trabajar a partir de esa versión —por ejemplo, para un parche—, crea una rama: git switch -c hotfix/v0.9.1 v0.9.0.

Notas de versión desde el historial

Los tags convierten el historial en un changelog. Los commits entre dos versiones son:

git log --oneline v0.9.0..v1.0.0
git log --pretty=format:"- %s" v0.9.0..v1.0.0
354d3a4 feat: convierte millas a km
2ed7be7 docs: documenta el uso
- feat: convierte millas a km
- docs: documenta el uso

El segundo comando ya es una lista pegable en un CHANGELOG.md. Aquí se cobra el trabajo de escribir buenos mensajes de commit desde el módulo 3: si los mensajes son cambios y arreglos, no hay notas de versión que sacar.

Empaquetar con git archive

Para entregar la versión a alguien que no usa Git:

git archive --format=zip --output=../conversor-v0.9.0.zip v0.9.0
unzip -l ../conversor-v0.9.0.zip
       48  08-04-2026 21:50   README.md
       46  08-04-2026 21:50   conversor.py

El zip contiene el proyecto tal como estaba en v0.9.0, sin el directorio .git y sin los archivos posteriores. Es la forma limpia de entregar una versión concreta.

Releases en GitHub

Al publicar un tag en GitHub, la forja lo detecta y permite crear una release: una página con el nombre de la versión, las notas y archivos adjuntos descargables. GitHub genera además automáticamente el zip y el tar.gz del código.

Como en el módulo 5, conviene tener claro el reparto: el tag lo pone Git; la release la pone GitHub. Si borras la release, el tag sigue existiendo; y un repositorio con tags que nunca se publicaron no tendrá ninguna release.

Errores frecuentes

  • «He hecho git push y el tag no aparece en el remoto».
    push envía la rama, no los tags. Publícalo con git push origin v1.0.0, y verifica con git ls-remote --tags origin.

  • «He etiquetado con archivos sin confirmar».
    Git no protesta: el tag apunta al último commit e ignora lo que no esté confirmado. Puedes acabar entregando algo distinto de lo que probaste. git status antes.

  • «Me equivoqué en la versión, muevo el tag».
    Quien ya la tenga no recibirá el cambio: su fetch responderá would clobber existing tag y se quedará con la versión antigua bajo el mismo nombre. Publica v1.0.1.

  • «He borrado el tag y sigue en el remoto».
    Son dos operaciones: git tag -d en local y git push origin --delete en el remoto.

  • «Uso tags ligeros porque son más rápidos de escribir».
    No guardan autor, fecha ni mensaje. Para marcar una versión, siempre -a.

  • «Estoy en HEAD separado y he hecho commits».
    No pertenecen a ninguna rama. Antes de cambiar de sitio, git switch -c <rama> para conservarlos; si ya te has movido, git reflog (módulo 6).

Ejercicio y criterios

Sobre un repositorio agenda-taller con un remoto de práctica y al menos cuatro commits con mensajes descriptivos:

  1. Verifica que el árbol está limpio y publica un tag anotado v0.1.0. Demuestra con git cat-file -t que es un objeto tag y no un commit.
  2. Comprueba que tras git push origin main el tag no está en el remoto, y publícalo después.
  3. Añade dos commits más y publica v0.2.0. Genera las notas de versión con git log --pretty=format:"- %s" v0.1.0..v0.2.0.
  4. Etiqueta retroactivamente un commit anterior como v0.0.1 y publícalo.
  5. Ejecuta git describe desde un commit posterior a v0.2.0 y explica cada parte de la salida.
  6. Clona el repositorio en otra carpeta. Desde el original, mueve v0.2.0 a otro commit y fuerza su publicación. Haz fetch en el clon y captura el mensaje de rechazo. Explica qué versión tiene cada copia y cómo debería haberse resuelto.
  7. Empaqueta v0.1.0 con git archive y comprueba que el contenido corresponde a esa versión y no a la actual.

Criterios de revisión:

  • Todos los tags de versión son anotados, con mensaje.
  • Se evidencia con salidas reales que push de la rama no publica tags, y que ls-remote los muestra después.
  • Las notas de versión salen del historial, no escritas a mano.
  • El punto 6 incluye el mensaje would clobber existing tag y una explicación de por qué la solución correcta era publicar v0.2.1.
  • El zip del punto 7 no contiene los archivos añadidos después de v0.1.0.
  • El estado final del repositorio es limpio.

Resumen

Al finalizar, el alumnado debe saber:

  • Explicar que un tag es un nombre estable para un commit, frente a una rama que se mueve.
  • Comprobar el estado antes de etiquetar, sabiendo que Git no avisa si está sucio.
  • Distinguir tag ligero de anotado, y usar siempre -a para versiones.
  • Publicar y borrar tags como operaciones independientes de la rama.
  • Justificar, con la evidencia del rechazo would clobber, por qué una versión publicada no se mueve.
  • Nombrar versiones con Semantic Versioning y leer la salida de git describe.
  • Situarse en una versión con HEAD separado sin perder trabajo.
  • Generar notas de versión desde el historial y empaquetar una entrega con git archive.
  • Distinguir el tag de Git de la release de GitHub.

Siguiente módulo: Módulo 8 · Proyecto final colaborativo y evaluación.

Estado editorial: listo.

Registro de revisión técnica

Revisado el 4 de agosto de 2026 con Git 2.47.1. Todas las secuencias se han reproducido desde una carpeta vacía, con identidad de práctica y configuración aislada. Se ha comprobado que:

  • git cat-file -t devuelve commit para un tag ligero y tag para uno anotado, y git show de un anotado antepone las líneas tag, Tagger y el mensaje al commit.
  • Tras git push -u origin main, git ls-remote --tags origin no devuelve nada; los tags requieren su propio push.
  • En ls-remote, un tag anotado ocupa dos líneas (el objeto y su desreferencia ^{}) y uno ligero, una. Es la evidencia más directa de la diferencia entre ambos.
  • git tag -d no afecta al remoto; hace falta git push origin --delete <tag>.
  • Demostración del tag movido: con un segundo clon que ya tenía v1.0.0, un git tag -f + git push -f en el original hace que el git fetch origin --tags del clon responda ! [rejected] v1.0.0 -> v1.0.0 (would clobber existing tag) y conserve el commit antiguo. Solo --force en el fetch lo actualiza. Queda así justificada con evidencia la norma de no mover versiones publicadas.
  • Etiquetar con el árbol sucio no produce ningún aviso: el tag apunta al último commit e ignora los archivos sin confirmar. Se ha corregido en consecuencia la formulación del error frecuente, que antes daba a entender que Git lo impedía.
  • git describe devuelve v1.0.0-1-g354d3a4 un commit después del tag.
  • git switch --detach v0.9.0 deja git status -sb en ## HEAD (no branch) y el contenido del archivo en el estado de esa versión; git switch - devuelve a la rama previa.
  • git archive --format=zip v0.9.0 genera un paquete con los dos archivos de esa versión, sin .git ni los cambios posteriores.

Los hashes de las salidas son los de esta ejecución y serán distintos en cada repositorio.

Pendiente al llevar el módulo a GitHub: crear una release real desde un tag publicado en el repositorio del curso y añadir la captura o la descripción del resultado, para cerrar la sección «Releases en GitHub» con evidencia propia.