Aquesta lliçó té un angle diferent de les anteriors, i convé dir-ho des del principi: no triaràs Travis CI per a un projecte nou. El que sí que és molt probable és que te'l trobis —en un repositori intern que ningú ha tocat des del 2019, en una dependència de codi obert on vols contribuir, en la migració que t'encarreguen en entrar en un equip— i que hagis d'entendre què fa aquell .travis.yml de trenta línies i traduir-lo. I hi ha una segona raó per dedicar-li una lliçó sencera: Travis va inventar bona part del que aquest curs dóna per evident. El fitxer de CI versionat al costat del codi, la matriu de versions de llenguatge, el CI gratuït i il·limitat per a codi obert i la insígnia verda al README són seus; GitHub Actions i GitLab CI hereten la seva idea central. I després va passar el que va passar, que és la part més instructiva: un canvi de propietari, un canvi de model de negoci, una migració massiva a una altra eina i un incident de seguretat que va deixar una lliçó duríssima sobre confiar secrets a un tercer. Veurem què va aportar, com funciona el seu model de fases fixes, el pipeline de Reservalia forçat a cabre-hi, què va passar, i sobretot com migrar un .travis.yml al que facis servir avui.

Contingut

  1. Què va aportar Travis i per què importa
  2. El model de fases fixes
  3. .travis.yml bàsic i matriu de versions
  4. El pipeline de Reservalia forçat a cabre-hi
  5. Stages, memòries cau i secrets xifrats
  6. Què va passar: propietat, model de negoci i èxode
  7. L'incident del 2021 i la lliçó sobre secrets
  8. Migrar un .travis.yml: taula d'equivalències
  9. Migració completa, pas a pas
  10. La lliçó de portabilitat
  11. Quan Travis encara té sentit
  12. Errors Comuns i Consells
  13. Exercicis
  14. Conclusió

  1. Què va aportar Travis i per què importa

Travis CI va aparèixer el 2011, quan el CI normal era una instal·lació de Jenkins que algú mantenia i on configurar un projecte significava demanar un job a un administrador i omplir un formulari (06-01). Travis va proposar una altra cosa: un fitxer al teu repositori i prou.

# .travis.yml del 2013. Això era tot. I era revolucionari.
language: node_js
node_js:
  - "0.10"
  - "0.12"

Amb aquestes quatre línies, cada push disparava una construcció, en dues versions de Node, en una màquina neta, sense haver demanat permís a ningú ni instal·lat res. Quatre innovacions concretes, totes avui invisibles perquè són l'estàndard:

Aportació Què va canviar On viu avui
Configuració al repositori El pipeline es ramifica i es revisa amb el codi Jenkinsfile, .gitlab-ci.yml, config.yml, workflows: tots
Convenció sobre configuració language: node_js implica instal·lar Node, executar npm ci i npm test sense dir-ho Les accions setup-*, els buildpacks, cimg/*
Matriu de versions Provar en diverses versions era una llista, no N jobs copiats matrix / parallel: matrix (02-04)
Gratuït i il·limitat per a codi obert El CI va deixar de ser un privilegi d'empreses Els plans gratuïts actuals, que existeixen perquè Travis els va normalitzar

L'efecte cultural va ser enorme: durant anys, la insígnia verda de Travis al README va ser el senyal que un projecte de codi obert era seriós. Milers de projectes van adoptar proves automatitzades perquè el cost d'entrada va baixar a un fitxer. És difícil exagerar quanta cultura de CI actual surt d'aquí.

Aquest és el motiu d'estudiar Travis encara que no el facis servir: entendre el .travis.yml és entendre el model mental del qual tots els altres deriven, incloses les seves limitacions, que és el que va empènyer els successors a dissenyar-se diferent.

  1. El model de fases fixes

Aquí hi ha la diferència estructural amb tot el que hem vist. Jenkins, GitLab, CircleCI i GitHub Actions et deixen definir les etapes i les seves dependències. Travis no: té un cicle de vida de fases predefinides i tu omples les que necessitis.

flowchart TD
    A["apt addons"] --> B["before_install"]
    B --> C["install<br/>per defecte segons language"]
    C --> D["before_script"]
    D --> E["script<br/>el que decideix exit o fallada"]
    E --> F{"resultat"}
    F -->|"exit"| G["after_success"]
    F -->|"fallada"| H["after_failure"]
    G --> I["before_deploy"]
    I --> J["deploy"]
    J --> K["after_deploy"]
    G --> L["after_script"]
    H --> L
Fase Per a què Nota important
before_install Preparar el sistema (repositoris apt, eines) Una fallada aquí és un error, no una fallada de build
install Instal·lar dependències Té valor per defecte segons language
before_script Preparar l'entorn (arrencar base de dades, migrar) Una fallada aquí també és error
script El que decideix si el build passa Una fallada aquí és una fallada de build. La distinció és subtil i confon
after_success / after_failure Cobertura, notificacions El seu resultat no afecta el build
before_deploy / deploy / after_deploy Publicar i desplegar deploy fa servir proveïdors predefinits
after_script Neteja Sempre s'executa

Què es guanya i què es perd amb les fases fixes:

Es guanya simplicitat brutal. No cal decidir l'estructura del pipeline: l'estructura ja hi és i tu omples forats. Per al 90 % dels projectes de codi obert del 2013 —instal·la, prova, informa— era exactament el que calia, i el fitxer cabia en una pantalla. A més fa els fitxers comparables entre projectes: saps on mirar.

Es perd tot el que necessita un pipeline real de lliurament:

  • No hi ha graf. No pots dir "publicar depèn de qualitat i de test però no de la documentació". L'ordre és el que hi ha.
  • No hi ha jobs amb dependències arbitràries. Els stages van arribar tard i són seqüencials, no un DAG.
  • No hi ha paral·lelització dins d'un job. El sharding de la 02-04 s'ha de simular amb entrades de matriu i variables d'entorn.
  • El pas d'artefactes entre jobs no existeix de manera nativa. Cada job de la matriu és independent i arrenca net; per passar el dist/ d'un job a un altre cal pujar-lo a S3 o a un altre magatzem tu mateix. Aquest és el límit més sever: la promoció de l'artefacte únic de la 02-06 no es pot expressar en el model.
  • La condicionalitat és pobra. if: existeix, però no hi ha res semblant a rules ni a expressions riques.

La conclusió, que és la que fa didàctica aquesta lliçó: el model de fases fixes és excel·lent per a CI i estructuralment insuficient per a CD. I això explica per què els successors —GitLab amb stages i després needs, CircleCI amb workflows des del principi, GitHub Actions amb needs— van posar el graf al centre. No va ser moda: va ser la correcció d'una limitació concreta.

  1. .travis.yml bàsic i matriu de versions

language: node_js
dist: jammy                       # 1 · imatge base d'Ubuntu
node_js: [ "20", "22" ]           # 2 · matriu implícita: 2 jobs

services: [ postgresql ]          # 3 · serveis preinstal·lats a la VM

cache:
  npm: true                       # 4 · memòria cau coneguda pel proveïdor
  directories: [ node_modules ]

before_install:
  - psql -c 'CREATE DATABASE reservalia_test;' -U postgres

install:
  - npm ci                        # substitueix l'install per defecte

script:                           # 5 · el que decideix el resultat
  - npm run lint
  - npm test

after_success:
  - bash <(curl -s https://codecov.io/bash)   # 6 · la seva fallada no trenca el build

notifications:
  email: false
  slack:
    rooms:
      secure: "AbCd...=="         # 7 · valor xifrat amb la clau pública del repositori
  1. dist tria la imatge base (trusty, xenial, bionic, focal, jammy). És el més semblant a runs-on, i una font clàssica de builds trencats: projectes ancorats a dist: trusty que van deixar de funcionar quan aquella imatge es va retirar.
  2. La matriu implícita és la idea que més es va copiar: llistar valors de la clau del llenguatge genera un job per valor.
  3. services activa serveis ja instal·lats a la VM. És diferent dels services de GitLab o GitHub, que aixequen contenidors: aquí no tries la versió amb precisió, corres la que porta la imatge. Menys control, menys cerimònia.
  4. cache: npm: true és memòria cau per convenció: el proveïdor sap què cal desar per a cada llenguatge. Còmode, i amb la contrapartida de sempre —desar node_modules en lloc del directori de descàrrega d'npm produeix estats corromputs quan canvia la versió de Node, exactament el problema d'invalidació de la 04-02—.
  5. script és la fase que decideix. Cada ordre de la llista s'executa i si una falla, les següents s'executen igualment; el build acaba fallant. És diferent de gairebé totes les eines modernes, on un step fallat talla el job, i és una font de confusió en llegir logs.
  6. after_success no afecta el resultat. Si la pujada de cobertura falla, el build continua verd. De vegades és el que vols i de vegades amaga que fa un mes que no puges cobertura.
  7. secure: és un valor xifrat amb la clau pública del repositori, generat amb travis encrypt. Apartat 5.

Matriu explícita, amb el que la feia potent:

jobs:                             # abans es deia "matrix"
  include:
    - node_js: "22"
      env: SUITE=integracio
      services: [ postgresql, redis ]
    - node_js: "22"
      os: osx                     # macOS, un dels seus arguments històrics
    - node_js: "22"
      arch: arm64
  exclude:
    - node_js: "20"
      os: osx
  allow_failures:                 # jobs que poden fallar sense trencar el build
    - node_js: "nightly"
  fast_finish: true               # informa del resultat sense esperar els allow_failures

allow_failures amb fast_finish és una bona idea que continua vigent: provar contra la versió en desenvolupament del llenguatge sense bloquejar ningú, i assabentar-te aviat que alguna cosa es trencarà. És l'equivalent de continue-on-error amb la matriu de la 02-04.

  1. El pipeline de Reservalia forçat a cabre-hi

Quarta traducció del mateix pipeline, i la primera en què cal explicar què no es pot expressar.

language: node_js
node_js: [ "22" ]
dist: jammy
os: linux

services: [ postgresql, docker ]

cache:
  directories: [ $HOME/.npm ]

env:
  global:
    - IMATGE=reservalia/api
    - ECR=123456789012.dkr.ecr.eu-west-1.amazonaws.com

stages:                                   # 1 · seqüencials, sense graf
  - verificar
  - construir
  - name: publicar
    if: branch = main AND type = push     # 2

jobs:
  include:
    # ---------------- verificar ----------------
    - stage: verificar
      name: "Qualitat"
      install: npm ci --prefer-offline
      script:
        - npx prettier --check .
        - npm run lint
        - npm run typecheck

    - stage: verificar                    # 3 · el sharding, a mà
      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
    # … i dues entrades més gairebé idèntiques per a 3/4 i 4/4

    # ---------------- construir ----------------
    - stage: construir
      name: "Build imatge"
      install: skip
      script:
        - docker build -f apps/api/Dockerfile -t $IMATGE:$TRAVIS_COMMIT .
        - trivy image --severity HIGH,CRITICAL --exit-code 1 $IMATGE:$TRAVIS_COMMIT
      after_success:
        # 4 · perquè el stage següent vegi la imatge cal pujar-la JA
        - echo "$ECR_PASSWORD" | docker login -u AWS --password-stdin $ECR
        - docker tag $IMATGE:$TRAVIS_COMMIT $ECR/$IMATGE:$TRAVIS_COMMIT
        - docker push $ECR/$IMATGE:$TRAVIS_COMMIT

    # ---------------- publicar ----------------
    - stage: publicar
      name: "Promocionar per digest"
      install: skip
      script:
        # 5 · el digest s'ha de tornar a consultar: no va viatjar des del 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/$IMATGE@$DIGEST"
      deploy:                             # 6 · proveïdors predefinits
        provider: s3
        bucket: reservalia-web
        local_dir: apps/web/dist
        skip_cleanup: true
        on: { branch: main }

Què queda forçat, que és la part instructiva:

  1. Els stages són seqüencials, no un graf. construir no pot començar fins que acabin els cinc jobs de verificar, encara que només depengui de les fonts. És la barrera d'etapa de la 04-01 sense escapatòria: no existeix needs.
  2. La condicionalitat és limitada. El llenguatge d'if: cobreix branca, tipus d'esdeveniment, tag i variables d'entorn, i poca cosa més. No hi ha filtres per ruta com els de la 02-07, així que l'execució selectiva en monorepo de la 04-04 no es pot expressar.
  3. El sharding és copiar i enganxar quatre entrades. No hi ha parallelism: 4 ni matriu que generi shards amb una línia; cada partició és una entrada de jobs.include amb el seu install i el seu before_script repetits. Quatre blocs gairebé idèntics: exactament la duplicació que la 04-05 va ensenyar a eliminar i que aquí no té solució dins del fitxer.
  4. No hi ha pas d'artefactes entre jobs. El dist/ i la imatge no viatgen del stage construir al stage publicar: cada job arrenca en una màquina neta. L'única sortida és pujar l'artefacte a un magatzem extern immediatament i tornar a descarregar-lo o a referenciar-lo després. Funciona, però converteix el magatzem extern en part obligatòria del pipeline i fa que la construcció i la publicació deixin de ser passos separables.
  5. La conseqüència més greu: com que el digest no pot viatjar, cal tornar a consultar-lo per etiqueta. I això trenca una garantia que el curs defensa des de la 02-06: la promoció per digest exigeix que l'identificador que es promociona sigui exactament el que va produir el build. Consultar-lo per etiqueta introdueix una finestra —petita, però real— en què aquella etiqueta podria apuntar a una altra imatge. A Travis es pot mitigar (etiquetes úniques per commit, registres amb etiquetes immutables), no eliminar netament.
  6. deploy amb proveïdors és la part més còmoda de Travis: dotzenes de proveïdors (S3, Heroku, npm, PyPI, GitHub Releases) configurats amb quatre línies. Per publicar un paquet de codi obert continua sent de les coses més directes que existeixen. Per desplegar a ECS amb canari, no hi ha proveïdor i acabes escrivint l'script igualment.

Balanç honest: el pipeline hi cap, però perd el graf, perd el sharding declaratiu, perd l'execució selectiva i compromet la promoció per digest. No és que Travis estigui mal fet; és que es va dissenyar per a "construeix i prova un projecte de codi obert" i aquest pipeline és de lliurament continu.

  1. Stages, memòries cau i secrets xifrats

Els stages van arribar el 2017, tard, i són grups seqüencials de jobs paral·lels —el model original de GitLab, sense el needs posterior—. Van afegir una cosa important: if: a nivell de stage, i la possibilitat d'una fase de desplegament després de les proves.

Memòries cau: per convenció (cache: npm|bundler|pip) o per directoris explícits. Amb dos avisos que valen per a qualsevol eina. Primer, desar node_modules a la memòria cau és una mala idea davant de desar el directori de descàrrega: node_modules conté binaris compilats per a una versió concreta de Node i arquitectura, i restaurar-lo en un altre context produeix fallades críptiques (04-02). Segon, la invalidació de la memòria cau de Travis es feia esborrant-la des de la interfície o amb la CLI, no canviant una clau: no hi ha clau per checksum, així que la memòria cau pot quedar-se obsoleta indefinidament. És justament el control que CircleCI va fer explícit (06-03).

Secrets xifrats, la part més característica i la més rellevant per al que ve:

# Xifra un valor amb la clau pública del repositori i l'afegeix al .travis.yml
travis encrypt AWS_SECRET_ACCESS_KEY="AKIA..." --add env.global

# Xifra un fitxer sencer (una clau privada, un keystore)
travis encrypt-file claus/deploy.pem --add
env:
  global:
    - secure: "Xy9k...=="          # el valor xifrat, versionat al repositori

La idea era elegant: el secret xifrat viu al repositori, així que el .travis.yml és autocontingut i funciona per a qualsevol que faci fork sense exposar res, perquè només Travis té la clau privada. Dues propietats de disseny que el feien raonablement segur: els valors xifrats no es desxifren en builds de pull requests des de forks —justament perquè un PR maliciós no els pogués llegir, que és el mateix raonament que pull_request_target de la 04-03—, i Travis emmascarava els valors als logs.

I en aquestes dues propietats hi ha la història de l'apartat 7.

  1. Què va passar: propietat, model de negoci i èxode

Els fets, en ordre, perquè el patró importa més que les dates:

Gener del 2019: canvi de propietari. Travis CI va ser adquirit per Idera. Poc després es va produir una sortida significativa de personal d'enginyeria, àmpliament comentada a la comunitat. Per als usuaris, l'efecte immediat va ser incertesa sobre el rumb del producte.

Finals del 2020: fi del model gratuït il·limitat per a codi obert. Es va introduir un sistema de crèdits amb una assignació limitada per a projectes oberts, renovable sota sol·licitud. La justificació era raonable —el cost del còmput és real i l'abús per mineria de criptomonedes era un problema real del sector—, però el canvi afectava la proposta de valor que havia definit Travis durant deu anys.

2019-2021: GitHub Actions. GitHub va llançar Actions al lloc on ja vivia el codi de la majoria dels projectes de codi obert, amb minuts gratuïts generosos per a repositoris públics i zero configuració d'integració. La combinació de tots dos factors va produir una migració massiva: milers de projectes van canviar el seu .travis.yml per .github/workflows/ci.yml en qüestió de mesos.

El que cal endur-se d'això no és una crítica a Travis ni a Idera, sinó tres observacions aplicables a qualsevol eina que triïs avui:

  • El model de negoci del teu proveïdor de CI pot canviar, i no ho decideixes tu. Un pla gratuït, uns minuts inclosos o un nivell de preus poden canviar amb un preavís curt. Si la teva operació depèn críticament d'aquestes condicions, tens una exposició que convé reconèixer.
  • La integració venç la qualitat tècnica. Actions no va guanyar per ser millor eina que Travis el 2020 —discutible en diversos aspectes—, sinó per ser on ja era el codi. És el criteri que la 06-07 posarà en primer lloc per pes real: on viu el codi i la identitat de l'equip decideix la majoria de les eleccions.
  • El cost de sortida es paga quan ja no hi ha alternativa. Els projectes que tenien la lògica en scripts del repositori van migrar en una tarda; els que la tenien dispersa al YAML van trigar setmanes. Apartat 10.

  1. L'incident del 2021 i la lliçó sobre secrets

El setembre del 2021, Travis CI va publicar un avís de seguretat sobre una fallada que provocava que variables d'entorn segures —inclosos tokens i claus— quedessin exposades en builds de pull requests procedents de forks de repositoris públics. És a dir, es va trencar precisament la propietat de disseny que feia segur el model de secrets xifrats: que un fork no hi pot accedir. Qualsevol que obrís un PR contra un repositori públic afectat podia, amb un .travis.yml modificat, llegir secrets que mai no haurien d'haver estat al seu abast.

Se'l va identificar com a CVE-2021-41077, i la comunicació inicial va generar força crítiques pel seu abast i el seu detall, cosa que va portar a una publicació posterior més explícita. La recomanació operativa per a qualsevol projecte públic afectat va ser rotar totes les credencials.

Quatre lliçons concretes, que no són sobre Travis:

1. Un secret confiat a un tercer és un secret la seguretat del qual no controles. No és un argument contra els SaaS —la 06-03 esmentava l'incident de CircleCI del 2023, amb la mateixa conclusió de rotar-ho tot, i cap plataforma és immune—. És un argument a favor d'assumir que passarà i dissenyar en conseqüència.

2. Les credencials de llarga vida són el problema; les efímeres acoten el dany. Una clau d'accés permanent filtrada és vàlida fins que algú la revoca, i aquest "algú" se n'assabenta tard. Un token OIDC de quinze minuts limitat a un rol concret (03-02) és material robat que ja no serveix per a gairebé res. Aquesta és la raó pràctica —no teòrica— per la qual el curs insisteix tant en la identitat federada davant dels secrets emmagatzemats.

3. Cal saber rotar-ho tot, i haver-ho assajat. La pregunta útil és: si demà el teu proveïdor et diu "rota totes les teves credencials", quant trigues i què es trenca? Si la resposta és "no ho sé", això és un pla de recuperació pendent, exactament en el sentit de la 03-05. Un inventari de secrets amb el seu propietari i el seu procediment de rotació val molt el dia que fa falta.

4. Els forks són la frontera de confiança més delicada de qualsevol CI. El mateix problema, amb un mecanisme diferent, és el de pull_request_target de la 04-03 que la 06-06 tornarà a tractar. La regla és universal i independent d'eina: el codi d'un fork no pot accedir a secrets, i qualsevol flux que sembli donar-li accés cal examinar-lo amb lupa.

  1. Migrar un .travis.yml: taula d'equivalències

Aquesta és la part que probablement facis servir de debò. La traducció és majoritàriament mecànica, amb dues o tres decisions de disseny.

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 amb tags
services: [postgresql] services: amb imatge de contenidor services: amb imatge
before_install Steps al principi del job before_script
install - run: npm ci before_script o script
before_script Steps previs before_script
script - run: (cadascun talla en fallar) script:
after_success - if: success() after_script o job amb when: on_success
after_failure - if: failure() when: on_failure
after_script - if: always() after_script
deploy: proveïdor Action del proveïdor o run Job de desplegament amb environment
cache: directories: actions/cache amb clau cache: key/paths
env.global env: a nivell de workflow variables:
secure: "..." Secrets del repositori Variables protegides i emmascarades
stages needs: entre jobs stages + needs
allow_failures continue-on-error: true allow_failure: true
fast_finish fail-fast (semàntica inversa) Comportament per defecte
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

Les quatre decisions que no són mecàniques:

  • La distinció entre fases que trenquen el build i fases que no desapareix. A Travis, after_success no afecta el resultat; a les altres, un step que falla trenca el job llevat que hi posis continue-on-error. En migrar, decideix explícitament què ha de continuar sent no bloquejant.
  • script amb diverses ordres no talla a la primera fallada; a les altres sí. Si el teu .travis.yml depenia que totes s'executessin per recollir tots els errors d'una vegada, cal reproduir-ho a propòsit.
  • Els secrets secure: no es migren: es roten. No es poden desxifrar sense la clau del repositori a Travis, i encara que es pogués, moure un secret de lloc és el moment perfecte per canviar-lo. I si el repositori és públic i va estar actiu el 2021, la rotació no és opcional (apartat 7).
  • Aprofita per introduir el graf. Una migració un a un reprodueix les esperes del model seqüencial. És el moment de posar needs i guanyar temps de franc.

  1. Migració completa, pas a pas

Partim d'un .travis.yml realista, del tipus que et trobaràs:

# ABANS — .travis.yml heretat
language: node_js
node_js: [ "18", "20" ]
dist: focal
services: [ postgresql ]
cache:
  directories: [ node_modules ]
env:
  global:
    - CI=true
    - secure: "K3jd...=="          # NPM_TOKEN xifrat
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 }
# DESPRÉS — .github/workflows/ci.yml
name: CI

on:
  push:      { branches: [main] }
  pull_request:
  release:   { types: [published] }        # 1 · substitueix `on: tags`

permissions:
  contents: read                            # 2 · mínim privilegi (04-03)

concurrency:                                # 3 · no existia a Travis
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-22.04
    strategy:
      fail-fast: false                      # 4 · volem veure els dos resultats
      matrix:
        node: [18, 20]
    services:
      postgres:                             # 5 · versió explícita, no la de la imatge
        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 · desa ~/.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 · graf explícit
    if: github.event_name == 'release'
    runs-on: ubuntu-22.04
    permissions:
      contents: read
      id-token: write                        # 9 · publicació amb procedència
    environment: npm-produccio               # 10 · aprovació i secrets acotats
    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 NOU, rotat
  1. on: tagson: release: publicar en crear una release és més explícit i evita publicar per una etiqueta accidental.
  2. permissions explícits són un concepte que Travis no tenia; aprofita la migració per posar-los (04-03).
  3. concurrency cancel·la execucions obsoletes: no existia a Travis i és estalvi immediat.
  4. fail-fast: false reprodueix el comportament habitual de la matriu de Travis, on veure els dos resultats era el normal.
  5. PostgreSQL com a contenidor amb versió fixada i health check, en comptes de "el que porta la imatge": més reproduïble, i elimina la cursa d'arrencada que a Travis es resolia amb reintents.
  6. Canvi deliberat: node_modules surt de la memòria cau i hi entra ~/.npm. Corregeix el problema de binaris de la 04-02 que el fitxer original arrossegava.
  7. La pujada de cobertura amb if: always() reprodueix la semàntica d'after_success sense trencar el build, però de manera explícita.
  8. needs: [test] és el guany estructural: ara hi ha graf.
  9. id-token: write amb --provenance afegeix procedència verificable al paquet (02-06 i 04-03): una cosa que al model de Travis no existia.
  10. environment aporta aprovació i secrets acotats a la publicació.
  11. NPM_TOKEN és un token nou. El de secure: es revoca. No es migra un secret: es rota.

Fases recomanades de la migració, amb el mateix criteri de la 05-04 —cada pas amb valor propi—:

Fase Què es fa Quan passar a la següent
1. Inventari Què fa el .travis.yml, quins secrets fa servir, qui depèn de la insígnia Quan estigui escrit
2. Pipeline paral·lel El workflow nou conviu amb Travis, tots dos a cada PR Quan coincideixin els resultats 2 setmanes
3. Canvi de senyal El check obligatori passa a ser el nou; Travis queda informatiu Quan ningú miri ja el de Travis
4. Retirada Esborrar .travis.yml, desactivar el projecte, revocar tots els secrets, actualitzar la insígnia del README

Què no migrar: jobs que ningú mira, la matriu de versions ja sense suport —migrar node_js: "0.10" és feina llençada—, i les fases after_* que fallen en silenci des de fa mesos. Una migració és la millor ocasió per esborrar, i desaprofitar-la és carregar el deute a l'eina nova.

  1. La lliçó de portabilitat

De tot l'episodi Travis en surt una recomanació pràctica que la 06-07 desenvoluparà i que convé ja:

# Fràgil: la lògica viu al YAML de l'eina
script:
  - npm ci
  - npm run lint
  - npx tsc --noEmit
  - npm test -- --coverage --shard=$SHARD/4
  - docker build -t $IMATGE:$TRAVIS_COMMIT -f apps/api/Dockerfile .
  - trivy image --severity HIGH,CRITICAL --exit-code 1 $IMATGE:$TRAVIS_COMMIT
#!/usr/bin/env bash
# scripts/verificar.sh — la lògica viu al repositori
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 com a orquestrador prim, i migrar és reescriure 15 línies
script:
  - ./scripts/verificar.sh

Els avantatges van més enllà de la migració, i per això val la pena encara que no canviïs mai d'eina: el desenvolupador pot executar exactament el mateix a la seva màquina —cosa que elimina la classe sencera de fallades "només passa en CI"—, l'script es prova i es revisa com a codi, i el YAML deixa de ser un lloc on amagar lògica de negoci, que era un dels antipatrons de la 04-01.

El límit honest: no tot es pot treure del YAML. Els disparadors, la matriu, els permisos, els entorns, la concurrència i la memòria cau són propis de cada eina i no s'abstreuen sense construir una capa d'indirecció que sol costar més del que estalvia. L'objectiu raonable no és la portabilitat total, és que el 80 % de l'esforç de migració sigui reescriure orquestració i no reconstruir lògica. Els projectes que van migrar de Travis en una tarda eren els que tenien un script: make ci.

  1. Quan Travis encara té sentit

Amb honestedat i sense nostàlgia:

  • Un projecte que ja el té, funciona i no es toca. Un .travis.yml estable en un repositori de manteniment no és una emergència. Migrar costa hores que potser rendeixen més en un altre lloc. Amb dues condicions: revisa que els secrets continuen sent vàlids i necessaris, i que les imatges dist: que fa servir continuen suportades.
  • Plataformes i arquitectures poc comunes. Travis va mantenir suport per a arquitectures com IBM Power, IBM Z o ARM64 abans i de manera més accessible que altres, i alguns projectes de sistemes el continuen fent servir per això. Si el teu problema és exactament aquest, continua sent una opció a considerar.
  • Publicació senzilla de paquets de codi obert. El bloc deploy: amb proveïdors predefinits continua sent de les coses més curtes que existeixen per publicar a npm, PyPI o GitHub Releases.

Quan no: qualsevol projecte nou, qualsevol pipeline amb lliurament continu real, qualsevol equip que ja sigui a GitHub o GitLab, i qualsevol cas on la promoció per digest o l'execució selectiva importin. No és una qüestió de gust: són limitacions estructurals del model de fases fixes, no defectes corregibles.

Errors Comuns i Consells

Migrar els secrets en comptes de rotar-los. Canviar d'eina és el moment ideal per rotar. I si el repositori és públic i va estar actiu el 2021, la rotació és obligatòria.

Migrar un a un sense introduir el graf. Reprodueix les esperes del model seqüencial i malbarata el millor guany de la migració: posar needs.

Copiar la memòria cau de node_modules a l'eina nova. Era una mala pràctica a Travis i ho continua sent. Desa el directori de descàrrega amb clau per lockfile.

Oblidar la insígnia del README. Queda apuntant a un servei inactiu i mostra un estat fals durant mesos. És cosmètic i és la primera cosa que veu qui arriba al projecte.

Suposar que script talla a la primera fallada. A Travis no ho fa; a les altres sí. Si l'ordre de les ordres amagava fallades, la migració les traurà totes de cop, i això és bo encara que incomodi el primer dia.

Deixar Travis actiu després de migrar. Consumeix crèdits, envia notificacions que ningú llegeix i manté vius uns secrets que ja no controla ningú. Desactivar el projecte i revocar credencials és part de la migració, no un extra.

Consell general: quan heretis un .travis.yml, llegeix-lo com a documentació arqueològica abans de traduir-lo. allow_failures et diu què portava trencat temps, la llista de dist i versions et diu quan es va tocar per última vegada, i after_success et diu quines integracions existien i potser ja no. Aquest context val més que la traducció mecànica i és exactament l'enfocament de la 05-04.

Exercicis

Exercici 1. Tradueix a GitHub Actions aquest .travis.yml, decidint explícitament quins steps han de ser bloquejants i quins no, i justifica cada decisió:

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/errors.log
deploy:
  provider: pypi
  user: __token__
  password: { secure: "Ab3d...==" }
  on: { tags: true, python: "3.11" }

Exercici 2. Un repositori públic de la teva empresa va fer servir Travis entre el 2018 i el 2022 amb secure: per a un token de desplegament d'AWS amb permisos amplis, un token d'npm i una clau SSH xifrada amb encrypt-file. Ningú l'ha tocat des del 2022. Escriu el pla de resposta: què assumeixes, què fas primer, en quin ordre i com verifiques que has acabat.

Exercici 3. La Marta pregunta: "Quant ens costaria canviar de GitHub Actions a una altra eina si demà canvien les seves condicions?". Dissenya l'exercici d'avaluació sobre el ci.yml de Reservalia: com es mesura la portabilitat actual, quins canvis la millorarien, quant costen i fins on val la pena arribar.

Solucions

Solució 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 .                                  # bloquejant
      - run: mypy src/                                 # bloquejant
      - run: pytest --cov=src --cov-report=xml         # bloquejant
      - name: Pujar cobertura
        if: always()                                   # NO bloquejant
        continue-on-error: true
        uses: coverallsapp/github-action@v2
      - name: Diagnòstic en cas de fallada
        if: failure()                                  # equivalent a after_failure
        run: cat logs/errors.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ó de confiança via OIDC

Decisions i la seva justificació. flake8, mypy i pytest són bloquejants: són la porta de qualitat de la 02-05 i la seva fallada ha d'impedir la fusió. La pujada a Coveralls és no bloquejant amb continue-on-error perquè una fallada d'un servei extern no diu res sobre el codi —és exactament la semàntica d'after_success, però ara explícita en comptes d'implícita al nom de la fase—. L'abocament de log fa servir if: failure(), reproduint after_failure.

Dos canvis deliberats respecte de l'original. on: tags passa a on: release per no publicar per una etiqueta accidental. I el password: secure: desapareix: se substitueix per publicació de confiança via OIDC amb id-token: write, que elimina el token de llarga vida completament. És la millora que més redueix el risc de tota la migració, i és l'aplicació directa de la lliçó de l'apartat 7: la millor resposta a "els secrets emmagatzemats es poden filtrar" és no emmagatzemar secrets.

Una nota sobre cache: pip amb cache-dependency-path: sense aquella segona línia, la clau no inclou requirements-dev.txt i la memòria cau no s'invalida en canviar les dependències de desenvolupament. És el mateix error de clau incompleta de la 04-02.

Solució 2.

El que s'assumeix, i és la decisió més important: que els tres secrets estan compromesos. El repositori és públic, va estar actiu durant el 2021 i feia servir exactament el mecanisme afectat. No cal buscar evidència d'explotació per actuar: l'absència d'evidència en logs de fa anys no és evidència d'absència, i el cost de rotar és molt inferior al d'equivocar-se en l'altra direcció.

Ordre d'actuació, per impacte potencial:

  1. Token d'AWS amb permisos amplis (primer, avui). Desactivar la clau —desactivar abans que esborrar, per poder investigar—, revisar CloudTrail del període per si hi ha activitat des d'adreces o regions inesperades, i substituir-la. La substitució no és "una altra clau": és un rol assumit per OIDC des de l'eina actual (03-02), amb permisos mínims. Si hi ha recursos que només aquella clau podia tocar, verificar-ne la integritat.
  2. Token d'npm. Revocar, revisar les versions publicades del paquet per si n'hi ha alguna que l'equip no reconegui —una publicació maliciosa és el pitjor escenari, perquè el dany va als consumidors—, i reemplaçar per publicació de confiança via OIDC.
  3. Clau SSH d'encrypt-file. Revocar la pública a tots els destins on estigui autoritzada (servidors, deploy keys de repositoris), generar-ne una de nova, i comprovar els authorized_keys dels servidors accessibles per si hi ha claus afegides que ningú reconegui.
  4. Neteja del repositori. Esborrar .travis.yml, el fitxer .enc de la clau xifrada i la insígnia. Compte: esborrar no elimina l'historial, i el fitxer xifrat continua sent accessible; per això la revocació és la mesura real i l'esborrat és només higiene.
  5. Desactivar el projecte a Travis perquè no hi quedin credencials emmagatzemades.

Com es verifica que has acabat: inventari escrit dels tres secrets amb el seu estat —revocat, substituït, verificat—, comprovació que cap sistema en producció no en depenia (si alguna cosa es trenca en revocar, apareix aquí, que és per això que es fa en horari laborable i no un divendres), i una nota al registre de decisions amb la data i el motiu. I una acció de fons que evita el pròxim episodi: crear l'inventari de secrets de tota l'organització amb propietari i procediment de rotació, perquè aquest exercici ha demostrat que no existia.

Solució 3. L'exercici es fa en tres passos i produeix un número, no una opinió.

Pas 1: classificar cada línia del ci.yml en tres categories.

Categoria Què és Cost de migrar Exemples a Reservalia
Portable Lògica que ja és en scripts o ordres estàndard Zero npm ci, npm test, docker build, ./scripts/desplegar.sh
Orquestració Estructura del pipeline: jobs, needs, matriu, disparadors, memòria cau Reescriptura mecànica on:, needs:, strategy: matrix, actions/cache
Lligat Depèn de l'eina o del seu ecosistema Reconstrucció real actions/* de tercers, environment: amb revisors, OIDC cap a AWS, preparar-node, reusable-build-publicar.yml, GITHUB_TOKEN

Pas 2: mesurar. Comptar línies de cada categoria als cinc workflows i estimar hores. Un resultat típic per a un repositori com Reservalia: l'orquestració es reescriu en 2-4 dies; el que és lligat és el car —cada acció de tercers cal substituir-la o reimplementar-la, la federació OIDC es refà contra el nou emissor, i les composite actions i els reusable workflows es tradueixen al mecanisme equivalent—. Si el 40 % del pipeline és a la categoria "lligat", la migració és de setmanes.

Pas 3: millores i el seu cost.

Millora Cost Efecte en portabilitat Val la pena?
Moure la lògica dels run llargs a scripts/*.sh i npm scripts 2-3 dies Alt: converteix "lligat" en "portable" Sí, i es paga sola encara que no migris mai: executable en local, revisable, provable
Substituir accions de tercers per ordres de la CLI equivalent 1-2 dies Mitjà-alt Sí on l'acció només embolcalla una CLI; no on aporta lògica real
Definir un make ci, make test, make build com a interfície única 1 dia Alt Sí: dóna un contracte estable independent de l'eina
Abstreure disparadors i entorns rere una capa pròpia Setmanes Baix No: costa més que la migració que evita

Fins on val la pena arribar, que és la pregunta real de la Marta: fins al punt en què la lògica sigui portable i l'orquestració sigui prima, i ni un pas més. És a dir, els tres primers punts sí; el quart no. La raó és de cost esperat: la probabilitat de migrar en els pròxims dos anys és baixa, però els tres primers punts tenen benefici immediat amb independència de la migració —reproductibilitat local, pipeline més llegible, lògica revisable i provable, menys "verd fals" per diferències entre local i CI—, mentre que el quart només rendeix en un escenari improbable.

La resposta curta per a la Marta: "Avui, unes tres o quatre setmanes. Amb dos o tres dies de feina que a més ens van bé per altres motius, ho baixem a una setmana. I no intentarem baixar-ho a zero, perquè això costa més que la migració que evitaria."

Conclusió

Travis CI va inventar la manera com avui s'escriu CI —un fitxer al repositori, convenció sobre configuració, matriu de versions— i va demostrar que el CI podia ser un dret de qualsevol projecte i no un privilegi de qui tingués un administrador de Jenkins. El seu model de fases fixes era perfecte per a "construeix i prova" i estructuralment insuficient per al lliurament continu: sense graf, sense pas d'artefactes entre jobs, sense execució selectiva i amb la promoció per digest compromesa. Traduir el pipeline de Reservalia a Travis ha estat l'exercici més útil del mòdul fins ara, no perquè el resultat serveixi, sinó perquè el que no hi cap explica per què els successors es van dissenyar amb el graf al centre.

I les tres lliçons que sobreviuen a l'eina: el model de negoci del teu proveïdor pot canviar i no ho decideixes tu; un secret confiat a un tercer és un secret la seguretat del qual no controles, d'on se segueix que les credencials efímeres federades no són una moda sinó una reducció real de dany; i el cost de canviar d'eina es decideix anys abans de canviar-la, segons quanta lògica vas deixar dins del YAML.

La lliçó següent canvia de categoria. Docker i Kubernetes no són eines de CI: són el substrat sobre el qual corren els pipelines moderns i el destí on despleguen. Veurem Docker en els seus dos papers —entorn d'execució del pipeline i format de l'artefacte que es desplega—, baixarem al que el curs no havia cobert (BuildKit, memòries cau de capes al registre, construcció multiplataforma, imatges mínimes, construcció sense dimoni privilegiat), i després a Kubernetes com a destí: els objectes mínims, les sondes, el rollback amb kubectl rollout undo, Helm davant de Kustomize, i per què a Kubernetes triomfa GitOps i què canvia respecte del cd.yml de push que fa servir Reservalia.

Curs de CI/CD: Integració i Desplegament Continu

Mòdul 1: Introducció al CI/CD

Mòdul 2: Integració Contínua (CI)

Mòdul 3: Desplegament Continu (CD)

Mòdul 4: Pràctiques Avançades de CI/CD

Mòdul 5: Implementació de CI/CD en Projectes Reals

Mòdul 6: Eines i Tecnologies

Mòdul 7: Exercicis Pràctics

Mòdul 8: Recursos Addicionals

© Copyright 2026. Tots els drets reservats