--- title: "Gestión de artefactos CI/CD con GitHub Actions y S3" description: "Versiona, empaqueta y verifica artefactos de build en GitHub Actions: paso entre jobs, releases con checksum SHA256, copia en S3 con rclone y retención." category: "CI/CD DevOps" language: es source: https://cubepath.com/es/docs/ci-cd-devops/gestion-de-artefactos-en-flujos-de-trabajo-cicd updated: 2026-09-25 --- # Gestión de artefactos CI/CD con GitHub Actions y S3 Un artefacto es el resultado de un build que vas a guardar o desplegar: un paquete comprimido, un binario, una imagen de contenedor o un informe de pruebas. Gestionarlos bien significa que cada artefacto tiene una versión que lo identifica, se construye una sola vez, se puede verificar que no ha cambiado y se borra cuando ya no hace falta. En este tutorial montarás ese ciclo con GitHub Actions para una aplicación web: versionado a partir de etiquetas Git, paquete con checksum SHA256, paso de artefactos entre jobs, publicación en GitHub Releases, copia en un almacenamiento compatible con S3 y descarga verificada en un servidor Ubuntu 24.04. ## Requisitos previos - Un repositorio en GitHub con un proyecto Node.js cuyo `npm run build` genere los archivos finales en `dist/` (el mismo esquema sirve para cualquier otro lenguaje cambiando el comando de build). - La CLI de GitHub (`gh`) autenticada en tu máquina local. - Un bucket en un servicio de almacenamiento de objetos compatible con S3, con un par de claves de acceso con permiso de escritura y otro de solo lectura. - Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con un usuario no root con `sudo`, donde se descargarán y desplegarán los artefactos. ## Dónde guardar cada tipo de artefacto No todos los artefactos tienen el mismo ciclo de vida. Elegir el almacén adecuado evita tanto perder builds que necesitas como acumular gigas de builds que nadie usará: | Almacén | Para qué | Retención típica | |---------|----------|------------------| | Artefactos de GitHub Actions (`upload-artifact`) | Pasar archivos entre jobs de una misma ejecución, informes de pruebas | Días | | Caché de Actions (`actions/cache`, `setup-node`) | Dependencias descargadas (`~/.npm`), no resultados del build | Se renueva sola | | GitHub Releases | Versiones publicadas para usuarios o para desplegar | Indefinida | | Almacenamiento S3 | Copia independiente de GitHub, builds de `main`, descargas desde servidores | Según la política del bucket | | Registro de contenedores | Imágenes Docker | Según política de limpieza del registro | Una confusión frecuente es usar la caché para pasar el resultado del build a otro job. La caché puede desaparecer en cualquier momento y se comparte entre ejecuciones; para resultados de build usa siempre artefactos. ## Paso 1: Definir el esquema de versiones Cada artefacto necesita un nombre que diga exactamente de qué código sale. La forma más simple y fiable es derivarlo de Git con `git describe`: en un commit etiquetado devuelve la etiqueta, y en cualquier otro commit añade cuántos commits hay desde la última etiqueta y el hash abreviado. Etiqueta la primera versión siguiendo versionado semántico (`MAJOR.MINOR.PATCH`) y compruébalo: ```bash git tag -a v1.2.0 -m "Versión 1.2.0" git describe --tags --always ``` ```output v1.2.0 ``` Tras dos commits más sin etiquetar: ```output v1.2.0-2-g8c1f3ab ``` Ese valor es único por commit y ordenable a simple vista. `--always` hace que, si el repositorio aún no tiene etiquetas, devuelva al menos el hash en lugar de fallar. ## Paso 2: Empaquetar el build con versión y checksum Crea un script que empaquete `dist/` con la versión en el nombre, incluya un archivo `version.json` dentro del propio paquete y genere su checksum SHA256. Así el mismo script funciona en tu máquina y en CI. ```bash mkdir -p scripts nano scripts/empaquetar.sh ``` ```bash #!/usr/bin/env bash set -euo pipefail version="$(git describe --tags --always)" commit="$(git rev-parse HEAD)" nombre="miapp-${version}" if [[ ! -d dist ]]; then echo "No existe dist/, ejecuta antes npm run build" >&2 exit 1 fi printf '{"version":"%s","commit":"%s","fecha":"%s"}\n' \ "$version" "$commit" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > dist/version.json mkdir -p out tar -czf "out/${nombre}.tar.gz" -C dist . (cd out && sha256sum "${nombre}.tar.gz" > "${nombre}.tar.gz.sha256") echo "$version" ``` ```bash chmod +x scripts/empaquetar.sh ``` Pruébalo en local: ```bash npm ci npm run build ./scripts/empaquetar.sh ls out/ ``` ```output v1.2.0 miapp-v1.2.0.tar.gz miapp-v1.2.0.tar.gz.sha256 ``` El archivo `.sha256` permite a cualquiera comprobar que el paquete descargado es idéntico al que construyó el pipeline. `version.json` dentro del paquete te dirá, una vez desplegado, qué versión y qué commit están sirviéndose. Añade `out/` a tu `.gitignore`. ## Paso 3: Construir una vez y pasar el artefacto entre jobs El workflow construye y empaqueta en un job y sube el resultado con `actions/upload-artifact`. Los jobs posteriores lo descargan con `actions/download-artifact` en lugar de volver a compilar: así lo que se prueba, se publica y se despliega es exactamente el mismo archivo. Crea `.github/workflows/artefactos.yml`: ```yaml name: artefactos on: push: branches: [main] tags: ["v*"] pull_request: permissions: contents: read jobs: build: runs-on: ubuntu-24.04 outputs: version: ${{ steps.paquete.outputs.version }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - run: npm ci - run: npm run build - name: Empaquetar id: paquete run: echo "version=$(./scripts/empaquetar.sh)" >> "$GITHUB_OUTPUT" - uses: actions/upload-artifact@v4 with: name: paquete path: out/ if-no-files-found: error retention-days: 7 ``` Algunos detalles importan: - `fetch-depth: 0` descarga el historial completo con las etiquetas. Con el clon superficial por defecto, `git describe` no encuentra ninguna etiqueta y devuelve solo el hash. - `cache: npm` en `setup-node` guarda la caché de npm entre ejecuciones. Es caché de dependencias, no el artefacto. - `if-no-files-found: error` hace fallar el job si el build no produjo nada, en lugar de subir un artefacto vacío. - `retention-days: 7` limita cuánto se conserva este artefacto. Las versiones que importan se guardarán en Releases y en S3 en los pasos siguientes. Haz un push y abre la ejecución en la pestaña **Actions**: al final del resumen, en **Artifacts**, aparece `paquete` con su tamaño y un enlace de descarga. ## Paso 4: Publicar las versiones en GitHub Releases Cuando empujas una etiqueta `v*`, el pipeline debe crear una release con el paquete y su checksum. Este job descarga el artefacto del job `build`, así que no recompila nada. Añádelo al final del workflow: ```yaml release: needs: build if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-24.04 permissions: contents: write steps: - uses: actions/download-artifact@v4 with: name: paquete path: out/ - name: Comprobar checksum working-directory: out run: sha256sum -c ./*.sha256 - name: Crear release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: >- gh release create "$GITHUB_REF_NAME" out/* --repo "$GITHUB_REPOSITORY" --title "$GITHUB_REF_NAME" --generate-notes --verify-tag ``` Solo este job tiene `contents: write`, el permiso necesario para crear releases. `--verify-tag` evita que `gh` cree una etiqueta nueva si por error no existiera, y `--generate-notes` redacta las notas a partir de los pull requests fusionados desde la versión anterior. Publica una versión y comprueba el resultado: ```bash git tag -a v1.2.1 -m "Versión 1.2.1" git push origin v1.2.1 ``` ```bash gh release view v1.2.1 ``` ```output v1.2.1 Published by github-actions[bot] less than a minute ago ## What's Changed ... ASSETS miapp-v1.2.1.tar.gz 1.4 MiB miapp-v1.2.1.tar.gz.sha256 86 B ``` ## Paso 5: Copiar los artefactos en un almacenamiento S3 Guardar además una copia en un bucket S3 te da un almacén independiente de GitHub, con acceso mediante claves de solo lectura desde tus servidores, y te permite conservar también los builds de `main` que no son versiones publicadas. Para subirlos se usa `rclone`, que funciona con cualquier proveedor compatible con S3 y se puede configurar solo con variables de entorno. Guarda las credenciales de escritura como secretos del repositorio: ```bash gh secret set S3_ACCESS_KEY_ID gh secret set S3_SECRET_ACCESS_KEY gh variable set S3_ENDPOINT --body "https://s3.tu_proveedor.com" gh variable set S3_BUCKET --body "artefactos-miapp" ``` `gh secret set` te pedirá el valor de forma interactiva, sin dejarlo en el historial de la shell. Añade este job al workflow: ```yaml s3: needs: build if: github.event_name == 'push' runs-on: ubuntu-24.04 env: RCLONE_CONFIG_ALMACEN_TYPE: s3 RCLONE_CONFIG_ALMACEN_PROVIDER: Other RCLONE_CONFIG_ALMACEN_ENDPOINT: ${{ vars.S3_ENDPOINT }} RCLONE_CONFIG_ALMACEN_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }} RCLONE_CONFIG_ALMACEN_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} steps: - uses: actions/download-artifact@v4 with: name: paquete path: out/ - name: Instalar rclone run: sudo apt-get update && sudo apt-get install -y rclone - name: Subir al bucket env: BUCKET: ${{ vars.S3_BUCKET }} VERSION: ${{ needs.build.outputs.version }} run: | if [[ "$GITHUB_REF" == refs/tags/v* ]]; then destino="releases/$VERSION" else destino="snapshots/$VERSION" fi rclone copy out/ "almacen:$BUCKET/$destino/" --immutable -v ``` Las variables `RCLONE_CONFIG_ALMACEN_*` definen un remoto llamado `almacen` sin archivo de configuración. Las versiones etiquetadas van a `releases/` y los builds de `main` a `snapshots/`, lo que permite aplicarles retenciones distintas en el paso 7. La opción `--immutable` hace fallar la subida si ya existe un archivo con ese nombre y contenido distinto: una versión publicada no debe sobrescribirse nunca. Tras el siguiente push, el log del paso muestra los archivos copiados: ```output INFO : miapp-v1.2.1-1-g3d9e0aa.tar.gz.sha256: Copied (new) INFO : miapp-v1.2.1-1-g3d9e0aa.tar.gz: Copied (new) ``` ## Paso 6: Descargar y verificar el artefacto en el servidor En el servidor Ubuntu 24.04, instala rclone desde los repositorios de Ubuntu: ```bash sudo apt update sudo apt install -y rclone ``` Crea su configuración con las claves de **solo lectura**, para que un servidor comprometido no pueda alterar los artefactos: ```bash mkdir -p ~/.config/rclone nano ~/.config/rclone/rclone.conf ``` ```ini [almacen] type = s3 provider = Other endpoint = https://s3.tu_proveedor.com access_key_id = tu_clave_de_lectura secret_access_key = tu_secreto_de_lectura ``` ```bash chmod 600 ~/.config/rclone/rclone.conf ``` Lista las versiones disponibles: ```bash rclone lsf almacen:artefactos-miapp/releases/ ``` ```output v1.2.0/ v1.2.1/ ``` Descarga una versión y verifica el checksum **antes** de descomprimir nada: ```bash rclone copy almacen:artefactos-miapp/releases/v1.2.1/ ~/descargas/v1.2.1/ cd ~/descargas/v1.2.1 sha256sum -c miapp-v1.2.1.tar.gz.sha256 ``` ```output miapp-v1.2.1.tar.gz: OK ``` Si la salida es `FAILED`, el archivo está corrupto o incompleto: no lo despliegues. Con la verificación superada, descomprime en un directorio por versión y activa la versión con un enlace simbólico. Así conservas las versiones anteriores y volver a una de ellas es cambiar el enlace: ```bash sudo mkdir -p /var/www/miapp/releases/v1.2.1 sudo tar -xzf miapp-v1.2.1.tar.gz -C /var/www/miapp/releases/v1.2.1 sudo ln -sfn /var/www/miapp/releases/v1.2.1 /var/www/miapp/current cat /var/www/miapp/current/version.json ``` ```output {"version":"v1.2.1","commit":"3d9e0aa51c7f...","fecha":"2026-09-25T09:41:07Z"} ``` Apunta la raíz de tu servidor web (por ejemplo, `root /var/www/miapp/current;` en Nginx) al enlace `current`. Si prefieres descargar desde GitHub Releases en lugar de S3, `gh` hace lo mismo: ```bash gh release download v1.2.1 --repo tu_usuario/miapp --pattern 'miapp-*' ``` ## Paso 7: Aplicar una política de retención Sin limpieza, los artefactos crecen hasta que el almacenamiento cuesta más que el proyecto. Define cuánto vive cada tipo: - **Artefactos de Actions**: el `retention-days: 7` del paso 3 ya los elimina. El valor por defecto del repositorio se ajusta en **Settings > Actions > General > Artifact and log retention**. - **Releases**: se conservan. Son pocas y son tu historial de versiones. - **Snapshots en S3**: basta con conservar unos días. Si tu proveedor admite reglas de ciclo de vida (*lifecycle rules*) en el bucket, crea una que expire los objetos con prefijo `snapshots/` a los 30 días. Si no, prográmalo con rclone en un servidor con credenciales de escritura. Prueba primero qué se borraría con `--dry-run`: ```bash rclone delete --min-age 30d --dry-run almacen:artefactos-miapp/snapshots/ ``` Cuando el resultado sea el esperado, prográmalo con cron para cada noche: ```bash crontab -e ``` ```text 15 4 * * * rclone delete --min-age 30d --rmdirs almacen:artefactos-miapp/snapshots/ >> "$HOME/rclone-limpieza.log" 2>&1 ``` En el servidor web, borra también las versiones antiguas de `/var/www/miapp/releases/`, conservando siempre la activa y al menos una anterior para poder volver atrás. ## Solución de problemas **`git describe` devuelve solo un hash aunque existen etiquetas**: el checkout es superficial. Añade `fetch-depth: 0` a `actions/checkout`, y comprueba que la etiqueta está subida con `git ls-remote --tags origin`. **`Failed to CreateArtifact: ... an artifact with this name already exists`**: en `upload-artifact@v4` los nombres son únicos por ejecución y los artefactos no se pueden modificar. Usa nombres distintos por job o por entrada de la matriz (por ejemplo, `paquete-${{ matrix.os }}`). **`Unable to download artifact(s): Artifact not found for name: paquete`**: el job que descarga no depende del que sube. Añade el job de build a `needs`. **`HTTP 403` al ejecutar `gh release create`**: falta `permissions: contents: write` en ese job. **rclone devuelve `SignatureDoesNotMatch` o `AccessDenied`**: el endpoint o las claves no son correctos, o el proveedor exige una región concreta. Algunos proveedores necesitan además la opción `region` en la configuración del remoto; consulta su documentación. ## Conclusión Tus builds ahora tienen una versión derivada de Git, se construyen una sola vez, viajan entre jobs como artefactos, se publican con checksum SHA256 en GitHub Releases y en un almacenamiento S3, y se verifican en el servidor antes de desplegarse, con una retención distinta para versiones y snapshots. Como siguientes pasos, puedes firmar los paquetes además de publicar su checksum (por ejemplo con `cosign sign-blob`), aplicar el mismo esquema de etiquetas inmutables a tus imágenes Docker y automatizar la descarga y el cambio de enlace `current` desde el propio pipeline por SSH.