Przejdź do treści
Utrzymanie i przekazanie

Node 20 wypadł z runnerów GitHuba i z Vercela. Projekt oddany rok temu dzisiaj nie przechodzi deploya

Node 20 zniknął z runnerów GitHuba 23 września, a z Vercela 1 października. Co sprawdzić w oddanym projekcie i co zamrozić przy przekazaniu.

Redakcjadeveloper front-end10 minkod + tabela
20liczba z tezy tekstu

Repozytorium, które rok temu przechodziło deploy w 90 sekund, dzisiaj zatrzymuje się na pierwszym kroku workflow – bez jednej nowej linii kodu. Zmieniło się otoczenie, nie projekt: 23 września 2026 runnery GitHub Actions straciły Node 20, a 1 października ten sam runtime zniknął z buildów i funkcji na Vercelu.

Klient nie zgłosi tego jako awarii, bo strona odpowiada dalej – raz wdrożone funkcje obsługują ruch bez zmian. Zgłosi literówkę w cenniku, której nagle nie da się wypchnąć na produkcję. Wtedy okazuje się, że osoba, która składała tę konfigurację, zamknęła zlecenie jedenaście miesięcy wcześniej, a w repo nie ma nic, co by mówiło, na czym to właściwie stoi.

Cztery daty z ostatniego półrocza, które unieważniły konfigurację sprzed roku

Kalendarz jest publiczny i nie wymaga zgadywania – leży w pliku schedule.json w repozytorium nodejs/Release. Wsparcie Node 20 skończyło się 30 kwietnia 2026. Node 22 jest w fazie maintenance od 21 października 2025 i kończy się 30 kwietnia 2027. Node 24 wchodzi w maintenance 20 października 2026, a 28 października 2026 linią LTS zostaje Node 26.

Data Zdarzenie Co przestaje działać
30.04.2026 koniec wsparcia Node 20 brak łatek bezpieczeństwa dla runtime’u; pierwsze pytanie w audycie klienta
16.06.2026 runnery GitHuba domyślnie uruchamiają akcje na Node 24 akcje przypięte do starych tagów zaczynają wypisywać ostrzeżenia
23.09.2026 Node 20 usunięty z runnerów obejście przez zmienną środowiskową przestaje cokolwiek dawać; workflow pada
01.10.2026 Vercel wyłącza Node 20 dla buildów i funkcji nowy deploy kończy się błędem, choć poprzedni wciąż działa

Daty z dwóch ostatnich wierszy mają konsekwencje, które warto rozpisać osobno, bo różnią się charakterem. GitHub opisał przejście w changelogu o wycofaniu Node 20 z runnerów: od 16 czerwca 2026 akcje JavaScriptowe idą domyślnie na Node 24, a kto potrzebował czasu, ustawiał ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. To obejście działało wyłącznie do usunięcia Node 20 z obrazu runnera. Przy okazji wypadły dwie rzeczy, o których łatwo zapomnieć przy starszych projektach: Node 24 nie współpracuje z macOS 13.4 i niższymi oraz nie ma oficjalnego wsparcia dla ARM32, więc self-hosted runner na ARM32 kończy się razem z Node 20.

Vercel rozegrał to inaczej i dla utrzymania jest to wariant bardziej podstępny. Zgodnie z komunikatem o deprecjacji Node.js 20 istniejące wdrożenia z funkcjami serverless działają dalej, a blokada dotyczy wyłącznie nowych deployów: Node 20 zostaje wyłączony w ustawieniach projektu, a projekt, który go używa, pokazuje błąd przy próbie wdrożenia. Projekt może więc stać w pełni sprawny przez miesiące i pęknąć dokładnie w momencie, w którym ktoś poprosi o drobną zmianę – czyli wtedy, gdy nikt nie ma budżetu na migrację runtime’u.

Projekt składany z modelem startuje z długiem, którego nie widać w dniu odbioru

Model proponuje wersje bibliotek, które dominowały w jego danych treningowych, a nie te, które są aktualne w dniu generowania kodu. Skala tego zjawiska jest zmierzona. W badaniu dziesięciu modeli na benchmarku tysiąca zadań ze Stack Overflow od 36,70 do 55,70 proc. zadań dostało wersję biblioteki z co najmniej jednym znanym CVE, przy czym w 72,27-91,37 proc. przypadków luka była ujawniona publicznie jeszcze przed datą odcięcia wiedzy modelu. Osobny przegląd stu wygenerowanych plików pokazał, że 84 proc. z nich zawiera co najmniej jeden błąd dotyczący bibliotek – w tej liczbie mieszczą się błędne ścieżki importu, brakujące importy, nieistniejące paczki, użycie przestarzałych API i importy nieużywane.

Dla odbioru projektu wniosek jest prosty: repozytorium może być oddane jako nowe i jednocześnie być starsze od kalendarza. Nic tego nie wyłapie w dniu przekazania, bo wszystko się buduje i wszystkie testy przechodzą. Pierwszy sygnał przychodzi razem z wycofaniem runtime’u albo paczki, czyli pół roku do roku później, gdy zlecenie jest zamknięte i rozliczone.

Ile tego długu jest w konkretnym repozytorium, nie wynika z żadnego badania – wynika z npm outdated i npm audit uruchomionych na tym lockfile. Liczby z prac naukowych mówią tylko tyle, że sprawdzenie ma sens w każdym projekcie, w którym część kodu powstała z modelem, a nie że akurat ten projekt jest zepsuty.

Numer wersji siedzi w pięciu miejscach i rozjeżdża się po cichu

Deklaracja wersji w projekcie frontendowym bywa rozsypana na kilka plików, z których każdy obsługuje inny etap. Model dopisuje zwykle jeden z nich – ten, o który został poproszony. Pozostałe zostają z wartościami domyślnymi albo nie powstają wcale.

  • .nvmrc – wersja dla człowieka na lokalnej maszynie, pomijana przez CI, jeśli workflow nie każe jej czytać.
  • engines w package.json – jedyne miejsce, które czyta zarówno npm, jak i Vercel, i jednocześnie najczęściej puste.
  • setup-node w workflow – decyduje o wersji w CI, często przypięty do konkretnego numeru bez związku z pozostałymi plikami.
  • runtime funkcji w panelu platformy – ustawienie poza repozytorium, więc niewidoczne w przeglądzie kodu.
  • Dockerfile albo obraz bazowy, jeśli projekt nie idzie przez build platformy.
// package.json - jedyny plik, który widzi i npm, i platforma hostingowa
{
  "engines": { "node": "24.x" },
  "packageManager": "npm@12.2.0"
}

# .nvmrc - ta sama wartość, inny odbiorca
24

# .github/workflows/build.yml - CI czyta .nvmrc, zamiast dublować numer
      - uses: actions/setup-node@v7
        with:
          node-version-file: '.nvmrc'
          cache: 'npm'

Układ z node-version-file ma jedną zaletę, której nie da wpisanie numeru w trzech miejscach: podniesienie wersji to zmiana jednej linii i jeden commit, który przechodzi przez CI przed wdrożeniem. Dopóki numery są rozsypane, każda migracja wymaga znalezienia wszystkich kopii, a to jest dokładnie ta praca, której nikt nie wycenia i nikt nie robi.

Dziesięć minut pomiaru: czy ten projekt dożyje następnego deploya

Sprawdzenie da się zrobić bez dostępu do dokumentacji i bez rozmowy z poprzednim wykonawcą. Potrzebny jest klon repozytorium i konto, na którym stoi hosting.

# 1. Co projekt deklaruje i czy deklaracje są zgodne
cat .nvmrc 2>/dev/null; node -p "require('./package.json').engines"

# 2. Czy wersja w ogóle jest jeszcze wspierana (dane z endoflife.date)
curl -s https://endoflife.date/api/v1/products/nodejs/ | jq '.result.releases[] | {name, eol: .eolFrom, lts: .ltsFrom}'

# 3. Jakie akcje uruchamia CI - stare tagi to kandydaci do wycofania
grep -rhoE "uses: [^ ]+" .github/workflows | sort -u

# 4. Dług w zależnościach, bez devDependencies, które nie jadą na produkcję
npm outdated || true
npm audit --omit=dev

# 5. Projekty na Vercelu wymagające podniesienia runtime'u
npm i -g vercel@latest && vercel project ls --update-required

Ostatnia komenda pochodzi wprost z komunikatu Vercela i jest najszybszym sposobem na przejrzenie całego portfela projektów, a nie jednego repozytorium. Jeśli obsługujesz kilkanaście stron, to ona odpowiada na pytanie, które z nich pękną przy pierwszej prośbie o zmianę.

Trzy wyniki warto zinterpretować ostrożnie. npm audit na projekcie frontendowym zwraca zwykle listę ostrzeżeń z łańcuchów narzędzi budujących, które nie trafiają do przeglądarki – to nie jest lista rzeczy do naprawy, tylko materiał do segregacji. Brak .nvmrc nie oznacza błędu, jeśli wersja siedzi w engines. Z kolei zgodność wszystkich plików na wersji, która wypadła z kalendarza, jest gorsza niż rozjechane deklaracje – oznacza, że projekt ma spójnie zapisane coś, czego już nie ma na runnerach.

Paczka przekazania, która przenosi ten koszt świadomie, a nie przypadkiem

Przy oddawaniu projektu obowiązuje jedna zasada praktyczna: nie da się zagwarantować, że build zadziała za rok, ale da się zagwarantować, że dzień, w którym przestanie działać, będzie znany z góry i że wiadomo, kto wtedy dzwoni do kogo. Minimum, które to realizuje, to pięć elementów. Każdy z nich jest procedurą do wykonania przez kogoś innego niż autor, więc obowiązują go te same reguły, co opis procesu pisany pod wykonanie przez inną osobę: konkretny wyzwalacz, konkretna osoba, konkretny plik.

  • Jedno źródło wersji w repozytorium – engines plus .nvmrc, a CI czyta plik, nie numer wpisany w workflow.
  • Sekcja w README z datami końca wsparcia dla runtime’u, frameworka i akcji CI, przepisanymi z oficjalnych harmonogramów, nie z pamięci.
  • Zacommitowana konfiguracja bota aktualizacji zależności, z pierwszym uruchomieniem przed odbiorem – żeby klient zobaczył, jak te pull requesty wyglądają, zanim zostanie z nimi sam.
  • Zaplanowany build raz w miesiącu bez zmian w kodzie. To najtańszy czujnik: pierwszy miesiąc, w którym pada bez commita, to miesiąc, w którym wypadło coś w otoczeniu.
  • Wskazana osoba po stronie klienta i zapis, co się stanie, gdy czujnik zapali się po zakończeniu umowy.

Czwarty punkt jest tym, który zwraca się najszybciej, bo zamienia awarię odkrywaną przy pilnej zmianie na powiadomienie w spokojnym tygodniu. Miesięczny harmonogram wystarcza – wycofania runtime’ów są ogłaszane z wyprzedzeniem liczonym w miesiącach, nie w dniach. Techniczna część tej listy kończy się tam, gdzie zaczyna się ustalanie zakresu oferty i stawki za utrzymanie – a to jest praca nad własną firmą, nie nad projektem klienta, i w jednoosobowej działalności zwykle nie robi jej nikt. Freelancerzy układają ją na przykład w mentoringu ULTRA, który obejmuje strategię, finanse i klientów.

Co wpisać do umowy utrzymania, żeby podniesienie runtime’u nie było Twoim kosztem

Większość umów utrzymaniowych opisuje usuwanie usterek i drobne zmiany treści. Podniesienie wersji Node, migracja akcji CI i wymiana porzuconej paczki nie są usterką – projekt działał poprawnie, zmienił się świat wokół niego. Jeśli umowa tego nie rozdziela, ta praca wpada do ryczałtu i zjada marżę w miesiącu, w którym zdarzą się dwie takie migracje naraz.

Działa rozdzielenie na trzy pozycje: usuwanie usterek, zmiany na życzenie i utrzymanie techniczne otoczenia z nazwanym wyzwalaczem. Wyzwalaczem jest data z publicznego harmonogramu, czyli coś, czego żadna ze stron nie ustala w negocjacjach. Zapis brzmi wtedy nudno i dlatego się broni: na trzydzieści dni przed końcem wsparcia używanej wersji wykonawca przedstawia zakres migracji i wycenę, a klient decyduje. Przy odmowie projekt zostaje na wersji bez łatek, co też jest decyzją klienta – tylko podjętą świadomie i na piśmie.

Druga rzecz, którą warto rozstrzygnąć przed podpisaniem: kto płaci za migrację wymuszoną przez platformę, a nie przez upływ czasu. Vercel zablokował nowe deploye na Node 20 pięć miesięcy po końcu wsparcia tej wersji, GitHub usunął go z runnerów niespełna pięć miesięcy po tej samej dacie. To nie są terminy, które wykonawca kontroluje, a jednocześnie bez tej migracji żadna zamówiona zmiana nie trafi na produkcję.

Decyzja na ten tydzień: jedna lista i jedna data w kalendarzu

Jeśli masz pod opieką więcej niż dwa oddane projekty, najtańszym krokiem jest zestawienie: projekt, zadeklarowana wersja Node, data końca wsparcia, kto jest właścicielem migracji. Powstaje w godzinę, głównie z komend z sekcji o pomiarze, i od razu rozdziela projekty na te do zaplanowania i te do zostawienia w spokoju.

Druga decyzja dotyczy kalendarza. 28 października 2026 linią LTS zostaje Node 26, a Node 24 od 20 października jest w maintenance – to dobry moment, żeby wpisać sobie przegląd portfela na jedną konkretną datę w roku, zamiast reagować na błąd deploya zgłoszony przez klienta, który chciał poprawić cenę w jednym miejscu. Jedna data w roku wystarcza, pod warunkiem że ma swoje miejsce w stałym rytmie przeglądów – przegląd bez terminu w kalendarzu nie odbywa się wcale.