Fa cinc mòduls que fas servir GitHub Actions: has escrit ci.yml, cd.yml, rollback.yml i infra.yml, has extret la composite action preparar-node i el flux de treball reutilitzable reusable-build-publicar.yml, has federat credencials amb OIDC i has posat concurrency i matrius amb sharding. El que no has fet és mirar l'eina com a eina: què passa exactament entre que algú empeny un commit i arrenca un executor, quins contextos existeixen i quins estan disponibles en quin moment, quins disparadors hi ha més enllà de push i pull_request, com s'escriu una action pròpia quan les existents no hi arriben, quins són els límits reals del servei i què fer quan els toques. Aquesta lliçó tanca aquests buits. No torna a explicar què és una memòria cau ni què és un artefacte immutable —això és a la 04-02 i la 02-06—: explica com els materialitza GitHub Actions, on es trenquen les intuïcions, i quines contrapartides té l'eina que el curs ha fet servir per defecte. Perquè el mòdul va prometre que no hi hauria fanatisme, i això també s'aplica a l'eina de casa.

Contingut

  1. El cicle complet d'un esdeveniment i on viu l'estat
  2. Contextos i expressions a fons
  3. Tots els disparadors que falten
  4. pull_request davant de pull_request_target
  5. GITHUB_TOKEN, permissions i OIDC
  6. Els tres tipus d'action, amb una JavaScript action completa
  7. Matrius avançades i matrius dinàmiques
  8. Control de flux: outputs, continue-on-error, timeout-minutes, concurrency
  9. Memòria cau i artefactes: claus, límits i desallotjament
  10. Executors autoallotjats i autoescalat amb ARC
  11. Resums, anotacions i ordres de flux de treball
  12. Entorns, regles i desplegaments
  13. Límits reals del servei i estratègies
  14. Depurar i provar fluxos de treball
  15. Errors Comuns i Consells
  16. Exercicis
  17. Conclusió

  1. El cicle complet d'un esdeveniment i on viu l'estat

flowchart TD
    E["Esdeveniment a GitHub<br/>push, PR, schedule, API"] --> F{"Hi ha workflow<br/>amb aquest on:?"}
    F -->|no| X["Res"]
    F -->|si| C["Es llegeix el YAML de la ref triada"]
    C --> W["Workflow run<br/>es congela la definicio"]
    W --> J["Jobs: es valoren needs i if"]
    J --> Q["Cua: es busca executor<br/>per runs-on"]
    Q --> R["Executor: maquina neta<br/>no clona res per defecte"]
    R --> S["Steps en sequencia<br/>mateix sistema de fitxers"]
    S --> O["Outputs, artefactes, memories cau, logs"]
    O --> N["Checks al commit / PR"]

Cinc detalls que expliquen comportaments que a molta gent li semblen màgics o trencats:

De quina branca es llegeix el flux de treball. Per a push i pull_request, del commit que dispara. Però per a schedule, workflow_dispatch i workflow_run es llegeix de la branca per defecte, sempre. Per això un schedule nou no s'executa fins que el canvi arriba a main, i per això un workflow_dispatch que arregles en una branca continua comportant-se malament quan el llances.

La definició es congela en arrencar. Si empenys un canvi al flux de treball mentre hi ha una execució en curs, aquella execució continua amb la versió antiga. Serveix per raonar sobre execucions en vol.

Cada job és una màquina nova. No persisteix res entre jobs llevat del que passis explícitament: outputs, artefactes o memòria cau. El que sí que persisteix entre steps del mateix job és el sistema de fitxers, i el procés de shell no (cada run és un shell nou: un export en un step no arriba al següent; per a això hi ha $GITHUB_ENV).

L'executor arriba buit. No hi ha checkout automàtic —a diferència de Travis o GitLab—; per això actions/checkout és sempre el primer pas, i per això oblidar-lo produeix l'error "no such file or directory" més repetit de l'eina.

El resultat es publica com a checks sobre el commit, i són aquests checks els que la protecció de branca de la 02-07 exigeix. Un job que no s'executa per un if fals queda com a skipped, i skipped compta com a aprovat per a la protecció de branca: aquesta asimetria és la causa del "verd fals" més subtil d'Actions, i hi tornarem.

GitHub Actions Equivalent al curs
Workflow Fitxer de pipeline
Job Job (una màquina)
Step Step
Action Pas empaquetat i reutilitzable
Runner Agent
Artifact Artefacte
Environment Entorn amb regles i aprovacions

  1. Contextos i expressions a fons

Els contextos són objectes disponibles dins de ${{ }}. Saber quins hi ha i quan estan disponibles evita la meitat dels errors.

Context Conté Disponible a
github Esdeveniment, sha, ref, actor, repository, event complet Tot arreu
env Variables definides amb env: Gairebé tot (no a l'env: del mateix nivell)
vars Variables de configuració (no secretes) del repo/org/entorn Tot arreu
secrets Secrets Job i step; no a l'if de nivell de workflow
job Estat del job actual, serveis i els seus ports Steps
jobs Resultats de jobs (només en fluxos de treball reutilitzables, per als outputs) outputs del reutilitzable
steps Outputs i outcome/conclusion de steps amb id Steps posteriors
runner os, arch, temp, tool_cache, debug Steps
needs Outputs i result dels jobs dels quals depens Job dependent
matrix Valors de la combinació actual Job amb matriu
inputs Entrades de workflow_dispatch, workflow_call o d'una action Segons el cas
strategy job-index, job-total, fail-fast Job amb matriu
env:
  ENTORN: staging

jobs:
  exemple:
    runs-on: ubuntu-22.04
    steps:
      - id: versio
        run: echo "valor=1.4.2" >> "$GITHUB_OUTPUT"      # 1

      - name: Fer servir l'output
        run: echo "Versió ${{ steps.versio.outputs.valor }}"

      - name: Variable per als steps següents
        run: |
          echo "SHA_CURT=${GITHUB_SHA::7}" >> "$GITHUB_ENV"    # 2
          echo "$PWD/bin" >> "$GITHUB_PATH"                     # afegeix al PATH

      - name: Condició amb funcions
        if: >-
          github.event_name == 'push' &&
          startsWith(github.ref, 'refs/tags/v') &&
          !contains(github.event.head_commit.message, '[skip ci]')
        run: ./scripts/publicar.sh
  1. $GITHUB_OUTPUT és el mecanisme actual (l'antic ::set-output està retirat per motius de seguretat). Per a valors multilínia cal fer servir un delimitador:
    {
      echo "notes<<EOF"
      cat CHANGELOG.md
      echo "EOF"
    } >> "$GITHUB_OUTPUT"
    
  2. $GITHUB_ENV defineix variables per als steps següents del mateix job; un export normal no surt del step perquè cada run és un shell diferent.

Funcions disponibles, amb els seus usos típics:

Funció Què fa Ús típic
contains(a, b) Subcadena o element de llista contains(github.event.pull_request.labels.*.name, 'urgent')
startsWith / endsWith Prefix / sufix startsWith(github.ref, 'refs/tags/')
format('{0}-{1}', a, b) Interpolació Construir noms
join(llista, ', ') Unir Missatges
toJSON(x) Serialitzar Depurar contextos: run: echo '${{ toJSON(github) }}'
fromJSON(s) Deserialitzar Matrius dinàmiques i convertir cadenes a números o booleans
hashFiles('**/package-lock.json') Hash de fitxers Claus de memòria cau
success(), failure(), cancelled(), always() Estat Condicionals de step i de job

I els detalls que mosseguen:

Tots els valors de matrix i dels inputs de workflow_dispatch arriben com a cadena. if: inputs.forcar == true és fals sempre si forcar ve d'un workflow_dispatch; cal comparar amb 'true' o fer servir fromJSON(inputs.forcar).

L'if a nivell de job no porta ${{ }} (encara que funciona amb elles); dins d'una expressió, sí.

if: always() davant de if: ${{ !cancelled() }}: always() executa el step fins i tot si el flux de treball s'ha cancel·lat, cosa que pot deixar recursos a mitges o endarrerir la cancel·lació. Per a "executar encara que falli, però no si cancel·len" —el cas habitual de publicar informes de test— el correcte és if: ${{ !cancelled() }}.

Els secrets no estan disponibles a l'if de nivell de workflow, i tampoc no els pots fer servir per decidir si un flux de treball s'executa. El patró per a "només si hi ha credencials" és un job previ que els comprova i exposa un output booleà.

  1. Tots els disparadors que falten

Més enllà de push i pull_request:

on:
  # Execució manual amb entrades TIPADES
  workflow_dispatch:
    inputs:
      entorn:
        description: Entorn de destí
        type: choice
        options: [staging, produccio]
        default: staging
      digest:
        description: Digest de la imatge que cal desplegar
        type: string
        required: true
      ometre-smoke:
        description: Ometre els smoke tests (només emergències)
        type: boolean
        default: false

  # Dispar des d'una API externa
  repository_dispatch:
    types: [desplegar-demanat, contracte-actualitzat]

  # Programat (UTC, sempre des de la branca per defecte)
  schedule:
    - cron: '17 3 * * *'          # 03:17 UTC, no en punt: vegeu més avall

  # Reacció a comentaris: el "/desplegar" en un PR
  issue_comment:
    types: [created]

  # Publicació d'una release
  release:
    types: [published]

  # Encadenat després d'un altre flux de treball
  workflow_run:
    workflows: ["CI"]
    types: [completed]
    branches: [main]

  # Merge queue
  merge_group:
    types: [checks_requested]
Disparador Quan fer-lo servir Parany
workflow_dispatch Desplegaments manuals, operacions, rollback Es llegeix de la branca per defecte; els inputs són cadenes
repository_dispatch Integració amb sistemes externs Requereix un token amb permís d'escriptura sobre contents
schedule Nocturns, neteja, escanejos No dispara puntual: la cua en punt està saturada; fes servir minuts estranys. I es desactiva després d'uns 60 dies d'inactivitat del repositori
issue_comment Ordres tipus /desplegar Es dispara en issues i en PR; cal filtrar github.event.issue.pull_request. I el comentari és text de tercers: no l'interpolis mai a run:
release Publicar paquets, notes published davant de created no és el mateix
workflow_run CD després de CI (el cd.yml de Reservalia) El YAML es llegeix de la branca per defecte; completed inclou fallades: cal comprovar conclusion == 'success'
merge_group Merge queue (02-07) Si el check obligatori no s'executa a merge_group, la cua es bloqueja

Dos patrons concrets que apareixen molt:

# Ordre /desplegar en un pull request
on: { issue_comment: { types: [created] } }
jobs:
  desplegar:
    if: >-
      github.event.issue.pull_request &&
      startsWith(github.event.comment.body, '/desplegar') &&
      contains(fromJSON('["OWNER","MEMBER"]'), github.event.comment.author_association)
    runs-on: ubuntu-22.04
    steps:
      - run: ./scripts/desplegar.sh preview     # NO interpolis MAI el cos del comentari

La comprovació d'author_association és imprescindible: sense ella, qualsevol persona d'internet pot disparar un desplegament escrivint un comentari. I el cos del comentari no s'interpola mai dins de run:, perquè és una injecció d'script directa (04-03): si necessites llegir-lo, passa'l per env: i tracta'l com a dada.

# CD encadenat després de CI, comprovant el resultat
on:
  workflow_run:
    workflows: ["CI"]
    types: [completed]
    branches: [main]
jobs:
  desplegar:
    if: github.event.workflow_run.conclusion == 'success'   # sense això, desplegues builds trencats
    runs-on: ubuntu-22.04

  1. pull_request davant de pull_request_target

La diferència de seguretat més important de tota l'eina. La 04-03 la va enunciar; aquí el mecanisme complet.

pull_request pull_request_target
Codi que s'executa El de la branca del PR (possiblement d'un fork) El de la branca base
Context de github.ref La branca base amb el merge La branca base
Accés a secrets No, si el PR ve d'un fork Sí, sempre
Permisos de GITHUB_TOKEN Només lectura des de forks Escriptura
Risc Baix Alt si fas checkout del codi del PR
# PERILLÓS: la combinació que filtra secrets
on: pull_request_target
jobs:
  build:
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}   # ← codi del fork
      - run: npm ci                                        # ← executa scripts de l'atacant
      #     …amb els secrets del repositori disponibles

Un npm ci executa els scripts postinstall del package.json del PR. Un atacant obre un PR amb un postinstall que envia process.env al seu servidor, i s'emporta tots els secrets. No és teòric: és la manera més comuna de comprometre repositoris públics.

Les regles, sense matisos:

  1. Fes servir pull_request per defecte. Cobreix el 95 % dels casos.
  2. Fes servir pull_request_target només quan necessitis secrets o escriptura des de forks —etiquetar, comentar, donar format— i sense fer checkout del codi del PR.
  3. Si necessites totes dues coses (construir codi d'un fork i comentar el resultat), parteix-ho: un flux de treball pull_request construeix sense secrets i puja un artefacte; un segon flux de treball amb workflow_run el descarrega i comenta. El segon té secrets però mai no executa codi del fork.
  4. GITHUB_TOKEN amb permissions mínims sempre, i sobretot aquí.

Aplicat a un repositori privat com el de Reservalia, el risc és menor —només l'equip obre PR— però no nul: un compte compromès n'hi ha prou. I si algun repositori es fa públic algun dia, els fluxos de treball viatgen amb ell.

  1. GITHUB_TOKEN, permissions i OIDC

Cada job rep un GITHUB_TOKEN efímer que mor amb ell. El seu abast depèn de la configuració de l'organització —el valor per defecte pot ser permissiu en repositoris antics— i del que declaris:

permissions:                # a nivell de workflow: s'aplica a tots els jobs
  contents: read            # el mínim per al checkout

jobs:
  etiquetar:
    permissions:            # a nivell de job: SUBSTITUEIX el del workflow, no s'hi suma
      contents: read
      pull-requests: write  # només aquest job pot comentar
    runs-on: ubuntu-22.04

  desplegar:
    permissions:
      contents: read
      id-token: write       # emetre el token OIDC
    environment: produccio
    runs-on: ubuntu-22.04
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/reservalia-desplegament-prod
          aws-region: eu-west-1
          # sense claus de llarga vida: el token OIDC es canvia per credencials temporals

Punts que importen:

  • Declarar permissions al flux de treball ho posa tot al que has declarat, no només el que has llistat: si poses contents: read, la resta queda en none. És el que vols.
  • Els permisos de job substitueixen, no acumulen. Un job amb permissions: { pull-requests: write } perd contents: read i el checkout falla.
  • GITHUB_TOKEN no dispara altres fluxos de treball. Un push fet amb ell no llança l'on: push, per disseny, per evitar bucles infinits. Si necessites encadenar, fes servir una GitHub App o un PAT —amb la precaució que llavors sí que pots crear un bucle—.
  • OIDC (id-token: write) és el que la 03-02 va establir: l'executor demana un token signat amb reclamacions sobre repositori, branca, entorn i flux, i AWS el canvia per credencials temporals. La condició de confiança a IAM ha de lligar sub amb precisió:
    "StringLike": { "token.actions.githubusercontent.com:sub": "repo:reservalia/reservalia:environment:produccio" }
    
    Escriure repo:reservalia/* o deixar el sub amb comodins amplis anul·la la major part de la protecció: qualsevol branca de qualsevol repositori de l'organització podria assumir el rol. És un error de configuració freqüent i greu.

  1. Els tres tipus d'action, amb una JavaScript action completa

Tipus Com s'executa Velocitat Quan
Composite Steps YAML a l'executor del job Molt ràpida Seqüències d'ordres; el primer que cal provar (04-05)
JavaScript Node.js a l'executor, amb @actions/* Ràpida Lògica real, ús de l'API, outputs calculats
Docker Contenidor construït o descarregat Lenta: construeix o descarrega la imatge Eines que no són JS i dependències del sistema. Només Linux

La composite ja la vas escriure a la 04-05. Aquí, una JavaScript action completa: comprova el pressupost de mida del bundle i publica el resultat, un cas real de Reservalia (05-01).

# .github/actions/pressupost-bundle/action.yml
name: 'Pressupost de bundle'
description: 'Compara la mida del bundle amb un pressupost i falla si el supera'
author: 'Reservalia'

inputs:
  ruta:
    description: 'Directori del build'
    required: true
    default: 'apps/web/dist'
  pressupost-kb:
    description: 'Pressupost en KB per al JS inicial'
    required: true
  fallar:
    description: 'Fer fallar el job si se supera'
    required: false
    default: 'true'

outputs:
  mida-kb:
    description: 'Mida mesurada en KB'
  supera:
    description: 'true si supera el pressupost'

runs:
  using: 'node20'
  main: 'dist/index.js'          # empaquetat amb @vercel/ncc, amb node_modules inclosos
// .github/actions/pressupost-bundle/src/index.js
const core = require('@actions/core');
const fs   = require('node:fs');
const path = require('node:path');

function midaJsInicial(dir) {
  // Suma la mida dels .js de primer nivell (els chunks diferits no compten)
  return fs.readdirSync(dir)
    .filter((f) => f.endsWith('.js'))
    .reduce((total, f) => total + fs.statSync(path.join(dir, f)).size, 0);
}

async function run() {
  try {
    const ruta      = core.getInput('ruta', { required: true });
    const pressupost = Number(core.getInput('pressupost-kb', { required: true }));
    const fallar    = core.getBooleanInput('fallar');            // 1 · parseig correcte de booleans

    if (!fs.existsSync(ruta)) {
      core.setFailed(`El directori ${ruta} no existeix. S'ha executat el build?`);
      return;
    }

    const kb = Math.round(midaJsInicial(path.join(ruta, 'assets')) / 1024);
    const supera = kb > pressupost;

    core.setOutput('mida-kb', String(kb));                        // 2
    core.setOutput('supera', String(supera));

    // 3 · Resum visible a la pestanya del job
    await core.summary
      .addHeading('Pressupost de bundle')
      .addTable([
        [{ data: 'Mètrica', header: true }, { data: 'Valor', header: true }],
        ['Mida del JS inicial', `${kb} KB`],
        ['Pressupost', `${pressupost} KB`],
        ['Marge', `${pressupost - kb} KB`],
      ])
      .write();

    if (supera) {
      // 4 · Anotació: apareix assenyalada a la interfície
      const missatge = `El bundle pesa ${kb} KB i el pressupost és ${pressupost} KB`;
      if (fallar) core.setFailed(missatge);
      else core.warning(missatge);
    } else {
      core.info(`Bundle dins del pressupost: ${kb}/${pressupost} KB`);
    }
  } catch (error) {
    core.setFailed(`Error inesperat: ${error.message}`);           // 5
  }
}

run();
  1. getBooleanInput parseja 'true'/'false' correctament. getInput('fallar') === true seria sempre fals: tots els inputs arriben com a cadena, que és el mateix parany de l'apartat 2.
  2. core.setOutput publica outputs consumibles amb steps.<id>.outputs.<nom>.
  3. core.summary escriu a $GITHUB_STEP_SUMMARY: markdown que apareix a la pàgina del job (apartat 11).
  4. core.setFailed marca el pas com a fallit amb missatge; core.warning i core.notice creen anotacions sense fer fallar. Amb file, startLine i endLine l'anotació s'ancora a una línia concreta del codi, que és el que fa útils els linters a la vista de diff.
  5. Captura global: sense ella, una excepció produeix una fallada amb traça crua i difícil d'interpretar.

Una funció més que cal conèixer: core.setSecret(valor) registra un valor perquè s'emmascari als logs a partir d'aquell moment. És imprescindible quan la teva action calcula o rep un secret que no venia de secrets —un token obtingut d'una API, per exemple—. I amb la mateixa advertència de la 06-01 i la 06-04: l'emmascarament cobreix la coincidència literal, no les transformacions.

# Ús
- uses: ./.github/actions/pressupost-bundle
  id: pressupost
  with:
    ruta: apps/web/dist
    pressupost-kb: '180'
    fallar: ${{ github.ref == 'refs/heads/main' }}
- run: echo "Bundle: ${{ steps.pressupost.outputs.mida-kb }} KB"

Detall operatiu que sorprèn: una JavaScript action necessita les seves dependències comeses al repositori, perquè l'executor no executa npm install. S'empaqueta amb @vercel/ncc (ncc build src/index.js -o dist) i es comprova al CI que dist/ està sincronitzat amb src/, o publicaràs una action que no conté els teus canvis.

Com triar: composite si són ordres; JavaScript si hi ha lògica, API o outputs calculats; Docker només si necessites dependències del sistema difícils —assumint que només funciona en executors Linux i que arrenca a poc a poc—.

  1. Matrius avançades i matrius dinàmiques

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false          # 1
      max-parallel: 4           # 2
      matrix:
        os: [ubuntu-22.04, macos-14]
        node: [20, 22]
        include:                # 3
          - os: ubuntu-22.04
            node: 22
            cobertura: true     # afegeix una propietat a aquella combinació
          - os: windows-2022    # afegeix una combinació sencera
            node: 22
        exclude:                # 4
          - os: macos-14
            node: 20
    steps:
      - run: npm test
      - if: matrix.cobertura
        run: npm run cobertura
  1. fail-fast: true (per defecte) cancel·la tota la matriu a la primera fallada. Estalvia minuts i amaga informació: si volies saber si falla a Node 20 i a Node 22, posa-ho a false. Regla pràctica: false en matrius de compatibilitat, true en shards de la mateixa suite.
  2. max-parallel limita la concurrència: útil quan la matriu colpeja un recurs compartit (una base de dades de proves, un límit d'API).
  3. include té dos comportaments i aquí hi ha la confusió: si les seves claus coincideixen amb una combinació existent, afegeix propietats a aquella combinació; si no coincideix, crea una combinació nova.
  4. exclude s'aplica després d'expandir-ho tot, inclosos els include.

Matriu dinàmica, que és el patró que resol el monorepo de la 04-04:

jobs:
  detectar:
    runs-on: ubuntu-22.04
    outputs:
      paquets: ${{ steps.calcular.outputs.paquets }}
      hi-ha-canvis: ${{ steps.calcular.outputs.hi-ha-canvis }}
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }          # necessari per comparar amb la base
      - id: calcular
        run: |
          # Emet un array JSON amb els workspaces afectats
          PAQUETS=$(node scripts/afectats.js --base "${{ github.event.pull_request.base.sha }}")
          echo "paquets=${PAQUETS}" >> "$GITHUB_OUTPUT"
          [ "$PAQUETS" = "[]" ] && echo "hi-ha-canvis=false" >> "$GITHUB_OUTPUT" \
                                || echo "hi-ha-canvis=true"  >> "$GITHUB_OUTPUT"

  test:
    needs: [detectar]
    if: needs.detectar.outputs.hi-ha-canvis == 'true'     # 1
    runs-on: ubuntu-22.04
    strategy:
      matrix:
        paquet: ${{ fromJSON(needs.detectar.outputs.paquets) }}     # 2
    steps:
      - uses: ./.github/actions/preparar-node
      - run: npm test --workspace ${{ matrix.paquet }}

  porta:                                                   # 3
    needs: [detectar, test]
    if: always()
    runs-on: ubuntu-22.04
    steps:
      - name: Comprovar el resultat
        run: |
          if [ "${{ needs.test.result }}" = "failure" ] || [ "${{ needs.test.result }}" = "cancelled" ]; then
            echo "Els tests han fallat"; exit 1
          fi
          echo "OK (tests: ${{ needs.test.result }})"
  1. Una matriu buida fa fallar el job, no l'omet. D'aquí l'output hi-ha-canvis i l'if.
  2. fromJSON converteix la cadena en un array real per a la matriu.
  3. El job porta és la peça clau i la que gairebé ningú no posa. Com que els jobs de matriu tenen noms dinàmics, no es poden exigir pel nom a la protecció de branca; i com que un job skipped compta com a aprovat, sense aquesta porta un PR on els tests s'ometin per un error de detecció passaria la protecció amb tot en verd. És el "verd fals" de la 04-04 en la seva forma més traïdora: no hi ha res vermell per mirar. porta sí que té nom fix, s'executa sempre (if: always()) i comprova needs.test.result explícitament.

  1. Control de flux: outputs, continue-on-error, timeout-minutes, concurrency

jobs:
  build:
    runs-on: ubuntu-22.04
    timeout-minutes: 20                        # 1
    outputs:
      digest: ${{ steps.publicar.outputs.digest }}
    steps:
      - id: publicar
        run: echo "digest=sha256:aaa..." >> "$GITHUB_OUTPUT"

      - name: Anàlisi opcional
        continue-on-error: true                # 2
        id: analisi
        run: ./scripts/analisi-experimental.sh

      - name: Avisar si l'anàlisi ha fallat
        if: steps.analisi.outcome == 'failure'  # 3
        run: echo "::warning::L'anàlisi experimental ha fallat"

  desplegar:
    needs: [build]
    runs-on: ubuntu-22.04
    concurrency:                               # 4
      group: desplegament-produccio
      cancel-in-progress: false
    steps:
      - run: ./scripts/desplegar.sh "${{ needs.build.outputs.digest }}"
  1. timeout-minutes a cada job, sense excepció. El valor per defecte són 6 hores: un job penjat consumeix minuts facturables durant tot aquest temps i bloqueja un forat de concurrència. És una línia que estalvia diners reals.
  2. continue-on-error: true deixa que el step falli sense trencar el job. També existeix a nivell de job.
  3. outcome davant de conclusion: outcome és el resultat abans d'aplicar continue-on-error; conclusion és el resultat final. Amb continue-on-error, conclusion és success encara que outcome sigui failure. Per detectar que alguna cosa ha fallat però no ha bloquejat, es mira outcome.
  4. Dos usos diferents de concurrency, i convé no barrejar-los. Per a CI: group: ci-${{ github.ref }} amb cancel-in-progress: true, per cancel·lar execucions obsoletes i estalviar (04-04). Per a desplegaments: cancel-in-progress: false i un grup global, per serialitzar i evitar dos desplegaments simultanis al mateix entorn. Cancel·lar un desplegament a mitges és molt pitjor que esperar.

Un detall que confon: amb concurrency, només es conserva l'execució més recent en espera; les intermèdies es cancel·len. En un desplegament serialitzat, si s'acumulen tres commits, es desplega el primer i l'últim, i el del mig mai. Sol ser el que es vol, però cal saber-ho.

  1. Memòria cau i artefactes: claus, límits i desallotjament

- uses: actions/cache@v4
  id: cache
  with:
    path: |
      ~/.npm
      apps/web/node_modules/.vite
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}     # 1
    restore-keys: |
      npm-${{ runner.os }}-                                                # 2

- if: steps.cache.outputs.cache-hit != 'true'                              # 3
  run: echo "Memòria cau freda"
  1. La clau ha de contenir tot el que la invalida: sistema operatiu, arquitectura si varia, i el hash dels lockfiles. hashFiles accepta patrons múltiples.
  2. restore-keys són prefixos de reserva: sense ells, qualsevol canvi del lockfile obliga a descarregar-ho tot.
  3. cache-hit és 'true' només amb coincidència exacta; una restauració parcial per restore-keys deixa cache-hit en false encara que hagi restaurat fitxers. Condicionar npm ci a cache-hit és un error clàssic.

Regles de comportament de la memòria cau que no són òbvies i expliquen moltes raresses:

  • L'àmbit de la memòria cau és la branca. Una branca pot llegir les seves pròpies memòries cau i les de la seva branca base; no les de branques germanes. Per això la primera execució d'un PR sol ser més lenta i per això convé que main pobli la memòria cau.
  • Les memòries cau són immutables: escrita una clau, no se sobreescriu. Per invalidar, canvia la clau (un prefix versionat hi ajuda).
  • Hi ha un límit de mida total per repositori i s'aplica desallotjament per menys usada recentment; a més, les memòries cau no usades durant un temps s'eliminen. Conseqüència pràctica: emmagatzemar diversos gigabytes per branca fa que s'expulsin les unes a les altres i la taxa d'encert es desploma. Menys i millor triat rendeix més.
  • La memòria cau no és un canal segur entre branques. Un PR pot escriure en una memòria cau que després restaura una altra execució. No hi guardis binaris construïts que després s'executin amb privilegis (04-03).

Artefactes:

- uses: actions/upload-artifact@v4
  if: ${{ !cancelled() }}
  with:
    name: informes-${{ matrix.shard }}        # 1 · noms únics per shard
    path: |
      informes/
      cobertura/
    retention-days: 7                          # 2
    compression-level: 9

- uses: actions/download-artifact@v4
  with:
    pattern: informes-*                        # 3
    merge-multiple: true
    path: informes/
  1. A la v4 els noms d'artefacte han de ser únics dins de l'execució: pujar el mateix nom des de diversos jobs de matriu falla. És un canvi respecte de la v3 que trenca molts fluxos de treball heretats.
  2. retention-days estalvia emmagatzematge facturable; la política de la 02-06 en una línia.
  3. pattern amb merge-multiple recompon els informes de tots els shards en un job d'agregació.

I una diferència amb la memòria cau que convé tenir present: els artefactes es pugen i es descarreguen per xarxa des del servei, no des de l'executor, així que els artefactes grans costen temps als dos extrems. Passar node_modules entre jobs com a artefacte sol ser més lent que reinstal·lar des de memòria cau.

  1. Executors autoallotjats i autoescalat amb ARC

Allotjats per GitHub Autoallotjats
Manteniment Cap Teu: sistema, eines, seguretat
Cost Per minut (gratis en repos públics) La teva infraestructura + operació
Aïllament Màquina neta i efímera per job Depèn de com ho muntis
Accés a xarxa privada No : el motiu principal
Maquinari especial Limitat al que s'ofereix Qualsevol: GPU, ARM, molta memòria
Rendiment Modest a les mides bàsiques El que paguis
jobs:
  test:
    runs-on: [self-hosted, linux, x64, reservalia-gran]      # totes les etiquetes han de coincidir

Els executors s'organitzen en grups (per organització) per restringir quins repositoris els poden fer servir —imprescindible si un executor té accés a una xarxa sensible—.

El risc greu, i cal dir-ho clar: no facis servir mai executors autoallotjats en repositoris públics. Qualsevol pot obrir un PR el flux de treball del qual executa codi arbitrari a la teva màquina, dins de la teva xarxa. I si l'executor és persistent, aquell codi pot deixar coses per al job següent —inclòs el d'un altre equip—. GitHub ho adverteix explícitament i l'advertència s'ignora amb massa freqüència.

ARC (Actions Runner Controller) resol el problema de l'executor persistent executant un Pod efímer per job a Kubernetes, que és exactament el model de la 06-05:

# values.yaml d'un conjunt d'executors escalables
githubConfigUrl: https://github.com/reservalia
githubConfigSecret: arc-github-app          # GitHub App, millor que un PAT
minRunners: 1                               # 1 · un de calent per al primer job
maxRunners: 30
containerMode:
  type: kubernetes                          # 2 · sense DinD privilegiat
template:
  spec:
    containers:
      - name: runner
        image: ghcr.io/actions/actions-runner:latest
        resources:
          requests: { cpu: "1", memory: "2Gi" }
          limits:   { cpu: "2", memory: "4Gi" }
  1. minRunners: 1 manté un executor calent: sense ell, el primer job del matí paga l'arrencada completa del Pod. És el compromís entre cost i temps de cua que la 04-04 demanava mesurar per separat.
  2. containerMode: kubernetes evita Docker-in-Docker privilegiat i fa servir Pods per als contenidors de servei, amb les implicacions de seguretat de la 06-05.

Contrapartides honestes d'ARC: és un component més per operar i actualitzar; l'arrencada del Pod afegeix temps de cua; i la memòria cau no persisteix entre Pods, així que depens de memòria cau remota. A canvi: entorn net per job, autoescalat, i cost proporcional a l'ús a la teva pròpia infraestructura.

  1. Resums, anotacions i ordres de flux de treball

Un pipeline el resultat del qual s'ha de buscar en 4.000 línies de log és un pipeline que ningú no mira (03-06). Tres mecanismes ho arreglen:

# Resum del job: markdown que es veu a la pestanya, sense obrir logs
{
  echo "## Resultat de les proves"
  echo ""
  echo "| Suite | Tests | Fallades | Temps |"
  echo "|---|---|---|---|"
  echo "| API | 412 | 0 | 2m 14s |"
  echo "| Web | 188 | 1 | 1m 02s |"
  echo ""
  echo "<details><summary>Tests lents</summary>"
  echo ""
  echo '```'
  cat informes/lents.txt
  echo '```'
  echo "</details>"
} >> "$GITHUB_STEP_SUMMARY"

# Anotacions: apareixen destacades i, amb file/line, ancorades al diff
echo "::error file=apps/api/src/reserves.ts,line=42::Falta la validació de la data"
echo "::warning::Cobertura per sota de l'objectiu: 78 %"
echo "::notice::Imatge publicada com a sha256:aaa..."

# Agrupar sortida llarga en seccions plegables
echo "::group::Sortida de npm ci"
npm ci
echo "::endgroup::"

# Emmascarar un valor calculat en temps d'execució
echo "::add-mask::$TOKEN_DERIVAT"

El resum de job és la millor inversió de llegibilitat que es pot fer en un pipeline amb poc esforç: un enllaç a la previsualització, el digest publicat, el resultat del pressupost de bundle i els tests fallits en una taula estalvien a la Nuria obrir logs cada vegada. I ::add-mask:: és l'equivalent de core.setSecret per a scripts de shell: si el teu script deriva un token, emmascara'l abans que pugui aparèixer en qualsevol sortida.

  1. Entorns, regles i desplegaments

jobs:
  desplegar-produccio:
    environment:
      name: produccio
      url: https://app.reservalia.example      # 1
    permissions: { contents: read, id-token: write }
    runs-on: ubuntu-22.04
    steps:
      - run: ./scripts/desplegar.sh produccio "${{ needs.build.outputs.digest }}"
  1. La URL apareix a la interfície de desplegaments i al PR.

El que aporta un Environment, que és més del que sembla:

  • Revisors obligatoris: el job espera aprovació humana. És la porta de la 03-01, i el job en espera no consumeix executor —diferència notable davant de l'input de Jenkins (06-01)—.
  • Temporitzador d'espera: minuts obligatoris abans de desplegar, per donar marge a cancel·lar.
  • Branques i etiquetes permeses: només main pot desplegar a produccio, encara que algú modifiqui el flux de treball en una altra branca.
  • Secrets i variables per entorn: secrets.DATABASE_URL val una cosa a staging i una altra a producció, sense condicionals al YAML.
  • Historial de desplegaments per entorn amb el seu commit.
  • I la peça que més importa combinada amb OIDC: la reclamació sub del token inclou l'entorn, així que el rol d'IAM de producció només es pot assumir des d'un job amb environment: produccio, que al seu torn només es pot executar des de main i després d'aprovació. Tres controls encadenats en comptes d'un.

  1. Límits reals del servei i estratègies

Sense fanatisme, també amb l'eina de casa. Els valors concrets canvien amb el pla i amb el temps —consulta'ls abans de dissenyar—, però els tipus de límit i les seves estratègies són estables:

Límit Efecte quan el toques Estratègia
Concurrència de jobs per pla Jobs en cua: es percep com que "el CI va lent" encara que l'execució sigui ràpida Mesurar cua i execució per separat (04-04); concurrency per cancel·lar obsolets; executors propis per desbordar
Minuts inclosos (repos privats) Facturació addicional paths i execució selectiva; timeout-minutes; matrius més petites
Mida total de memòria cau per repositori Desallotjament per menys usada: la taxa d'encert es desploma Guardar menys i millor triat; no guardar node_modules
Retenció d'artefactes i logs Emmagatzematge facturable retention-days baix; artefactes petits
API rate limit del GITHUB_TOKEN Fallades intermitents en fluxos de treball que criden molt l'API Reduir crides, paginar bé, GitHub App amb límits propis
schedule no dispara puntual Un cron a les 0 * * * * es pot endarrerir Minuts estranys; no donar per feta la puntualitat; per a precisió, disparador extern amb repository_dispatch
schedule es desactiva després d'uns 60 dies sense activitat al repositori El nocturn deixa d'executar-se en silenci Alerta de "fa X dies que no s'executa"
Durada màxima de job i de workflow Cancel·lació Partir en jobs; timeout-minutes explícit
Imbricació de fluxos de treball reutilitzables Error en validar Aplanar la jerarquia
Matriu: nombre màxim de jobs Error Matrius dinàmiques acotades

Dos límits conceptuals, més importants que els numèrics:

  • El pla de control és de GitHub. Si el servei té un incident, no desplegues —ni amb executors propis—. Convé tenir un camí d'emergència documentat: un script que desplegui des d'una màquina amb credencials temporals, assajat. És el mateix raonament de la 03-05 aplicat a l'eina.
  • La dependència de l'ecosistema d'actions de tercers és una superfície de subministrament. És la seva fortalesa més gran i un risc real: fixar per SHA (04-03), revisar el que s'afegeix, i preferir ordres de CLI quan l'action només n'embolcalla una.

  1. Depurar i provar fluxos de treball

# 1 · Logs de depuració: secrets ACTIONS_STEP_DEBUG i ACTIONS_RUNNER_DEBUG a "true"
- run: echo "runner.debug = ${{ runner.debug }}"     # '1' si està actiu

# 2 · Abocar un context sencer per entendre què arriba
- run: echo '${{ toJSON(github.event) }}'

# 3 · Sessió interactiva a l'executor (només repos privats i amb criteri)
- uses: mxschmitt/action-tmate@v3
  if: ${{ failure() && github.event_name == 'workflow_dispatch' }}
  timeout-minutes: 15
# 4 · Executar fluxos de treball en local amb act (aproximació, no equivalència)
act pull_request -j qualitat --container-architecture linux/amd64
act -j test --secret-file .secrets.local

Notes d'ús real: ACTIONS_STEP_DEBUG mostra quins inputs rep cada action i quines expressions s'avaluen a què, i resol la majoria dels "per què aquest if no es compleix?". toJSON(github.event) és la manera més ràpida de descobrir el nom exacte d'un camp del payload. tmate obre una sessió interactiva en un executor amb els secrets del job: fes-lo servir només en repositoris privats, amb timeout-minutes i preferiblement en un job sense credencials de producció; és el mateix raonament de l'SSH into build de la 06-03.

I act: útil per iterar sobre la lògica d'un job, però no reprodueix l'entorn. No implementa igual les memòries cau, els serveis, els permisos del token, OIDC ni els entorns, i les seves imatges no són les de GitHub. Serveix per a "el meu script funciona?", no per a "el meu flux de treball és correcte?".

Per al segon, l'estratègia de la 04-05 continua sent la bona: actionlint al mateix CI per detectar errors de sintaxi i d'expressions abans de fusionar, una branca de proves on exercitar el flux de treball complet, i canvis progressius amb el pipeline nou en paral·lel al vell.

# Validar els fluxos de treball com a part del CI
- uses: actions/checkout@v4
- run: |
    bash <(curl -s https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
    ./actionlint -color

Errors Comuns i Consells

Oblidar actions/checkout. L'executor arriba buit. És el primer error de tothom.

Esperar que export viatgi entre steps. Cada run és un shell nou: fes servir $GITHUB_ENV.

Comparar inputs booleans amb true. Arriben com a cadena. == 'true' o fromJSON(...).

permissions a nivell de job creient que suma. Substitueix: si poses pull-requests: write sense contents: read, el checkout falla.

pull_request_target amb checkout del codi del PR. És la manera més directa de filtrar tots els teus secrets.

sub d'OIDC amb comodins amplis. repo:org/* permet que qualsevol repositori de l'organització assumeixi el teu rol de producció. Lliga repositori, branca o entorn.

Sense timeout-minutes. El valor per defecte són 6 hores de minuts facturables per un job penjat.

fail-fast: true en matrius de compatibilitat. Cancel·la i amaga la informació que buscaves.

Matriu dinàmica sense job de porta. Els jobs skipped compten com a aprovats a la protecció de branca: verd fals sense res vermell per mirar.

Condicionar la instal·lació a cache-hit. Una restauració parcial deixa cache-hit en false; i encara que encertés, npm ci continua sent necessari.

Guardar node_modules a la memòria cau. Binaris lligats a versió i arquitectura, i ocupa el pressupost de memòria cau del repositori expulsant memòries cau útils.

Executors autoallotjats en repositoris públics. Execució de codi arbitrari dins de la teva xarxa.

Actions de tercers sense fixar per SHA. Una etiqueta és mutable: qui controla el repositori de l'action controla el teu pipeline (04-03).

Noms d'artefacte repetits en matriu amb la v4. Falla la pujada; fes servir un sufix amb l'índex.

if: always() per publicar informes. S'executa també en cancel·lar. Fes servir if: ${{ !cancelled() }}.

Exercicis

Exercici 1. Escriu una JavaScript action verificar-migracions que, donat un directori de migracions, comprovi tres coses: que no hi ha dos fitxers amb el mateix número de versió, que cap migració nova del PR no conté DROP COLUMN o DROP TABLE (la regla d'expand and contract de la 04-06), i que cada migració té el seu fitxer de reversió. Ha d'exposar outputs, escriure un resum de job, generar anotacions ancorades al fitxer i poder configurar-se com a bloquejant o informativa.

Exercici 2. Un repositori públic de Reservalia té aquest flux de treball. Troba tots els problemes de seguretat, explica l'impacte de cadascun i reescriu-lo:

on: pull_request_target
jobs:
  comentar-mida:
    runs-on: [self-hosted, linux]
    steps:
      - uses: actions/checkout@v3
        with: { ref: ${{ github.event.pull_request.head.sha }} }
      - run: npm install && npm run build
      - run: |
          MIDA=$(du -sh dist | cut -f1)
          curl -X POST -H "Authorization: token ${{ secrets.PAT_ADMIN }}" \
            -d "{\"body\":\"Bundle: $MIDA — ${{ github.event.pull_request.title }}\"}" \
            "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.number }}/comments"

Exercici 3. El ci.yml de Reservalia triga 11 minuts i l'equip es queixa que "GitHub Actions va lent". Les dades: el temps mitjà en cua és de 4 minuts entre les 10:00 i les 12:00; els sis jobs restauren memòria cau però la taxa d'encert és del 40 %; la matriu de tests té fail-fast: true i sovint es cancel·la per una prova inestable; hi ha tres jobs sense timeout-minutes i un es queda penjat un parell de vegades per setmana; i la memòria cau total del repositori està al límit amb 9 GB, la majoria en node_modules de branques velles. Diagnostica cada punt i dona un pla ordenat.

Solucions

Solució 1.

# .github/actions/verificar-migracions/action.yml
name: 'Verificar migracions'
description: 'Comprova numeració única, absència d''operacions destructives i reversió'
inputs:
  directori: { description: 'Directori de migracions', required: true, default: 'apps/api/migracions' }
  base:      { description: 'SHA base per detectar migracions noves', required: true }
  fallar:    { description: 'Fer fallar el job si hi ha problemes', required: false, default: 'true' }
outputs:
  problemes:    { description: 'Nombre de problemes trobats' }
  destructives: { description: 'true si hi ha operacions destructives' }
runs:
  using: 'node20'
  main: 'dist/index.js'
const core = require('@actions/core');
const exec = require('@actions/exec');
const fs   = require('node:fs');
const path = require('node:path');

const DESTRUCTIVES = /\b(DROP\s+(COLUMN|TABLE)|TRUNCATE|ALTER\s+COLUMN\s+\w+\s+TYPE)\b/i;

async function fitxersNous(base, dir) {
  let sortida = '';
  await exec.exec('git', ['diff', '--name-only', '--diff-filter=A', `${base}...HEAD`, '--', dir],
    { listeners: { stdout: (d) => (sortida += d.toString()) } });
  return sortida.split('\n').filter(Boolean);
}

async function run() {
  try {
    const dir    = core.getInput('directori', { required: true });
    const base   = core.getInput('base', { required: true });
    const fallar = core.getBooleanInput('fallar');
    const problemes = [];

    // 1 · Numeració duplicada (sobre TOTES les migracions, no només les noves)
    const perNumero = new Map();
    for (const f of fs.readdirSync(dir).filter((f) => f.endsWith('.sql') && !f.endsWith('.down.sql'))) {
      const num = f.split('_')[0];
      if (perNumero.has(num)) {
        problemes.push({ fitxer: path.join(dir, f), linia: 1,
          missatge: `Número de versió duplicat ${num} (col·lisiona amb ${perNumero.get(num)})` });
      } else {
        perNumero.set(num, f);
      }
    }

    // 2 i 3 · Només sobre les migracions NOVES del PR
    let hiHaDestructives = false;
    for (const ruta of await fitxersNous(base, dir)) {
      if (ruta.endsWith('.down.sql')) continue;

      const linies = fs.readFileSync(ruta, 'utf8').split('\n');
      linies.forEach((linia, i) => {
        if (DESTRUCTIVES.test(linia)) {
          hiHaDestructives = true;
          problemes.push({ fitxer: ruta, linia: i + 1,
            missatge: `Operació destructiva. Fes servir expand and contract: desplega el codi que ja no fa servir la columna, i elimina-la en una migració posterior` });
        }
      });

      const reversio = ruta.replace(/\.sql$/, '.down.sql');
      if (!fs.existsSync(reversio)) {
        problemes.push({ fitxer: ruta, linia: 1, missatge: `Falta el fitxer de reversió ${path.basename(reversio)}` });
      }
    }

    for (const p of problemes) {
      core.error(p.missatge, { file: p.fitxer, startLine: p.linia, title: 'Migracions' });
    }

    core.setOutput('problemes', String(problemes.length));
    core.setOutput('destructives', String(hiHaDestructives));

    const resum = core.summary.addHeading('Verificació de migracions');
    if (problemes.length === 0) {
      resum.addRaw('Sense problemes. Numeració única, sense operacions destructives i amb reversió.');
    } else {
      resum.addTable([
        [{ data: 'Fitxer', header: true }, { data: 'Línia', header: true }, { data: 'Problema', header: true }],
        ...problemes.map((p) => [p.fitxer, String(p.linia), p.missatge]),
      ]);
    }
    await resum.write();

    if (problemes.length > 0) {
      const msg = `${problemes.length} problema(es) a les migracions`;
      fallar ? core.setFailed(msg) : core.warning(msg);
    }
  } catch (e) {
    core.setFailed(`Error inesperat: ${e.message}`);
  }
}
run();

Decisions de disseny que convé justificar. La numeració duplicada es comprova sobre tot el directori, perquè la col·lisió pot venir de dos PR oberts en paral·lel que individualment són correctes —és un problema d'integració, no de fitxer—. Les operacions destructives i la reversió es comproven només sobre les migracions noves, perquè les històriques ja es van aplicar i marcar-les seria soroll que faria ignorar l'eina. L'input fallar permet el desplegament progressiu de la 04-05: primer informativa durant dues setmanes per mesurar quant soroll genera, després bloquejant. I les anotacions ancorades a fitxer i línia apareixen a la vista de diff, on el revisor les veu sense obrir logs. L'ús requereix fetch-depth: 0 i passar base: ${{ github.event.pull_request.base.sha }}.

Solució 2. Sis problemes, tres dels quals greus:

# Problema Impacte
1 pull_request_target + checkout del codi del PR Qualsevol obre un PR i executa codi amb tots els secrets del repositori
2 npm install sobre codi del PR Executa scripts postinstall de l'atacant. Consumació immediata del punt 1
3 Executor autoallotjat en un repositori públic Execució arbitrària dins de la teva xarxa, i contaminació de jobs posteriors si és persistent
4 secrets.PAT_ADMIN Un PAT d'administrador on n'hi hauria prou amb GITHUB_TOKEN amb pull-requests: write. Màxim privilegi
5 Interpolar pull_request.title dins d'un curl Injecció d'ordres: un títol amb cometes i $( ) executa el que vulgui
6 actions/checkout@v3 sense fixar per SHA, sense permissions, sense timeout-minutes Dependència mutable, permisos amplis, cost sense límit

Reescriptura en dos fluxos de treball, que és el patró correcte:

# .github/workflows/pr-build.yml — SENSE secrets, executa codi no confiable
name: PR · build
on: pull_request
permissions: { contents: read }
jobs:
  build:
    runs-on: ubuntu-22.04                 # executor allotjat, efímer
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
      - uses: actions/setup-node@v4
        with: { node-version-file: .nvmrc, cache: npm }
      - run: npm ci
      - run: npm run build
      - name: Guardar dades per al comentari
        run: |
          mkdir -p resultat
          du -sk dist | cut -f1 > resultat/mida-kb.txt
          echo "${{ github.event.number }}" > resultat/pr.txt
      - uses: actions/upload-artifact@v4
        with: { name: resultat, path: resultat/, retention-days: 1 }
# .github/workflows/pr-comentar.yml — AMB permisos, NO executa codi del PR
name: PR · comentar
on:
  workflow_run:
    workflows: ["PR · build"]
    types: [completed]
permissions: { contents: read, pull-requests: write }
jobs:
  comentar:
    if: github.event.workflow_run.conclusion == 'success'
    runs-on: ubuntu-22.04
    timeout-minutes: 5
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: resultat
          run-id: ${{ github.event.workflow_run.id }}
          github-token: ${{ secrets.GITHUB_TOKEN }}
      - id: dades
        run: |
          echo "kb=$(cat mida-kb.txt)" >> "$GITHUB_OUTPUT"
          echo "pr=$(cat pr.txt)"      >> "$GITHUB_OUTPUT"
      - uses: actions/github-script@v7
        env:
          KB: ${{ steps.dades.outputs.kb }}     # per env, mai interpolat al cos
          PR: ${{ steps.dades.outputs.pr }}
        with:
          script: |
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: Number(process.env.PR),
              body: `Mida del bundle: ${process.env.KB} KB`
            });

La idea central: separar el privilegi de l'execució de codi no confiable. El primer flux de treball executa codi del PR sense cap secret i amb permisos de només lectura; el segon té permís d'escriptura però només processa dades ja generades i mai no executa codi del fork. Fixa't a més que el títol del PR desapareix del comentari: no aportava res i era el vector d'injecció. I que les dades es passen per env, no interpolades al cos de l'script, que és la regla general per a qualsevol dada d'origen extern.

Solució 3. El diagnòstic separa dues coses que l'equip està barrejant: 11 minuts d'execució i 4 de cua són problemes diferents amb causes diferents, i dir-ho tot "GitHub Actions va lent" impedeix arreglar-ne cap.

# Símptoma Causa Acció Efecte
1 3 jobs sense timeout-minutes, penjats 2 cops/setmana Valor per defecte de 6 h timeout-minutes a tots els jobs Deixa de consumir concurrència i minuts; causa parcial de la cua
2 Memòria cau al límit (9 GB) amb encert del 40 % node_modules guardat per branca; desallotjament per menys usada Guardar ~/.npm amb clau per lockfile; esborrar memòries cau velles; no guardar node_modules Encert al 85-90 %; ~1,5 min per job
3 4 min de cua en hores punta Límit de concurrència del pla, agreujat per l'1 concurrency amb cancel-in-progress al CI; paths per no llançar-ho tot sempre; avaluar executors propis si persisteix Cua a ~1 min
4 Matriu cancel·lada per una prova inestable fail-fast: true en shards + prova inestable sense quarantena fail-fast: false i, sobretot, quarantena de la prova inestable (02-04) Menys reexecucions completes
5 11 min d'execució Per determinar després de l'anterior Mesurar per job amb els temps de la interfície abans de tocar res més

Ordre i raons. Primer el punt 1, perquè costa cinc minuts de feina i és causa parcial del punt 3: jobs penjats ocupant forats de concurrència són part de la cua. Després el 2, que és l'estalvi de temps més gran per esforç i a més allibera pressupost de memòria cau. Després el 4, amb el matís important: fail-fast: false redueix el símptoma però el problema real és la prova inestable, i deixar-la sense quarantena mentre s'amaga el símptoma és exactament el que la 02-04 advertia. I només llavors el 3 i el 5, quan es pugui mesurar quant queda de cada cosa.

El que cal endur-se: dels cinc punts, tres no són de l'eina —una prova inestable sense quarantena, jobs sense timeout, memòria cau mal triada— sinó de com s'està fent servir. La conclusió que cal portar a l'equip és que abans de canviar d'eina o de pagar més concurrència, cal separar cua d'execució i arreglar el que és propi, que és literalment la primera regla de la 04-04. Si després d'això la cua continua sent el coll d'ampolla, llavors sí que hi ha una conversa legítima sobre pla o executors propis, i aquesta conversa es té amb dades.

Conclusió

GitHub Actions ha estat l'eina del curs des del mòdul 2, i aquesta lliçó l'ha mirat per fi com a objecte d'estudi. El que tanca els buits que quedaven: el cicle d'un esdeveniment i de quina branca es llegeix el flux de treball —que explica per què schedule i workflow_run es comporten com es comporten—; els contextos, amb el parany que tots els inputs són cadenes; els disparadors que faltaven i la regla d'or de pull_request davant de pull_request_target; els permisos que substitueixen en comptes de sumar i el sub d'OIDC que cal lligar amb precisió; els tres tipus d'action i quan baixar a JavaScript; les matrius dinàmiques amb el seu job de porta obligatori; el comportament real de la memòria cau amb el seu àmbit per branca i el seu desallotjament; ARC per a executors efímers; i els límits del servei amb les seves estratègies.

Les contrapartides, dites sense adorns, perquè el mòdul va prometre no fer màrqueting amb cap eina: el pla de control no és teu i un incident del servei et deixa sense desplegar; l'ecosistema d'actions de tercers és la seva fortalesa més gran i una superfície de subministrament real; la memòria cau té un pressupost per repositori que castiga l'excés; schedule no és puntual i s'apaga sol; hi ha asimetries perilloses com que un job skipped compti com a aprovat; i pull_request_target és un peu de guerra permanent en repositoris públics. Cap d'aquestes coses no la fa mala elecció —per a un equip el codi del qual ja és a GitHub continua sent l'opció per defecte més raonable—, però conèixer-les és la diferència entre fer-la servir i patir-la.

Amb això queden les sis eines recorregudes i el mateix pipeline de Reservalia traduït sis vegades. La lliçó que tanca el mòdul, Comparativa i Criteris per Triar Eina, ho posa tot a la mateixa taula: la taula d'equivalències de vocabulari, la comparació per dimensions —model d'execució, allotjament, reutilització, identitat, cost—, els criteris de decisió ordenats pel seu pes real (amb un factor que decideix el 80 % dels casos), un arbre de decisió, un exercici de cost total de propietat, el cost de canviar d'eina i com reduir-lo, i una guia de migració per fases. I després, el mòdul 7: construir el teu propi pipeline d'extrem a extrem.

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