Projekt po wykonawcy nie buduje się na czystej maszynie: co odtworzyć i w jakiej kolejności
Przejmujesz stronę po innym wykonawcy: npm ci, wersja Node, sekrety, audyt zależności i test smoke. Sprawdzenia do zrobienia przed wyceną utrzymania.
Klient przysyła dostęp do repozytorium strony, którą przez pół roku budował ktoś inny z modelem w edytorze. Pierwsza komenda, npm ci, kończy się błędem o braku package-lock.json. Druga próba, npm install, przechodzi bez protestu i instaluje zestaw paczek, którego nikt nigdy nie uruchomił na produkcji, bo zakresy wersji z package.json rozwinęły się przez rok do nowszych wydań.
W tym momencie rozstrzyga się, czy utrzymanie tego projektu będzie kosztowało dwie godziny miesięcznie, czy dwadzieścia. Poniżej kolejność sprawdzeń, którą da się wykonać w jeden wieczór — zanim ktokolwiek poda stawkę.
Zanim odtworzysz build: ustal, co jest źródłem prawdy
Repozytorium bywa kopią, nie oryginałem. Projekty prowadzone szybko, z generowaniem dużych fragmentów kodu, częściej niż inne kończą z poprawką wklejoną bezpośrednio w edytorze plików na hostingu — bo tak było o dziesięć minut szybciej. Ta poprawka nie istnieje w gicie i zniknie przy pierwszym wdrożeniu, które zrobisz.
Dwa polecenia dają pierwszy sygnał:
git log -1 --format=%cI
curl -sI https://przyklad.pl/assets/app.js | grep -i last-modified
Jeśli plik na produkcji jest nowszy niż ostatni commit, ktoś edytował go poza repozytorium. To sygnał, nie dowód: CDN i cache potrafią przestawić nagłówek Last-Modified na datę wdrożenia albo usunąć go w całości. Rozstrzyga porównanie zawartości — pobierz zbudowany plik z produkcji, zbuduj projekt lokalnie z tego samego commita i porównaj sumy kontrolne. Rozbieżność przy deterministycznym buildzie oznacza, że produkcja i repozytorium to dwie różne wersje strony.
npm ci na czystym kontenerze, nie npm install u siebie
Dokumentacja npm opisuje npm ci jednoznacznie: projekt musi mieć package-lock.json albo npm-shrinkwrap.json, a jeśli zawartość pliku blokady nie zgadza się z package.json, komenda kończy się błędem zamiast aktualizować blokadę. Istniejący katalog node_modules jest przed instalacją usuwany, a sama instalacja nigdy nie zapisuje niczego do package.json ani do pliku blokady (dokumentacja npm CLI).
Z punktu widzenia przejmowania projektu to jest test zero-jedynkowy. Powodzenie oznacza, że da się odtworzyć dokładnie ten zestaw zależności, na którym projekt działał. Niepowodzenie oznacza, że nie ma czegoś takiego jak „te same paczki co u poprzednika” — i to jest pierwsza pozycja do wyceny, jeszcze przed jakąkolwiek zmianą w kodzie.
Sprawdzenie ma sens tylko poza własnym środowiskiem, w którym stoją globalne narzędzia i cache sprzed lat:
docker run --rm -v "$PWD":/app -w /app node:24-bookworm \
bash -lc "time npm ci && npm run build"
Zanotuj czas, który wypisze time. To liczba, którą później wstawisz do umowy jako czas odtworzenia środowiska od zera, i to samo pytanie zadasz sobie po każdej większej aktualizacji zależności.
Wersja Node: .nvmrc mówi, engines nie wymusza
Pole engines w package.json jest z definicji doradcze. Dokumentacja npm stwierdza wprost, że dopóki nie ustawiono flagi engine-strict, pole to generuje wyłącznie ostrzeżenia. Deklaracja "node": ">=18" nie powstrzyma nikogo przed zbudowaniem projektu na wersji, na której ten kod nigdy nie działał.
Ustawienie, które zamienia ostrzeżenie w błąd, to jedna linia w .npmrc obok zawężonego zakresu:
# .npmrc
engine-strict=true
// package.json
"engines": { "node": ">=22 <25" }
Druga sprawa to wsparcie samego runtime’u. Według harmonogramu wydań Node.js linia v20 zakończyła wsparcie 30 kwietnia 2026, v22 weszła w tryb utrzymaniowy 21 października 2025 i kończy się 30 kwietnia 2027, a v24 jest aktywnym wydaniem LTS do 20 października 2026 i ma wsparcie do 30 kwietnia 2028 (harmonogram nodejs/Release). Projekt, który uruchamia się wyłącznie na v20, nie jest kwestią gustu — jest zobowiązaniem z datą. Wpisz tę datę do oferty utrzymania zamiast odkrywać ją za pół roku razem z klientem.
Zmienne środowiskowe: listę sekretów wyprowadź z kodu, nie z pamięci
Plik .env.example w projekcie przekazanym w pośpiechu opisuje stan sprzed kilku miesięcy. Pełną listę ma tylko kod:
grep -rhoE "process\.env\.[A-Z0-9_]+" src | sort -u
grep -rhoE "import\.meta\.env\.[A-Z0-9_]+" src | sort -u
Różnica między tą listą a .env.example to zbiór zmiennych, o których nie wie nikt poza autorem kodu. Każda z nich zatrzyma wdrożenie w losowym momencie, zwykle na produkcji, bo lokalnie brakująca wartość często wpada w cichy fallback.
Przy okazji sprawdź prefiksy. Klucze zaczynające się od VITE_ lub NEXT_PUBLIC_ trafiają do paczki wysyłanej do przeglądarki — jeśli w tej grupie znajdzie się token API, jest publiczny od dnia wdrożenia. Reakcja na taki przypadek ma własną kolejność kroków, opisaną w tekście o kluczu API znalezionym w bundlu, i rotacja klucza jest w niej pierwsza, nie ostatnia.
Audyt zależności: co znaczy npm audit na projekcie sprzed roku
Surowe npm audit na przejętym projekcie wypisze listę, której długość przestraszy klienta i nie powie nic o ryzyku. Flaga --audit-level przyjmuje wartości info, low, moderate, high, critical i none, przy czym — jak zaznacza dokumentacja — nie filtruje raportu, tylko przesuwa próg, przy którym komenda kończy się kodem różnym od zera. Flaga --omit=dev pomija zależności deweloperskie, które nie trafiają na produkcję (dokumentacja npm audit).
npm audit --omit=dev --audit-level=high
echo "kod wyjscia: $?"
To jest wersja, którą można uzgodnić z klientem jako warunek wdrożenia, bo odnosi się do kodu realnie serwowanego użytkownikom. Osobna uwaga dotyczy naprawy: npm audit fix --force ma dokumentowane prawo instalować pakiety spoza zadeklarowanych zakresów, łącznie ze zmianami łamiącymi zgodność w rozumieniu SemVer. Na projekcie przejętym wczoraj, bez jednego działającego testu, ta komenda zamienia znane ryzyko w nieznane.
Jeden test smoke, zanim cokolwiek zaktualizujesz
Przejęty projekt prawie nigdy nie ma testów, a rozmowa o pokryciu na tym etapie jest stratą czasu. Potrzebny jest jeden scenariusz, który przechodzi przez ścieżkę zarabiającą pieniądze — najczęściej formularz kontaktowy albo koszyk — i który uruchamia się jedną komendą.
Playwright w środowisku CI sprowadza się do trzech kroków: npm ci, npx playwright install --with-deps, npx playwright test (dokumentacja Playwright). Sam scenariusz może mieć kilkanaście linii:
import { test, expect } from '@playwright/test';
test('strona główna prowadzi do formularza kontaktu', async ({ page }) => {
const odpowiedz = await page.goto('/');
expect(odpowiedz?.status()).toBe(200);
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
await page.getByRole('link', { name: /kontakt/i }).click();
await expect(page.getByRole('button', { name: /wyślij/i })).toBeVisible();
});
Ten test nie sprawdza poprawności aplikacji. Jest czujnikiem alarmowym przy aktualizacjach: gdy po podbiciu zależności przestaje przechodzić, wiadomo w ciągu minuty, że coś się zepsuło, i wiadomo co. Bez niego każda aktualizacja jest zakładem o to, że klient nie zajrzy na własną stronę przed poniedziałkiem.
Pakiet przekazania: co ma być w repozytorium, zanim podpiszesz utrzymanie
Poniższa tabela dzieli elementy przekazania na dwa poziomy. Kolumna „minimum” opisuje stan, przy którym projekt da się utrzymywać bez zgadywania. Kolumna „komplet” to stan, przy którym utrzymanie da się przekazać dalej innej osobie — a to jest inny, wyższy próg, bo opis wdrożenia musi być wykonalny dla kogoś, kto nie brał udziału w projekcie, nie tylko zrozumiały dla autora.
| Element | Minimum | Komplet | Konsekwencja braku |
|---|---|---|---|
| Instalacja zależności | plik blokady w repozytorium | blokada plus build odtworzony w CI | instalacja daje inny zestaw paczek niż produkcja |
| Wersja runtime | plik .nvmrc |
engines z engine-strict=true |
build działa u jednej osoby w zespole |
| Sekrety | .env.example z pełną listą kluczy |
lista z właścicielem i miejscem przechowywania każdego klucza | wdrożenie zatrzymuje się na brakującej zmiennej |
| Wdrożenie | opisana jedna komenda | pipeline w repozytorium, uruchamiany z commita | wdrożenie zna wyłącznie osoba, która odeszła |
| Testy | jeden scenariusz smoke | smoke plus ścieżki płatności i formularzy | każda aktualizacja zależności jest loterią |
| Dane | dostęp do bazy i kopia zapasowa | kopia odtworzona na środowisku zapasowym | kopia istnieje, ale nikt jej nie odtworzył |
| Dostępy | lista kont z rolami | potwierdzone przeniesienie własności domeny i DNS | domena wygasa na koncie byłego wykonawcy |
Kolejność wypełniania tej tabeli nie jest dowolna. Rozsądna wygląda tak:
- Odtworzenie instalacji i buildu na czystym kontenerze — bez tego pozostałe punkty nie mają jak zostać sprawdzone.
- Ustalenie wersji runtime’u i zapisanie jej w repozytorium.
- Kompletna lista zmiennych środowiskowych wyprowadzona z kodu.
- Jeden test smoke, uruchamiany jedną komendą.
- Audyt zależności produkcyjnych z ustalonym progiem.
- Przeniesienie dostępów, domeny i DNS na konto klienta.
Co wpisać do oferty, zanim podasz stawkę miesięczną
Wynik powyższych sprawdzeń rozkłada się na trzy przypadki i każdy z nich ma inną cenę. Pierwszy: npm ci i build przechodzą na czystym kontenerze, runtime jest wspierany, zmienne opisane. To jest zwykłe utrzymanie, stawka miesięczna wystarczy.
Drugi: build daje się odtworzyć, ale brakuje testów, opisu wdrożenia albo połowy zmiennych. Odtworzenie tych rzeczy to praca jednorazowa, z własnym zakresem i własną fakturą — nie da się jej rozpuścić w abonamencie, bo zajmie kilkanaście godzin w pierwszym miesiącu i zero w kolejnych.
Trzeci: brak pliku blokady, nieznana zawartość produkcji, runtime po zakończeniu wsparcia. To nie jest utrzymanie strony, tylko odbudowa ścieżki wdrożenia dla kodu, którego nikt nie zna. Można się tego podjąć, ale trzeba to nazwać po imieniu w ofercie, zanim klient zapamięta pierwszą wymienioną kwotę jako cenę całości.
Decyzja do podjęcia po lekturze jest jedna: nie podawaj stawki utrzymania, dopóki nie zobaczysz wyniku npm ci i npm run build na maszynie, na której nigdy wcześniej nie stał ten projekt. Ta jedna komenda w kontenerze kosztuje kwadrans i decyduje o tym, po której stronie tego podziału jesteś.