From 0421d7d57a03b31737aa517a9bc26c7dba27871d Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Wed, 14 Oct 2026 09:02:55 +0200 Subject: [PATCH] ci: Qualitaets-Gate vor dem Image-Build Der Workflow baute bisher bei jedem Push auf main ungeprueft beide Images. Jetzt laufen lint (inkl. pnpm audit ab high), typecheck und test auf dem isolierten Runner (ci-isolated, ohne Docker-Socket und Clusternetz); die Tests bekommen Postgres 18.6 als Service ueber TEST_DATABASE_URL. build-and-push startet nur, wenn alle drei gruen sind. Muster und Runner-Pruefskript aus dem VyCept-Repo (docs/BETRIEB.md 11b). Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitea/workflows/docker-build.yml | 87 +++++++++++++++++++++++++++++++ scripts/ci-runner-isolation.sh | 60 +++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 scripts/ci-runner-isolation.sh diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index 5f2c601..a0cd429 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -7,14 +7,101 @@ name: Docker-Images bauen und veroeffentlichen # # Ausloeser: Push auf main (":latest" + ":sha-"), Versions-Tags "v1.2.3" # (zusaetzlich ":1.2.3"), manuell ueber "Run workflow". +# +# Qualitaets-Gate: Images entstehen 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- + - 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 + # embedded-postgres startet nicht als root; deshalb ein Datenbank-Service + # (gleiche Version wie im Cluster), genutzt ueber TEST_DATABASE_URL + # (tests/support/global-setup.ts). + services: + postgres: + image: postgres:18.6-alpine + env: + POSTGRES_USER: test + POSTGRES_PASSWORD: test + POSTGRES_DB: artista_test + options: >- + --health-cmd "pg_isready -U test" --health-interval 5s --health-retries 10 + env: + TEST_DATABASE_URL: postgresql://test:test@postgres:5432/artista_test + 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 test + build-and-push: + needs: [lint, typecheck, test] runs-on: docker-builder env: # Oeffentlicher Registry-Host (Pull durch Kubernetes, OCI-Labels). 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)"