Esta lección tiene un ángulo distinto a las anteriores, y conviene decirlo desde el principio: no vas a elegir Travis CI para un proyecto nuevo. Lo que sí es muy probable es que te lo encuentres —en un repositorio interno que nadie ha tocado desde 2019, en una dependencia de código abierto que quieres contribuir, en la migración que te encargan al entrar en un equipo— y que tengas que entender qué hace ese .travis.yml de treinta líneas y traducirlo. Y hay una segunda razón para dedicarle una lección entera: Travis inventó buena parte de lo que este curso da por evidente. El fichero de CI versionado junto al código, la matriz de versiones de lenguaje, el CI gratuito e ilimitado para código abierto y la insignia verde en el README son suyos; GitHub Actions y GitLab CI heredan su idea central. Y luego pasó lo que pasó, que es la parte más instructiva: un cambio de propietario, un cambio de modelo de negocio, una migración masiva a otra herramienta y un incidente de seguridad que dejó una lección durísima sobre confiar secretos a un tercero. Vamos a ver qué aportó, cómo funciona su modelo de fases fijas, el pipeline de Reservalia forzado a caber en él, qué pasó, y sobre todo cómo migrar un .travis.yml a lo que uses hoy.
Contenido
- Qué aportó Travis y por qué importa
- El modelo de fases fijas
.travis.ymlbásico y matriz de versiones- El pipeline de Reservalia forzado a caber
- Stages, cachés y secretos cifrados
- Qué pasó: propiedad, modelo de negocio y éxodo
- El incidente de 2021 y la lección sobre secretos
- Migrar un
.travis.yml: tabla de equivalencias - Migración completa, paso a paso
- La lección de portabilidad
- Cuándo Travis todavía tiene sentido
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Qué aportó Travis y por qué importa
Travis CI apareció en 2011, cuando el CI normal era una instalación de Jenkins que alguien mantenía y donde configurar un proyecto significaba pedir un job a un administrador y rellenar un formulario (06-01). Travis propuso otra cosa: un fichero en tu repositorio y ya está.
# .travis.yml de 2013. Esto era todo. Y era revolucionario.
language: node_js
node_js:
- "0.10"
- "0.12"Con esas cuatro líneas, cada push disparaba una construcción, en dos versiones de Node, en una máquina limpia, sin haber pedido permiso a nadie ni instalado nada. Cuatro innovaciones concretas, todas hoy invisibles porque son el estándar:
| Aportación | Qué cambió | Dónde vive hoy |
|---|---|---|
| Configuración en el repositorio | El pipeline se ramifica y se revisa con el código | Jenkinsfile, .gitlab-ci.yml, config.yml, workflows: todos |
| Convención sobre configuración | language: node_js implica instalar Node, ejecutar npm ci y npm test sin decirlo |
Las acciones setup-*, los buildpacks, cimg/* |
| Matriz de versiones | Probar en varias versiones era una lista, no N jobs copiados | matrix / parallel: matrix (02-04) |
| Gratuito e ilimitado para código abierto | El CI dejó de ser un privilegio de empresas | Los planes gratuitos actuales, que existen porque Travis los normalizó |
El efecto cultural fue enorme: durante años, la insignia verde de Travis en el README fue la señal de que un proyecto de código abierto era serio. Miles de proyectos adoptaron pruebas automatizadas porque el coste de entrada bajó a un fichero. Es difícil exagerar cuánto de la cultura de CI actual sale de ahí.
Ese es el motivo de estudiar Travis aunque no lo uses: entender el .travis.yml es entender el modelo mental del que todos los demás derivan, incluidas sus limitaciones, que es lo que empujó a los sucesores a diseñarse distinto.
- El modelo de fases fijas
Aquí está la diferencia estructural con todo lo visto. Jenkins, GitLab, CircleCI y GitHub Actions te dejan definir las etapas y sus dependencias. Travis no: tiene un ciclo de vida de fases predefinidas y tú rellenas las que necesites.
flowchart TD
A["apt addons"] --> B["before_install"]
B --> C["install<br/>por defecto segun language"]
C --> D["before_script"]
D --> E["script<br/>el que decide exito o fallo"]
E --> F{"resultado"}
F -->|"exito"| G["after_success"]
F -->|"fallo"| H["after_failure"]
G --> I["before_deploy"]
I --> J["deploy"]
J --> K["after_deploy"]
G --> L["after_script"]
H --> L
| Fase | Para qué | Nota importante |
|---|---|---|
before_install |
Preparar el sistema (repositorios apt, herramientas) | Un fallo aquí es un error, no un fallo de build |
install |
Instalar dependencias | Tiene valor por defecto según language |
before_script |
Preparar el entorno (arrancar base de datos, migrar) | Un fallo aquí también es error |
script |
Lo que decide si el build pasa | Un fallo aquí es un fallo de build. La distinción es sutil y confunde |
after_success / after_failure |
Cobertura, notificaciones | Su resultado no afecta al build |
before_deploy / deploy / after_deploy |
Publicar y desplegar | deploy usa proveedores predefinidos |
after_script |
Limpieza | Siempre se ejecuta |
Qué se gana y qué se pierde con las fases fijas:
Se gana simplicidad brutal. No hay que decidir la estructura del pipeline: la estructura ya está y tú rellenas huecos. Para el 90 % de los proyectos de código abierto de 2013 —instala, prueba, informa— era exactamente lo necesario, y el fichero cabía en una pantalla. Además hace los ficheros comparables entre proyectos: sabes dónde mirar.
Se pierde todo lo que necesita un pipeline real de entrega:
- No hay grafo. No puedes decir "publicar depende de calidad y de test pero no de la documentación". El orden es el que hay.
- No hay jobs con dependencias arbitrarias. Los
stagesllegaron tarde y son secuenciales, no un DAG. - No hay paralelización dentro de un job. El sharding de la 02-04 hay que simularlo con entradas de matriz y variables de entorno.
- El paso de artefactos entre jobs no existe de forma nativa. Cada job de la matriz es independiente y arranca limpio; para pasar el
dist/de un job a otro hay que subirlo a S3 o a otro almacén tú mismo. Este es el límite más severo: la promoción del artefacto único de la 02-06 no se puede expresar en el modelo. - La condicionalidad es pobre.
if:existe, pero no hay nada parecido aruleso a expresiones ricas.
La conclusión, que es la que hace didáctica esta lección: el modelo de fases fijas es excelente para CI y estructuralmente insuficiente para CD. Y eso explica por qué los sucesores —GitLab con stages y luego needs, CircleCI con workflows desde el principio, GitHub Actions con needs— pusieron el grafo en el centro. No fue moda: fue la corrección de una limitación concreta.
.travis.yml básico y matriz de versiones
.travis.yml básico y matriz de versioneslanguage: node_js
dist: jammy # 1 · imagen base de Ubuntu
node_js: [ "20", "22" ] # 2 · matriz implícita: 2 jobs
services: [ postgresql ] # 3 · servicios preinstalados en la VM
cache:
npm: true # 4 · caché conocida por el proveedor
directories: [ node_modules ]
before_install:
- psql -c 'CREATE DATABASE reservalia_test;' -U postgres
install:
- npm ci # sustituye al install por defecto
script: # 5 · lo que decide el resultado
- npm run lint
- npm test
after_success:
- bash <(curl -s https://codecov.io/bash) # 6 · su fallo no rompe el build
notifications:
email: false
slack:
rooms:
secure: "AbCd...==" # 7 · valor cifrado con la clave pública del repositoriodistelige la imagen base (trusty,xenial,bionic,focal,jammy). Es lo más parecido aruns-on, y una fuente clásica de builds rotos: proyectos anclados adist: trustyque dejaron de funcionar cuando esa imagen se retiró.- La matriz implícita es la idea que más se copió: listar valores de la clave del lenguaje genera un job por valor.
servicesactiva servicios ya instalados en la VM. Es distinto de losservicesde GitLab o GitHub, que levantan contenedores: aquí no eliges la versión con precisión, corres la que trae la imagen. Menos control, menos ceremonia.cache: npm: truees caché por convención: el proveedor sabe qué cachear para cada lenguaje. Cómodo, y con la contrapartida de siempre —cachearnode_modulesen lugar del directorio de descarga de npm produce estados corruptos cuando cambia la versión de Node, exactamente el problema de invalidación de la 04-02—.scriptes la fase que decide. Cada comando de la lista se ejecuta y si uno falla, los siguientes se ejecutan igualmente; el build acaba fallando. Es distinto de casi todas las herramientas modernas, donde un step fallido corta el job, y es una fuente de confusión al leer logs.after_successno afecta al resultado. Si la subida de cobertura falla, el build sigue verde. A veces es lo que quieres y a veces oculta que llevas un mes sin subir cobertura.secure:es un valor cifrado con la clave pública del repositorio, generado contravis encrypt. Apartado 5.
Matriz explícita, con lo que la hacía potente:
jobs: # antes se llamaba "matrix"
include:
- node_js: "22"
env: SUITE=integracion
services: [ postgresql, redis ]
- node_js: "22"
os: osx # macOS, uno de sus argumentos históricos
- node_js: "22"
arch: arm64
exclude:
- node_js: "20"
os: osx
allow_failures: # jobs que pueden fallar sin romper el build
- node_js: "nightly"
fast_finish: true # informa el resultado sin esperar a los allow_failuresallow_failures con fast_finish es una buena idea que sigue vigente: probar contra la versión en desarrollo del lenguaje sin bloquear a nadie, y enterarte pronto de que algo va a romperse. Es el equivalente de continue-on-error con la matriz de la 02-04.
- El pipeline de Reservalia forzado a caber
Cuarta traducción del mismo pipeline, y la primera en la que hay que explicar qué no se puede expresar.
language: node_js
node_js: [ "22" ]
dist: jammy
os: linux
services: [ postgresql, docker ]
cache:
directories: [ $HOME/.npm ]
env:
global:
- IMAGEN=reservalia/api
- ECR=123456789012.dkr.ecr.eu-west-1.amazonaws.com
stages: # 1 · secuenciales, sin grafo
- verificar
- construir
- name: publicar
if: branch = main AND type = push # 2
jobs:
include:
# ---------------- verificar ----------------
- stage: verificar
name: "Calidad"
install: npm ci --prefer-offline
script:
- npx prettier --check .
- npm run lint
- npm run typecheck
- stage: verificar # 3 · el sharding, a mano
name: "Tests 1/4"
env: SHARD=1
install: npm ci --prefer-offline
before_script:
- psql -c 'CREATE DATABASE reservalia_test;' -U postgres
- npm run migrate
script: npm test -- --shard=$SHARD/4
- stage: verificar
name: "Tests 2/4"
env: SHARD=2
install: npm ci --prefer-offline
before_script:
- psql -c 'CREATE DATABASE reservalia_test;' -U postgres
- npm run migrate
script: npm test -- --shard=$SHARD/4
# … y otras dos entradas casi idénticas para 3/4 y 4/4
# ---------------- construir ----------------
- stage: construir
name: "Build imagen"
install: skip
script:
- docker build -f apps/api/Dockerfile -t $IMAGEN:$TRAVIS_COMMIT .
- trivy image --severity HIGH,CRITICAL --exit-code 1 $IMAGEN:$TRAVIS_COMMIT
after_success:
# 4 · para que el stage siguiente vea la imagen hay que subirla YA
- echo "$ECR_PASSWORD" | docker login -u AWS --password-stdin $ECR
- docker tag $IMAGEN:$TRAVIS_COMMIT $ECR/$IMAGEN:$TRAVIS_COMMIT
- docker push $ECR/$IMAGEN:$TRAVIS_COMMIT
# ---------------- publicar ----------------
- stage: publicar
name: "Promocionar por digest"
install: skip
script:
# 5 · el digest hay que volver a consultarlo: no viajó desde el stage anterior
- DIGEST=$(aws ecr describe-images --repository-name reservalia/api
--image-ids imageTag=$TRAVIS_COMMIT
--query 'imageDetails[0].imageDigest' --output text)
- ./scripts/promocionar.sh "$ECR/$IMAGEN@$DIGEST"
deploy: # 6 · proveedores predefinidos
provider: s3
bucket: reservalia-web
local_dir: apps/web/dist
skip_cleanup: true
on: { branch: main }Qué queda forzado, que es la parte instructiva:
- Los
stagesson secuenciales, no un grafo.construirno puede empezar hasta que terminen los cinco jobs deverificar, aunque solo dependa de las fuentes. Es la barrera de etapa de la 04-01 sin escapatoria: no existeneeds. - La condicionalidad es limitada. El lenguaje de
if:cubre rama, tipo de evento, tag y variables de entorno, y poco más. No hay filtros por ruta como los de la 02-07, así que la ejecución selectiva en monorepo de la 04-04 no se puede expresar. - El sharding es copiar y pegar cuatro entradas. No hay
parallelism: 4ni matriz que genere shards con una línea; cada partición es una entrada dejobs.includecon suinstally subefore_scriptrepetidos. Cuatro bloques casi idénticos: exactamente la duplicación que la 04-05 enseñó a eliminar y que aquí no tiene solución dentro del fichero. - No hay paso de artefactos entre jobs. El
dist/y la imagen no viajan del stageconstruiral stagepublicar: cada job arranca en una máquina limpia. La única salida es subir el artefacto a un almacén externo inmediatamente y volver a descargarlo o a referenciarlo después. Funciona, pero convierte el almacén externo en parte obligatoria del pipeline y hace que la construcción y la publicación dejen de ser pasos separables. - La consecuencia más grave: como el digest no puede viajar, hay que volver a consultarlo por tag. Y eso rompe una garantía que el curso lleva defendiendo desde la 02-06: la promoción por digest exige que el identificador que se promociona sea exactamente el que produjo el build. Consultarlo por tag introduce una ventana —pequeña, pero real— en la que ese tag podría apuntar a otra imagen. En Travis se puede mitigar (tags únicos por commit, registros con tags inmutables), no eliminar limpiamente.
deploycon proveedores es la parte más cómoda de Travis: docenas de proveedores (S3, Heroku, npm, PyPI, GitHub Releases) configurados con cuatro líneas. Para publicar un paquete de código abierto sigue siendo de lo más directo que existe. Para desplegar en ECS con canary, no hay proveedor y acabas escribiendo el script igualmente.
Balance honesto: el pipeline cabe, pero pierde el grafo, pierde el sharding declarativo, pierde la ejecución selectiva y compromete la promoción por digest. No es que Travis esté mal hecho; es que se diseñó para "construye y prueba un proyecto de código abierto" y este pipeline es de entrega continua.
- Stages, cachés y secretos cifrados
Stages llegaron en 2017, tarde, y son grupos secuenciales de jobs paralelos —el modelo original de GitLab, sin el needs posterior—. Añadieron algo importante: if: a nivel de stage, y la posibilidad de una fase de despliegue tras las pruebas.
Cachés: por convención (cache: npm|bundler|pip) o por directorios explícitos. Con dos avisos que valen para cualquier herramienta. Primero, cachear node_modules es una mala idea frente a cachear el directorio de descarga: node_modules contiene binarios compilados para una versión concreta de Node y arquitectura, y restaurarlo en otro contexto produce fallos crípticos (04-02). Segundo, la invalidación de la caché de Travis se hacía borrándola desde la interfaz o con la CLI, no cambiando una clave: no hay clave por checksum, así que la caché puede quedarse obsoleta indefinidamente. Es justo el control que CircleCI hizo explícito (06-03).
Secretos cifrados, la parte más característica y la más relevante para lo que viene:
# Cifra un valor con la clave pública del repositorio y lo añade al .travis.yml
travis encrypt AWS_SECRET_ACCESS_KEY="AKIA..." --add env.global
# Cifra un fichero entero (una clave privada, un keystore)
travis encrypt-file claves/deploy.pem --addLa idea era elegante: el secreto cifrado vive en el repositorio, así que el .travis.yml es autocontenido y funciona para cualquiera que haga fork sin exponer nada, porque solo Travis tiene la clave privada. Dos propiedades de diseño que lo hacían razonablemente seguro: los valores cifrados no se descifran en builds de pull requests desde forks —justamente para que un PR malicioso no pudiera leerlos, que es el mismo razonamiento que pull_request_target de la 04-03—, y Travis enmascaraba los valores en los logs.
Y en esas dos propiedades está la historia del apartado 7.
- Qué pasó: propiedad, modelo de negocio y éxodo
Los hechos, en orden, porque el patrón importa más que las fechas:
Enero de 2019: cambio de propietario. Travis CI fue adquirido por Idera. Poco después se produjo una salida significativa de personal de ingeniería, ampliamente comentada en la comunidad. Para los usuarios, el efecto inmediato fue incertidumbre sobre el rumbo del producto.
Finales de 2020: fin del modelo gratuito ilimitado para código abierto. Se introdujo un sistema de créditos con una asignación limitada para proyectos abiertos, renovable bajo solicitud. La justificación era razonable —el coste del cómputo es real y el abuso por minería de criptomonedas era un problema real del sector—, pero el cambio afectaba a la propuesta de valor que había definido a Travis durante diez años.
2019-2021: GitHub Actions. GitHub lanzó Actions en el sitio donde ya vivía el código de la mayoría de los proyectos de código abierto, con minutos gratuitos generosos para repositorios públicos y cero configuración de integración. La combinación de ambos factores produjo una migración masiva: miles de proyectos cambiaron su .travis.yml por .github/workflows/ci.yml en cuestión de meses.
Lo que hay que llevarse de esto no es una crítica a Travis ni a Idera, sino tres observaciones aplicables a cualquier herramienta que elijas hoy:
- El modelo de negocio de tu proveedor de CI puede cambiar, y no lo decides tú. Un plan gratuito, unos minutos incluidos o un nivel de precios pueden cambiar con un preaviso corto. Si tu operación depende críticamente de esas condiciones, tienes una exposición que conviene reconocer.
- La integración vence a la calidad técnica. Actions no ganó por ser mejor herramienta que Travis en 2020 —discutible en varios aspectos—, sino por estar donde ya estaba el código. Es el criterio que la 06-07 pondrá en primer lugar por peso real: dónde vive el código y la identidad del equipo decide la mayoría de las elecciones.
- El coste de salida se paga cuando ya no hay alternativa. Los proyectos que tenían la lógica en scripts del repositorio migraron en una tarde; los que la tenían dispersa en el YAML tardaron semanas. Apartado 10.
- El incidente de 2021 y la lección sobre secretos
En septiembre de 2021, Travis CI publicó un aviso de seguridad sobre un fallo que provocaba que variables de entorno seguras —incluidos tokens y claves— quedaran expuestas en builds de pull requests procedentes de forks de repositorios públicos. Es decir, se rompió precisamente la propiedad de diseño que hacía seguro el modelo de secretos cifrados: que un fork no puede acceder a ellos. Cualquiera que abriera un PR contra un repositorio público afectado podía, con un .travis.yml modificado, leer secretos que jamás deberían haber estado a su alcance.
Se le identificó como CVE-2021-41077, y la comunicación inicial generó bastantes críticas por su alcance y su detalle, lo que llevó a una publicación posterior más explícita. La recomendación operativa para cualquier proyecto público afectado fue rotar todas las credenciales.
Cuatro lecciones concretas, que no son sobre Travis:
1. Un secreto confiado a un tercero es un secreto cuya seguridad no controlas. No es un argumento contra los SaaS —la 06-03 mencionaba el incidente de CircleCI de 2023, con la misma conclusión de rotar todo, y ninguna plataforma es inmune—. Es un argumento a favor de asumir que ocurrirá y diseñar en consecuencia.
2. Las credenciales de larga vida son el problema; las efímeras acotan el daño. Una clave de acceso permanente filtrada es válida hasta que alguien la revoca, y ese "alguien" suele enterarse tarde. Un token OIDC de quince minutos limitado a un rol concreto (03-02) es material robado que ya no sirve para casi nada. Esta es la razón práctica —no teórica— por la que el curso insiste tanto en la identidad federada frente a los secretos almacenados.
3. Hay que saber rotar todo, y haberlo ensayado. La pregunta útil es: si mañana tu proveedor te dice "rota todas tus credenciales", ¿cuánto tardas y qué se rompe? Si la respuesta es "no lo sé", eso es un plan de recuperación pendiente, exactamente en el sentido de la 03-05. Un inventario de secretos con su propietario y su procedimiento de rotación vale mucho el día que hace falta.
4. Los forks son la frontera de confianza más delicada de cualquier CI. El mismo problema, con distinto mecanismo, es el de pull_request_target de la 04-03 que la 06-06 volverá a tratar. La regla es universal e independiente de herramienta: el código de un fork no puede acceder a secretos, y cualquier flujo que parezca darle acceso hay que examinarlo con lupa.
- Migrar un
.travis.yml: tabla de equivalencias
.travis.yml: tabla de equivalenciasEsta es la parte que probablemente uses de verdad. La traducción es en su mayor parte mecánica, con dos o tres decisiones de diseño.
| Travis CI | GitHub Actions | GitLab CI/CD |
|---|---|---|
language: node_js |
uses: actions/setup-node@v4 |
image: node:22 |
node_js: ["20","22"] |
strategy: matrix: node: [20, 22] |
parallel: matrix: NODE: ["20","22"] |
dist: jammy |
runs-on: ubuntu-22.04 |
image: / etiqueta de runner |
os: [linux, osx] |
runs-on: [ubuntu-latest, macos-latest] |
Runners con tags |
services: [postgresql] |
services: con imagen de contenedor |
services: con imagen |
before_install |
Steps al principio del job | before_script |
install |
- run: npm ci |
before_script o script |
before_script |
Steps previos | before_script |
script |
- run: (cada uno corta al fallar) |
script: |
after_success |
- if: success() |
after_script o job con when: on_success |
after_failure |
- if: failure() |
when: on_failure |
after_script |
- if: always() |
after_script |
deploy: proveedor |
Action del proveedor o run |
Job de despliegue con environment |
cache: directories: |
actions/cache con clave |
cache: key/paths |
env.global |
env: a nivel de workflow |
variables: |
secure: "..." |
Secretos del repositorio | Variables protegidas y enmascaradas |
stages |
needs: entre jobs |
stages + needs |
allow_failures |
continue-on-error: true |
allow_failure: true |
fast_finish |
fail-fast (semántica inversa) |
Comportamiento por defecto |
if: branch = main |
if: github.ref == 'refs/heads/main' |
rules: - if: $CI_COMMIT_BRANCH == ... |
TRAVIS_COMMIT |
github.sha |
$CI_COMMIT_SHA |
TRAVIS_PULL_REQUEST |
github.event_name == 'pull_request' |
$CI_PIPELINE_SOURCE |
TRAVIS_BRANCH |
github.ref_name |
$CI_COMMIT_BRANCH |
Las cuatro decisiones que no son mecánicas:
- La distinción entre fases que rompen el build y fases que no desaparece. En Travis,
after_successno afecta al resultado; en las demás, un step que falla rompe el job salvo que pongascontinue-on-error. Al migrar, decide explícitamente qué debe seguir siendo no bloqueante. scriptcon varios comandos no corta al primer fallo; en las demás sí. Si tu.travis.ymldependía de que todos se ejecutaran para recoger todos los errores de una vez, hay que reproducirlo a propósito.- Los secretos
secure:no se migran: se rotan. No se pueden descifrar sin la clave del repositorio en Travis, y aunque se pudiera, mover un secreto de sitio es el momento perfecto para cambiarlo. Y si el repositorio es público y estuvo activo en 2021, la rotación no es opcional (apartado 7). - Aprovecha para introducir el grafo. Una migración uno a uno reproduce las esperas del modelo secuencial. Es el momento de poner
needsy ganar tiempo gratis.
- Migración completa, paso a paso
Partimos de un .travis.yml realista, del tipo que te encontrarás:
# ANTES — .travis.yml heredado
language: node_js
node_js: [ "18", "20" ]
dist: focal
services: [ postgresql ]
cache:
directories: [ node_modules ]
env:
global:
- CI=true
- secure: "K3jd...==" # NPM_TOKEN cifrado
before_install:
- psql -c 'CREATE DATABASE app_test;' -U postgres
install:
- npm ci
before_script:
- npm run migrate
script:
- npm run lint
- npm test
after_success:
- bash <(curl -s https://codecov.io/bash)
deploy:
provider: npm
email: [email protected]
api_key: { secure: "9dLp...==" }
on: { tags: true }# DESPUÉS — .github/workflows/ci.yml
name: CI
on:
push: { branches: [main] }
pull_request:
release: { types: [published] } # 1 · sustituye a `on: tags`
permissions:
contents: read # 2 · mínimo privilegio (04-03)
concurrency: # 3 · no existía en Travis
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-22.04
strategy:
fail-fast: false # 4 · queremos ver los dos resultados
matrix:
node: [18, 20]
services:
postgres: # 5 · versión explícita, no la de la imagen
image: postgres:16-alpine
env: { POSTGRES_PASSWORD: test, POSTGRES_DB: app_test }
options: >-
--health-cmd pg_isready --health-interval 5s --health-retries 10
ports: [ '5432:5432' ]
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/app_test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm # 6 · cachea ~/.npm, NO node_modules
- run: npm ci
- run: npm run migrate
- run: npm run lint
- run: npm test -- --coverage
- uses: codecov/codecov-action@v4 # 7
if: always()
with: { token: '${{ secrets.CODECOV_TOKEN }}' }
publicar:
needs: [test] # 8 · grafo explícito
if: github.event_name == 'release'
runs-on: ubuntu-22.04
permissions:
contents: read
id-token: write # 9 · publicación con procedencia
environment: npm-produccion # 10 · aprobación y secretos acotados
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, registry-url: 'https://registry.npmjs.org' }
- run: npm ci
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # 11 · token NUEVO, rotadoon: tags→on: release: publicar al crear una release es más explícito y evita publicar por un tag accidental.permissionsexplícitos son un concepto que Travis no tenía; aprovecha la migración para ponerlos (04-03).concurrencycancela ejecuciones obsoletas: no existía en Travis y es ahorro inmediato.fail-fast: falsereproduce el comportamiento habitual de la matriz de Travis, donde ver los dos resultados era lo normal.- PostgreSQL como contenedor con versión fijada y health check, en vez de "el que trae la imagen": más reproducible, y elimina la carrera de arranque que en Travis se resolvía con reintentos.
- Cambio deliberado:
node_modulessale de la caché y entra~/.npm. Corrige el problema de binarios de la 04-02 que el fichero original arrastraba. - La subida de cobertura con
if: always()reproduce la semántica deafter_successsin romper el build, pero de forma explícita. needs: [test]es la ganancia estructural: ahora hay grafo.id-token: writecon--provenanceañade procedencia verificable al paquete (02-06 y 04-03): algo que en el modelo de Travis no existía.environmentaporta aprobación y secretos acotados a la publicación.NPM_TOKENes un token nuevo. El desecure:se revoca. No se migra un secreto: se rota.
Fases recomendadas de la migración, con el mismo criterio de la 05-04 —cada paso con valor propio—:
| Fase | Qué se hace | Cuándo pasar a la siguiente |
|---|---|---|
| 1. Inventario | Qué hace el .travis.yml, qué secretos usa, quién depende de la insignia |
Cuando esté escrito |
| 2. Pipeline paralelo | El workflow nuevo convive con Travis, ambos en cada PR | Cuando coincidan los resultados 2 semanas |
| 3. Cambio de señal | El check obligatorio pasa a ser el nuevo; Travis queda informativo | Cuando nadie mire ya el de Travis |
| 4. Retirada | Borrar .travis.yml, desactivar el proyecto, revocar todos los secretos, actualizar la insignia del README |
— |
Qué no migrar: jobs que nadie mira, la matriz de versiones ya sin soporte —migrar node_js: "0.10" es trabajo tirado—, y las fases after_* que fallan en silencio desde hace meses. Una migración es la mejor ocasión para borrar, y desaprovecharla es cargar la deuda a la herramienta nueva.
- La lección de portabilidad
De todo el episodio Travis sale una recomendación práctica que la 06-07 desarrollará y que conviene ya:
# Frágil: la lógica vive en el YAML de la herramienta
script:
- npm ci
- npm run lint
- npx tsc --noEmit
- npm test -- --coverage --shard=$SHARD/4
- docker build -t $IMAGEN:$TRAVIS_COMMIT -f apps/api/Dockerfile .
- trivy image --severity HIGH,CRITICAL --exit-code 1 $IMAGEN:$TRAVIS_COMMIT#!/usr/bin/env bash
# scripts/verificar.sh — la lógica vive en el repositorio
set -euo pipefail
SHARD="${SHARD:-1}"; TOTAL="${TOTAL:-1}"
npm ci --prefer-offline
npm run lint
npx tsc --noEmit
npm test -- --coverage --shard="${SHARD}/${TOTAL}"# El YAML queda como orquestador delgado, y migrar es reescribir 15 líneas
script:
- ./scripts/verificar.shLas ventajas van más allá de la migración, y por eso merece la pena aunque nunca cambies de herramienta: el desarrollador puede ejecutar exactamente lo mismo en su máquina —lo que elimina la clase entera de fallos "solo pasa en CI"—, el script se prueba y se revisa como código, y el YAML deja de ser un lugar donde esconder lógica de negocio, que era uno de los antipatrones de la 04-01.
El límite honesto: no todo se puede sacar del YAML. Los disparadores, la matriz, los permisos, los entornos, la concurrencia y la caché son propios de cada herramienta y no se abstraen sin construir una capa de indirección que suele costar más de lo que ahorra. El objetivo razonable no es la portabilidad total, es que el 80 % del esfuerzo de migración sea reescribir orquestación y no reconstruir lógica. Los proyectos que migraron de Travis en una tarde eran los que tenían un script: make ci.
- Cuándo Travis todavía tiene sentido
Con honestidad y sin nostalgia:
- Un proyecto que ya lo tiene, funciona y no se toca. Un
.travis.ymlestable en un repositorio de mantenimiento no es una emergencia. Migrar cuesta horas que quizá rinden más en otro sitio. Con dos condiciones: revisa que los secretos siguen siendo válidos y necesarios, y que las imágenesdist:que usa siguen soportadas. - Plataformas y arquitecturas poco comunes. Travis mantuvo soporte para arquitecturas como IBM Power, IBM Z o ARM64 antes y de forma más accesible que otros, y algunos proyectos de sistemas lo siguen usando por eso. Si tu problema es exactamente ese, sigue siendo una opción a considerar.
- Publicación sencilla de paquetes de código abierto. El bloque
deploy:con proveedores predefinidos sigue siendo de lo más corto que existe para publicar en npm, PyPI o GitHub Releases.
Cuándo no: cualquier proyecto nuevo, cualquier pipeline con entrega continua real, cualquier equipo que ya esté en GitHub o GitLab, y cualquier caso donde la promoción por digest o la ejecución selectiva importen. No es una cuestión de gusto: son limitaciones estructurales del modelo de fases fijas, no defectos corregibles.
Errores Comunes y Consejos
Migrar los secretos en vez de rotarlos. Cambiar de herramienta es el momento ideal para rotar. Y si el repositorio es público y estuvo activo en 2021, la rotación es obligatoria.
Migrar uno a uno sin introducir el grafo. Reproduce las esperas del modelo secuencial y desperdicia la mejor ganancia de la migración: poner needs.
Copiar la caché de node_modules a la herramienta nueva. Era una mala práctica en Travis y sigue siéndolo. Cachea el directorio de descarga con clave por lockfile.
Olvidar la insignia del README. Queda apuntando a un servicio inactivo y muestra un estado falso durante meses. Es cosmético y es la primera cosa que ve quien llega al proyecto.
Suponer que script corta al primer fallo. En Travis no lo hace; en las demás sí. Si el orden de los comandos ocultaba fallos, la migración los sacará todos a la vez, y eso es bueno aunque incomode el primer día.
Dejar Travis activo tras migrar. Consume créditos, envía notificaciones que nadie lee y mantiene vivos unos secretos que ya no controla nadie. Desactivar el proyecto y revocar credenciales es parte de la migración, no un extra.
Consejo general: cuando heredes un .travis.yml, léelo como documentación arqueológica antes de traducirlo. allow_failures te dice qué llevaba roto tiempo, la lista de dist y versiones te dice cuándo se tocó por última vez, y after_success te dice qué integraciones existían y quizá ya no. Ese contexto vale más que la traducción mecánica y es exactamente el enfoque de la 05-04.
Ejercicios
Ejercicio 1. Traduce a GitHub Actions este .travis.yml, decidiendo explícitamente qué steps deben ser bloqueantes y cuáles no, y justifica cada decisión:
language: python
python: [ "3.10", "3.11" ]
dist: focal
services: [ redis ]
cache: pip
install:
- pip install -r requirements.txt -r requirements-dev.txt
before_script:
- flake8 --version
script:
- flake8 .
- mypy src/
- pytest --cov=src
after_success:
- coveralls
after_failure:
- cat logs/errores.log
deploy:
provider: pypi
user: __token__
password: { secure: "Ab3d...==" }
on: { tags: true, python: "3.11" }Ejercicio 2. Un repositorio público de tu empresa usó Travis entre 2018 y 2022 con secure: para un token de despliegue de AWS con permisos amplios, un token de npm y una clave SSH cifrada con encrypt-file. Nadie lo ha tocado desde 2022. Escribe el plan de respuesta: qué asumes, qué haces primero, en qué orden y cómo verificas que has terminado.
Ejercicio 3. Marta pregunta: "¿Cuánto nos costaría cambiar de GitHub Actions a otra herramienta si mañana cambian sus condiciones?". Diseña el ejercicio de evaluación sobre el ci.yml de Reservalia: cómo se mide la portabilidad actual, qué cambios la mejorarían, cuánto cuestan y hasta dónde merece la pena llegar.
Soluciones
Solución 1.
name: CI
on:
push: { branches: [main] }
pull_request:
release: { types: [published] }
permissions: { contents: read }
concurrency: { group: ci-${{ github.ref }}, cancel-in-progress: true }
jobs:
verificar:
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
python: ["3.10", "3.11"]
services:
redis:
image: redis:7-alpine
options: --health-cmd "redis-cli ping" --health-interval 5s --health-retries 10
ports: ['6379:6379']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: pip
cache-dependency-path: 'requirements*.txt'
- run: pip install -r requirements.txt -r requirements-dev.txt
- run: flake8 . # bloqueante
- run: mypy src/ # bloqueante
- run: pytest --cov=src --cov-report=xml # bloqueante
- name: Subir cobertura
if: always() # NO bloqueante
continue-on-error: true
uses: coverallsapp/github-action@v2
- name: Diagnóstico en caso de fallo
if: failure() # equivalente a after_failure
run: cat logs/errores.log || true
publicar:
needs: [verificar]
if: github.event_name == 'release'
runs-on: ubuntu-22.04
permissions: { contents: read, id-token: write }
environment: pypi
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- run: pip install build && python -m build
- uses: pypa/gh-action-pypi-publish@release/v1 # publicación de confianza vía OIDCDecisiones y su justificación. flake8, mypy y pytest son bloqueantes: son la puerta de calidad de la 02-05 y su fallo debe impedir la fusión. La subida a Coveralls es no bloqueante con continue-on-error porque un fallo de un servicio externo no dice nada sobre el código —es exactamente la semántica de after_success, pero ahora explícita en vez de implícita en el nombre de la fase—. El volcado de log usa if: failure(), reproduciendo after_failure.
Dos cambios deliberados respecto al original. on: tags pasa a on: release para no publicar por un tag accidental. Y el password: secure: desaparece: se sustituye por publicación de confianza vía OIDC con id-token: write, que elimina el token de larga vida por completo. Es la mejora que más reduce riesgo de toda la migración, y es la aplicación directa de la lección del apartado 7: la mejor respuesta a "los secretos almacenados pueden filtrarse" es no almacenar secretos.
Una nota sobre cache: pip con cache-dependency-path: sin esa segunda línea, la clave no incluye requirements-dev.txt y la caché no se invalida al cambiar las dependencias de desarrollo. Es el mismo error de clave incompleta de la 04-02.
Solución 2.
Lo que se asume, y es la decisión más importante: que los tres secretos están comprometidos. El repositorio es público, estuvo activo durante 2021 y usaba exactamente el mecanismo afectado. No hay que buscar evidencia de explotación para actuar: la ausencia de evidencia en logs de hace años no es evidencia de ausencia, y el coste de rotar es muy inferior al de equivocarse en la otra dirección.
Orden de actuación, por impacto potencial:
- Token de AWS con permisos amplios (primero, hoy). Desactivar la clave —desactivar antes que borrar, para poder investigar—, revisar CloudTrail del periodo por si hay actividad desde direcciones o regiones inesperadas, y sustituirla. La sustitución no es "otra clave": es un rol asumido por OIDC desde la herramienta actual (03-02), con permisos mínimos. Si hay recursos que solo esa clave podía tocar, verificar su integridad.
- Token de npm. Revocar, revisar las versiones publicadas del paquete por si hay alguna que el equipo no reconozca —una publicación maliciosa es el peor escenario, porque el daño va a los consumidores—, y reemplazar por publicación de confianza vía OIDC.
- Clave SSH de
encrypt-file. Revocar la pública en todos los destinos donde esté autorizada (servidores,deploy keysde repositorios), generar una nueva, y comprobar losauthorized_keysde los servidores accesibles por si hay claves añadidas que nadie reconozca. - Limpieza del repositorio. Borrar
.travis.yml, el fichero.encde la clave cifrada y la insignia. Ojo: borrar no elimina el historial, y el fichero cifrado sigue accesible; por eso la revocación es la medida real y el borrado es solo higiene. - Desactivar el proyecto en Travis para que no queden credenciales almacenadas allí.
Cómo se verifica que has terminado: inventario escrito de los tres secretos con su estado —revocado, sustituido, verificado—, comprobación de que ningún sistema en producción dependía de ellos (si algo se rompe al revocar, aparece aquí, que es por lo que se hace en horario laborable y no un viernes), y una nota en el registro de decisiones con la fecha y el motivo. Y una acción de fondo que evita el próximo episodio: crear el inventario de secretos de toda la organización con propietario y procedimiento de rotación, porque este ejercicio ha demostrado que no existía.
Solución 3. El ejercicio se hace en tres pasos y produce un número, no una opinión.
Paso 1: clasificar cada línea del ci.yml en tres categorías.
| Categoría | Qué es | Coste de migrar | Ejemplos en Reservalia |
|---|---|---|---|
| Portable | Lógica que ya está en scripts o comandos estándar | Cero | npm ci, npm test, docker build, ./scripts/desplegar.sh |
| Orquestación | Estructura del pipeline: jobs, needs, matriz, disparadores, caché |
Reescritura mecánica | on:, needs:, strategy: matrix, actions/cache |
| Atado | Depende de la herramienta o de su ecosistema | Reconstrucción real | actions/* de terceros, environment: con revisores, OIDC hacia AWS, preparar-node, reusable-build-publicar.yml, GITHUB_TOKEN |
Paso 2: medir. Contar líneas de cada categoría en los cinco workflows y estimar horas. Un resultado típico para un repositorio como Reservalia: la orquestación se reescribe en 2-4 días; lo atado es lo caro —cada acción de terceros hay que sustituirla o reimplementarla, la federación OIDC se rehace contra el nuevo emisor, y las composite actions y los reusable workflows se traducen al mecanismo equivalente—. Si el 40 % del pipeline está en la categoría "atado", la migración es de semanas.
Paso 3: mejoras y su coste.
| Mejora | Coste | Efecto en portabilidad | ¿Merece la pena? |
|---|---|---|---|
Mover la lógica de los run largos a scripts/*.sh y npm scripts |
2-3 días | Alto: convierte "atado" en "portable" | Sí, y se paga sola aunque nunca migres: ejecutable en local, revisable, probable |
| Sustituir acciones de terceros por comandos de la CLI equivalente | 1-2 días | Medio-alto | Sí donde la acción solo envuelve una CLI; no donde aporta lógica real |
Definir un make ci, make test, make build como interfaz única |
1 día | Alto | Sí: da un contrato estable independiente de la herramienta |
| Abstraer disparadores y entornos tras una capa propia | Semanas | Bajo | No: cuesta más que la migración que evita |
Hasta dónde merece la pena llegar, que es la pregunta real de Marta: hasta el punto en que la lógica sea portable y la orquestación sea delgada, y ni un paso más. Es decir, los tres primeros puntos sí; el cuarto no. La razón es de coste esperado: la probabilidad de migrar en los próximos dos años es baja, pero los tres primeros puntos tienen beneficio inmediato con independencia de la migración —reproducibilidad local, pipeline más legible, lógica revisable y testeable, menos "verde falso" por diferencias entre local y CI—, mientras que el cuarto solo rinde en un escenario improbable.
La respuesta corta para Marta: "Hoy, unas tres o cuatro semanas. Con dos o tres días de trabajo que además nos vienen bien por otros motivos, lo bajamos a una semana. Y no vamos a intentar bajarlo a cero, porque eso cuesta más que la migración que evitaría."
Conclusión
Travis CI inventó la forma en que hoy se escribe CI —un fichero en el repositorio, convención sobre configuración, matriz de versiones— y demostró que el CI podía ser un derecho de cualquier proyecto y no un privilegio de quien tuviera un administrador de Jenkins. Su modelo de fases fijas era perfecto para "construye y prueba" y estructuralmente insuficiente para la entrega continua: sin grafo, sin paso de artefactos entre jobs, sin ejecución selectiva y con la promoción por digest comprometida. Traducir el pipeline de Reservalia a Travis fue el ejercicio más útil del módulo hasta ahora, no porque el resultado sirva, sino porque lo que no cabe explica por qué los sucesores se diseñaron con el grafo en el centro.
Y las tres lecciones que sobreviven a la herramienta: el modelo de negocio de tu proveedor puede cambiar y no lo decides tú; un secreto confiado a un tercero es un secreto cuya seguridad no controlas, de donde se sigue que las credenciales efímeras federadas no son una moda sino una reducción real de daño; y el coste de cambiar de herramienta se decide años antes de cambiarla, según cuánta lógica dejaste dentro del YAML.
La siguiente lección cambia de categoría. Docker y Kubernetes no son herramientas de CI: son el sustrato sobre el que corren los pipelines modernos y el destino al que despliegan. Veremos a Docker en sus dos papeles —entorno de ejecución del pipeline y formato del artefacto que se despliega—, bajaremos a lo que el curso no había cubierto (BuildKit, cachés de capas en el registro, construcción multi-plataforma, imágenes mínimas, construcción sin daemon privilegiado), y luego a Kubernetes como destino: los objetos mínimos, las sondas, el rollback con kubectl rollout undo, Helm frente a Kustomize, y por qué en Kubernetes triunfa GitOps y qué cambia respecto al cd.yml de push que usa Reservalia.
Curso de CI/CD: Integración y Despliegue Continuo
Módulo 1: Introducción a CI/CD
- Conceptos Básicos de CI/CD
- Beneficios de CI/CD
- Herramientas Populares de CI/CD
- El Proyecto del Curso: la Aplicación que Vamos a Automatizar
- Métricas DORA: Cómo se Mide la Entrega de Software
Módulo 2: Integración Continua (CI)
- Introducción a la Integración Continua
- Configuración de un Entorno de CI
- Automatización de la Construcción
- Pruebas Automatizadas
- Calidad de Código y Análisis Estático
- Artefactos, Versionado y Promoción
- Integración con Control de Versiones
Módulo 3: Despliegue Continuo (CD)
- Introducción al Despliegue Continuo
- Automatización del Despliegue
- Infraestructura como Código y Entornos Reproducibles
- Estrategias de Despliegue
- Feature Flags, Rollback y Recuperación ante Fallos
- Monitoreo y Retroalimentación
Módulo 4: Prácticas Avanzadas de CI/CD
- Pipelines de CI/CD
- Gestión de Dependencias
- Seguridad en CI/CD
- Escalabilidad y Rendimiento
- Pipeline as Code: Plantillas, Reutilización y Pruebas del Pipeline
- Bases de Datos en el Pipeline: Migraciones Seguras
Módulo 5: Implementación de CI/CD en Proyectos Reales
- Caso de Estudio: Proyecto Web
- Caso de Estudio: Aplicación Móvil
- Caso de Estudio: Microservicios
- Caso de Estudio: Modernizar un Proyecto Legacy
Módulo 6: Herramientas y Tecnologías
- Jenkins
- GitLab CI/CD
- CircleCI
- Travis CI
- Docker y Kubernetes
- GitHub Actions a Fondo
- Comparativa y Criterios para Elegir Herramienta
Módulo 7: Ejercicios Prácticos
- Ejercicio 1: Configuración de un Pipeline Básico
- Ejercicio 2: Integración de Pruebas Automatizadas
- Ejercicio 3: Despliegue en un Entorno de Producción
- Ejercicio 4: Monitoreo y Retroalimentación
- Ejercicio 5: Endurecer el Pipeline con Seguridad y Secretos
- Proyecto Final: Pipeline Completo de Extremo a Extremo
