diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index 363435b..cfa9da0 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -3,14 +3,89 @@ name: Docker Image bauen und veroeffentlichen # Laeuft bei jedem Push auf main (rollierendes ":latest" + ":") # und bei Versions-Tags wie "v1.2.3" (zusaetzlich als eigener Tag). Manuell # ueber "Run workflow" in der Gitea-Weboberflaeche ebenfalls startbar. +# +# Qualitaets-Gate: Ein Image entsteht nur, wenn lint (inkl. audit), typecheck +# und test gruen sind. Die Pruefungen laufen auf "ci-isolated" (Jobs ohne +# Docker-Socket und ohne Clusternetz, Runner-Doku im VyCept-Repo, +# docs/BETRIEB.md 11b); nur der Image-Build braucht "docker-builder". on: push: branches: [main] tags: ["v*"] workflow_dispatch: +# Ein neuer Push bricht den ueberholten Lauf ab, statt ihn zu Ende zu rechnen. +concurrency: + group: docker-build-${{ gitea.ref }} + cancel-in-progress: true + +env: + NEXT_TELEMETRY_DISABLED: "1" + jobs: + lint: + runs-on: ci-isolated + timeout-minutes: 10 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Runner-Isolation pruefen + run: bash scripts/ci-runner-isolation.sh + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: { node-version: "22" } + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ~/.cache/pnpm + ~/.cache/pnpm-store + key: pnpm-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: pnpm- + - run: export pnpm_config_store_dir="$HOME/.cache/pnpm-store" pnpm_config_cache_dir="$HOME/.cache/pnpm" && corepack enable && pnpm install --frozen-lockfile + - run: pnpm lint + # Bekannte Luecken in Laufzeit-Abhaengigkeiten (ab "high") brechen den Lauf ab. + - run: pnpm audit --prod --audit-level high + + typecheck: + runs-on: ci-isolated + timeout-minutes: 10 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Runner-Isolation pruefen + run: bash scripts/ci-runner-isolation.sh + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: { node-version: "22" } + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ~/.cache/pnpm + ~/.cache/pnpm-store + key: pnpm-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: pnpm- + # postinstall erzeugt beide Prisma-Clients (Postgres + MySQL), die tsc braucht. + - run: export pnpm_config_store_dir="$HOME/.cache/pnpm-store" pnpm_config_cache_dir="$HOME/.cache/pnpm" && corepack enable && pnpm install --frozen-lockfile + - run: pnpm typecheck + + test: + runs-on: ci-isolated + timeout-minutes: 15 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Runner-Isolation pruefen + run: bash scripts/ci-runner-isolation.sh + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: { node-version: "22" } + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ~/.cache/pnpm + ~/.cache/pnpm-store + key: pnpm-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: pnpm- + - run: export pnpm_config_store_dir="$HOME/.cache/pnpm-store" pnpm_config_cache_dir="$HOME/.cache/pnpm" && corepack enable && pnpm install --frozen-lockfile + # Unit- und Komponententests (jsdom), ohne Datenbank. + - run: pnpm test + build-and-push: + needs: [lint, typecheck, test] runs-on: docker-builder env: # Fest hinterlegt statt ${{ gitea.server_url }}: das hat in manchen diff --git a/scripts/ci-runner-isolation.sh b/scripts/ci-runner-isolation.sh new file mode 100644 index 0000000..34c779f --- /dev/null +++ b/scripts/ci-runner-isolation.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Prüft im CI-Job die Grenzen des isolierten Runners (Label ci-isolated, +# Runner-Doku im VyCept-Repo, docs/BETRIEB.md 11b; Kopie von dort). Läuft als erster Schritt jedes Prüf-Jobs, damit jeder +# Runner bei jedem Lauf geprüft wird. Schlägt fehl, wenn der Job mehr darf als +# vorgesehen – oder wenn die Prüfung selbst nichts aussagen würde. +set -euo pipefail + +fail() { + echo "Runner-Isolation verletzt: $*" >&2 + exit 1 +} + +reachable() { + timeout 5 bash -c "exec 3<>/dev/tcp/$1/$2" 2>/dev/null +} + +# Namen mit Wiederholung auflösen: Das DNS des Job-Netzes antwortet unter Last +# gelegentlich erst beim zweiten Versuch. +resolve() { + local address="" + for _ in 1 2 3 4 5; do + address=$(getent hosts "$1" | awk '{ print $1; exit }' || true) + if [ -n "$address" ]; then echo "$address"; return 0; fi + sleep 2 + done + return 1 +} + +# 1. Kein Zugang zum Docker-Daemon über den Socket. +[ -z "${DOCKER_HOST:-}" ] || fail "DOCKER_HOST ist gesetzt ($DOCKER_HOST)" +for socket in /var/run/docker.sock /run/docker.sock; do + [ ! -e "$socket" ] || fail "Docker-Socket $socket ist sichtbar" +done + +# 2. Kein Zugang zur Docker-API des Daemons im Runner-Pod (Gateway des Job-Netzes): +# 2375 (ohne TLS) zu, 2376 nur mit Client-Zertifikat, das der Job nicht hat. +gateway_hex=$(awk '$2 == "00000000" { print $3; exit }' /proc/net/route) +[ -n "$gateway_hex" ] || fail "kein Standard-Gateway gefunden – Prüfung wäre wirkungslos" +gateway=$(printf '%d.%d.%d.%d' "0x${gateway_hex:6:2}" "0x${gateway_hex:4:2}" "0x${gateway_hex:2:2}" "0x${gateway_hex:0:2}") +if reachable "$gateway" 2375; then fail "Docker-API ohne TLS am Gateway $gateway:2375"; fi +if curl -sk --max-time 5 "https://$gateway:2376/version" >/dev/null 2>&1; then + fail "Docker-API am Gateway $gateway:2376 ohne Client-Zertifikat nutzbar" +fi + +# 3. Kein Clusternetz: Kubernetes-API (Service und Knoten) und fremde Namespaces. +for target in 10.43.0.1:443 10.50.1.51:6443 10.50.1.52:6443 10.50.1.53:6443; do + if reachable "${target%:*}" "${target##*:}"; then fail "$target ist erreichbar"; fi +done +resolve kubernetes.default.svc.cluster.local >/dev/null || fail "Clusternamen nicht auflösbar – Prüfung wäre wirkungslos" +# Fremder Namespace (VyCept-Datenbank) und eigener Namespace (Gitea-Datenbank). +for target in vycept-postgres.vycept.svc.cluster.local:5432 gitea-postgres.gitea.svc.cluster.local:5432; do + host=${target%:*} + address=$(resolve "$host") || fail "$host nicht auflösbar – Prüfung wäre wirkungslos" + if reachable "$address" "${target##*:}"; then fail "$host ($address) ist erreichbar"; fi +done + +# 4. Internet bleibt erreichbar (npm, GitHub, Images). +reachable registry.npmjs.org 443 || fail "Internet (registry.npmjs.org) nicht erreichbar" + +echo "Runner-Isolation in Ordnung (Gateway $gateway, kein Socket, kein Clusternetz)"