Bliver din JavaScript-CI også sur over noget, der virker lokalt?
“Men… den kører jo fint på min maskine?”
Jeg har sagt den sætning alt for mange gange, mens GitHub Actions-workflowet lyser rødt som et vredt stoplys. Det er næsten et ritual, første gang man sætter CI op til et JavaScript-projekt.
I den her artikel bygger vi et minimalt, men brugbart CI-setup med GitHub Actions til et typisk Node/JavaScript-projekt: lint, test og build. Undervejs peger jeg på de klassiske faldgruber, som gør at noget “virker lokalt” men fejler i CI.
1. Hvad skal din JavaScript-CI egentlig gøre?
CI (Continuous Integration) i GitHub Actions handler ikke om at imponere nogen med avancerede YAML-filer. Formålet er ret lavpraktisk:
- Tjek at koden kan installeres fra et rent miljø
- Køre lint, så åbenlyse fejl bliver fanget tidligt
- Køre tests, så du ikke kun stoler på din mavefornemmelse
- Bygge projektet, hvis der er et build-step
Det CI ikke skal i første omgang, er at deploye til 4 miljøer, generere 12 rapporter og køre integrationstests mod 3 eksterne API’er. Du kan komme dertil senere.
Hvis du ikke har (mindst) disse kommandoer i dit projekt, er du ikke klar til at få en fornuftig CI op at køre:
{
"scripts": {
"lint": "eslint .",
"test": "vitest run", // eller jest / mocha / node test
"build": "vite build" // eller next build / webpack / parcel
}
}
Har du kun én af dem, så start dér. En CI, der blot kører tests, er stadig meget bedre end ingen CI. Hvis du vil have lidt mere kontekst på hele CI/CD-ideologien, har jeg en mere generel intro i en anden artikel om CI/CD for små projekter.
2. Dit minimum GitHub Actions workflow til JavaScript
Her er en minimal, men fornuftig GitHub Actions workflow-fil til et Node/JavaScript-projekt med lint, test og build:
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm test
- name: Build
run: npm run build
Det her kan du næsten copy/paste direkte ind i .github/workflows/ci.yml. Men lad os lige pille det fra hinanden.
2.1 Hvad sker der faktisk i workflowet?
- on: kører ved push og pull request mod
main. Du kan tilpasse det til dine branches. - runs-on: GitHub stiller en Linux-maskine til rådighed.
- strategy.matrix: gør det muligt at køre med flere Node-versioner. Her nøjes vi med 18.x.
- setup-node: sikrer en specifik Node-version og aktiverer npm-cache.
- npm ci: installerer afhængigheder ud fra din lockfile (kommer vi tilbage til).
- Lint / Test / Build: kører dine scripts fra
package.json.
Hvis du vil gå dybere ned i Actions generelt, er GitHubs egen docs faktisk ok: GitHub Actions dokumentation. Men for et almindeligt Node-projekt er ovenstående nok til at komme i gang.
2.2 Hvorfor npm ci og ikke npm install?
I et CI-miljø vil du have præcis de versioner af dine afhængigheder, som din lockfile beskriver. Ikke “noget der ligner”.
npm ci gør blandt andet:
- Bruger
package-lock.json100 % som sandhed - Fejler, hvis lockfile og
package.jsonikke matcher - Er ofte hurtigere, fordi den ikke skal regne et nyt dependency tree ud
npm install kan finde på at opdatere din lockfile eller hente lidt andre versionskombinationer, end du lige havde troet. Det kan være fint lokalt, men i CI vil du have deterministisk adfærd.
Pointen: Hvis din CI fejler på npm ci, men det virker med npm install, så er problemet ikke CI. Problemet er, at dit projekt ikke er konsistent. Og det er godt, at CI råber op.
3. Node-version: få CI til at ligne din maskine
En meget klassisk “virker lokalt, fejler i CI”-situation skyldes, at du og GitHub Actions bruger forskellige Node-versioner.
Tre måder at styre Node-version på:
.nvmrc(hvis du bruger nvm lokalt).node-version(bruges af flere værktøjer)enginesipackage.json
Hvis du har en .nvmrc-fil:
18.19.0
Så kan du få GitHub Actions til at læse den:
- name: Use Node.js from .nvmrc
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
Det gør dit liv lettere, fordi du kun skal opdatere Node-version ét sted. Sørg for, at din lokale version matcher den i CI. Tjek med:
node -v
Hvis der står 20.x lokalt og 18.x i CI, må du ikke være overrasket, hvis ting opfører sig anderledes.
4. Cache af dependencies: hurtigt, men lidt snedigt
GitHub Actions understøtter caching af npm dependencies via setup-node, hvilket cache: 'npm' i eksemplet.
Grundideen er:
- Første run: dependencies hentes fra npm og bliver gemt i en cache
- Senere run med samme lockfile: dependencies bliver hentet fra cachen i stedet
Typisk behøver du ikke selv rode med cache keys. setup-node klarer det ud fra din lockfile.
Men: Hvis din CI pludselig opfører sig mærkeligt efter dependency-ændringer, kan det være cachen.
Lille debug-trick: slå cache fra midlertidigt og se, om fejlen forsvinder:
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 18
# cache: 'npm' <-- midlertidigt kommenteret ud
Hvis fejlen forsvinder uden cache, var det nok en halvkorrupt cache eller en underlig kombination af versioner. Ryd cachen ved at ændre lockfile eller cache-key, og slå cache til igen, når det spiller.
Jeg har en anden artikel om, hvordan cachen lyver oftere end din kode, hvis du vil nørde det aspekt.
5. Typiske CI-fejl i JavaScript-projekter og hvordan du fikser dem
Nu til det sjove. Her er nogle klassiske fejlmønstre, set fra virkelige projekter. Formatet er:
- Symptom: hvad du ser i CI-loggen
- Sandsynlig årsag: det, der typisk ligger bag
- Fix: hvad du konkret skal ændre
5.1 “Cannot find module” i test- eller build-step
Symptom:
Error: Cannot find module 'some-package'
Sandsynlig årsag:
- Pakken ligger kun i
devDependencies, men bruges i runtime-kode - Du har glemt at køre build-step, før du kører tests, der forventer byggede filer
- Du har ændret import-stier, men ikke opdateret tests eller build
Fix:
- Tjek om modulet bliver brugt i kode, der kører i produktion. Hvis ja, skal det i
dependencies. - Hvis dine tests kører mod build-output (f.eks.
dist/-mapper), så tilføj build-step før tests i workflowet:
- name: Build
run: npm run build
- name: Test
run: npm test
Typisk fejl: lokalt har du en gammel dist/-mappe liggende fra tidligere builds, som dine tests (ubevidst) bruger. I CI er mappen tom, fordi det er et helt friskt miljø.
5.2 Tests der hænger i CI men ikke lokalt
Symptom: CI-jobbet rammer en timeout. I loggen står den og hænger ved “Test”-steppet i lang tid.
Sandsynlig årsag:
- Tests, som aldrig lukker en server, database connection eller browser
- Front-end tests, der ikke kører i headless-mode
- Tests, der afhænger af interaktive ting (f.eks. prompt) eller langvarige timeouts
Fix:
- Sørg for at rydde op i ressourcer i
afterAll/afterEachi Jest/Vitest. - Brug headless browser i CI, f.eks.
--runInBandeller specifik flag til Playwright/Cypress. - Tilføj en generel timeout i din test-runner-konfiguration.
Hvis du kører Jest:
{
"jest": {
"testTimeout": 10000
}
}
Og husk: kør test-kommandoen lokalt i et “rent” miljø. Eksempel:
rm -rf node_modules
npm ci
npm test
Hvis den hænger dér, er problemet i dine tests, ikke i CI.
5.3 “Flaky” tests: nogle gange grøn, nogle gange rød
Symptom: Du rerunner det samme workflow og nogle gange fejler 1-2 tests tilfældigt.
Sandsynlig årsag:
- Tests der afhænger af tid (f.eks.
setTimeoutuden ordentlig mocking) - Tests der deler global state og bliver kørt parallelt
- Afhængighed af eksterne API’er eller netværk
Fix:
- Mock tid (f.eks. med
vi.useFakeTimers()i Vitest ellerjest.useFakeTimers()i Jest). - Kør tests seriøst hvis du mistænker race conditions:
{
"scripts": {
"test": "jest --runInBand"
}
}
- Stub eksterne API-kald i stedet for at ramme nettet.
Det føles irriterende at “downgrade” til seriøs testkørsel, men for mange små projekter er det bedre med stabile tests end en marginal tidsbesparelse.
5.4 CI fejler på lint, men du får ingen fejl lokalt
Symptom:
npm run lint exited with code 1
Men når du kører npm run lint lokalt, er alt fint.
Sandsynlig årsag:
- Du har forskellige versioner af ESLint eller plugins lokalt og i CI
- Din editor fixer ting automatisk (prettier / ESLint on save), men du har ikke committet ændringerne
Fix:
- Sørg for, at ESLint er installeret lokalt i projektet, ikke globalt.
- Tjek din lockfile ind (
package-lock.jsonellerpnpm-lock.yaml). - Kør
npm cilokalt for at matche CI-miljøet bedre.
Hvis du vil have lint til at være mere stabil, kan du også gemme dine ESLint- og formatter-regler i repoet og ikke i editorens personlige settings. Der er en fin intro til ESLint opsætning i vores artikel om ESLint i dit første JavaScript-projekt.
6. Artifacts og test reports uden at bygge et helt enterprise-setup
På et tidspunkt vil du måske gerne gemme dit build-output eller dine test-rapporter direkte fra CI. Det behøver ikke være kompliceret.
6.1 Gem dit build som artifact
Hvis du bygger en frontend-app, kan du gemme dist/ mappen som artifact:
- name: Build
run: npm run build
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: app-build
path: dist/
Så kan du hente filerne direkte fra GitHub UI’et for at teste noget manuelt eller give det videre til et andet job.
6.2 Gem test-rapport i JUnit-format
Mange test-runners kan spytte JUnit-XML ud, som CI-værktøjer forstår. Eksempel med Jest:
npm install --save-dev jest-junit
{
"scripts": {
"test:ci": "jest --runInBand --reporters=default --reporters=jest-junit"
},
"jest-junit": {
"outputDirectory": "test-results",
"outputName": "junit.xml"
}
}
Og i workflowet:
- name: Test
run: npm run test:ci
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results/
if: always() sikrer, at artifacts bliver uploadet, også når tests fejler. Det er især rart, når du fejlsøger.
7. Lille tjekliste inden du stoler på din JavaScript-CI
Før du begynder at stole blindt på, at “grøn CI” betyder “alt er godt”, er her en kort tjekliste, jeg selv bruger:
- Scripts er konsistente: Kan du køre
npm run lint,npm testognpm run buildlokalt fra et friskt miljø (rm -rf node_modules && npm ci)? - Node-version matcher: Bruger du samme Node-version lokalt, i
.nvmrc/enginesog i CI? - Lockfile er committed: Har du en opdateret
package-lock.json(eller tilsvarende) i repoet, og bruger dunpm cii CI? - Tests er deterministiske: Falder og står tests tilfældigt, eller fejler de kun, når du faktisk har ændret noget?
- Ingen skjulte afhængigheder: Er der noget, der kun virker, fordi du har en global pakke installeret lokalt eller en gammel build-mappe liggende?
Hvis du kan sige ja til alle fem, er du et godt stykke foran rigtig mange små projekter, jeg har set.
8. Min ærlige holdning til GitHub Actions og JavaScript-CI
Jeg synes GitHub Actions er et af de værktøjer, hvor man får mest “rigtigt udviklerliv” for mindst mulig indsats i et hobby- eller studieprojekt. En pæn, stabil CI, der kører test, lint og build, siger mere om dig som udvikler end endnu en fancy feature i README’en.
Og hvis du en dag drømmer om at automatisere deploy ovenpå det her, så er det ret tilfredsstillende første gang du kan lave en pull request, se grøn CI og trykke merge uden at få svedige håndflader.







Send kommentar
Du skal være logget ind for at skrive en kommentar.