Archivar en vez de borrar: 38.000 versiones movidas sin romper un solo build, de forma automática y reversible.
El problema
En Ualá, sólo nuestro equipo de Android maneja más de 30 repositorios y cada uno publica su propia librería a JFrog Artifactory. Con cada release, se sube una nueva versión a libs-release-local, pero las versiones anteriores nunca se eliminan.
Con el tiempo, algunos artifacts acumularon más de 50 versiones. Como todas seguían disponibles en el repositorio que Gradle resuelve, nada impedía que un equipo usara una versión de 6 meses o más.
Las consecuencias fueron:
PRs con dependencias desactualizadas que pasaban code review sin problemas.
Bugs que ya estaban resueltos, pero que seguían apareciendo.
JFrog cada vez más pesado y difícil de navegar.
Ningún incentivo real para que los equipos actualizaran.
La limpieza manual no era viable, ya que con más de 30 repositorios y múltiples artifacts por cada uno, el riesgo de borrar una versión en uso era inaceptable.
La idea: archivar en vez de borrar
La clave fue cambiar el enfoque. En lugar de eliminar versiones, las movemos a un repositorio de archivo (android-libs-archive) que no está incluido en el repositorio virtual de Gradle.
Desde el punto de vista de Android Studio y el build de CI, esas versiones dejan de existir. Sin embargo, siguen vivas en JFrog, por lo que, si algo sale mal, se pueden restaurar en minutos.
Esto nos aportó tres beneficios clave:
Seguridad: nada se pierde, todo es reversible.
Forzar actualización: los equipos que dependan de una versión archivada verán un build roto, con una solución simple (actualizar el número de versión).
Limpieza automática: la operación se realiza sin intervención humana.
La solución técnica
Implementamos la solución dentro de android-settings, un repositorio centralizado para workflows compartidos.
Arquitectura
La solución consiste en:
2 scripts de archivado: uno para releases y otro para snapshots.
1 script de rollback: para restaurar versiones archivadas.
1 workflow de archivado: cron mensual más ejecución manual.
1 workflow de restauración: ejecución manual.

Descubrimiento dinámico
Para evitar listas hardcodeadas y frágiles de artifacts, usamos la API de storage de JFrog para descubrir todo dinámicamente, filtrando por el prefijo android-*.
Primero, GET /api/storage/libs-release-local/ devuelve la lista de group-paths (android-home, android-core, etc.). Luego, GET /api/storage/libs-release-local/{group-path}/ devuelve la lista de artifact-ids (home, di-utils, etc.). Finalmente, GET /api/storage/libs-release-local/{group-path}/{artifact-id}/ devuelve la lista de carpetas de versiones.
Esto asegura que si mañana se agrega un nuevo repositorio, se incluye automáticamente en el proceso de limpieza. Usamos el listado de carpetas vía /api/storage/ en vez de maven-metadata.xml para evitar que el cap de maxUniqueReleases/maxUniqueSnapshots devuelva menos versiones de las que realmente existen en storage.
Lógica de archivado
El criterio de archivado para las versiones release se basa en la posición del grupo en la historia de versiones
Se obtienen y ordenan todas las versiones.
Se extraen los prefijos MAJOR.MINOR.PATCH distintos.
Los 3 grupos más recientes quedan protegidos.
Cualquier versión cuyo prefijo MAJOR.MINOR.PATCH esté en esos 3 grupos se conserva siempre.
Todas las versiones de grupos más viejos se mueven a android-libs-archive sin excepción.
Para snapshots se basa también en la fecha, esto es, se conservan 10 versiones como fallback y cualquier versión con más de 60 días de antigüedad se archiva. El movimiento se ejecuta con la API move de JFrog:
Para mover un artifact, usamos POST /api/move/libs-release-local/{group}/{artifact}/{version}?to=/android-libs-archive/{group}/{artifact}/{version}&dry=0, donde el parámetro to indica la ruta de destino y dry=0 ejecuta el movimiento de forma real (con dry=1 sería solo una simulación). Esto nos permitió validar el comportamiento del script antes de la ejecución real.
Target mode para testing
La variable TARGET_GROUP_PATH permite ejecutar el script para un solo group-path, facilitando las pruebas específicas antes de soltar el script sobre todo el repositorio. Por ejemplo: DRY_RUN=true TARGET_GROUP_PATH=android-visual ./scripts/archive_old_snapshots.sh
Rollback
El script de restauración recibe group-id, artifact-id y version, verifica que existe en el archivo y lo mueve de vuelta a su repositorio original (release o snapshot). Por ejemplo, en el caso de un release: POST /api/move/android-libs-archive/{group}/{artifact}/{version}?to=/libs-release-local/{group}/{artifact}/{version}&dry=0
Automatización
Un cron de GitHub Actions corre el primer día de cada mes:
on:
schedule:
- cron: '0 9 1 * *' # 1ro de cada mes, 9:00 UTC
workflow_dispatch:
inputs:
dry-run:
type: boolean
default: true
target-group-path:
type: string
default: ''
En la ejecución por cron, DRY_RUN=false (ejecución real). En la ejecución manual, DRY_RUN=true por defecto.
El reporte se publica como Job Summary en GitHub Actions. Además, el workflow envía automáticamente un mensaje a Slack con la lista de artifacts archivados cuando la operación se ejecuta en modo real:
Artifact | Version | Estado
android-home/home | 1.0.0 | ARCHIVADO
android-home/home | 1.1.0 | ARCHIVADO
android-core/core | 2.0.0 | DRY-RUN
Totales: Archivados=2 | Skipped=15 | Errores=0
Setup
El único paso manual es crear el repositorio de archivo en JFrog (android-libs-archive), lo que requiere permisos de administrador.
Resultados
Luego de la primera ejecución:
Se movieron 32.195 artifacts de releases en 4h 18m de tiempo de ejecución.
Se movieron 6.144 artifacts de snapshots en 1h 41m (la ejecución se interrumpió a las 6 horas y se decidió continuar el próximo mes).
Una semana después del archivado, 5 equipos actualizaron las dependencias de sus repositorios.
Nos encontramos con el desafío de las dependencias transitivas desactualizadas. Para acotar el scope de cada actualización a un repositorio individual, se decidió restaurar excepcionalmente las versiones problemáticas, pero listarlas para resolver la deuda técnica antes de la próxima ejecución.
Próximas mejoras
Dentro del feedback recibido por otros desarrolladores y, para agilizar la actualización de librerías, se sugiere un mecanismo doble: además de la notificación por Slack con un resumen de los cambios, se implementará la detección de repositorios que utilizan las versiones a archivar. Automáticamente, se crearán Pull Requests (PRs) con las modificaciones necesarias. Esto facilita enormemente a los desarrolladores la adopción de versiones de librerías más recientes.
Reflexión
Algunas cosas que aprendimos:
Archivar > Borrar. La reversibilidad nos dio la confianza para automatizar. Si hubiéramos eliminado versiones, probablemente seguiríamos haciéndolo manual (o sea, no haciéndolo).
Descubrimiento dinámico > Listas hardcodeadas. Empezamos con un mapeo manual de repositorios, pero lo descartamos porque era imposible de mantener. La API de JFrog nos dio todo lo que necesitábamos.
Dry-run nativo. Que JFrog soporte dry=1 en la API de move fue clave.
Forzar la actualización es un feature, no un bug. El build roto es el incentivo que faltaba, ya que la solución es simplemente cambiar un número en gradle/libs.versions.toml.
A veces la mejor solución no es la más compleja, sino la más segura.
¿Te resultó útil nuestro contenido? Hacé clic acá y guardanos como tu fuente favorita en Google.

