Bliver din JavaScript-CI også sur over noget, der virker lokalt?

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.json 100 % som sandhed
  • Fejler, hvis lockfile og package.json ikke 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å:

  1. .nvmrc (hvis du bruger nvm lokalt)
  2. .node-version (bruges af flere værktøjer)
  3. engines i package.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 spare en del tid. Vi aktiverede det allerede med 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 / afterEach i Jest/Vitest.
  • Brug headless browser i CI, f.eks. --runInBand eller 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. setTimeout uden 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 eller jest.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.json eller pnpm-lock.yaml).
  • Kør npm ci lokalt 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:

  1. Scripts er konsistente: Kan du køre npm run lint, npm test og npm run build lokalt fra et friskt miljø (rm -rf node_modules && npm ci)?
  2. Node-version matcher: Bruger du samme Node-version lokalt, i .nvmrc/engines og i CI?
  3. Lockfile er committed: Har du en opdateret package-lock.json (eller tilsvarende) i repoet, og bruger du npm ci i CI?
  4. Tests er deterministiske: Falder og står tests tilfældigt, eller fejler de kun, når du faktisk har ændret noget?
  5. 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.

Brug værktøjer som nektos/act til at køre dine workflows lokalt eller kør en container med samme baseimage (fx node:18) og kør npm ci && npm test. Husk at act ikke altid matcher GitHub-runners 100 procent, så for fuld parity kan du starte en Ubuntu-container og gentage de samme steps der.
Typiske årsager er OS-forskelle (case-sensitive filsystemer), native native-moduler der kræver build-værktøjer, manglende miljøvariabler/secrets eller mismatch mellem lockfile og node/npm-version. Løsninger inkluderer at pinne Node-version i setup-node, installere nødvendige build-tools som devDependency eller i runner, bruge lockfiles korrekt og reproducere miljøet i en container.
Mock eller stub eksterne kald i unit-tests med værktøjer som nock eller msw, og gem optagede responses som fixtures til hurtige tests. Kør rigtige integrationstests separat (fx i en længere kørende pipeline eller kun på main) og opbevar nødvendige nøgler i GitHub Secrets med adgangsbegrænsning.
npm ci er designet til automatiserede miljøer: den bruger lockfilen, er hurtigere og giver deterministiske installs, men fejler hvis package.json og lockfilen er ude af sync. Brug npm ci i CI for konsistente builds, og lad udviklere bruge npm install lokalt mens I holder lockfilen opdateret.

Lasse Falkenberg er typen, der begyndte at rode med HTML og CSS for at lave en simpel bandside – og opdagede, at det var langt sjovere at få knapperne til at virke end at stå på scenen. Siden har han kastet sig over alt fra små JavaScript-snippets til Python-scripts, der kan spare ham for kedeligt, manuelt arbejde i hverdagen.

Han har lært det meste ved at bygge ting, der lige præcis løser hans egne problemer: en lille webapp til at holde styr på brætspilsaftener, et script til at rydde op i rodede mapper, eller en enkel side til at dele noter med venner. Undervejs har han kæmpet sig gennem alle de klassiske fejl – semikolon, forkerte indrykninger og variabler, der hedder noget helt andet end man tror – og det er præcis den rejse, han deler på Coding Class.

På Coding Class skriver Lasse praktiske, jordnære guides, der tager udgangspunkt i små, konkrete opgaver: noget du kan se, teste og bygge videre på med det samme. Han elsker at bryde en opgave ned i små bidder, vise den fulde kode og forklare linje for linje, hvad der sker – inklusive de typiske bugs, du med stor sandsynlighed også støder på.

For Lasse handler kodning ikke om flotte titler eller store ord, men om følelsen af at få noget til at virke – og om at du som læser kan gå derfra med noget, du selv har bygget. Hvis du kan kende glæden ved at få en fejl til endelig at forsvinde, er du lige på bølgelængde med hans måde at lære fra sig på.

Send kommentar

You May Have Missed