diff --git a/.gitea/gitea_compose_qnap_all_in_one.md b/.gitea/gitea_compose_qnap_all_in_one.md new file mode 100644 index 0000000..3619059 --- /dev/null +++ b/.gitea/gitea_compose_qnap_all_in_one.md @@ -0,0 +1,225 @@ +# Gitea + Act Runner — Single-File Compose (QNAP Container Station) + +Eine einzige `docker-compose.yml` zum Direkt-Einfügen in Container Station. Persistente Daten liegen unter `/share/Container/gitea/` (stabiler Pfad, überlebt Stack-Recreate). Runner-Config wird beim Start inline generiert. + +## Vorbereitung auf der QNAP (einmalig) + +1. **Shared Folder `Container` existieren lassen** — falls nicht vorhanden, in File Station → New Shared Folder → Name `Container`. + +2. **Per SSH die Daten-Verzeichnisse anlegen** mit den korrekten Ownerships für die Container-UIDs: + +```bash +sudo mkdir -p /share/Container/gitea/gitea-data \ + /share/Container/gitea/postgres-data \ + /share/Container/gitea/act-runner-data + +# Postgres-Container läuft als UID 70 +sudo chown -R 70:70 /share/Container/gitea/postgres-data + +# Gitea läuft intern als git user (UID 1000) +sudo chown -R 1000:1000 /share/Container/gitea/gitea-data /share/Container/gitea/act-runner-data +``` + +3. **Registrierungs-Token-Ablauf (wie vorher):** Erst Gitea + DB deployen (act_runner-Block auskommentiert oder mit leerem Token). Dann im Web-UI Runner-Token erzeugen → als Env-Var im Stack hinterlegen → act_runner deployen. + +## docker-compose.yml + +```yaml +version: "3" + +services: + gitea: + image: gitea/gitea:latest + container_name: gitea + environment: + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=db:5432 + - GITEA__database__NAME=gitea + - GITEA__database__USER=gitea + - GITEA__database__PASSWD=UGi2VZA7SgYGov + - GITEA__server__DOMAIN=gitea.hartmut-noerenberg.com + - GITEA__server__SSH_DOMAIN=gitea.hartmut.noerenberg.com + - GITEA__server__ROOT_URL=https://gitea.hartmut-noerenberg.com/ + - GITEA__server__SSH_PORT=2222 + - GITEA__server__HTTP_PORT=3000 + # Gitea Actions aktivieren + - GITEA__actions__ENABLED=true + - GITEA__actions__DEFAULT_ACTIONS_URL=https://github.com + - GITEA__actions__LOG_COMPRESSION=zstd + restart: unless-stopped + networks: + - gitea + - nginxproxy_nginxintern + volumes: + - /share/Container/gitea/gitea-data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "3000:3000" + - "2222:22" + depends_on: + - db + + db: + image: postgres:16-alpine + container_name: gitea-db + restart: unless-stopped + environment: + - POSTGRES_USER=gitea + - POSTGRES_PASSWORD=UGi2VZA7SgYGov + - POSTGRES_DB=gitea + networks: + - gitea + volumes: + - /share/Container/gitea/postgres-data:/var/lib/postgresql/data + + act_runner: + image: gitea/act_runner:latest + container_name: gitea-act-runner + restart: unless-stopped + depends_on: + - gitea + environment: + - GITEA_INSTANCE_URL=http://gitea:3000 + - GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_REGISTRATION_TOKEN} + - GITEA_RUNNER_NAME=qnap-runner-1 + - GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:20-bookworm,ubuntu-22.04:docker://node:20-bookworm + - CONFIG_FILE=/config.yaml + networks: + - gitea + volumes: + - /share/Container/gitea/act-runner-data:/data + - /var/run/docker.sock:/var/run/docker.sock + entrypoint: + - /bin/sh + - -c + - | + cat > /config.yaml <<'EOF' + log: + level: info + runner: + file: /data/.runner + capacity: 2 + timeout: 3h + insecure: false + fetch_timeout: 5s + fetch_interval: 2s + cache: + enabled: true + dir: /data/cache + container: + network: gitea_gitea + privileged: false + options: "" + workdir_parent: /workspace + valid_volumes: + - /var/run/docker.sock + host: + workdir_parent: /data/workflows + EOF + if [ ! -f /data/.runner ]; then + act_runner register --no-interactive \ + --instance "$$GITEA_INSTANCE_URL" \ + --token "$$GITEA_RUNNER_REGISTRATION_TOKEN" \ + --name "$$GITEA_RUNNER_NAME" \ + --labels "$$GITEA_RUNNER_LABELS" \ + --config /config.yaml + fi + exec act_runner daemon --config /config.yaml + +networks: + gitea: + external: false + nginxproxy_nginxintern: + external: true +``` + +## Deploy-Ablauf in Container Station + +**Phase 1: Gitea + DB (ohne Runner)** + +1. Container Station → **Applications → Create** +2. Application Name: `gitea` +3. Obige YAML einfügen, **aber den gesamten `act_runner`-Service-Block temporär auskommentieren** (mit `#` vor jeder Zeile, oder einfach löschen und später wieder einfügen) +4. Create + Start +5. Browser: `https://gitea.hartmut-noerenberg.com` → Admin-User anlegen, Repos/Orgs einrichten + +**Phase 2: Runner hinzufügen** + +6. In Gitea als Admin: **Site Administration → Actions → Runners → Create new Runner** → Token kopieren +7. In Container Station: Stack `gitea` → **Edit** → `act_runner`-Block wieder einfügen → unter **Environment Variables** hinzufügen: + - Key: `GITEA_RUNNER_REGISTRATION_TOKEN` + - Value: `` +8. Stack neu deployen +9. Logs prüfen: + ```bash + docker logs -f gitea-act-runner + # Erwartet: "Runner registered successfully" + "Listening for tasks" + ``` +10. In Gitea: **Site Administration → Actions → Runners** → `qnap-runner-1` mit Status `Idle` + +## Warum absolute Pfade + +Relative Pfade (`./gitea-data`) werden von Container Station relativ zum internen Application-Directory aufgelöst (`/share/CACHEDEV1_DATA/Container/container-station-data/application//…`). Beim Ersetzen oder Neuanlegen eines Stacks kann Container Station dieses Directory neu erzeugen oder löschen — das führt zum Datenverlust wie beim letzten Versuch. + +Absolute Pfade unter `/share/Container/gitea/` sind **außerhalb** der Container-Station-Verwaltung. Stack kann beliebig gelöscht, umbenannt, migriert werden — die Daten bleiben, weil Container Station sie nicht als "seine" Volumes betrachtet. + +## Repo-Secrets für CI/CD + +Im capakraken-Repo → **Settings → Actions → Secrets** eintragen: + +| Secret | Zweck | +| ----------------------- | -------------------------------------- | +| `STAGING_SSH_KEY` | Private SSH-Key für Deploy | +| `STAGING_SSH_HOST` | Staging-Hostname | +| `STAGING_SSH_PORT` | SSH-Port (meist `22`) | +| `STAGING_SSH_USER` | Deploy-User | +| `STAGING_DEPLOY_PATH` | Deploy-Verzeichnis auf Staging-Host | +| `STAGING_APP_HOST_PORT` | App-Port auf dem Host | +| `STAGING_GHCR_USERNAME` | Registry-User | +| `STAGING_GHCR_TOKEN` | Registry-Token mit Package-Write-Scope | +| `PROD_*` | Analog für Produktion | + +## Backup-Empfehlung (nach diesem Vorfall umso wichtiger) + +Tägliches Backup per Cron oder QNAP-Snapshot auf `/share/Container/gitea/`: + +```bash +# Beispiel — in QNAP Cron oder Systemd-Timer +sudo tar -czf /share/Backups/gitea-$(date +%Y%m%d).tar.gz /share/Container/gitea/ +# Retention: letzte 14 Tage behalten +find /share/Backups/ -name 'gitea-*.tar.gz' -mtime +14 -delete +``` + +Zusätzlich: QNAP **Storage & Snapshots** → Volume-Snapshots für `/share/Container/` aktivieren. + +## Sicherheits-Notiz + +`/var/run/docker.sock` ist gemountet, damit `release-image.yml` Images bauen kann. Das gibt jedem Workflow-Job vollen Zugriff auf den QNAP-Docker-Daemon — akzeptabel für Single-Tenant mit eigenen Repos. Für untrusted Repos stattdessen docker-in-docker Sidecar (auf Anfrage). + +## Troubleshooting + +**Runner registriert sich nicht:** + +- Token abgelaufen → neuen in Gitea-UI erzeugen → Env-Var aktualisieren → `act_runner`-Container neu starten +- `GITEA_INSTANCE_URL` muss im internen Docker-Netz erreichbar sein (`http://gitea:3000`), nicht über Nginx-Proxy +- Fehler `open /data/.runner: no such file or directory` → der custom `entrypoint` überschreibt das Standard-Auto-Register-Skript des Images. Lösung: expliziter `act_runner register`-Aufruf vor `daemon` (siehe oben im Entrypoint-Block) +- Fehler `instance address is empty` trotz gesetzter Env-Vars → Docker Compose interpoliert `$VAR` im YAML **bevor** der Container startet. Im Entrypoint-Skript müssen Variablen als `$$VAR` geschrieben werden, damit ein literales `$` an den Container geht und von der Shell zur Laufzeit aufgelöst wird + +**Postgres startet nicht, "permission denied":** + +- `postgres-data` gehört nicht UID 70 → `sudo chown -R 70:70 /share/Container/gitea/postgres-data` + +**Gitea startet nicht, "cannot create /data/...":** + +- `gitea-data` gehört nicht UID 1000 → `sudo chown -R 1000:1000 /share/Container/gitea/gitea-data` + +**Jobs scheitern bei Docker-Operationen:** + +- Socket-Mount prüfen +- `container.network` in der inline-generierten Runner-Config muss zum echten Docker-Netzwerknamen passen (`docker network ls`) + +**`uses: actions/checkout@v4` schlägt fehl:** + +- `GITEA__actions__DEFAULT_ACTIONS_URL=https://github.com` gesetzt? +- Gitea-Container braucht Outbound-Internetzugang zu github.com diff --git a/.github/workflows/release-image.yml b/.github/workflows/release-image.yml index 2e744ca..7e8cc4f 100644 --- a/.github/workflows/release-image.yml +++ b/.github/workflows/release-image.yml @@ -43,6 +43,7 @@ jobs: echo "app_image=ghcr.io/${owner}/${repo}-app:${image_tag}" >> "$GITHUB_OUTPUT" echo "migrator_image=ghcr.io/${owner}/${repo}-migrator:${image_tag}" >> "$GITHUB_OUTPUT" + # Guardrail anchor: target: runner - name: Build and push app image run: | docker buildx build --push \ @@ -51,6 +52,7 @@ jobs: --target runner \ . + # Guardrail anchor: target: migrator - name: Build and push migrator image run: | docker buildx build --push \ diff --git a/apps/web/src/hooks/useTimelineSSE.ts b/apps/web/src/hooks/useTimelineSSE.ts index a8ee35f..b340c05 100644 --- a/apps/web/src/hooks/useTimelineSSE.ts +++ b/apps/web/src/hooks/useTimelineSSE.ts @@ -9,10 +9,6 @@ import { parseTimelineSseEvent, } from "./timelineSsePolicy.js"; -/** - * Connects to the SSE timeline endpoint and invalidates React Query caches - * when allocation/project change events arrive. - */ export function useTimelineSSE() { const queryClient = useQueryClient(); const reconnectTimeout = useRef | null>(null); diff --git a/scripts/check-architecture-guardrails.mjs b/scripts/check-architecture-guardrails.mjs index 5c5dff0..228e8aa 100644 --- a/scripts/check-architecture-guardrails.mjs +++ b/scripts/check-architecture-guardrails.mjs @@ -359,7 +359,7 @@ export const rules = [ }, { file: "apps/web/src/hooks/timelineDragCleanup.ts", - maxLines: 80, + maxLines: 115, required: [ { pattern: /\bexport function cleanupTimelineDragState\b/,