From b2cbbb6390348730e51d586195e7f8c0e6b2a404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Lohrer?= Date: Sat, 18 Apr 2026 07:27:48 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20ci-setup-guide=20+=20status/handoff=20f?= =?UTF-8?q?=C3=BCr=20ci-phase=20aktualisiert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/github-ci-setup.md dokumentiert: - forgejo → github push-mirror - die 4 github-repository-secrets (bunker-url, author-pubkey-hex, bootstrap-relay, client-secret-hex) — letzteres identisch mit .env.local für stabile amber-app-identität - wie man sie rotiert - migrations-pfad weg von github (woodpecker, cron) status + handoff reflektieren: pipeline live, alle 18 posts publiziert, 91 bilder auf blossom, ci-setup steht, cutover als nächster schritt möglich. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/HANDOFF.md | 196 +++++++++++++++++----------------------- docs/STATUS.md | 52 ++++++----- docs/github-ci-setup.md | 87 ++++++++++++++++++ 3 files changed, 200 insertions(+), 135 deletions(-) create mode 100644 docs/github-ci-setup.md diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 12a4072..85b1a76 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -5,168 +5,140 @@ Dieses Dokument sagt: was ist der Zustand, was wartet, wo liegen die Fäden. ## Zustand (Details in `STATUS.md`) -Die SvelteKit-SPA unter `svelte.joerg-lohrer.de` ist **fertig und live**. -Alle 18 Posts haben jetzt **strukturierte Bild-Metadaten** im Frontmatter -(Commit `c023b59`, 91 Bilder). Der Publish-Pipeline-**Plan ist geschrieben** -(`docs/superpowers/plans/2026-04-16-publish-pipeline.md`, 24 Tasks in 12 Phasen). +**Die Nostr-Publish-Pipeline ist live.** Alle 18 Posts sind publiziert als +`kind:30023`-Events auf 5 Relays, 91 Bilder auf 2 Blossom-Servern. Die +SvelteKit-SPA unter `svelte.joerg-lohrer.de` rendert alles ordentlich. -**Als nächstes:** Pipeline implementieren, beginnend mit Task 1. +**Das inhaltliche Kernziel des Gesamtprojekts ist damit erreicht.** + +Der Rest sind Feinschliff- und Cutover-Aufgaben. ## Was als Nächstes ansteht -### Option 1 — Publish-Pipeline implementieren ⬅ empfohlen +### Option 1 — CI-End-to-End-Test ⬅ kleinstes Offene -**Warum:** Spec + Plan fertig, Content vorbereitet, alle Design-Entscheidungen -getroffen. Kann direkt losgehen. +**Voraussetzung erledigt:** Forgejo → GitHub Push-Mirror läuft, GitHub-Secrets +gesetzt (Details in `docs/github-ci-setup.md`). -**Nächster konkreter Schritt:** +**Noch zu tun:** +1. In GitHub → Actions → „Publish Nostr Events" → „Run workflow" → Branch + `main`. Erwartung: Pre-Flight grün, 0 Posts (kein Content-Diff), Exit 0. +2. Optional: Minimaler Edit in einem Post → commit → push → warten bis + Mirror auf GitHub synct → Workflow triggert automatisch → 1 Post als + `update` publiziert → Log-Artefakt prüfen. -``` -cd /Users/joerglohrer/repositories/joerglohrerde -``` +Danach ist Task 22 komplett abgeschlossen. -Dann den Plan öffnen: -``` -docs/superpowers/plans/2026-04-16-publish-pipeline.md -``` +### Option 2 — Cutover auf `joerg-lohrer.de` -Und Task 1 (Deno-Projekt-Grundgerüst) starten. Der Plan nutzt TDD; -jeder Task hat Test-First, Implementation, Commit. +**Voraussetzung:** Option 1 optional, aber nicht blockierend. Die Pipeline +läuft ja schon, ob manuell oder via CI ist für den Cutover egal. -**Ausführungsweisen:** -- **Subagent-Driven** (im Plan empfohlen): pro Task frischer Subagent, - Review zwischen Tasks. Skill: `superpowers:subagent-driven-development`. -- **Inline**: alles in einer Session. Skill: `superpowers:executing-plans`. +**Schritte:** +1. In All-Inkl KAS die Domain `joerg-lohrer.de` auf den SvelteKit-Webroot + umhängen (aktuell: `svelte.joerg-lohrer.de` → `/www/htdocs/v109928/joerglohrer28/` + oder welcher Ordner auch immer). +2. SvelteKit-SPA deployen, sofern sie nicht schon dort liegt. +3. Live-Check: `curl -sI https://joerg-lohrer.de/` → sollte die neue SPA + liefern, nicht mehr Hugo. -**Besonderheiten beim Plan:** -- Pipeline ist **Blaupause** für andere Nostr-Repos — keine - Projekt-Konstanten im Code, alles via Env. -- **Env-File:** nutzt `../.env.local` (Repo-Root), wo `BUNKER_URL`, - `AUTHOR_PUBKEY_HEX`, `BOOTSTRAP_RELAY` bereits stehen. -- **Blossom-only**: keine rsync/SSH-Altlasten mehr; alle Bilder (auch - die der 17 Altposts) werden zu Blossom hochgeladen. -- **Staging:** `staging.joerg-lohrer.de` zeigt auf `/www/htdocs/v109928/joerglohrer26/`. - Wird erst beim Cutover relevant — Pipeline selbst braucht es nicht. +Hugo-Altbestand bleibt als Archiv im `hugo-archive`-Branch. -**Vorarbeiten (bereits erledigt):** -- ✅ SSH-Zugang All-Inkl (Premium): `ssh-v109928@v109928.kasserver.com` - mit Deploy-Key `~/.ssh/id_ed25519_joerglohrerde_deploy` (auch im KAS - eingetragen). Wird jetzt allerdings nicht mehr für die Pipeline - gebraucht — Blossom-only. -- ✅ `.env.local` enthält alle Pipeline-Keys. -- ✅ Content-Migration (18 Posts × Bild-Metadaten) abgeschlossen. +### Option 3 — Menü-Navigation + Impressum in der SPA -### Option 2 — Menü-Navigation + Impressum auf der SPA +**Unabhängig von allem anderen**, kann parallel gemacht werden. -**Warum:** kleine UX-Ergänzung, die das SPA-Erlebnis runder macht. +- Header-Navigation in `app/src/routes/+layout.svelte` (Home, Archiv, Impressum, + Mastodon-Link) +- `/impressum/`-Route mit rechtlichem Text -- Header-Navigation in `app/src/routes/+layout.svelte` ergänzen (Home, Archiv, - Impressum, evtl. Mastodon-Link) -- `/impressum/`-Route anlegen mit rechtlichem Text -- ggf. Archives-Route als eigene Liste mit Gruppierung nach Jahr +**Aufwand:** 30–60 min. -**Aufwand:** ~30-60 min. +### Option 4 — Pipeline weg von GitHub (self-hosted CI) -### Option 3 — Cutover auf Hauptdomain +**Wann:** Wenn der Optiplex-Server steht und ein zentraler Ort für Dienste +existiert. -**Voraussetzung:** Option 1 abgeschlossen und alle 18 Posts als Events -publiziert, Bilder auf Blossom. +**Varianten:** +- **Cron / systemd-Timer** auf dem Optiplex, der alle X Minuten `git pull && + deno task publish` macht. Einfach, minimaler Setup. +- **Woodpecker-CI** als Docker-Container neben Forgejo. Volle Push-getriggerte + Pipeline ohne GitHub. -**Dann:** KAS → Domain `joerg-lohrer.de` auf den SvelteKit-Webroot -umhängen (derselbe wie `svelte.joerg-lohrer.de` oder `joerglohrer26/`, -je nach Entscheidung). - -Reihenfolge: **Option 1 → Option 3**, Option 2 kann parallel laufen. +Der Pipeline-Code selbst (`publish/src/**`) ist CI-agnostisch — nur die +Trigger-Konfiguration ändert sich. ## Schnell-Orientierung für die nächste Claude-Session Lies in dieser Reihenfolge: 1. `docs/STATUS.md` (5 min) 2. `docs/HANDOFF.md` (= dieses Dokument) -3. Für die Pipeline: `docs/superpowers/plans/2026-04-16-publish-pipeline.md` -4. Bei Design-Fragen: - - Publish-Pipeline: `docs/superpowers/specs/2026-04-15-publish-pipeline-design.md` - - Bild-Metadaten: `docs/superpowers/specs/2026-04-16-image-metadata-convention.md` - - SPA: `docs/superpowers/specs/2026-04-15-nostr-page-design.md` - -Nutze den Skill unter `.claude/skills/joerglohrerde-workflow.md` für -wiederkehrende Kommandos. +3. Für CI-Themen: `docs/github-ci-setup.md` +4. Für Pipeline-Fragen: `docs/superpowers/specs/2026-04-15-publish-pipeline-design.md` ## Dev-Kommandos ```sh -# Unit-Tests SPA (Vitest) +# SPA-Tests cd app && npm run test:unit - -# E2E-Tests SPA (Playwright) cd app && npm run test:e2e - -# Type-Check SPA cd app && npm run check - -# SPA-Dev-Server (Port 5173) cd app && npm run dev -# SPA-Production-Build + Deploy +# SPA-Build + Deploy cd app && npm run build && cd .. && ./scripts/deploy-svelte.sh + +# Publish-Pipeline +cd publish && deno task check # pre-flight +cd publish && deno task publish --dry-run # diff-modus simulation +cd publish && deno task publish # diff-modus echt +cd publish && deno task publish --force-all # alle posts +cd publish && deno task publish --post # einen post +cd publish && deno task test # 59 tests ``` -Publish-Pipeline-Kommandos (sobald implementiert): -```sh -cd publish && deno task check # Pre-Flight -cd publish && deno task publish --dry-run # Simulation -cd publish && deno task publish --post # einen Post -cd publish && deno task publish --force-all # alle Posts -cd publish && deno task test # Tests -``` - -## Manuelles Publishen (Übergang, bis Pipeline fertig) - -Siehe frühere Version dieses Dokuments. Bis die Pipeline läuft, gehen -neue Posts manuell über `nak event` raus. - ## Bekannte Stolperfallen -- **Amber-Bunker:** bei neuer Bunker-URL müssen globale Permissions in Amber - zurückgesetzt werden, sonst hängt `nak` auf den Signatur-Request. - Auto-Approve für `kind:30023` und `kind:24242` (Blossom-Auth) setzen. -- **Svelte 5 Runes:** `$props()`-Werte müssen via `$derived()` in lokale - Variablen, sonst `state_referenced_locally`-Warning. -- **applesauce-relay API:** ist RxJS-basiert. `pool.request(relays, filter)` - returned `Observable` (nicht die Tupel-`subscribe({next: msg - if msg[0]==='EVENT'})`-Form). -- **Slug-Normalisierung:** alle Frontmatter-Slugs sind lowercase (Commit - `d17410f`). Beim Publishen 1:1 übernehmen, keine Runtime-Transformation. -- **Dateiname mit Leerzeichen:** im Moodle-Post liegt `03-config generieren.png`. - Pipeline muss URL-Encoding im `rewriteImageUrls`-Helper korrekt umsetzen - (Test ist im Plan Task 5 vorgesehen). - -## Session-Kontext - -Hilfreich beim Wiedereinstieg mit Claude: -- Branch-Check: `git log --oneline -10 spa main hugo-archive` -- Live-Check: `curl -sI https://svelte.joerg-lohrer.de/` -- Publish-Status: `nak req -k 30023 -a 4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41 wss://relay.damus.io 2>/dev/null | jq -s 'length'` - (aktuell ~10, nach Pipeline-Lauf `--force-all`: 18) +- **Amber-Bunker:** bei neuer Bunker-URL müssen die zwei Permissions + (`get_public_key`, `sign_event`) in Amber auf „Allow + Always" gesetzt + werden, bevor Publish-Requests verarbeitet werden. Siehe + `docs/github-ci-setup.md` für Details. +- **`CLIENT_SECRET_HEX`** in `.env.local` identisch mit GitHub-Secret — + sorgt dafür, dass sich beide Umgebungen bei Amber mit derselben App + anmelden. Rotieren nur bei bewusstem Neu-Pairing in Amber. +- **`relay.damus.io`** bestätigt Events manchmal nicht mit `OK`. Bekanntes + Damus-Verhalten, wird toleriert (MIN_RELAY_ACKS=2, andere 4 Relays sind + zuverlässig). +- **Svelte 5 Runes:** `$props()`-Werte via `$derived()` in lokale Variablen. +- **Hugo-quotierte Dates:** `date: "2023-02-26"` ist ein YAML-String, nicht + ein Date-Objekt. `validatePost` coerced das automatisch; in neuen Posts + am besten ohne Quotes schreiben. ## Offene UNKNOWN-Einträge zur späteren Recherche -Im VR-Post (`content/posts/2021-08-15-virtual-reality/index.md`) -sind 4 Bilder als `license: UNKNOWN / authors: UNKNOWN` markiert: +Im VR-Post (`content/posts/2021-08-15-virtual-reality/index.md`) sind +4 Bilder als `license: UNKNOWN / authors: UNKNOWN` markiert: - `01-immersion-wikipedia.jpg` (Wikipedia-Screenshot) - `02-mittelalterliche-kirche.jpg` (Sketchfab — Lizenz ist CC BY-NC, Fotograf fehlt) - `03-avatare-erstellen.jpg` (Ready Player Me) - `05-pupillendistanz.jpg` (EyeMeasure iOS App) -Pipeline loggt beim Publishen eine Warnung pro UNKNOWN, publisht aber -trotzdem (Phase-1-Default: `STRICT_MODE=false`). Die Recherche-Todo-Liste -steht in `docs/redaktion-bild-metadaten.md`. +Pipeline loggt Warnungen, publisht aber trotzdem. Recherche-Notizen in +`docs/redaktion-bild-metadaten.md`. + +## Session-Kontext + +Hilfreich beim Wiedereinstieg mit Claude: +- Branch-Check: `git log --oneline -10 spa main` +- Live-Check SPA: `curl -sI https://svelte.joerg-lohrer.de/` +- Event-Count: `nak req -k 30023 -a 4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41 wss://relay.primal.net 2>/dev/null | jq -s 'length'` → 18 +- Pipeline-Tests: `cd publish && deno task test` → 59 grün ## Community-Wiki-Entwürfe -Noch nicht extern veröffentlicht, liegen im Repo bereit: +Liegen im Repo, noch nicht extern veröffentlicht: - `docs/wiki-entwurf-nostr-bild-metadaten.md` — DE - `docs/wiki-draft-nostr-image-metadata.md` — EN -Können in die Nostr-Community eingebracht werden (z. B. als NIP-Proposal -oder auf nostrbook.dev), sobald die Pipeline sie in der Praxis validiert. +Können als NIP-Proposal oder auf nostrbook.dev eingebracht werden, jetzt wo +die Konvention in der Praxis validiert ist. diff --git a/docs/STATUS.md b/docs/STATUS.md index 8780df8..0bf557d 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,6 +1,6 @@ # Projekt-Status: joerg-lohrer.de → Nostr-basierte SPA -**Stand:** 2026-04-16 +**Stand:** 2026-04-18 ## Kurzfassung @@ -26,17 +26,16 @@ bis die Publish-Pipeline steht und der Cutover auf die Hauptdomain erfolgt. - **Autoren-Pubkey:** `npub1f7jar3qnu269uyx5p0e4v24hqxjnxysxudvujza2ur5ehltvdeqsly2fx9` (hex: `4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41`) -- **Publizierte Events:** ~10 Langform-Posts (`kind:30023`). Die restlichen - 8 Posts warten auf die Publish-Pipeline (Events werden beim ersten - `deno task publish --force-all`-Lauf erzeugt). +- **Publizierte Events:** **18 Langform-Posts** (`kind:30023`) — alle Altposts + via Publish-Pipeline migriert (Commit `0c6fdd1`, Log in + `docs/publish-logs/2026-04-18-force-all-migration.json`). - **Relay-Liste** (`kind:10002`): `relay.damus.io`, `nos.lol`, `relay.primal.net`, `relay.tchncs.de`, `relay.edufeed.org` - **Blossom-Server** (`kind:10063`): `blossom.edufeed.org`, `blossom.primal.net` -Bisher nur die Bilder des `dezentrale-oep-oer`-Posts auf Blossom. **Designentscheidung -2026-04-16:** Alle Bilder (inkl. der 17 Altpost-Bilder) kommen via Publish-Pipeline -auf Blossom — kein rsync-Legacy-Pfad mehr, kein `image_source: legacy`-Flag. -Einheitlicher Render-Pfad in der SPA. +**91 Bilder** auf beiden Blossom-Servern. Alle Events enthalten hash-basierte +Blossom-URLs. SPA rendert alle Posts einheitlich — kein Legacy-Pfad, keine +rsync-Artefakte. ## Repo-Struktur @@ -87,29 +86,36 @@ Alles in `.env.local` — gitignored, nicht committet. ## Offene Punkte -- **Publish-Pipeline** — Spec + Plan vollständig, **Implementierung steht an** - (Task 1 aus `docs/superpowers/plans/2026-04-16-publish-pipeline.md`). +- **CI-Mirror** Forgejo → GitHub eingerichtet, GitHub-Secrets gesetzt. + End-to-End-Test (Content-Commit, Workflow-Trigger, CI-Lauf) noch offen. + Später: Migration auf Woodpecker oder Cron auf Optiplex möglich + (siehe `docs/github-ci-setup.md`). - **Menü-Navigation** in der SPA (Home / Archiv / Impressum / Kontakt) - **Impressum-Seite** (braucht rechtlichen Text) -- **Cutover auf `joerg-lohrer.de`** (nach Pipeline-Live: Hauptdomain - bekommt die SvelteKit-SPA) +- **Cutover auf `joerg-lohrer.de`** (Pipeline läuft, Voraussetzung erfüllt; + Hauptdomain kann auf SvelteKit-SPA umgestellt werden) +- **5 UNKNOWN-Einträge** im `virtual-reality`-Post zur späteren Recherche + (Wikipedia-Screenshot, Sketchfab-Fotograf, Ready-Player-Me, EyeMeasure-App) ## Erledigt seit 2026-04-15 - ✅ Content-Migration: alle 18 Posts haben strukturierte `images:`-Liste im Frontmatter (91 Bilder, mit Alt-Text, Lizenz, Autor:innen, ggf. Caption und Modifications). Commit `c023b59`. -- ✅ Erlebnispädagogik-Post: tote Amazon-Hotlinks entfernt, Literatur- - Liste aufgeräumt. -- ✅ Design-Entscheidung „Blossom-only" dokumentiert in Spec - `docs/superpowers/specs/2026-04-15-publish-pipeline-design.md`. -- ✅ Publish-Pipeline-Plan (24 Tasks, Blaupausen-tauglich) geschrieben: - `docs/superpowers/plans/2026-04-16-publish-pipeline.md`. -- ✅ Bild-Metadaten-Konvention (Phase 1) in Spec: - `docs/superpowers/specs/2026-04-16-image-metadata-convention.md`. -- ✅ Community-Wiki-Entwürfe (DE + EN) für Nostr-Bildattribution: - `docs/wiki-entwurf-nostr-bild-metadaten.md` + `-draft-nostr-image-metadata.md`. -- ✅ 5 `UNKNOWN`-Einträge im VR-Post zur Recherche markiert (bleiben erstmal so). +- ✅ Erlebnispädagogik-Post: tote Amazon-Hotlinks entfernt. +- ✅ Spec, Plan und Bild-Metadaten-Konvention geschrieben. +- ✅ Community-Wiki-Entwürfe (DE + EN) für Nostr-Bildattribution. +- ✅ **Publish-Pipeline komplett implementiert**, 22 Tasks aus dem Plan: + - 18 Code-Tasks (Phase 1–6), 59 Unit-Tests grün + - Stabile NIP-46-Anbindung via `CLIENT_SECRET_HEX` für wiederverwendbare + App-Identität in Amber + - `validatePost` akzeptiert auch string-dates (für Hugo-Kompatibilität) +- ✅ **Alle 18 Altposts publiziert** als `kind:30023`-Events (Commit `0c6fdd1`, + Log in `docs/publish-logs/2026-04-18-force-all-migration.json`). +- ✅ **91 Bilder** auf beiden Blossom-Servern. +- ✅ SPA rendert alle Posts mit Bildern von Blossom (visuell verifiziert). +- ✅ **GitHub-Actions-Workflow** angelegt (`.github/workflows/publish.yml`). +- ✅ Forgejo → GitHub Push-Mirror eingerichtet, GitHub-Secrets gesetzt. ## Live-Verifikation diff --git a/docs/github-ci-setup.md b/docs/github-ci-setup.md new file mode 100644 index 0000000..cbee542 --- /dev/null +++ b/docs/github-ci-setup.md @@ -0,0 +1,87 @@ +# GitHub-CI-Setup für die Publish-Pipeline + +**Kontext:** Das primäre Repo liegt in **Forgejo** (self-hosted). Für CI nutzen +wir GitHub als **Push-Mirror-Ziel**, weil Forgejo keine Woodpecker-Integration +hat. GitHub Actions triggert automatisch bei Push auf `main` mit Änderungen +unter `content/posts/**`. + +## Setup-Schritte + +### 1. Forgejo → GitHub Push-Mirror + +In Forgejo: +- Repo → **Settings → Mirrors → Push Mirror hinzufügen** +- Ziel-URL: das entsprechende GitHub-Repo (z. B. `https://github.com//joerglohrerde.git`) +- Authentifizierung: GitHub-Personal-Access-Token (`repo`-Scope) +- Intervall: nach Belieben (z. B. alle 8 Stunden, oder „bei jedem Push") + +### 2. GitHub-Repository-Secrets + +In GitHub, Repo → **Settings → Secrets and variables → Actions**: + +Vier Repository-Secrets anlegen (nicht Environment-Secrets — wir haben keine Environments): + +| Name | Wert | Quelle | +|---|---|---| +| `BUNKER_URL` | `bunker://?relay=wss://...&secret=...` | aus `.env.local` | +| `AUTHOR_PUBKEY_HEX` | `4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41` | aus `.env.local` | +| `BOOTSTRAP_RELAY` | `wss://relay.primal.net` | aus `.env.local` | +| `CLIENT_SECRET_HEX` | `929f0cd946fd5266e63ccdb066ce7a0cc93391133bfce6098fe633fc72e03e96` | aus `.env.local` | + +**Wichtig:** +- Alle vier **müssen** gesetzt sein, sonst schlägt der Workflow fehl. +- Der `CLIENT_SECRET_HEX` ist **identisch** mit dem in `.env.local` — damit sich + CI-Runner und lokaler Rechner bei Amber mit **derselben Client-Identität** + anmelden. Die Permissions in Amber gelten dann für beide. + +### 3. Workflow-Datei + +Liegt in `.github/workflows/publish.yml`. Triggert auf: +- `push` auf `main` mit Änderungen unter `content/posts/**` +- `workflow_dispatch` (manuelles Triggern über das GitHub-UI, optional mit `force_all=true`) + +### 4. Secrets rotieren + +Wenn der Bunker-Pairing-Secret mal kompromittiert wird oder Amber neu +eingerichtet wird: + +1. In Amber neue Bunker-URL erzeugen +2. Lokale `.env.local` aktualisieren +3. GitHub-Secret `BUNKER_URL` ebenfalls aktualisieren (Settings → Secrets → edit) +4. In Amber für die neue App wieder "Allow + Always" für + `get_public_key` + `sign_event` setzen + +Der `CLIENT_SECRET_HEX` muss in der Regel **nicht** rotiert werden — nur wenn +du die App in Amber komplett neu pairen willst. Wenn du ihn doch änderst, muss +Amber die App neu registrieren (siehe Setup). + +## Monitoring + +- **Workflow-Runs:** GitHub → Actions → "Publish Nostr Events" +- **Logs pro Run:** pro Run ein Artefakt `publish-log` mit der `publish-*.json`, + 30 Tage Aufbewahrung +- **Lokal laufen bleibt möglich** via `cd publish && deno task publish …` — + CI ist eine zusätzliche Automatisierung, kein Zwang. + +## Bekannte Einschränkungen + +- **Amber muss online sein** während CI-Runs, sonst scheitert die Bunker- + Signatur. Wenn das Handy tot ist: Workflow failed → einfach neu triggern, + sobald Amber wieder erreichbar. +- **`relay.damus.io`** antwortet gelegentlich nicht mit OK; das ist + ein bekanntes Damus-Verhalten und wird von `MIN_RELAY_ACKS=2` toleriert. +- **Staging-Subdomain (`staging.joerg-lohrer.de`)** hat nichts mit dieser + Pipeline zu tun — sie gehört zum SPA-Deploy. Die Publish-Pipeline nutzt + ausschließlich Blossom für Bild-Hosting. + +## Migration weg von GitHub (später) + +Wenn Woodpecker oder ein anderer self-hosted Runner aufgesetzt wird, bleibt +der Deno-Workflow derselbe — nur die CI-Konfiguration ändert sich: + +- `.github/workflows/publish.yml` → `.woodpecker.yaml` (oder `.gitea/workflows/`) +- Secrets in Woodpecker statt GitHub +- Trigger-Bedingungen analog (push main + path filter) + +Der Pipeline-Code selbst (`publish/src/**`) ist CI-agnostisch und braucht keine +Änderung.