Hver gang du trykker “push” i et Git-repository, starter der potentielt en hel maskine op et sted, kører din kode igennem tests, bygger et image og sender det videre til produktion, uden at du selv rører en finger. Det er GitHub Actions i en nøddeskal, og det er blevet det mest udbredte CI/CD-værktøj blandt udviklere. Ifølge JetBrains’ State of Developer Ecosystem-rapport for 2025 bruger 33 procent af udviklerorganisationer GitHub Actions, foran Jenkins på 28 procent og GitLab CI på 19 procent. Til personlige projekter er forspringet endnu større: 39 procent mod Jenkins’ 13 procent.

Denne guide tager dig fra et tomt repository til en komplet, sikker CI/CD-pipeline i 14 konkrete trin. Du lærer YAML-syntaksen, bygger et rigtigt Node.js-projekt med tests og deployment, sætter hemmeligheder op korrekt, og får et kapitel om de sikkerhedshuller der har ramt tusindvis af repositories i 2025 og 2026. Sæt cirka 60 minutter af, og hav en GitHub-konto klar.

Vi bruger et Node.js-projekt som gennemgående eksempel, fordi det er det mest almindelige udgangspunkt blandt webudviklere, men principperne fra hvert trin gælder lige så godt for Python, Go eller et helt tredje sprog. Du skal blot bytte setup-node ud med det tilsvarende sprogspecifikke setup-action, resten af strukturen, triggers, jobs, secrets og deployment-logik, er identisk uanset sprog.

Hvorfor GitHub Actions dominerer CI/CD i 2026

GitHub Actions blev lanceret som en indbygget del af GitHub, hvilket betyder, at du ikke skal forlade platformen for at sætte automatisering op. Det er den store forskel fra Jenkins, der kræver egen server og vedligeholdelse, og GitLab CI, der kun giver fuld værdi hvis koden allerede ligger på GitLab. GitHub.com beskriver selv Actions som byggestenen i “a beginner’s guide to CI/CD and automation on GitHub”, hvor workflows trigges direkte af begivenheder i dit repository, som push, pull request eller en tidsplan.

Fordelen er lav startomkostning. Du opretter en YAML-fil, committer den, og GitHub kører den på en server, du aldrig behøver at administrere. Det forklarer, hvorfor adoptionen er størst blandt mindre teams og soloudviklere: ifølge JetBrains-tallene ligger GitHub Actions på 39 procent for personlige projekter, mens tunge enterprise-værktøjer som Jenkins halter langt bagefter i den kategori. Til gengæld stiller det højere krav til, at du selv forstår sikkerhedsmodellen, for som du vil se senere i denne guide, har netop den lave barriere også åbnet døren for nye former for supply chain-angreb.

Det andet store argument er økosystemet. GitHub Marketplace rummer tusindvis af færdige actions til alt fra at sende en Slack-besked til at bygge og publicere Docker-images, hvilket betyder, at de fleste standardopgaver ikke kræver, at du selv skriver scriptet fra bunden. Det er samtidig grunden til, at sikkerhedskapitlet senere i denne guide fortjener din fulde opmærksomhed, for hver ekstern action, du trækker ind i et workflow, er reelt tredjepartskode, der kører med adgang til dine secrets.

Forudsætninger: Dette skal du bruge

Du behøver ikke meget for at komme i gang, men følgende bør være på plads, før du åbner din editor:

  • En gratis eller betalt GitHub-konto med mindst ét repository
  • Git installeret lokalt (version 2.40 eller nyere anbefales)
  • Node.js 20 LTS installeret, hvis du følger eksempelprojektet
  • Grundlæggende kendskab til YAML-formatering (indrykning betyder alt)
  • En editor med YAML-understøttelse, for eksempel VS Code med YAML-udvidelsen
  • Adgang til at oprette secrets i dit repository (kræver admin- eller write-rettigheder)

Hvis du arbejder i en organisation, skal du desuden tjekke, om en administrator har begrænset, hvilke actions der må bruges. Det er en stigende praksis efter sikkerhedshændelserne i 2025, som vi kommer tilbage til. Nogle organisationer kræver, at alle actions eksplicit godkendes på en allow-liste, før de kan bruges i noget repository overhovedet, hvilket kan betyde, at et workflow, der virker fint på din personlige konto, fejler med en tilladelsesfejl, den dag du flytter det ind i en virksomheds organisation.

Trin 1: Opret .github/workflows-mappen

Alt starter med en mappestruktur. GitHub Actions leder specifikt efter YAML-filer i .github/workflows/ i roden af dit repository. Filnavnet er frit, men endelsen skal være .yml eller .yaml. Opret strukturen sådan her:

mkdir -p .github/workflows
touch .github/workflows/ci.yml

Så snart denne fil er committet og pushet til GitHub, begynder platformen at overvåge den for gyldige workflows. Du kan følge GitHub’s egen quickstart-guide som supplement, hvis du vil se den officielle dokumentation ved siden af denne tutorial.

Trin 2: Skriv dit første workflow i YAML

Et workflow består af et navn, en eller flere triggers, og mindst ét job. Her er den mindste version, der faktisk virker:

name: CI

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "Hej fra GitHub Actions"

Push den til main, og gå til fanen “Actions” i dit repository. Du bør se workflowet køre inden for få sekunder, med en grøn markering når det lykkes. Hvis du ser rødt, tjek indrykningen først, det er den hyppigste fejlkilde i YAML.

Trin 3: Forstå trigger-events med “on”

Nøglen on bestemmer, hvornår workflowet kører. Det kan være ét event eller en liste. De mest brugte i praksis er push, pull_request og workflow_dispatch, sidstnævnte giver dig en manuel “kør nu”-knap i GitHub-brugerfladen:

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  workflow_dispatch: {}
  schedule:
    - cron: "0 6 * * 1"

Cron-eksemplet kører hver mandag klokken 06:00 UTC, praktisk til for eksempel ugentlige afhængighedstjek. Bemærk at pull_request og dens mere risikable variant pull_request_target ikke er det samme, en detalje der har kostet flere projekter dyrt, som du kan læse om i sikkerhedsafsnittet nedenfor.

Trin 4: Vælg runner med runs-on

Hvert job skal fortælle GitHub, hvilken maskine det skal køre på. Det gøres med runs-on. De mest almindelige værdier er ubuntu-latest, windows-latest og macos-latest, men du kan også pinne til en specifik version som ubuntu-22.04 for at undgå overraskelser, når GitHub opdaterer standardimagerne.

jobs:
  test:
    runs-on: ubuntu-22.04
  test-windows:
    runs-on: windows-latest
  test-mac:
    runs-on: macos-latest

Vælg altid den billigste runner, der kan løse opgaven. Linux er markant billigere end macOS, og det bør styre dit valg, medmindre du reelt tester noget macOS-specifikt som en iOS-app. Du kan også køre flere jobs parallelt i samme workflow ved blot at definere dem ved siden af hinanden under jobs, uden nogen indbyrdes needs-relation. GitHub kører dem så samtidig, i stedet for i rækkefølge, hvilket typisk halverer eller mere den samlede ventetid for et workflow med flere uafhængige test-typer.

Trin 5: Hent din kode med actions/checkout

En runner starter som en helt tom maskine. Den kender ikke til din kode, før du eksplicit henter den. Det er jobbet for actions/checkout, den mest brugte action på hele GitHub Marketplace. Den aktuelle stabile version i produktionsbrug er @v4:

steps:
  - uses: actions/checkout@v4
    with:
      fetch-depth: 0

fetch-depth: 0 henter hele git-historikken i stedet for kun det seneste commit. Det er nødvendigt, hvis dit workflow senere skal generere changelogs eller sammenligne branches, men du kan udelade det for en hurtigere, lettere clone i almindelige test-jobs.

Trin 6: Installer afhængigheder med setup-node og cache

Næste skridt er at få det rigtige sprogmiljø op at køre. For et Node.js-projekt bruger du actions/setup-node, som også har indbygget cache-understøttelse, så du ikke downloader de samme npm-pakker igen ved hvert eneste workflow-kald:

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: "20"
      cache: "npm"
  - run: npm ci

Brug altid npm ci fremfor npm install i CI-miljøer. Kommandoen læser udelukkende package-lock.json og fejler, hvis den ikke matcher, hvilket giver dig deterministiske builds i stedet for at afhængigheder kan drifte mellem kørsler.

Trin 7: Kør tests automatisk ved hvert push

Med afhængigheder på plads kan du tilføje det, der egentlig gør pipelinen nyttig: automatiske tests. Tilføj blot et ekstra step efter installationen:

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: "20"
      cache: "npm"
  - run: npm ci
  - run: npm run lint
  - run: npm test

Sådan ser en typisk vellykket kørsel ud i logvinduet under fanen Actions:

Run npm test

> [email protected] test
> jest

 PASS  src/app.test.js
 PASS  src/utils.test.js

Test Suites: 2 passed, 2 total
Tests:       14 passed, 14 total
Time:        3.241 s

npm test exited with code 0

Fejler et af trinnene, stopper workflowet med det samme, og du får en rød markering direkte i pull requesten, før nogen når at merge fejlbehæftet kode til main. En fejlet kørsel ser typisk sådan ud, med et klart peg på hvilken test der brød og hvorfor:

Run npm test

> [email protected] test
> jest

 PASS  src/utils.test.js
 FAIL  src/app.test.js
  ● renders welcome message

    expect(received).toBe(expected)

    Expected: "Velkommen"
    Received: "undefined"

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 13 passed, 14 total

Error: Process completed with exit code 1.

Den slags output er guld værd, fordi du ser præcis, hvilken linje der forventede noget andet end den fik, uden at skulle reproducere fejlen lokalt først. Kombinér det med branch protection rules, som beskrevet nedenfor, så ingen kan merge, før dette output er grønt igen.

Trin 8: Brug secrets og miljøvariabler sikkert

Så snart din pipeline skal snakke med eksterne tjenester, som en database, en cloud-udbyder eller et deploy-mål, får du brug for hemmeligheder. Dem skal du aldrig skrive direkte i YAML-filen. Gå til repository-indstillinger, Secrets and variables, Actions, og opret dem der. GitHub’s egen dokumentation om variabler i Actions skelner klart mellem almindelige variabler og krypterede secrets.

steps:
  - name: Deploy til produktion
    env:
      API_KEY: ${{ secrets.PROD_API_KEY }}
      DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
    run: ./scripts/deploy.sh

GitHub maskerer automatisk secret-værdier i logs, så de vises som ***, selv hvis et script ved en fejl printer dem. Men det beskytter ikke mod, at en compromitteret action eksfiltrerer dem via netværkskald, hvilket er præcis den type angreb, du finder i sikkerhedsafsnittet længere nede.

Har du forskellige secrets til test og produktion, for eksempel to forskellige database-URL’er, bør du bruge GitHub Environments fremfor at proppe alt ind som repository-secrets. Et environment kan have sit eget sæt secrets, sine egne godkendelseskrav, og kan endda begrænses til kun at kunne bruges fra bestemte branches, hvilket gør det markant sværere for et forkert konfigureret workflow ved et uheld at ramme produktionsdata under en almindelig test-kørsel.

Branch protection: Kræv grønne tjek før merge

En pipeline, der kører, men som ingen er tvunget til at lytte til, giver kun halv værdi. Gå til repository-indstillinger, Branches, og opret en branch protection rule for main. Under “Require status checks to pass before merging” vælger du det jobnavn, workflowet bruger, i vores tilfælde test. Herefter kan ingen merge en pull request til main, før testjobbet reelt er grønt, uanset hvor travlt nogen har.

Kombinér gerne dette med krav om mindst én godkendt code review, og overvej at slå “Require branches to be up to date before merging” til. Sidstnævnte tvinger en pull request til at blive rebaset eller merget med main igen, hvis main har ændret sig, siden testene sidst kørte, hvilket fanger integrationskonflikter, før de rammer produktion.

Trin 9: Byg matrix-strategier til flere versioner

Hvis dit projekt skal understøtte flere Node-versioner eller flere operativsystemer samtidig, er det spild af tid at skrive et separat job for hver kombination. Matrix-strategien løser det med få linjer:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci
      - run: npm test

Denne konfiguration kører automatisk seks parallelle jobs, alle kombinationer af to operativsystemer og tre Node-versioner. Det er en billig måde at fange versionsspecifikke fejl på, længe før en bruger gør det for dig. Vil du udelukke bestemte kombinationer, for eksempel fordi Node 18 ikke understøttes på Windows i dit projekt, kan du tilføje en exclude-liste under strategien i stedet for at oprette separate jobs manuelt.

Sæt desuden fail-fast: false, hvis du vil se resultatet af alle kombinationer, selv når én fejler. Standardopførslen er at annullere de resterende jobs, så snart det første fejler, hvilket sparer minutter, men også kan skjule, at flere versioner reelt har samme problem.

Trin 10: Automatisk deployment til produktion

Når tests er grønne, er næste naturlige skridt at deploye automatisk. Et almindeligt mønster er at lade deployment kun køre, når push sker til main, og efter test-jobbet er bestået:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npm test

  deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - name: Deploy
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: ./scripts/deploy.sh

needs: test sikrer, at deploy-jobbet aldrig kører, hvis test-jobbet fejler. Brug desuden environment: production til at kræve manuel godkendelse fra en teamlead, før deploy rent faktisk udføres, hvilket er en indbygget beskyttelse mod uheld.

Trin 11: Notifikationer og status badges

En pipeline er kun nyttig, hvis teamet faktisk ser, når den fejler. Tilføj et status badge til din README, så alle kan se pipelinens tilstand med det samme:

![CI](https://github.com/dit-brugernavn/dit-repo/actions/workflows/ci.yml/badge.svg)

Til Slack- eller Teams-notifikationer ved fejl kan du tilføje et sidste step, der kun kører, hvis noget forinden gik galt, ved hjælp af betingelsen if: failure(). Det holder teamet orienteret uden at nogen behøver at tjekke fanen Actions manuelt hver dag. Omvendt kan du bruge if: success() til kun at sende en notifikation, når noget lykkedes efter en tidligere fejl, hvilket undgår at oversvømme kanalen med gentagne “alt er fint”-beskeder ved hver eneste grønne kørsel.

Artifacts: Del filer mellem jobs

Nogle gange skal ét job bruge output fra et andet, for eksempel skal deploy-jobbet bruge den byggede applikation fra test-jobbet, uden at bygge den forfra. Det løses med actions/upload-artifact og actions/download-artifact:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
          retention-days: 5

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - run: ./scripts/deploy.sh

Artifacts er også praktiske til at gemme testrapporter, coverage-filer eller build-logs, som du vil kunne downloade og inspicere manuelt fra GitHub-brugerfladen, selv efter workflowet er færdigt. Sæt altid en eksplicit retention-days, ellers opbevarer GitHub filerne længere end nødvendigt og bruger af din storage-kvote fra prisafsnittet nedenfor.

Trin 12: Selvhostede runners vs. GitHub-hostede runners

GitHub-hostede runners er nemme, men har begrænset CPU, RAM og ingen adgang til dit interne netværk. Har du brug for specialhardware, GPU-adgang eller adgang til et internt system bag firewall, er selvhostede runners løsningen. Sådan registrerer du en:

# På din egen server eller VM
mkdir actions-runner && cd actions-runner
curl -o actions-runner.tar.gz -L \
  https://github.com/actions/runner/releases/latest/download/actions-runner-linux-x64.tar.gz
tar xzf actions-runner.tar.gz
./config.sh --url https://github.com/dit-brugernavn/dit-repo --token DIN_REGISTRERINGSTOKEN
./run.sh

Selvhostede runners er fortsat gratis og falder uden for GitHub’s brugskvote for Actions, ifølge GitHub’s egen dokumentation. Det ændrer regnestykket for teams, der tidligere brugte selvhostede runners primært for at spare penge, uden at det nødvendigvis gør dem mindre attraktive til de andre fordele: kontrol, hastighed på gentagne builds gennem persistent cache, og adgang til interne ressourcer.

EgenskabGitHub-hostede runnersSelvhostede runners
OpsætningstidIngen, klar med det sammeKræver server, patching og vedligehold
BetalingPr. minut efter OS og arkitekturOmfattet af gratis kvote fra marts 2026
Hardware-kontrolStandardiseret, ingen GPU-adgangFuld kontrol, kan bruge GPU eller special-hardware
NetværksadgangIngen adgang til internt netværkKan placeres bag firewall
Bedst tilStandard CI, hurtig onboardingStore, gentagne builds og interne ressourcer

Trin 13: Sikkerhedshærdning mod pwn-request-angreb

Det her trin er ikke valgfrit. GitHub Actions’ åbne økosystem af tredjeparts-actions har gjort platformen til et attraktivt mål, og 2025 og 2026 har budt på flere alvorlige hændelser. I marts 2025 blev de populære actions tj-actions/changed-files og reviewdog/action-setup kompromitteret, registreret som henholdsvis CVE-2025-30066 og CVE-2025-30154, begge med en CVSS-score på 8,6. Sammen ramte hændelserne over 23.000 repositories, fordi utallige projekter havde disse actions som indirekte afhængighed, ofte uden selv at vide det, da en action de brugte direkte igen trak på en af de kompromitterede pakker under motorhjelmen. Det er præcis den slags kædeafhængighed, der gør supply chain-angreb i Actions-økosystemet svære at opdage med det blotte øje, fordi ingen af de linjer, teamet selv har skrevet, er ændret.

I maj 2026 blev npm-organisationen TanStack ramt af et koordineret angreb: mellem klokken 19:20 og 19:26 UTC den 11. maj udgav en angriber 84 ondsindede versioner fordelt på 42 forskellige @tanstack/*-pakker. Angrebet kombinerede et såkaldt “pwn request”-mønster via pull_request_target, cache-forgiftning i selve Actions-pipelinen, og udtrækning af et OIDC-token direkte fra runner-processen under kørslen. Wiz har desuden dokumenteret et lignende angreb mod AsyncAPI, hvor angriberen udnyttede, at et workflow med pull_request_target tjekkede pull request-koden ud i stedet for base-branchen, hvilket giver angriberens kode adgang til privilegier og secrets, den aldrig burde have.

Cloud Security Alliance har navngivet en bredere, AI-assisteret kampagne “prt-scan”, der systematisk leder efter fejlkonfigurerede pull_request_target-workflows for at stjæle repository-secrets, cloud-legitimationsoplysninger og CI-tokens i stor skala. Som reaktion ændrede GitHub i juni 2026 standardopførslen for actions/checkout, så koden fra en fork som udgangspunkt ikke længere tjekkes ud i privilegerede triggers, medmindre man eksplicit fravælger den beskyttelse, ifølge GitHub’s egen sikkerhedsblog.

Konkrete modforanstaltninger, du bør indføre i dag:

  • Pin tredjeparts-actions til en commit-SHA i stedet for en tag, fx actions/checkout@8f4b7f..., så en kompromitteret opdatering ikke automatisk rammer dig
  • Undgå pull_request_target medmindre du aktivt har brug for adgang til secrets fra en fork, og tjek aldrig PR-koden ud i den kontekst uden ekstra sikring
  • Sæt eksplicitte permissions på jobniveau, standard bør være contents: read
  • Brug OIDC-baseret autentificering til cloud-udbydere fremfor langtidsholdbare secrets, hvor det er muligt
  • Gennemgå jævnligt, hvilke actions dit repository afhænger af, og fjern dem, du ikke længere bruger

Læs mere i GitHub’s officielle guide til sikkerhedshærdning af Actions, som opdateres løbende med nye anbefalinger. Ingen af de fem punkter ovenfor kræver dyr tooling eller et dedikeret sikkerhedsteam, de er ren konfiguration, og den slags lavthængende frugt er netop, hvad der har manglet i de fleste af de dokumenterede hændelser fra 2025 og 2026.

Trin 14: Byg det komplette eksempelprojekt

Her er den samlede pipeline, der binder alle 13 forrige trin sammen i én fungerende workflow-fil til et Node.js-projekt med lint, tests på tre versioner, sikkerhedshærdning og betinget deployment:

name: Fuld CI/CD Pipeline

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

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: "npm"
      - run: npm ci
      - run: npm run lint
      - run: npm test

  deploy:
    needs: test
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: production
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
      - run: npm ci
      - run: npm run build
      - name: Deploy til produktion
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: ./scripts/deploy.sh

Læg mærke til id-token: write i deploy-jobbet. Den tilladelse gør det muligt at bruge OIDC til at autentificere mod eksempelvis AWS eller Azure uden at gemme et langtidsholdbart cloud-secret i repositoryet overhovedet, en af de mest effektive sikkerhedsforbedringer, du kan lave med minimal indsats. Gem denne fil som .github/workflows/ci.yml, commit den, og push til main. Herefter har du en pipeline, der tester på tre Node-versioner ved hvert push og hver pull request, og som deployer automatisk, når koden lander på main og alle tests er bestået.

Prisstruktur: Hvad koster GitHub Actions i 2026?

GitHub Actions er gratis op til et vist punkt, hvorefter du betaler pr. minut, afhængigt af operativsystem og arkitektur. Ifølge GitHub’s officielle billing-dokumentation ser de inkluderede minutter sådan ud:

PlanInkluderede minutter/månedInkluderet storage
Free2.000500 MB
Team3.0002 GB
Enterprise Cloud50.00050 GB

Bruger du mere end din kvote, betaler du pr. minut efter runner-type. Linux er billigst, macOS markant dyrest, fordi Apple-hardware koster mere at drifte i skala:

Runner-typePris pr. minut
Linux 1-core x64$0,002
Linux 2-core x64$0,006
Linux 2-core arm64$0,005
Windows 2-core x64$0,010
Windows 2-core arm64$0,010
macOS 3- eller 4-core$0,062

For et lille dansk team med et par aktive repositories rækker gratisplanens 2.000 minutter typisk langt, især hvis du bruger caching aktivt som beskrevet i trin 6. Store monorepos med hyppige pull requests og lange test-suiter rammer derimod hurtigt loftet og bør enten opgradere plan, optimere pipelinen, eller overveje selvhostede runners.

Et konkret regnestykke gør det håndgribeligt. Kører en pipeline i gennemsnit 4 minutter pr. kørsel på en Linux 2-core x64-runner, og teamet pusher eller åbner pull requests 20 gange om dagen på hverdage, giver det omkring 80 minutter dagligt og cirka 1.700 minutter om måneden, altså stadig inden for Free-planens 2.000 minutter. Fordobler teamet aktiviteten, eller tilføjer macOS-tests til mixet, vipper regnestykket hurtigt over i betalt territorium, fordi macOS-minutter som vist i tabellen ovenfor koster mere end ti gange så meget pr. minut som Linux.

GitHub Actions vs. Jenkins og GitLab CI

Valget mellem CI/CD-værktøjer handler sjældent kun om funktioner, det handler om, hvor koden allerede ligger, og hvor meget drift teamet vil tage på sig. GitHub’s egen artikel om CI/CD og automation fremhæver netop den tætte integration som hovedargumentet for Actions, mens Jenkins historisk har vundet på fleksibilitet i komplekse, selvhostede miljøer.

VærktøjAdoption i organisationerAdoption i personlige projekterDrift
GitHub Actions33%39%Fuldt hostet, ingen server nødvendig
Jenkins28%13%Selvhostet, kræver egen server
GitLab CI19%10%Hostet eller selvhostet, kræver GitLab

Tallene stammer fra JetBrains’ State of Developer Ecosystem 2025. Konklusionen for de fleste teams, der allerede har kode på GitHub, er enkel: medmindre I har en stor eksisterende Jenkins-infrastruktur eller specifikke krav, GitLab CI ikke kan matche, er GitHub Actions det naturlige startpunkt i 2026.

Det betyder ikke, at Jenkins er på vej ud. Store, etablerede enterprise-miljøer har ofte årtiers plugins og interne integrationer bygget op omkring Jenkins, og en fuld migrering væk er sjældent en triviel beslutning, uanset hvor pænt GitHub Actions’ adoptionstal ser ud på papiret. For nye projekter og teams uden den historiske bagage er regnestykket til gengæld markant enklere, fordi der ikke er nogen migreringsomkostning at veje op mod.

Almindelige faldgruber

De fleste problemer med GitHub Actions skyldes ikke platformen selv, men gentagne fejl i, hvordan workflows skrives:

  • Forkert indrykning i YAML. Et enkelt mellemrum for meget eller for lidt kan gøre hele filen ugyldig uden en tydelig fejlmeddelelse. YAML skelner strengt mellem tabs og mellemrum, så brug altid mellemrum konsekvent, aldrig en blanding.
  • At bruge npm install i stedet for npm ci. Det giver ikke-deterministiske builds, hvor pipelinen kan fejle uden at koden er ændret, fordi en transitiv afhængighed er opdateret i mellemtiden.
  • At glemme permissions-blokken. Uden eksplicit contents: read får jobbet ofte flere rettigheder, end det reelt har brug for, hvilket øger skadesomfanget, hvis en action i kæden bliver kompromitteret.
  • Hardkodede secrets i YAML-filen. Selv i et privat repository er det en tikkende bombe, hvis repositoryet nogensinde bliver offentligt eller forket. Git-historik glemmer aldrig, selv hvis du sletter linjen i et senere commit.
  • Ingen cache på afhængigheder. Uden cache downloades hele node_modules forfra ved hver eneste kørsel, hvilket forlænger byggetiden unødigt og i sidste ende koster flere af dine gratis minutter.
  • Brug af pull_request_target uden forståelse for risikoen. Som beskrevet i trin 13 er det den mest udnyttede sikkerhedsfejl i 2025 og 2026, og den er sværere at få øje på i en kodegennemgang, end de fleste tror.
  • At lade deploy køre uden betingelser. Uden if-tjek på branch kan et workflow deploye fra en feature-branch ved en fejl, med al den forvirring det skaber, når produktion pludselig kører kode, der aldrig blev godkendt.

Fejlfinding: Løsninger på typiske problemer

Herunder er de problemer, der oftest sender udviklere til Google, sammen med den konkrete løsning. De fleste af dem tager under fem minutter at rette, når du først ved, hvor du skal kigge:

  • “Workflow ikke fundet” efter push. Tjek at filen ligger præcis i .github/workflows/, ikke i en undermappe, og at endelsen er .yml. GitHub scanner kun den ene mappe, uanset hvor logisk en undermappe kan virke.
  • “Permission denied” ved deploy. Gå til repository-indstillinger, Actions, General, og tjek at “Workflow permissions” er sat korrekt for jobbets behov. Standardindstillingen er ofte mere restriktiv end nødvendigt, hvilket er godt for sikkerheden, men kan forvirre, når et deploy-script pludselig ikke kan skrive noget.
  • Secret vises som tom streng. Secrets defineret på repository-niveau er ikke automatisk tilgængelige i forked pull requests af sikkerhedsmæssige årsager. Løsningen er enten at teste på en branch i selve repositoryet, eller bruge pull_request_target med al den forsigtighed, der er beskrevet i trin 13.
  • Cache virker ikke som forventet. Tjek at cache-nøglen inkluderer en hash af lock-filen, ellers genbruger GitHub en forældet cache, selv efter du har ændret afhængigheder markant.
  • Job hænger i “queued” i lang tid. Det sker typisk ved høj belastning på GitHub-hostede runners eller hvis din plan har nået sin samtidighedsgrænse. Tjek fanen “Billing and plans” for at se, om du er tæt på grænsen for parallelle jobs.
  • Matrix-job fejler kun på én kombination. Tilføj fail-fast: false i strategien, så du kan se alle resultater i stedet for at workflowet stopper ved første fejl, hvilket gør det langt nemmere at afgøre, om problemet er versionsspecifikt.
  • YAML-parser-fejl uden linjenummer. Kør filen gennem en online YAML-validator eller VS Code’s indbyggede linter, før du pusher igen. Det sparer dig for flere runder af “push og håb”.
  • Action fungerer lokalt, men fejler i CI. Miljøvariabler eller filstier, der findes lokalt, findes sjældent identisk på en frisk runner. Log env og pwd som et debug-step, så du hurtigt kan se, hvad der reelt er anderledes.
  • Deploy kører to gange ved samme push. Kontroller om flere workflow-filer trigges af samme event, en overset duplikering, der er nem at overse i store repositories med mange bidragydere over tid.

Avancerede tips til erfarne teams

Når grundpipelinen fungerer, er der en række forbedringer, der for alvor sparer tid og penge. Brug concurrency-nøglen til automatisk at annullere gamle, kørende workflows, når en ny commit lander på samme branch, så du ikke betaler for at teste kode, der allerede er forældet:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Byg desuden composite actions eller reusable workflows, hvis flere repositories deler den samme logik. En reusable workflow kaldes med uses: ./.github/workflows/shared.yml og undgår, at ti forskellige teams vedligeholder ti næsten identiske YAML-filer. Sæt også en fornuftig retention-days på artifacts, standardværdien opbevarer data længere end de fleste teams reelt har brug for, hvilket stille og roligt æder af din storage-kvote. Endelig bør du overveje at flytte langsomme, ikke-kritiske jobs, som fuld end-to-end-test-suiter, til en natlig schedule-trigger fremfor at køre dem ved hvert eneste push.

Kombinér desuden pipelinen med Dependabot eller et lignende værktøj, der automatisk åbner pull requests, når en afhængighed har en sikkerhedsopdatering. Fordelen ved at have en solid test-suite kørende, som beskrevet i trin 7, er, at de fleste af disse opdaterings-PR’er kan merges automatisk, hvis testene er grønne, uden at et menneske skal bruge tid på rutinemæssige versionsopdateringer. Sæt et minimumsniveau for, hvilken alvorlighed af sårbarheder der udløser automatisk merge, og lad resten gå til manuel gennemgang.

Sådan passer GitHub Actions ind i resten af værktøjskassen

GitHub Actions erstatter ikke et overvågningssystem eller en fejlsporingsplatform, det automatiserer selve byggeprocessen. I praksis kombinerer de fleste teams det med et deploy-mål som en cloud-udbyder eller en container-registry, og med en notifikationskanal som Slack. Jo mere du læner dig op ad tredjeparts-actions fra Marketplace for at binde de dele sammen, jo vigtigere bliver sikkerhedstrinnet fra trin 13, for hver ny afhængighed er en ny mulig angrebsflade.

For danske og nordiske teams, der arbejder under GDPR, er det desuden værd at holde øje med, hvor dine runners og artifacts fysisk befinder sig. GitHub-hostede runners kører typisk i amerikanske datacentre, hvilket for de fleste CI/CD-formål er uproblematisk, da selve build- og testprocessen sjældent involverer persondata. Rammer din pipeline derimod databaser med rigtige brugerdata, for eksempel i et migrations-script, bør du enten anonymisere data i testmiljøet eller flytte det trin til en selvhostet runner inden for egen infrastruktur.

Ofte stillede spørgsmål

Hvad er GitHub Actions, og er det gratis?
GitHub Actions er GitHub’s indbyggede CI/CD-platform til at automatisere bygning, test og deployment. Free-planen inkluderer 2.000 minutter om måneden, hvilket dækker de fleste mindre projekter uden ekstra omkostning, medmindre du kører mange parallelle jobs eller bruger dyre macOS-runners ofte.

Hvor mange gratis minutter får jeg med GitHub Actions?
Free-planen giver 2.000 minutter, Team-planen 3.000 minutter, og Enterprise Cloud 50.000 minutter om måneden, ifølge GitHub’s officielle billing-side.

Hvad er forskellen på GitHub-hostede og selvhostede runners?
GitHub-hostede runners kræver ingen opsætning, men koster pr. minut og har ingen adgang til dit interne netværk. Selvhostede runners kræver egen server, men er omfattet af den gratis kvote fra 1. marts 2026 og giver fuld kontrol over hardware og netværk.

Hvordan gemmer jeg hemmeligheder sikkert i et workflow?
Brug repositoryets Secrets and variables-indstillinger, aldrig hardkodede værdier i YAML-filen. Referér dem i workflowet med ${{ secrets.NAVN }}, så bliver værdien automatisk maskeret i logs.

Kan GitHub Actions bruges til deployment, eller kun tests?
Begge dele. Som vist i trin 10 og 14 kan du lade et deploy-job køre automatisk, når test-jobbet er bestået, og kombinere det med manuel godkendelse via environment-nøglen for ekstra sikkerhed. Mange teams bruger desuden artifacts, som beskrevet ovenfor, til at sende det byggede resultat videre mellem jobs uden at bygge det flere gange.

Hvad er et “pwn request”-angreb, og hvordan undgår jeg det?
Det opstår, når et workflow bruger pull_request_target og samtidig tjekker kode fra en pull request ud, hvilket giver en ekstern angribers kode adgang til secrets og privilegier, den ikke burde have. Undgå mønstret, eller sikr det eksplicit, som beskrevet i trin 13.

Er GitHub Actions bedre end Jenkins eller GitLab CI?
Det afhænger af konteksten. GitHub Actions vinder på adoption, med 33 procent i organisationer og 39 procent i personlige projekter ifølge JetBrains, primært fordi det kræver nul infrastruktur, hvis koden allerede ligger på GitHub. Jenkins er stadig relevant i store, selvhostede enterprise-miljøer.

Hvor lang tid tager det at lære GitHub Actions fra bunden?
Med denne guide kan du have en fungerende pipeline med tests og deployment kørende på omkring 60 minutter. At mestre avancerede emner som reusable workflows, matrix-strategier og sikkerhedshærdning mod pwn-request-angreb tager typisk et par ugers praktisk erfaring oveni, primært fordi de færreste fejl viser sig, før pipelinen møder virkelighedens edge cases.