|

CI de GitHub Actions para Node.js: 4 ajustes que importan

En pocas palabras: Un CI de Node.js en GitHub Actions entra en un solo archivo, .github/workflows/ci.yml: hace checkout, setup-node con cache: npm, npm ci, lint, test y build en ubuntu-latest, y suma concurrency con cancel-in-progress: true, permissions: contents: read y –if-present en los scripts.

Un CI de GitHub Actions Node.js entra en un solo archivo, .github/workflows/ci.yml, y rinde más con cuatro ajustes que varias plantillas omiten: concurrency para cancelar corridas viejas, permissions: contents: read, cache: npm en setup-node y --if-present en los scripts.

GitHub Actions es el sistema de automatización de GitHub: ejecuta workflows descritos en archivos YAML dentro de .github/workflows, en runners que aloja GitHub o que montás vos. En un proyecto Node.js sirve para instalar dependencias, correr lint, tests y build en cada push y pull request, y dejar una señal verde o roja antes de mergear.

En 30 segundos

  • Un archivo, seis pasos. El workflow hace checkout, setup-node, npm ci, lint, test y build en un runner ubuntu-latest.
  • Cancelás corridas viejas con concurrency. El grupo ${ github.workflow }-${ github.ref } con cancel-in-progress: true deja vivo solo el último push; en deploys se usa false.
  • Es gratis en repos públicos. En privados, según la documentación de billing consultada el 2026-10-13, hay 2.000 minutos mensuales en GitHub Free y 3.000 en Pro y Team.
  • Revisá las versiones antes de copiar. El post original usa checkout@v4, setup-node@v4 y Node 20; a 2026-10-13, la documentación de setup-node muestra v7 y Node 24.

¿Qué dispara este CI y qué corre en cada paso?

El workflow corre en cada push a main y en cada pull request, sobre un runner ubuntu-latest. Ejecuta seis pasos: checkout, setup-node con Node 20 y caché de npm, npm ci, lint, test y build. Es la versión que publicó un desarrollador en dev.to, con sus versiones originales:

name: CI (Node.js)

on:
 push:
 branches: [main]
 pull_request:

concurrency:
 group: ${ github.workflow }-${ github.ref }
 cancel-in-progress: true

permissions:
 contents: read

jobs:
 build:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v4
 - uses: actions/setup-node@v4
 with:
 node-version: 20
 cache: npm
 - run: npm ci
 - run: npm run lint --if-present
 - run: npm test --if-present
 - run: npm run build --if-present

Ahora lo que el post no trae: ninguna medición. Dice que los ajustes ahorran minutos y dan una señal más rápida, pero no muestra cuántos. Tomalo como una base razonable, no como un resultado probado. Lo explicamos a fondo en comparativa de herramientas de CI/CD en 2026.

¿Cómo cancelar ejecuciones viejas de un workflow en GitHub Actions?

github actions node js diagrama explicativo

Con concurrency y cancel-in-progress: true, cuando llega un push nuevo al mismo grupo GitHub corta la corrida en curso y arranca la nueva. El grupo ${ github.workflow }-${ github.ref } separa por workflow y por rama o PR, así que cada pull request tiene su propia fila, según la documentación oficial de concurrency.

Pensalo así: hacés un push, te das cuenta de que olvidaste un import, hacés otro, el linter se queja de una coma, hacés un tercero, y mientras tanto las tres corridas ocupan runners, queman minutos y te dejan tres estados en el PR de los cuales solo el último te sirve.

Sin cancel-in-progress, el comportamiento es otro. La corrida que ya está andando sigue, la nueva queda en pending y cualquier corrida pendiente anterior del mismo grupo se cancela y se reemplaza. Es decir, no se cancela lo que está corriendo.

Un ejemplo hipotético (no es una medición): si el workflow dura 4 minutos y empujás tres commits con un minuto de diferencia, sin cancelación sumás 12 minutos de runner. Con cancelación, las dos primeras corridas se cortan al minuto y la tercera completa los 4, o sea 6 en total. Esa cuenta supone que una corrida cortada gasta solo lo que alcanzó a correr, algo que es inferencia mía a partir de cómo mide el uso la documentación de billing.

En deploys, no.

Si cortás un deploy a la mitad podés dejar el entorno en un estado raro, por eso el autor del post recomienda cancel-in-progress: false ahí. La documentación suma una opción: queue: max deja hasta 100 corridas pendientes en fila, procesadas de a una, pero no se puede combinar con cancel-in-progress: true porque da error de validación. Para más detalles técnicos, mirá armar un pipeline completo para Node.js.

¿Por qué poner permissions: contents: read en un workflow de CI?

Porque un CI solo necesita leer el código, y declararlo limita lo que podría hacer con el GITHUB_TOKEN una dependencia o una action comprometida. Es una práctica de mínimo privilegio, no una garantía de seguridad. El README de actions/setup-node también recomienda contents: read para acceder al código e instalar dependencias.

permissions:
 contents: read

Si no declarás nada, los permisos del token dependen de la configuración del repo o la organización, y ahí es donde se te puede colar algo más amplio de lo que pensabas. Si un job necesita escribir (comentar un PR, publicar un paquete), agregá solo ese permiso en ese job. Llamarlo “mejor práctica” es fácil; lo que cuesta es revisarlo cuando alguien copia el archivo a otro repo.

¿Cómo cachear dependencias de npm en GitHub Actions con setup-node?

Agregá cache: npm en el paso de actions/setup-node. Según el README del action, usa actions/cache por dentro, toma el hash de package-lock.json (o npm-shrinkwrap.json) como parte de la clave y no necesita un paso extra. Combinalo con npm ci para instalar exactamente lo que dice el lockfile.

  • Cachea descargas, no node_modules. El README aclara que el action no cachea node_modules; npm ci igual reinstala, pero desde el caché global.
  • Monorepos. Si hay varios lockfiles o están en subcarpetas, usá cache-dependency-path, por ejemplo subdir/package-lock.json.
  • Workflows con privilegios elevados. Para esos casos el README recomienda package-manager-cache: false cuando el caché no hace falta.

¿Qué hace –if-present en lint, test y build?

npm run lint --if-present saltea el paso si el script no existe en el package.json. Por eso el mismo archivo sirve el día uno de un proyecto y el día 500. Ojo con el costado oscuro: si borrás el script de test por error, el CI sigue en verde y nadie se entera.

¿Hay que actualizar checkout, setup-node y Node 20 antes de copiar el archivo?

Conviene verificarlo. A 2026-10-13, los ejemplos del README de setup-node usan actions/checkout@v7, actions/setup-node@v7 y node-version: 24. Ese README indica que v5 pasó el action de node20 a node24 y pide un runner v2.327.1 o posterior, algo que importa si usás runners propios. Que el post use v4 y Node 20 no significa que esté roto; significa que fue escrito con otras versiones. Tema relacionado: qué conviene más: Jenkins o Actions.

Propuesta editorial (no sale de la fuente): antes de adoptar el archivo, abrí un PR de prueba, mirá en la pestaña Actions que no aparezcan avisos de versión y fijá en el package.json la línea de Node que realmente soporta tu proyecto.

¿Cómo validar un workflow de GitHub Actions con actionlint?

actionlint es un linter para archivos de workflow. Según el post, detecta expresiones ${ } mal escritas, typos en los inputs de las actions y, con shellcheck instalado, errores en los scripts de run:, todo antes de hacer push. El comando del post es este:

bash <(curl -sL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
./actionlint

A mí me hace ruido ejecutar un script remoto de la rama main sin leerlo. La alternativa que propongo es bajarlo a un archivo, leerlo y recién ahí correrlo:

curl -sL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash -o download-actionlint.bash
less download-actionlint.bash
bash download-actionlint.bash
./actionlint

¿GitHub Actions para Node.js es gratis o gasta minutos?

Es gratis en repositorios públicos con runners estándar de GitHub y en runners self-hosted. En repos privados tenés minutos mensuales incluidos según el plan, y lo que excede se factura, según la documentación de billing de GitHub consultada el 2026-10-13.

  • Minutos incluidos por mes. 2.000 en GitHub Free (y en Free para organizaciones), 3.000 en Pro y en Team, 50.000 en Enterprise Cloud.
  • Caché. 10 GB por repositorio, separado de artifacts y paquetes.
  • Tarifa al pasarte. Linux 2-core cuesta US$ 0,006 el minuto, Windows US$ 0,010 y macOS US$ 0,062, más de diez veces Linux.
  • Quién paga. Los minutos se cargan al dueño del repo, no a quien disparó el workflow, y cualquiera con permiso de escritura puede ejecutar actions.
  • Self-hosted. No consume minutos, pero el servidor es tuyo (un VPS, por ejemplo, en donweb.com) y lo mantenés y asegurás vos.

Lo que sigue es análisis mío: cancelar corridas viejas y cachear dependencias achican el tiempo de runner, y en un repo privado eso se traduce en minutos. En mi ejemplo hipotético, 6 minutos menos a US$ 0,006 son unos US$ 0,036 por tanda de pushes. Poco por vez; pesa en equipos con muchos PRs por día. Las cifras son las de la documentación consultada el 2026-10-13, y GitHub las ajusta, así que mirá la página vigente antes de presupuestar. En desplegar una app React con Vite profundizamos sobre esto.

Errores comunes al copiar este workflow

  • Dejar cancel-in-progress en true en un deploy. Corrección: usá false o queue: max para que cada despliegue termine.
  • Armar el grupo sin github.workflow. La documentación advierte que, si los nombres de grupo se repiten entre workflows, una corrida puede cancelar a la de otro workflow. Corrección: prefijá con ${ github.workflow }.
  • Usar github.head_ref sin fallback. Esa propiedad solo existe en eventos pull_request; si el workflow responde a otros eventos, da error de sintaxis. Corrección: ${ github.head_ref || github.run_id }.
  • No commitear el lockfile. El README de setup-node recomienda con fuerza subirlo, por seguridad y rendimiento, y sin él el caché y npm ci pierden sentido.
  • Mover el job a macOS sin mirar la tarifa. Una matriz con macOS multiplica el costo por minuto; si no necesitás ese sistema, quedate en Linux.

Preguntas Frecuentes

¿Cómo configurar un CI para Node.js con GitHub Actions?

Creá el archivo .github/workflows/ci.yml con triggers de push y pull request, un job en ubuntu-latest y los pasos checkout, setup-node, npm ci, lint, test y build. Hacé commit y GitHub lo ejecuta en el siguiente push.

¿Cómo cancelar ejecuciones viejas de un workflow en GitHub Actions?

Agregá un bloque concurrency con group: ${ github.workflow }-${ github.ref } y cancel-in-progress: true. Cada push nuevo cancela la corrida en curso del mismo workflow y la misma rama o PR.

¿Para qué sirve cache: npm en actions/setup-node?

Sirve para cachear las descargas de npm usando el hash de package-lock.json como clave, sin sumar un paso de actions/cache. No cachea node_modules.

¿Qué permisos necesita el GITHUB_TOKEN en un workflow de CI?

Para un CI que solo instala, testea y compila alcanza con contents: read. Si un job necesita escribir, otorgá ese permiso puntual solo en ese job.

¿GitHub Actions es gratis?

Es gratis en repos públicos con runners estándar y en runners self-hosted. En repos privados, según la documentación oficial de billing consultada el 2026-10-13, hay 2.000 minutos mensuales en GitHub Free y 3.000 en Pro y Team; después se factura, con Linux 2-core a US$ 0,006 el minuto.

Conclusión

El workflow del post es una base sensata para un proyecto Node.js: corto, con permisos mínimos y sin pasos redundantes. Lo que le falta es evidencia de ahorro y una actualización de versiones.

Qué hacer: copialo, subí a las versiones que muestra hoy la documentación de setup-node si tu runner y tu proyecto lo soportan, validalo con actionlint (leyendo antes el script) y, si el repo es privado, mirá tu consumo de minutos después de una semana. Eso te dice si la cancelación y el caché te ahorran algo real.

Fuentes

Te puede interesar...