GitHub Actions en otoño de 2026: qué se rompe (y por qué node20 no es el problema)
Tabla de Contenidos
El 23 de septiembre de 2026 GitHub quita Node 20 de los runners de Actions, y casi todo el mundo lo está leyendo como «mis actions con using: node20 van a fallar». No van a fallar. Desde el 16 de junio el runner ya reescribe node20 a node24 antes de ejecutar, y lo mismo hace con node12 y node16. Lo que se quita el 23 es la salida de emergencia: la variable ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION deja de funcionar.
Eso invierte quién está en riesgo. Si tu CI lleva verde desde junio sin esa variable, el 23 no te cambia nada en los runners de GitHub. Los que se rompen son justo los que ya se toparon con una action que no aguanta Node 24, pusieron la variable para seguir avanzando y dejaron el arreglo para después. Y alrededor de esa fecha hay otras cuatro con consecuencias reales: la versión mínima de los runners self-hosted (25 de septiembre), la retención de checks y ejecuciones (1 de octubre), los apagones de macos-14 (octubre) y su retiro (2 de noviembre).
Lo verifiqué contra el código fuente del runner y contra el CI de este mismo sitio, que usa dos actions node20. Abajo va todo con su fuente.
El calendario, por fecha
| Fecha | Qué cambia | Qué se rompe | Cómo lo detectas |
|---|---|---|---|
| 14, 16 y 18 sep (11:00-15:00 ET, 9:00-13:00 en CDMX) | Apagones previos a la versión mínima de runners self-hosted | Los runners desactualizados no se registran ni ejecutan jobs durante la ventana | API de deprecaciones del runner (abajo) |
| 17 sep | ubuntu-22.04 y ubuntu-22.04-arm entran en deprecación | Nada falla todavía; puede haber colas más largas | grep de la etiqueta |
| 21-30 sep | windows-11-arm pasa a la imagen con Visual Studio 2026 | Cambia la cadena de herramientas por debajo | grep windows-11-arm |
| 23 sep | Node 20 sale de los runners | ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION se ignora; runners Linux ARM32 sin soporte | grep de la variable |
| 25 sep | Se aplica la versión mínima de runners self-hosted | Registrar exige ≥ 2.329.0; un runner que no instala cada versión en 30 días deja de recibir jobs | API de deprecaciones |
| 1 oct | La retención de Actions pasa a cubrir checks, ejecuciones y statuses | Lo que pase tu periodo de retención (90 días por defecto) se borra | Ajuste de retención del repo/org |
| 5-31 oct | Ocho apagones de macos-14 | Los jobs programados en la ventana fallan | grep macos-14 |
| 2 nov | macos-14 retirado | Los jobs con esa etiqueta terminan con error | grep macos-14 |
| 17 abr 2027 | ubuntu-22.04 retirado (apagones antes, en marzo y abril) | Los jobs terminan con error | grep de la etiqueta |
1. Node 20: qué pasa de verdad el 23 de septiembre
Lo que dice el aviso
El changelog de GitHub del 19 de septiembre de 2025, con una nota del editor del 25 de agosto de 2026, fija dos fechas: desde el 16 de junio de 2026 los runners usan Node 24 por defecto, y la variable ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true para volver a Node 20 «solo funcionará hasta que actualicemos el runner y quitemos Node20 el 23 de septiembre de 2026». Añade dos límites de plataforma: Node 24 es incompatible con macOS 13.4 y anteriores, y no tiene soporte oficial para ARM32.
Lo que el aviso no explica es qué le pasa a una action cuyo action.yml sigue diciendo using: node20. Eso está en el código.
Lo que dice el código del runner
En HandlerFactory.cs (igual en la etiqueta v2.337.0, la versión actual), el runner hace dos cosas antes de lanzar una action de JavaScript:
- Si la action declara
node12onode16, la sube anode20. - Si queda en
node20, le pregunta aNodeUtil.DetermineActionsNodeVersionqué versión usar.
Y esa función tiene tres fases, controladas por feature flags del lado de GitHub:
// Phase 3: Always use Node 24 regardless of environment variables
if (requireNode24)
{
return (Constants.Runner.NodeMigration.Node24, null);
}
// ...
// Phase 2: Node 24 is the default
if (useNode24ByDefault)
{
if (allowUnsecureNode)
{
return (Constants.Runner.NodeMigration.Node20, null);
}
return (Constants.Runner.NodeMigration.Node24, null);
}
El PR #3948 que introdujo esto lo describe sin rodeos: en la fase 3, «todas las actions usan Node 24» y «no hay opciones de exclusión». Ninguna rama devuelve un error por declarar node20. La action se ejecuta con otro Node, que es muy distinto de no ejecutarse.
Fíjate también en el orden: node12 → node20 → node24. Una action vieja con using: node16 (por ejemplo actions/checkout@v3) acaba igualmente en Node 24.
La prueba: el CI de este sitio
El workflow de CI de este blog usa actions/checkout@v4 y actions/setup-node@v4. Los action.yml oficiales de esas etiquetas declaran using: node20; los de v5 en adelante declaran node24 (lo comprobé en las etiquetas v4 a v7 de las dos). La ejecución del 11 de septiembre, en el runner 2.337.0 sobre la imagen ubuntu-24.04, terminó en verde con esta anotación:
Node.js 20 is deprecated. The following actions target Node.js 20 but are being
forced to run on Node.js 24: actions/checkout@v4, actions/setup-node@v4.
Y en el log de los pasos de esas dos actions, esta línea:
Node 20 is being deprecated. This workflow is running with Node 24 by default.
If you need to temporarily use Node 20, you can set the
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable.
La misma anotación sale desde la primera ejecución de ese workflow, el 23 de agosto. Es decir: mis dos actions node20 nunca han corrido en Node 20 en este CI, y yo no toqué nada para eso. El 23 de septiembre no les cambia el runtime, porque el cambio ya ocurrió en junio. Actualizarlas a v5 o superior quita la anotación, y conviene hacerlo, pero no hay reloj.
Cómo se ve cuando sí se rompe
Hay tres formas reales de fallar, y ninguna es un mensaje de GitHub que diga «Node 20 ya no existe».
Tenías la variable puesta. Alguna action tuya tropezó con Node 24 después del 16 de junio, pusiste ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true y siguió en Node 20. En la fase 3 el runner ignora la variable sin avisar: la rama de requireNode24 devuelve null como mensaje. La action vuelve a Node 24 y el error que ves es el de su propio código, el mismo que viste la primera vez. Una pista de que la fase 3 ya está activa: con el código actual del runner, la línea «If you need to temporarily use Node 20…» deja de salir en el log, porque solo se imprime cuando Node 24 es el valor por defecto pero todavía no es obligatorio.
La action usa algo que Node 24 eliminó. Según la tabla de deprecaciones de Node 24 y las notas de la 24.0.0, llegaron a fin de vida en esa versión:
| API | Estado en Node 24 | Cómo falla |
|---|---|---|
tls.createSecurePair() | Eliminada (DEP0064) | TypeError: ... is not a function al llamarla |
fs.truncate() con un descriptor de archivo | Eliminado (DEP0081) | Lanza ERR_INVALID_ARG_TYPE: espera una ruta. Usa fs.ftruncate() |
dirent.path | Eliminada (DEP0178) | No lanza por sí sola: la propiedad ya no existe y lees undefined. Usa dirent.parentPath |
OutgoingMessage.prototype._headers / _headerNames | Eliminadas (DEP0066) | Lees undefined. Usa getHeaders() |
El caso de dirent.path es el más traicionero de la lista, porque leerla no lanza nada: el fallo aparece más adelante, donde se use esa ruta, o nunca, si solo se concatena en un texto y sale undefined/archivo. Si mantienes una action que recorre directorios, busca esa propiedad antes de fiarte del check verde.
Tu runner self-hosted no puede ejecutar Node 24. En Linux ARM32, el mismo NodeUtil.cs tiene un interruptor de apagado que hace fallar el paso con:
Linux ARM32 runners are no longer supported. Please migrate to a supported platform.
Antes de que se active, el runner mantiene esas actions en Node 20 con un aviso que usa como fecha por defecto la de la retirada de Node 20: «Linux ARM32 runners are deprecated and will no longer be supported after September 23rd, 2026». GitHub no ha publicado el día exacto en que se activa; el changelog dice que ARM32 deja de tener soporte tras la deprecación de Node 20. En macOS, el BUILDING.md de Node 24 exige macOS 13.5 o superior.
2. La auditoría: qué buscar y dónde
En un repositorio
Corre esto desde la raíz:
# 1. El interruptor que deja de funcionar el 23 de septiembre
grep -rn 'ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION' .github/
# 2. Etiquetas de imagen con fecha. Sin "runs-on:" a propósito:
# así también aparecen las matrices (os: [ubuntu-22.04, ...])
grep -rnE 'ubuntu-22\.04|macos-14|windows-11-arm' .github/
# 3. Actions propias o locales que declaran un Node viejo
grep -rnE --include=action.yml --include=action.yaml \
"using:[[:space:]]*['\"]?node(12|16|20)" .
La búsqueda 2 tiene un punto ciego: runs-on: ${{ vars.RUNNER }} o una etiqueta que llega como input a un workflow reutilizable no aparece en ningún grep. Para esos casos, la fuente de verdad es el log. El paso «Set up job» imprime la imagen real:
gh run view <run-id> --log | grep -m1 'Image:'
# Image: ubuntu-24.04
Si la matriz es larga o viene anidada, pasarla a JSON y filtrarla con jq suele ser más rápido que leer el YAML a ojo:
Para saber qué runtime declara cada action de terceros que usas, incluidas las fijadas por SHA, hay que leer su action.yml en esa referencia exacta. Este script lo hace con gh:
grep -rhoE "uses:[[:space:]]*['\"]?[^[:space:]'\"#]+@[^[:space:]'\"#]+" .github/ \
| sed -E "s/uses:[[:space:]]*['\"]?//" | sort -u \
| while read -r ref; do
spec=${ref%@*}; ver=${ref#*@}
case "$spec" in ./*|docker://*) continue ;; esac
repo=$(cut -d/ -f1-2 <<<"$spec"); sub=$(cut -d/ -f3- <<<"$spec")
for f in action.yml action.yaml; do
rt=$(gh api "repos/$repo/contents/${sub:+$sub/}$f?ref=$ver" --jq .content 2>/dev/null \
| base64 -d | grep -E '^[[:space:]]*using:' | tr -d " '\"" | cut -d: -f2)
[ -n "$rt" ] && { echo "$rt $ref"; break; }
done
done
En este repo imprime:
node20 actions/checkout@v4
node20 actions/setup-node@v4
Soporta subrutas (github/codeql-action/init@v3) y SHAs. Un SHA que apunta a una versión node20 no falla el 23 por lo mismo que no falla @v4: el runner la fuerza a Node 24. Lo que cambia con un SHA es cómo lo actualizas: no recibe nada solo, tiene que moverlo Dependabot o una persona.
Un aviso antes de subir de major: lee las notas de versión. actions/setup-node v5 activó caché automática cuando package.json trae el campo packageManager, y la v6 la limitó a npm. Si ya pasas cache: npm explícito, como en mi CI, no te afecta.
En una organización
La búsqueda de código de GitHub llega a todos los repos de una vez:
gh search code --owner TU_ORG 'ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION'
gh search code --owner TU_ORG 'ubuntu-22.04 path:.github/workflows'
gh search code --owner TU_ORG 'macos-14 path:.github/workflows'
gh search code --owner TU_ORG node20 --filename action.yml
Dos límites de la API de búsqueda de código que te van a morder: solo busca en la rama por defecto (un workflow en una rama de release no sale) y permite 10 peticiones por minuto. Preparando este post me topé con HTTP 403: API rate limit exceeded en cuanto encadené unas cuantas búsquedas; mete un sleep entre ellas si lo vas a automatizar.
3. Runners self-hosted: dos fechas en tres días
Si solo usas runners de GitHub, salta esta sección. Si tienes los tuyos, el 25 de septiembre pesa más que el 23.
El changelog del 12 de junio retoma la aplicación de dos reglas en github.com:
- Para registrar (o volver a registrar) un runner hace falta la versión 2.329.0 o superior.
- Para seguir ejecutando jobs, el runner tiene que instalar cada versión nueva en menos de 30 días desde su publicación. «Un runner fijado en 2.329.0 que nunca se actualiza no recibirá jobs».
La aplicación completa empieza el 25 de septiembre de 2026 (la tabla del aviso rotula esa fecha como GitHub Enterprise Cloud, y el mismo aviso aclara que el cambio aplica a github.com), con apagones antes: los días 14, 16 y 18 de septiembre, de 11:00 a 15:00 hora del Este (9:00 a 13:00 en la Ciudad de México), los runners fuera de versión ni se registran ni ejecutan. Lo que ves, según el aviso, es que el workflow «puede quedarse en cola o fallar».
La forma de saber cuándo caduca tu versión es un endpoint que GitHub añadió el 3 de septiembre. Lo consulté hoy:
gh api repos/OWNER/REPO/actions/runners/deprecations/2.335.1
# {"runner_version":"2.335.1","runtime_deprecates_at":"2026-09-24T15:30:55Z"}
gh api repos/OWNER/REPO/actions/runners/deprecations/2.337.0
# {"runner_version":"2.337.0","runtime_deprecates_at":null}
Un runner en 2.335.1 queda fuera de plazo el 24 de septiembre, así que cuando empiece la aplicación completa, el 25, ya no recibe jobs. La versión instalada la sacas en la propia máquina con ./config.sh --version.
El checklist para self-hosted, en orden:
- Actualiza el runner a la versión actual y deja activada la actualización automática. Si la tienes apagada, necesitas una cadencia manual de menos de 30 días.
- Rehaz las imágenes, plantillas de VM y contenedores con los que creas runners. El changelog pide rehacer los que salen de imágenes o plantillas viejas: si la imagen trae un runner anterior a 2.329.0, no se registra.
- Linux ARM32: migra a arm64. Node 24 no tiene soporte oficial ahí, el paquete del runner para
linux-armsolo trae el binario de Node 20 (externals.sh) y el código que hace fallar el paso ya está en el runner. - macOS 13.4 o anterior: actualiza el sistema operativo. GitHub declara Node 24 incompatible con esas versiones.
- Las actions que ya declaran
node24(comoactions/checkout@v5) piden como mínimo el runner v2.327.1. Si cumples el punto 1, esto ya lo tienes.
4. Retención, 1 de octubre: el historial se borra
Este cambio no rompe ningún build, pero borra datos. Según el changelog del 27 de agosto, desde el 1 de octubre de 2026 los checks, las ejecuciones de workflows y los statuses siguen el mismo ajuste de retención que ya controlaba los artefactos y los logs, 90 días por defecto. Hasta ahora se guardaban «400+ días» sin importar lo que configuraras.
- En repositorios públicos el máximo es 90 días.
- El cambio no es retroactivo: subir la retención después no recupera nada.
- Los metadatos de checks y ejecuciones no se cobran como almacenamiento; los artefactos y logs asociados sí.
Lo que se rompe es todo lo que lea ese historial más allá de tu periodo: métricas de despliegue calculadas desde gh run list, auditorías que enlazan a una ejecución concreta o reportes trimestrales. Exporta lo que necesites antes del 1 de octubre.
Lo que el changelog no dice: qué pasa con un pull request abierto más de 90 días cuyo check requerido caduca. No lo encontré documentado. Si tienes PRs de larga vida con protección de rama, revísalos después del 1 de octubre antes de dar nada por hecho.
5. Imágenes: macos-14 sí tiene fecha, ubuntu-22.04 todavía no
macos-14 (actions/runner-images#13518): la deprecación empezó el 6 de julio y el soporte termina el 2 de noviembre de 2026. Antes hay ocho apagones en los que los jobs fallan: 5, 12, 16, 19, 23, 26, 29 y 30 de octubre, cada uno de 14:00 UTC a 00:00 UTC del día siguiente. Afecta a macos-14, macos-14-large y macos-14-xlarge. El reemplazo es macos-15, macos-26 o macos-latest, que con la migración iniciada el 15 de junio pasó a apuntar a macOS 26.
ubuntu-22.04 (actions/runner-images#14254): el 17 de septiembre solo empieza la deprecación. El issue avisa de colas más largas en horas pico, no de fallos. El retiro es el 17 de abril de 2027, con apagones previos que el issue fecha en marzo y abril (sin año, pero caen antes del retiro). Afecta también a ubuntu-22.04-arm. El reemplazo es ubuntu-24.04, ubuntu-26.04 o ubuntu-latest.
Si alguien te dice que ubuntu-22.04 se rompe esta semana, no es cierto. Migra con calma, pero no lo dejes para marzo.
Lo que sale en el changelog y no rompe nada
cache-mode(10 de septiembre): una clave nueva para limitar el acceso a la caché por workflow o job. Es opcional: los workflows que no la declaran «siguen usando los valores seguros existentes».- La ruta separada de Code Quality (20 de agosto): solo te afecta si tienes reportes que filtran por
dynamic/github-code-scanning/codeqlo por el actorgithub-advanced-security. actions/checkoutypull_request_target(18 de junio): esto sí rompe, pero ya pasó. Desde el 20 de julio las etiquetas flotantes,@v4incluida, rechazan hacer checkout del código de un fork enpull_request_target. Si tu workflow dependía de eso, ya está en rojo. Es la misma familia de riesgo que conté en el gusano que se coló por el trusted publishing de npm.
Qué hago con mi CI
Nada urgente. actions/checkout@v4 y actions/setup-node@v4 corren en Node 24 desde junio, el workflow no usa la variable de exclusión, corre en ubuntu-latest (hoy ubuntu-24.04) y en runners de GitHub. El 23 de septiembre no le cambia nada.
Sí voy a subir las dos actions a su major actual, por dos razones que no tienen que ver con la fecha: la anotación amarilla entrena a ignorar las anotaciones, y una action que su autor ya no prueba en el runtime donde de verdad se ejecuta es deuda. Si además estás pensando en el Node de tu propio proyecto, no el de las actions, lo tengo contado en qué se rompe al actualizar a Node 26.
Preguntas frecuentes
¿Qué pasa con GitHub Actions el 23 de septiembre de 2026?
GitHub quita Node 20 de los runners. Desde el 16 de junio de 2026 los runners ya ejecutaban las actions con Node 24 por defecto, y la variable ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true permitía volver a Node 20. A partir del 23 de septiembre esa variable deja de funcionar y todas las actions de JavaScript corren en Node 24. Los runners self-hosted en Linux ARM32 pierden soporte, porque Node 24 no tiene soporte oficial para esa plataforma.
¿Falla una action que declara using: node20 después del 23 de septiembre?
No por declararlo. El código del runner (HandlerFactory.cs y NodeUtil.cs en actions/runner) reescribe node12 y node16 a node20, y node20 a node24; en la fase final ignora las variables de entorno y siempre elige Node 24, sin devolver error. La action falla si su código usa algo que Node 24 eliminó, como tls.createSecurePair o fs.truncate con un descriptor, o si corre en un runner self-hosted Linux ARM32 o con macOS 13.4 o anterior. En los logs aparece la anotación: The following actions target Node.js 20 but are being forced to run on Node.js 24.
¿Tengo que actualizar actions/checkout@v4 y actions/setup-node@v4?
Conviene, pero no hay fecha que lo obligue. Las etiquetas v4 de las dos declaran node20 y las v5 en adelante declaran node24. En runners de GitHub, las v4 ya corren en Node 24 desde junio y siguen funcionando. Al subir setup-node, revisa las notas de versión: la v5 activó caché automática cuando package.json tiene packageManager y la v6 la limitó a npm.
¿Cómo encuentro en mi organización los workflows afectados?
Con la búsqueda de código de GitHub: gh search code --owner TU_ORG con ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION, con ubuntu-22.04 path:.github/workflows, con macos-14 path:.github/workflows, y con node20 --filename action.yml para tus actions propias. La API solo busca en la rama por defecto y permite 10 peticiones por minuto. Las etiquetas que llegan por variables o expresiones no aparecen en ninguna búsqueda: para esas, revisa la línea Image: del paso Set up job en el log.
¿Se rompe ubuntu-22.04 el 17 de septiembre de 2026?
No. El 17 de septiembre solo empieza la deprecación y el aviso habla de colas más largas en horas pico. Los jobs empiezan a fallar en los apagones que el issue 14254 de actions/runner-images programa para marzo y abril, antes de que la imagen se retire el 17 de abril de 2027. La que sí tiene fecha este otoño es macos-14: apagones en octubre y retiro el 2 de noviembre de 2026.
¿Qué tienen que hacer los runners self-hosted antes del 25 de septiembre?
Actualizar el runner. Desde el 25 de septiembre de 2026 GitHub exige la versión 2.329.0 para registrar un runner y que se instale cada versión nueva en menos de 30 días para seguir recibiendo jobs. Hay apagones previos el 14, 16 y 18 de septiembre, de 11:00 a 15:00 hora del Este. El endpoint GET /repos/OWNER/REPO/actions/runners/deprecations/VERSION devuelve cuándo deja de ejecutar tu versión. Rehaz también las imágenes con las que creas runners.
¿Qué cambia con la retención de GitHub Actions el 1 de octubre de 2026?
Los checks, las ejecuciones de workflows y los statuses pasan a seguir el ajuste de retención de artefactos y logs, que por defecto es de 90 días; antes se guardaban más de 400 días. En repositorios públicos el máximo es 90 días y el cambio no es retroactivo. Si tienes métricas o auditorías que leen ejecuciones antiguas, exporta los datos antes de esa fecha.