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
- El cicle complet d'un esdeveniment i on viu l'estat
- Contextos i expressions a fons
- Tots els disparadors que falten
pull_requestdavant depull_request_targetGITHUB_TOKEN,permissionsi OIDC- Els tres tipus d'action, amb una JavaScript action completa
- Matrius avançades i matrius dinàmiques
- Control de flux: outputs,
continue-on-error,timeout-minutes,concurrency - Memòria cau i artefactes: claus, límits i desallotjament
- Executors autoallotjats i autoescalat amb ARC
- Resums, anotacions i ordres de flux de treball
- Entorns, regles i desplegaments
- Límits reals del servei i estratègies
- Depurar i provar fluxos de treball
- Errors Comuns i Consells
- Exercicis
- Conclusió
- 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 |
- 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$GITHUB_OUTPUTés el mecanisme actual (l'antic::set-outputestà 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"$GITHUB_ENVdefineix variables per als steps següents del mateix job; unexportnormal no surt del step perquè cadaruné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à.
- 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 comentariLa 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
pull_request davant de pull_request_target
pull_request davant de pull_request_targetLa 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 disponiblesUn 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:
- Fes servir
pull_requestper defecte. Cobreix el 95 % dels casos. - Fes servir
pull_request_targetnomés quan necessitis secrets o escriptura des de forks —etiquetar, comentar, donar format— i sense fer checkout del codi del PR. - Si necessites totes dues coses (construir codi d'un fork i comentar el resultat), parteix-ho: un flux de treball
pull_requestconstrueix sense secrets i puja un artefacte; un segon flux de treball ambworkflow_runel descarrega i comenta. El segon té secrets però mai no executa codi del fork. GITHUB_TOKENambpermissionsmí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.
GITHUB_TOKEN, permissions i OIDC
GITHUB_TOKEN, permissions i OIDCCada 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 temporalsPunts que importen:
- Declarar
permissionsal flux de treball ho posa tot al que has declarat, no només el que has llistat: si posescontents: read, la resta queda ennone. És el que vols. - Els permisos de job substitueixen, no acumulen. Un job amb
permissions: { pull-requests: write }perdcontents: readi el checkout falla. GITHUB_TOKENno 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 lligarsubamb precisió:
Escriure"StringLike": { "token.actions.githubusercontent.com:sub": "repo:reservalia/reservalia:environment:produccio" }repo:reservalia/*o deixar elsubamb 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.
- 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();getBooleanInputparseja'true'/'false'correctament.getInput('fallar') === trueseria sempre fals: tots els inputs arriben com a cadena, que és el mateix parany de l'apartat 2.core.setOutputpublica outputs consumibles ambsteps.<id>.outputs.<nom>.core.summaryescriu a$GITHUB_STEP_SUMMARY: markdown que apareix a la pàgina del job (apartat 11).core.setFailedmarca el pas com a fallit amb missatge;core.warningicore.noticecreen anotacions sense fer fallar. Ambfile,startLineiendLinel'anotació s'ancora a una línia concreta del codi, que és el que fa útils els linters a la vista de diff.- 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—.
- 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 coberturafail-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 afalse. Regla pràctica:falseen matrius de compatibilitat,trueen shards de la mateixa suite.max-parallellimita la concurrència: útil quan la matriu colpeja un recurs compartit (una base de dades de proves, un límit d'API).includeté 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.excludes'aplica després d'expandir-ho tot, inclosos elsinclude.
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 }})"- Una matriu buida fa fallar el job, no l'omet. D'aquí l'output
hi-ha-canvisi l'if. fromJSONconverteix la cadena en un array real per a la matriu.- 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.portasí que té nom fix, s'executa sempre (if: always()) i comprovaneeds.test.resultexplícitament.
- Control de flux: outputs,
continue-on-error, timeout-minutes, concurrency
continue-on-error, timeout-minutes, concurrencyjobs:
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 }}"timeout-minutesa 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.continue-on-error: truedeixa que el step falli sense trencar el job. També existeix a nivell de job.outcomedavant deconclusion:outcomeés el resultat abans d'aplicarcontinue-on-error;conclusionés el resultat final. Ambcontinue-on-error,conclusionéssuccessencara queoutcomesiguifailure. Per detectar que alguna cosa ha fallat però no ha bloquejat, es miraoutcome.- Dos usos diferents de
concurrency, i convé no barrejar-los. Per a CI:group: ci-${{ github.ref }}ambcancel-in-progress: true, per cancel·lar execucions obsoletes i estalviar (04-04). Per a desplegaments:cancel-in-progress: falsei 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.
- 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"- La clau ha de contenir tot el que la invalida: sistema operatiu, arquitectura si varia, i el hash dels lockfiles.
hashFilesaccepta patrons múltiples. restore-keyssón prefixos de reserva: sense ells, qualsevol canvi del lockfile obliga a descarregar-ho tot.cache-hités'true'només amb coincidència exacta; una restauració parcial perrestore-keysdeixacache-hitenfalseencara que hagi restaurat fitxers. Condicionarnpm ciacache-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
mainpobli 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/- 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.
retention-daysestalvia emmagatzematge facturable; la política de la 02-06 en una línia.patternambmerge-multiplerecompon 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.
- 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 | Sí: 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 coincidirEls 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" }minRunners: 1manté 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.containerMode: kubernetesevita 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.
- 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.
- 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 }}"- 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'
inputde Jenkins (06-01)—. - Temporitzador d'espera: minuts obligatoris abans de desplegar, per donar marge a cancel·lar.
- Branques i etiquetes permeses: només
mainpot desplegar aproduccio, encara que algú modifiqui el flux de treball en una altra branca. - Secrets i variables per entorn:
secrets.DATABASE_URLval 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ó
subdel token inclou l'entorn, així que el rol d'IAM de producció només es pot assumir des d'un job ambenvironment: produccio, que al seu torn només es pot executar des demaini després d'aprovació. Tres controls encadenats en comptes d'un.
- 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.
- 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.localNotes 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 -colorErrors 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
- Conceptes Bàsics de CI/CD
- Beneficis del CI/CD
- Eines Populars de CI/CD
- El Projecte del Curs: l'Aplicació que Automatitzarem
- Mètriques DORA: Com es Mesura el Lliurament de Programari
Mòdul 2: Integració Contínua (CI)
- Introducció a la Integració Contínua
- Configuració d'un Entorn de CI
- Automatització de la Construcció
- Proves Automatitzades
- Qualitat de Codi i Anàlisi Estàtica
- Artefactes, Versionat i Promoció
- Integració amb el Control de Versions
Mòdul 3: Desplegament Continu (CD)
- Introducció al Desplegament Continu
- Automatització del Desplegament
- Infraestructura com a Codi i Entorns Reproduïbles
- Estratègies de Desplegament
- Feature Flags, Rollback i Recuperació davant Errors
- Monitoratge i Retroalimentació
Mòdul 4: Pràctiques Avançades de CI/CD
- Pipelines de CI/CD
- Gestió de Dependències
- Seguretat en CI/CD
- Escalabilitat i Rendiment
- Pipeline as Code: Plantilles, Reutilització i Proves del Pipeline
- Bases de Dades al Pipeline: Migracions Segures
Mòdul 5: Implementació de CI/CD en Projectes Reals
- Cas d'Estudi: Projecte Web
- Cas d'Estudi: Aplicació Mòbil
- Cas d'Estudi: Microserveis
- Cas d'Estudi: Modernitzar un Projecte Legacy
Mòdul 6: Eines i Tecnologies
- Jenkins
- GitLab CI/CD
- CircleCI
- Travis CI
- Docker i Kubernetes
- GitHub Actions a Fons
- Comparativa i Criteris per Triar Eina
Mòdul 7: Exercicis Pràctics
- Exercici 1: Configuració d'un Pipeline Bàsic
- Exercici 2: Integració de Proves Automatitzades
- Exercici 3: Desplegament en un Entorn de Producció
- Exercici 4: Monitoratge i Retroalimentació
- Exercici 5: Enfortir el Pipeline amb Seguretat i Secrets
- Projecte Final: Pipeline Complet d'Extrem a Extrem
