Por qué el versionado manual es un dolor para cada desarrollador Go
\n¿Alguna vez te has sorprendido pensando: «¿Qué versión está funcionando ahora en producción?» o has perdido una hora para entender qué commit causó un error en un binario compilado hace un mes? Si es así, has llegado al lugar indicado. La pregunta «Application auto build versioning» es una de las más populares en Stack Overflow (miles de visitas y decenas de soluciones). Y con razón: en el desarrollo moderno, donde los pipelines de CI/CD ejecutan compilaciones cada 10 minutos, el versionado manual es un camino hacia el caos.
\nEn este artículo analizaremos el versionado automático de compilaciones en Go de principio a fin. Aprenderás cómo implementar un sistema que asigna automáticamente el número de versión, el hash del commit, la fecha de compilación e incluso una bandera de repositorio «sucio». El código funcionará tanto en desarrollo local como en GitHub Actions, GitLab CI o Jenkins.
\n\nEl problema: ¿qué tiene de malo el version.go manual?
\nMuchos desarrolladores principiantes crean un archivo version.go y escriben:
package main\n\nvar Version = "1.0.0"\nY esto funciona exactamente hasta el primer commit. Luego comienza:
\n- \n
- «Olvidé actualizar la versión antes de compilar» \n
- «En producción hay una versión, en el código hay otra» \n
- «¿Qué commit corresponde a esta compilación?» \n
El problema es que la versión son metadatos de la compilación, no una parte del código fuente. Debe generarse automáticamente en la etapa de compilación.
\n\nLa solución: ldflags — el arma secreta de Go
\nEl compilador de Go permite «inyectar» valores de variables durante la compilación mediante la bandera -ldflags. Esto es el auto build versioning en su forma clásica. No almacenamos la versión en el código — se la pasamos al compilador.
Paso 1: Definimos la estructura de la versión
\nCreemos un paquete version con variables que se completarán en la etapa de compilación:
// version/version.go\npackage version\n\nvar (\n // Version — versión semántica (se asigna manualmente o mediante git tag)\n Version = "dev"\n\n // Commit — hash corto del commit\n Commit = "none"\n\n // BuildTime — hora de compilación en formato ISO 8601\n BuildTime = "unknown"\n\n // IsDirty — bandera que indica que el repositorio tiene cambios sin commit\n IsDirty = "false"\n)\n\nfunc String() string {\n return "v" + Version + " (commit: " + Commit + ", built: " + BuildTime + ")"\n}\n\nPaso 2: Compilamos con ldflags
\nAhora la parte interesante. En lugar de go build usaremos un script o Makefile que calcule los valores necesarios y se los pase al compilador:
#!/bin/bash\n# build.sh\n\nVERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")\nCOMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "none")\nBUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")\nIS_DIRTY=$(git diff --quiet || echo "true")\n\ngo build -ldflags="\\\n -X 'main/version.Version=$VERSION' \\\n -X 'main/version.Commit=$COMMIT' \\\n -X 'main/version.BuildTime=$BUILD_TIME' \\\n -X 'main/version.IsDirty=$IS_DIRTY'" \\\n -o myapp .\nImportante: La ruta main/version.Version depende de la estructura de tu proyecto. Si el paquete version está en la raíz — escribe main/version.Version. Si está en un directorio separado — github.com/tu-usuario/proyecto/version.Version.
Paso 3: Lo usamos en la aplicación
\nAhora, en cualquier parte del programa se puede mostrar la versión:
\n// main.go\npackage main\n\nimport (\n "fmt"\n "main/version"\n)\n\nfunc main() {\n fmt.Println("App version:", version.String())\n fmt.Println("Is dirty repo:", version.IsDirty)\n}\nCompila y ejecuta:
\n$ ./build.sh\n$ ./myapp\nApp version: v1.2.3 (commit: a1b2c3d, built: 2024-03-15T10:30:00Z)\nIs dirty repo: false\n\n