Struktur, erster Artikel, Publish-Workflow

This commit is contained in:
Jeuner 2026-08-28 21:31:58 +02:00
commit fe9cc8f492
5 changed files with 312 additions and 0 deletions

90
.github/workflows/publish.yml vendored Normal file
View file

@ -0,0 +1,90 @@
name: publish
# Jeder neue Artikel unter artikel/ wird zu:
# - einem Release (taggt den Artikel, erzeugt automatisch einen Atom-Feed-Eintrag)
# - einer Discussion in der Kategorie Announcements (der Kommentarthread)
#
# Veroeffentlichen ist damit: git push.
on:
push:
branches: [main]
paths:
- 'artikel/**'
permissions:
contents: write
discussions: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Neue Artikel veroeffentlichen
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BEFORE: ${{ github.event.before }}
AFTER: ${{ github.sha }}
CATEGORY: ${{ vars.DISCUSSION_CATEGORY || 'Announcements' }}
run: |
set -euo pipefail
# Erster Push oder Force-Push: BEFORE ist unbrauchbar, dann alles nehmen.
if git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then
FILES="$(git diff --name-only --diff-filter=A "${BEFORE}" "${AFTER}" -- artikel | grep '\.md$' || true)"
else
FILES="$(git ls-files -- artikel | grep '\.md$' || true)"
fi
if [ -z "${FILES}" ]; then
echo "Keine neuen Artikel in diesem Push."
exit 0
fi
echo "Gefunden:"
echo "${FILES}"
while IFS= read -r file; do
[ -n "$file" ] || continue
[ -f "$file" ] || continue
slug="$(basename "$file" .md)"
# Vorlagen und Entwuerfe (fuehrender Unterstrich) werden nicht veroeffentlicht.
case "$slug" in
_*) echo "Vorlage/Entwurf ${file}, uebersprungen."; continue ;;
esac
tag="artikel/${slug}"
if gh release view "$tag" >/dev/null 2>&1; then
echo "Release ${tag} existiert bereits, uebersprungen."
continue
fi
# Titel aus dem Frontmatter, sonst aus der ersten H1, sonst der Slug.
title="$(sed -n 's/^title:[[:space:]]*//p' "$file" | head -n1 | sed 's/^"//; s/"$//')"
if [ -z "$title" ]; then
title="$(sed -n 's/^# //p' "$file" | head -n1)"
fi
[ -n "$title" ] || title="$slug"
# Frontmatter aus den Release-Notes entfernen.
body="$(mktemp)"
awk '
NR==1 && $0=="---" { fm=1; next }
fm==1 && $0=="---" { fm=0; next }
fm!=1 { print }
' "$file" > "$body"
echo "Veroeffentliche: ${title} -> ${tag}"
gh release create "$tag" \
--title "$title" \
--notes-file "$body" \
--discussion-category "$CATEGORY"
done <<< "${FILES}"

64
README.md Normal file
View file

@ -0,0 +1,64 @@
# Logbuch
Notes and articles on sovereign, local-first AI infrastructure.
Written in German. Discussion happens in this repo, not on a platform that can revoke it.
**[Discussions](../../discussions)** ... comment, argue, ask.
**[Feed](../../releases.atom)** ... every article as an Atom entry. No account needed.
---
## Warum hier und nicht auf einer Plattform
Der Text liegt in Git. Er ist versioniert, diffbar, exportierbar und gehoert dem Autor.
Kein Feed-Algorithmus entscheidet, wer ihn sieht. Kein Moderationsteam kann ihn entfernen.
Der Preis dafuer ist Reichweite: hier findet dich niemand von selbst. Das ist bewusst so.
---
## Artikel
<!-- Neue Eintraege oben anfuegen, Format: - [Titel](artikel/DATEI.md) ... ein Satz -->
- [Souveraenitaet ist eine Betriebsentscheidung, keine Haltung](artikel/2026-08-27-souveraenitaet-ist-eine-betriebsentscheidung.md) ... warum "lokal" erst dann zaehlt, wenn es den Ausfall ueberlebt.
---
## Ablauf
Veroeffentlichen ist ein Push:
```bash
cp artikel/_vorlage.md artikel/2026-09-01-mein-thema.md
$EDITOR artikel/2026-09-01-mein-thema.md
git add artikel/2026-09-01-mein-thema.md
git commit -m "Artikel: mein Thema"
git push
```
Der Workflow in `.github/workflows/publish.yml` macht daraus:
1. ein Release mit dem Tag `artikel/<slug>` ... damit landet der Text automatisch im Atom-Feed
2. eine Discussion in der Kategorie *Announcements* ... das ist der Kommentarthread
Dateiname ist die Adresse: `JJJJ-MM-TT-slug.md`. Kein Datum im Titel noetig, das Datum steht vorn.
---
## Struktur
```
artikel/ die Texte, ein Markdown pro Artikel
_vorlage.md Kopiervorlage, wird vom Workflow ignoriert (Unterstrich)
.github/
workflows/
publish.yml Release + Discussion pro neuem Artikel
preview.sh lokale Vorschau, sucht sich einen freien Port
```
---
## Lizenz
Texte: CC BY 4.0. Code in diesem Repo: MIT.
Uebernehmen, uebersetzen, widersprechen ... alles erlaubt, Nennung genuegt.

View file

@ -0,0 +1,64 @@
---
title: "Souveränität ist eine Betriebsentscheidung, keine Haltung"
date: 2026-08-27
tags: [souveraenitaet, lokale-ki, on-premise]
---
# Souveränität ist eine Betriebsentscheidung, keine Haltung
Digitale Souveränität wird meistens als Gesinnung verhandelt. Sie ist aber eine
Betriebsfrage, und Betriebsfragen entscheiden sich nicht in Manifesten, sondern im
Störfall. Die brauchbare Prüfung lautet: Was von deinem System läuft weiter, wenn
der Anbieter morgen früh den Zugang sperrt, ohne Begründung und ohne Ansprechpartner?
## Der Befund
Sperrungen sind kein Randfall. Ein Konto wird eingeschränkt, der Einspruch verweist
auf einen allgemeinen Verstoß gegen die Nutzungsbedingungen, ein konkreter Grund wird
nicht genannt, ein Mensch antwortet nicht. Das ist der reguläre Ablauf, nicht die
Ausnahme. Wer seine Reichweite, seine Texte und seine Kundenkommunikation in dieselbe
Infrastruktur gelegt hat, verliert in dem Moment alles drei gleichzeitig.
Dasselbe Muster gilt für KI-Systeme. Ein Modell hinter einer API ist kein Bestandteil
deines Systems, sondern eine Zusage. Zusagen werden geändert: Preise, Kontingente,
Modellversionen, zulässige Anwendungsfälle. Jede dieser Änderungen kann eine
Anwendung, die im Betrieb steht, von einem Tag auf den anderen unbrauchbar machen.
## Was daraus folgt
Souveränität heißt nicht, alles selbst zu bauen. Sie heißt, die Abhängigkeiten zu
sortieren, und zwar nach einem einzigen Kriterium: Ist der Ausfall verkraftbar oder
existenziell?
Ein Beispiel aus der Praxis. Ein Telefonagent für eine Hausarztpraxis nimmt Anrufe
entgegen, transkribiert lokal, sortiert das Anliegen in Termin, Rezept, Überweisung
oder Notfall und legt es strukturiert ab. Die Spracherkennung läuft auf dem Gerät,
die Kategorisierung ebenfalls. Nicht weil lokale Modelle in jedem Benchmark gewinnen,
sondern weil Patientendaten den Rechner nicht verlassen dürfen und weil die Praxis
auch dann noch Anrufe annehmen muss, wenn die Leitung nach draußen tot ist.
Die Rechnung ist unspektakulär. Ein Mac mit genug Speicher steht in der Praxis. Er
kostet einmal Geld statt monatlich. Er kennt keine Kontingente. Er wird nicht
abgeschaltet, weil jemand in einem anderen Land eine Richtlinie ändert. Der Preis
dafür ist Arbeit: Updates, Monitoring, Löschfristen, Notfallpfade. Diese Arbeit ist
der eigentliche Inhalt des Wortes Souveränität. Wer sie nicht einplant, hat kein
souveränes System, sondern ein unbetreutes.
## Was offen bleibt
Die harte Grenze ist Kapazität. Lokale Modelle sind für Klassifikation, Transkription
und strukturierte Extraktion längst ausreichend. Für lange Argumentationsketten sind
sie es oft nicht. Wer behauptet, man brauche nie etwas Größeres, verkauft etwas.
Ehrlicher wäre: Trenne die Ebenen. Alles, was Identität, Vertrauen und
personenbezogene Daten berührt, bleibt lokal, ohne Ausnahme. Alles andere darf nach
draußen, solange der Ausfall dieses Teils das System nicht anhält, sondern nur
langsamer macht.
Der Teil, für den ich noch keine gute Antwort habe: Wie prüft man das nach, ohne dem
Betreiber einfach zu glauben? Nachweisbarkeit ohne Vertrauensvorschuss ist das
eigentlich ungelöste Problem, nicht die Frage, wo die GPU steht.
---
*Widerspruch willkommen. Der Diskussionsthread hängt an diesem Release.*

27
artikel/_vorlage.md Normal file
View file

@ -0,0 +1,27 @@
---
title: "Titel des Artikels"
date: 2026-09-01
tags: [souveraenitaet, lokale-ki]
---
# Titel des Artikels
Erster Absatz: die These, in zwei Saetzen, ohne Anlauf. Wer nach dem ersten Absatz
nicht weiss, worum es geht und warum es ihn angeht, liest nicht weiter.
## Der Befund
Was ist der Fall. Konkret, mit Zahlen oder mit einem Vorgang, den man nachvollziehen kann.
## Was daraus folgt
Die eigentliche Arbeit. Hier steht das Argument, nicht die Meinung.
## Was offen bleibt
Ehrlich. Was hast du nicht geloest, was weisst du nicht, wo koenntest du falsch liegen.
Das ist der Absatz, an dem sich zeigt, ob jemand mitdenkt oder nur nickt.
---
*Diskussion zu diesem Text: siehe verlinkter Thread unter diesem Release.*

67
preview.sh Executable file
View file

@ -0,0 +1,67 @@
#!/usr/bin/env bash
#
# Lokale Vorschau der Artikel.
#
# ./preview.sh -> einfacher Markdown-Server (Python, keine Abhaengigkeiten)
# ./preview.sh jekyll -> Jekyll, falls GitHub Pages genutzt wird
# PORT=4100 ./preview.sh -> Startport vorgeben
#
# Der Port wird vor dem Start geprueft. Ist er belegt, wird der naechste freie genommen,
# statt den laufenden Dienst abzuschiessen oder mit "address already in use" zu sterben.
#
set -euo pipefail
MODE="${1:-simple}"
PORT="${PORT:-4000}"
MAX_TRIES="${MAX_TRIES:-20}"
port_is_free() {
local p="$1"
if command -v lsof >/dev/null 2>&1; then
! lsof -nP -iTCP:"$p" -sTCP:LISTEN >/dev/null 2>&1
elif command -v ss >/dev/null 2>&1; then
! ss -ltnH "sport = :$p" 2>/dev/null | grep -q .
elif command -v netstat >/dev/null 2>&1; then
! netstat -an 2>/dev/null | grep -qE "[.:]${p}[[:space:]].*LISTEN"
else
# Letzter Ausweg: Verbindungsversuch. Klappt er, hoert dort jemand zu.
! (exec 3<>"/dev/tcp/127.0.0.1/$p") 2>/dev/null
fi
}
find_free_port() {
local p="$1" tries=0
while ! port_is_free "$p"; do
printf 'Port %s ist belegt.\n' "$p" >&2
p=$((p + 1))
tries=$((tries + 1))
if [ "$tries" -ge "$MAX_TRIES" ]; then
printf 'Kein freier Port zwischen %s und %s gefunden.\n' "$PORT" "$p" >&2
exit 1
fi
done
printf '%s' "$p"
}
PORT="$(find_free_port "$PORT")"
case "$MODE" in
jekyll)
command -v bundle >/dev/null 2>&1 || {
echo "bundler fehlt. Entweder installieren oder ./preview.sh ohne Argument nutzen." >&2
exit 1
}
echo "Jekyll auf http://127.0.0.1:${PORT}"
exec bundle exec jekyll serve --port "$PORT" --livereload
;;
simple)
command -v python3 >/dev/null 2>&1 || { echo "python3 fehlt." >&2; exit 1; }
echo "Dateiserver auf http://127.0.0.1:${PORT}/artikel/"
echo "(rendert kein Markdown, zeigt aber Struktur und Rohtext)"
exec python3 -m http.server "$PORT" --bind 127.0.0.1
;;
*)
echo "Unbekannter Modus: $MODE (erlaubt: simple, jekyll)" >&2
exit 1
;;
esac