name: CI on: push: branches: [main, master] tags: ["v*"] pull_request: branches: [main, master] workflow_dispatch: jobs: # ───────────────────────────────────────────── # Lint, typecheck & unit tests # Draait op de host (self-hosted) waar Node 26 + # pnpm 11 geïnstalleerd zijn en internet beschikbaar is. # De job-container (docker mode) heeft GEEN outbound # internet, dus we draaien alles direct op de host. # ───────────────────────────────────────────── check: runs-on: self-hosted steps: - name: Checkout uses: actions/checkout@v4 with: repository: ${{ gitea.repository }} token: ${{ gitea.token }} - name: Toolchain check run: node scripts/check-node-toolchain.mjs - name: Install dependencies run: pnpm install --frozen-lockfile - name: Dependency security audit run: pnpm deps:audit - name: Lint run: pnpm biome:lint - name: Typecheck run: pnpm typecheck - name: Test env: SKIP_ENV_VALIDATION: 1 NODE_ENV: test DATABASE_URL: "mysql://test:test@localhost:3306/test?charset=utf8mb4" REDIS_URL: "redis://127.0.0.1:6379?connect_timeout=2" AUTH_SECRET: "ci-test-secret-key-that-is-long-enough" BCRYPT_ROUNDS: 4 run: pnpm test --maxWorkers=1 - name: Knip (unused files/exports) run: pnpm knip # ───────────────────────────────────────────── # Docker build & deploy # Draait op de host (self-hosted) zodat Docker # toegang heeft tot de daemon en volumes. # ───────────────────────────────────────────── deploy: needs: check if: gitea.event_name == 'push' && (gitea.ref_name == 'main' || gitea.ref_name == 'master') runs-on: self-hosted steps: - name: Checkout uses: actions/checkout@v4 with: repository: ${{ gitea.repository }} token: ${{ gitea.token }} - name: Provide production env for build run: | # De Next.js-build bakt NEXT_PUBLIC_* in de client-bundle en # valideert DATABASE_URL/HOTEL_NAME (zie src/env.ts — validatie # overslaan is verboden voor productie). .env staat niet in git, # dus kopieer de productie-.env van de host in de build-context. # Hij belandt alleen in de wegwerp-builder-stage, niet in de # runtime-image (die krijgt env via -e flags bij docker run). cp /var/www/atom-nexst/.env .env - name: Build image run: | # --network=host is vereist: deze host heeft Docker iptables # uitgeschakeld, dus build-containers op het bridge-netwerk # hebben geen outbound internet (npm/pnpm zouden hangen). DOCKER_BUILDKIT=1 docker build \ --network=host \ --build-arg NODE_OPTIONS="--max-old-space-size=1536" \ --cache-from epicnext-cms:latest \ -t epicnext-cms:latest . - name: Run migrations run: | # Draai DB-migraties van deze commit op de host (de slimme # runtime-image heeft geen source/tsx, zie docker-compose.yml). # Idempotent: al toegepaste migraties worden overgeslagen. # Gebruikt .env uit de eerdere stap (productie-DB). Vóór het # vervangen van de container, zodat het schema klaarstaat. pnpm install --frozen-lockfile pnpm db:migrate - name: Deploy container shell: bash run: | # Maak poort 3002 vrij: stop zowel de vorige CI-container als de # compose-container (beide draaien op het host-netwerk). docker stop epicnext-cms-app 2>/dev/null || true docker rm epicnext-cms-app 2>/dev/null || true docker stop epicnext-cms 2>/dev/null || true docker rm epicnext-cms 2>/dev/null || true # Geef de productie-env 1-op-1 door. GEEN env-file-flag: `docker run` # behoudt letterlijke quotes uit het bestand (DATABASE_URL="..." → # ongeldige URL en crash), terwijl de shell ze correct stript. # Sourcen + elke sleutel met -e doorgeven geeft de container exact # dezelfde waarden als waarmee de image gebouwd is. set -a # shellcheck disable=SC1091 . /var/www/atom-nexst/.env set +a ENV_ARGS=() while IFS='=' read -r key _; do case "$key" in ''|'#'*|*[!A-Za-z0-9_]* ) continue ;; esac ENV_ARGS+=(-e "$key") done < /var/www/atom-nexst/.env # Zelfde env + volumes als docker-compose.yml, zodat de CI-container # functioneel gelijk is aan de compose-container die hij vervangt. docker run -d \ --name epicnext-cms-app \ --restart always \ --net=host \ "${ENV_ARGS[@]}" \ -v /var/www/atom-nexst/public/nitro-assets:/app/public/nitro-assets \ -v /var/www/atom-nexst/public/swf:/app/public/swf \ -v /var/www/atom-nexst/storage:/app/storage \ -v /var/www/Gamedata:/var/www/Gamedata \ epicnext-cms:latest docker image prune -f - name: Health check run: | for i in $(seq 1 30); do if curl -sf --max-time 5 http://127.0.0.1:3002/api/health \ | grep -q '"database":true'; then echo "Deploy OK" exit 0 fi echo "Waiting... ($i/30)" sleep 3 done echo "ERROR: Health check failed" >&2 docker logs epicnext-cms-app --tail 50 >&2 || true echo "Rolling back to compose container..." >&2 docker stop epicnext-cms-app 2>/dev/null || true docker rm epicnext-cms-app 2>/dev/null || true docker compose -f /var/www/atom-nexst/docker-compose.yml up -d --no-build 2>&1 || true exit 1 # ───────────────────────────────────────────── # E2E smoke against the freshly deployed container. # Runs after a successful deploy on main/master only # (on PRs the deploy job is skipped, so this is skipped too). # Public read-only endpoints only — safe against production. # ───────────────────────────────────────────── e2e: needs: deploy if: gitea.event_name == 'push' && (gitea.ref_name == 'main' || gitea.ref_name == 'master') runs-on: self-hosted steps: - name: Checkout uses: actions/checkout@v4 with: repository: ${{ gitea.repository }} token: ${{ gitea.token }} - name: Install dependencies run: pnpm install --frozen-lockfile - name: Install Playwright browser run: pnpm exec playwright install chromium - name: Smoke test deployed app env: PLAYWRIGHT_BASE_URL: "http://127.0.0.1:3002" run: pnpm test:e2e