3 små YAML-filer der gør dit JavaScript-projekt meget mindre kaotisk

3 små YAML-filer der gør dit JavaScript-projekt meget mindre kaotisk

“Det virkede jo lige før” – og hvorfor CI sparer dig for hovedpine

Første gang jeg ødelagde et ellers fint lille hobbyprojekt, var det ikke på grund af en stor refactor.

Det var én linje. Jeg fik lige “optimeret” lidt i en utils-fil sent om aftenen. Alt virkede lokalt. Næste dag puller en ven koden, kører npm test, og halvdelen af tingene vælter.

Vi havde forskellig Node-version. Forskellige dependencies. Og ingen af os gad faktisk køre tests hver gang, vi lavede en lille ændring. Klassikeren: “det virker på min maskine”.

Det er præcis her, GitHub Actions og CI (Continuous Integration) begynder at give mening. Ikke som enterprise-opsætning, men som en lille vagtpost, der automatisk:

  • installerer dine dependencies fra bunden
  • kører lint og format-check
  • kører tests på hver push og pull_request

Og siger stop, før du deployer noget, der er i stykker.

I den her tekst bygger jeg den CI, jeg selv ville ønske, jeg havde sat op meget tidligere: én simpel workflow-fil til et JavaScript/Node-projekt, som du kan copy/paste, forstå og selv udvide.

Først: Hvad GitHub Actions og CI egentlig gør for dig

GitHub Actions er GitHubs egen automatiseringsmotor. Du beskriver “arbejdsgange” i små YAML-filer. GitHub kører dem for dig, når noget sker i dit repo, fx når du pusher eller åbner et pull request.

CI betyder bare: hver gang du integrerer ny kode i dit repo, kører der et sæt checks automatisk. Typisk:

  • byg projektet
  • kør lint
  • kør tests

Pointen er, at computeren er bedre end os til at huske at gøre det hver eneste gang.

Hvis du vil have lidt mere baggrund om workflows og deploy, kan du senere kigge på artiklen om SPA vs serverrouting og hosting, men vi holder os til CI her.

En minimal workflow der kører på push og pull request

Udgangspunktet: du har et Node/JavaScript-projekt med f.eks. Vite, Jest eller Vitest og ESLint.

Strukturen ligner noget i den her stil:

my-project/
  package.json
  package-lock.json eller pnpm-lock.yaml / yarn.lock
  src/
  test/ eller tests/

Nu laver vi en første CI-fil:

  1. Lav mappen .github/workflows i roden af dit repo.
  2. Lav en fil, fx ci.yml.

Læg det her i den:

name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - name: Check out kode
        uses: actions/checkout@v4

      - name: Brug Node 20
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Installer dependencies
        run: npm ci

      - name: Byg projekt (valgfrit)
        run: npm run build --if-present

      - name: Kør tests
        run: npm test -- --runInBand

Det her er en “one-file CI starter”:

  • on: hvornår workflowet kører (her: på push og pull_request mod main).
  • jobs: et job er en virtuel maskine, der laver tingene.
  • steps: sekvensen af ting, der sker: checkout, setup Node, install, build, test.

Når du committer og pusher den fil, får du en ny faneblad på GitHub der hedder Actions, hvor du kan se dit workflow køre.

Sæt Node-version og caching, så det ikke føles langsomt

Hvis du lader være med at sætte Node-version, får du måske en anden version end lokalt. Så kan du få de der dejlige “det virker hos mig”-forskelle.

I eksemplet ovenfor satte vi:

      - name: Brug Node 20
        uses: actions/setup-node@v4
        with:
          node-version: 20

Det betyder, at Actions-miljøet bruger Node 20 hver gang. Du kan også skrive node-version-file: .nvmrc, hvis du bruger sådan en.

Caching gør det hurtigere, ved at genbruge din node_modules-installation mellem runs. En simpel måde er at lade setup-node håndtere det:

      - name: Brug Node 20 med cache
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

Så bruger den din package-lock.json som nøgle og slipper for at hente og installere alt fra nul hver gang.

Bruger du pnpm eller yarn, kan du ændre cache til pnpm eller yarn og justere din install-kommando.

Lint og format-check med ESLint og Prettier

Næste skridt er at lade CI’en være den irriterende ven, som konsekvent fanger dine småting.

Hvis du har scripts i din package.json i stil med:

{
  "scripts": {
    "lint": "eslint src",
    "format:check": "prettier --check "src/**/*.{js,ts,jsx,tsx}""
  }
}

så kan du udvide workflowet:

      - name: Kør ESLint
        run: npm run lint

      - name: Tjek format med Prettier
        run: npm run format:check

Hvis en af de to fejler, stopper workflowet. Det føles lidt hårdt første gang, men du får:

  • ens stil i projektet
  • fanget små logiske fejl tidligt

Hvis du endnu ikke har fået sat ESLint/Prettier op, så er det værd at gøre. Der ligger mange gode introer, f.eks. i samme stil som SQL-øvelserne i den her artikel om JOIN-øvelser: små skridt, én kommando ad gangen.

Tests i CI med Vitest eller Jest

Når lint og format spiller, er det næste trin at få dine tests ind.

Typisk har du noget a la:

{
  "scripts": {
    "test": "vitest",
    "test:ci": "vitest run --coverage"
  }
}

Eller for Jest:

{
  "scripts": {
    "test": "jest",
    "test:ci": "jest --runInBand"
  }
}

Personligt kan jeg godt lide at have et separat test:ci-script, så jeg kan tweake det uden at ændre min lokale udviklingskommando.

Så ændrer vi workflowet:

      - name: Kør tests
        run: npm run test:ci

Hvis du kører Vitest i et Vite-projekt, kan du også bruge officiel setup-action eller bare køre det direkte, som her. Det vigtigste er, at npm run test:ci fejler med exit code 1, når noget er galt. Resten er bare støj.

Gate deploy – idéen om “kun grøn kode må komme videre”

Forestil dig, at du har sat automatisk deploy op til Netlify, Vercel eller GitHub Pages. Det er ret nemt, og der er en artikel om deploy-fælder på Coding Class, hvis du vil gå i den retning.

Konceptet er simpelt:

  • Ét workflow kører lint, build og tests.
  • KUN hvis det workflow lykkes, må deploy ske.

Der er to klassiske måder at gøre det på i GitHub Actions:

  1. Ét workflow med to jobs, hvor nr. 2 afhænger af nr. 1.
  2. To workflows, hvor det ene trigges af et workflow_run event.

Vi tager den simple: to jobs i samme fil.

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      # ... checkout, setup node, install, lint, test ...

  deploy:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Check out kode
        uses: actions/checkout@v4

      - name: Byg og deploy
        run: |
          npm ci
          npm run build
          # kald dit deploy-script her

needs: build-and-test betyder: deploy-jobbet starter kun, hvis build-and-test lykkes. Hvis lint eller tests fejler, sker der intet deploy. Det er præcis den “grøn port”, du vil have.

Secrets og miljøvariabler uden at skyde dig selv i foden

Før eller siden skal din CI bruge et token, en API-key eller et password. Det er her, mange første gang laver den helt klassiske fejl: committe hemmeligheder i repoet.

Det er en dårlig idé. Hvis du vil have et lidt længere rant om det, har jeg skrevet om hemmeligheder i kode i artiklen du skal holde op med at hardcode hemmeligheder.

Den korte version til GitHub Actions:

  • Læg aldrig tokens i kode eller YAML.
  • Brug GitHub Secrets til alt, der skal holdes skjult.
  • Giv hvert token mindst mulige rettigheder (least privilege).

Sådan bruger du et secret i et workflow:

  1. Gå ind i dit repo på GitHub.
  2. Settings → Secrets and variables → Actions.
  3. Opret et nyt secret, fx DEPLOY_TOKEN.

I din CI-fil kan du så gøre:

  deploy:
    needs: build-and-test
    runs-on: ubuntu-latest
    steps:
      - name: Check out kode
        uses: actions/checkout@v4

      - name: Deploy med token
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: |
          echo "Bruger token: $DEPLOY_TOKEN"
          # Her ville du kalde dit rigtige deploy-script

Når workflowet kører, er DEPLOY_TOKEN sat som environment variable i det step. GitHub forsøger også at maskere værdien i logs, så den ikke bliver vist i klartekst.

Husk at rotere (genoprette) tokens, hvis du er i tvivl om, om noget er lækket. Og ja, vi har alle prøvet at lægge noget i et public repo, som ikke skulle have været der.

Typiske fejl og hvordan jeg selv leder efter dem i Actions-loggen

Nu til den del, hvor ting går galt. For det gør de. Det gode er, at Actions-loggen ofte fortæller dig præcis, hvad der er i vejen, hvis du gider læse den.

1. Permissions og “Resource not accessible by integration”

Hvis du prøver at pushe tilbage til repoet, oprette en release eller lignende fra Actions, kan du få fejl om tokens eller rettigheder.

Når du ser noget i stil med Resource not accessible by integration i loggen, så kig på:

  • hvilken action du bruger (kræver den ekstra scopes?)
  • om du har sat permissions: i workflowet

Eksempel:

permissions:
  contents: write

øverst i din YAML kan være nødvendigt, hvis du vil lave commits eller tags fra Actions.

2. Lockfiles og mismatch mellem lokalt og CI

En anden klassiker: du bruger npm install lokalt, men Actions kører npm ci, og pludselig fejler noget på CI.

Min tommelfingerregel:

  • Brug det samme package manager-kommando lokalt og i CI (npm ci eller pnpm install --frozen-lockfile).
  • Commit altid din lockfile (package-lock.json, pnpm-lock.yaml osv.).

Hvis dependencies fejler i Actions, kigger jeg først efter:

  • er der en fejl i npm ci-steppet?
  • er Node-versionen den samme som lokalt?
  • har jeg husket at committe lockfile efter dependency-opdateringer?

3. Flaky builds og tests der fejler “nogle gange”

Den værste kategori: ting, der fejler uden at være deterministiske. Flaky tests. Tidsafhængig logik. Netværkskald.

Her plejer jeg at gøre tre ting:

  1. Kør samme test lokalt med det samme kommando som CI (npm run test:ci).
  2. Slå evt. --runInBand til i CI, så tests kører sekventielt.
  3. Mock alt netværk og tid (f.eks. dato, random) i tests.

Hvis du kan få fejlen til at opstå lokalt med en deterministisk kommando, er du halvvejs. Så er resten bare almindelig debug, som i artiklen om at begynde at debugge rigtigt.

4. Hvor jeg klikker i Actions, når noget går galt

Jeg bruger næsten altid samme lille rutine, når et workflow fejler:

  1. Åbn fanen Actions i repoet.
  2. Klik på det seneste run med rødt kryds.
  3. Klik på det job, der er fejlet (fx build-and-test).
  4. Fold det step ud, der er markeret med rødt.
  5. Scroll helt ned i loggen. Den sidste fejl-linje er som regel den vigtigste.

Det lyder banalt, men det er ofte der, man opdager, at det ikke er “noget med Actions”, men bare en almindelig Node-fejl, en import, der mangler, eller en env-var, der ikke er sat.

Hvordan du kommer videre uden at drukne i YAML

Hvis du først har én fil, der kan:

  • køre lint
  • tjekke format
  • køre tests
  • eventuelt gate en deploy

så er du et helt andet sted end “jeg pusher bare og håber det bedste”.

Derfra kan du roligt begynde at:

  • tilføje flere jobs (fx e2e-tests)
  • køre på flere Node-versioner med matrix
  • splitte workflows op i ci.yml og deploy.yml

Men jeg vil faktisk anbefale, at du bliver i den lille, simple én-fil-opsætning et stykke tid. Lidt ligesom at holde sig til én surdej, før man begynder at eksperimentere med fuldkorn og koldhævning.

Min erfaring er, at en lille, stabil CI, der gør få ting godt, gør dit repo langt mere “seriøst” at arbejde i, også selvom det bare er et hobbyprojekt, du bygger om aftenen ved spisebordet, mens resten af huset laver noget helt andet.

Og ja, den siger nej til dig en gang imellem. Men det er typisk der, man faktisk lærer noget.

Brug actions/cache@v4 til at cache npm-pakken eller mappen ~/.npm og/eller node_modules. En almindelig key er ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} og restore-keys kan være prefixet ${{ runner.os }}-node-. Det reducerer markant tiden på efterfølgende runs uden at ændre din npm ci-opførsel.
Tilføj et step før tests med run: npm run lint --if-present og evt. run: npm run format:check eller npx prettier --check . Hvis dine scripts returnerer en ikke-0 exitkode, stopper CI automatisk. Overvej --max-warnings=0 på ESLint hvis du vil gøre warnings til fejl.
Aktivér branch protection i GitHub Settings -> Branches, opret en regel for main og slå 'Require status checks to pass' til, hvorefter du vælger din CI-workflow som krævet check. Du kan også kræve up-to-date branch og approvals for ekstra sikkerhed.
Brug en matrix-strategi i jobbet, fx strategy: matrix: node-version: [18,20], og sæt actions/setup-node til node-version: ${{ matrix.node-version }}. Det kører job parallelt på hver version og fanger kompatibilitetsfejl tidligt.

Jonas Kirkeby har skrevet kode siden han som teenager forsøgte at lave en helt simpel hjemmeside til sin fars lille vvs-firma – og endte med at sidde oppe hele natten for at få en knap til at skifte farve. Siden da har han lært sig det meste ved at prøve sig frem, kopiere andres eksempler, ødelægge dem og langsomt forstå, hvorfor tingene virker, som de gør.

Til daglig arbejder han slet ikke med IT, men bruger aftener og morgener på små projekter: en lille side til en forening, et simpelt værktøj til at holde styr på familiens madplan eller et Python-script, der rydder op i rodede filer. Det er den slags konkrete hverdags-behov, der har formet hans måde at tænke kodning på – hvad kan jeg bygge nu, som faktisk hjælper mig eller nogen, jeg kender?

På Coding Class deler Jonas de guides, han selv ville ønske, han havde haft: korte, konkrete forløb, hvor du kan se noget på skærmen efter få minutters læsning. Han viser hele vejen fra idé til færdig løsning, inklusive de typiske fejl og små snubletråde på vejen, så du ikke kun får den pæne, polerede version.

Hans mål er, at du som begynder eller let øvet hurtigt får følelsen af: “Det her kan jeg faktisk selv finde ud af” – uanset om du vil bygge din første lille hjemmeside, forstå JavaScript-funktioner eller bruge Python til at automatisere en kedelig opgave.

Send kommentar

You May Have Missed