feat(kb): skrypt publish.sh dla kb-site + skill przypominajacy o publikacji

Automatyzuje kroki 2-3 z kb/runbooks/kb-site-deploy.md (generacja, kontrola
wyciekow, upload przez helper alpine, smoke test) w jednym fail-closed
skrypcie zamiast recznego klikania. Runbook zaktualizowany, zeby
referencjonowal skrypt, z zachowaniem wyjasnien co/dlaczego dla kazdej bramki.

Dodaje skill kb-publish, ktory przypomina operatorowi o publikacji po
zmianach w kb/**/*.md, ale nigdy sam nie uruchamia skryptu (dotyka prod
przez SSH).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
oskar 2026-08-26 16:00:34 +02:00
parent 04884ed67d
commit 003f83d453
3 changed files with 126 additions and 47 deletions

View file

@ -0,0 +1,26 @@
---
name: kb-publish
description: Reminds the operator to publish the KB site after changes to kb/**/*.md. Trigger whenever this session's own edits, or a merge/diff visible in git, touch files under kb/.
---
## What this skill does
At the end of your response, if this session touched `kb/**/*.md` — either
through your own edits or through a merge/diff you observed in git — append
exactly one reminder sentence:
> Zmiany w kb/ — pamiętaj odpalić `bash scripts/kb/publish.sh` żeby opublikować.
One sentence, no more. Don't repeat it more than once per response, and don't
add it if nothing under `kb/` changed.
## What this skill does NOT do
**Never run `scripts/kb/publish.sh` yourself**, under any circumstances, even
if the operator's task prompt says to deploy, publish, or calibrate. The
script SSHes into PIHA and overwrites the live `kb-site` content volume on
production — that is out of scope for a worktree agent and requires an
explicit, direct instruction from the operator in the current turn.
If the operator explicitly asks you to run it, that instruction stands on its
own — this skill only governs the passive reminder, not that request.

View file

@ -61,19 +61,27 @@ docker volume ls | grep kb-site # kb-site_kb-site_content
---
## 2. Generacja treści + kontrola wycieków (SATURN / SOLARIA)
## 2-3. Generacja, kontrola wycieków, wgranie treści (SATURN / SOLARIA)
Generujemy w checkoucie repo, na węźle z którego pracujesz — nie na PIHA.
Kroki 2-4 (generacja → `--check` → upload przez helper alpine → smoke test)
robi jeden skrypt:
```bash
set -a; . /opt/homelab/config/kb-site/.env; set +a # ACCESS_TOKEN
test -n "$ACCESS_TOKEN" || echo "BRAK TOKENU — nie publikuj tego builda"
python3 scripts/kb/gen_pages.py --base-url https://kb-e2a24af3.okit.pl
python3 scripts/kb/gen_pages.py --check
echo $? # 0 = czysto, 1 = trafienia
bash scripts/kb/publish.sh
```
Zobacz kod (`scripts/kb/publish.sh`) dla dokładnej kolejności komend. Poniżej
zostaje wyjaśnienie **co** skrypt robi i **dlaczego** — nie trzeba już klikać
kroków ręcznie, ale bramki i uzasadnienia poniżej wciąż obowiązują.
Uruchamiamy w checkoucie repo, na węźle z którego pracujesz — nie na PIHA.
Docelowy host PIHA jest parametrem na górze skryptu (zmienna `PIHA_HOST`,
domyślnie `piha.local`).
Skrypt jest fail-closed na każdym etapie: brak/pusty `ACCESS_TOKEN`, `--check`
z exit 1, albo 0 wystąpień `key=` w wygenerowanym `index.html` — każde z tych
zatrzymuje publikację przed dotknięciem PIHA.
### Token w linkach
Generator czyta `ACCESS_TOKEN` ze środowiska i dopisuje `?key=<token>` do
@ -127,39 +135,21 @@ Każdy kolejny wyjątek podlega tej samej regule: najpierw próba wyczyszczenia
źródła, wpis do whitelisty dopiero jako świadoma decyzja, zawsze z komentarzem
i datą.
---
## 3. Wgranie treści do wolumenu (helper alpine)
### Wgranie treści do wolumenu (helper alpine)
Wzorzec z `services/narty27/README.md`, rozszerzony z jednego pliku na drzewo:
`docker cp` nie zapisze do montowania `:ro`, więc treść wjeżdża jednorazowym
kontenerem pomocniczym, który montuje wolumen zapisywalnie.
```bash
# 1. spakuj build (na węźle generującym)
tar -czf /tmp/kb-site.tgz -C build/kb-site .
# 2. wyślij na PIHA
scp /tmp/kb-site.tgz piha:/tmp/kb-site.tgz
# 3. podmień zawartość wolumenu przez helper
ssh piha 'docker run --rm \
-v kb-site_kb-site_content:/content \
-v /tmp:/src:ro \
alpine sh -c "rm -rf /content/* /content/.[!.]* 2>/dev/null; \
tar -xzf /src/kb-site.tgz -C /content"'
# 4. posprzątaj staging
rm /tmp/kb-site.tgz
ssh piha 'rm /tmp/kb-site.tgz'
```
kontenerem pomocniczym, który montuje wolumen zapisywalnie. `publish.sh` robi
dokładnie to: pakuje `build/kb-site` do tarballa, `scp` na PIHA, i podmienia
wolumen przez ten sam wzorzec helpera co niżej.
`rm -rf /content/*` przed rozpakowaniem jest **obowiązkowe**: bez tego strona
dokumentu przełączonego z `public` na `private` zostałaby w wolumenie i dalej
serwowała treść, której już nie publikujemy. Publikacja jest podmianą całości,
nie dogrywaniem plików.
Weryfikacja z PIHA:
Skrypt kończy smoke testem (`ssh $PIHA_HOST curl -s localhost:8250`) — jeśli
chcesz dodatkowej weryfikacji z samej PIHA:
```bash
services/kb-site/healthcheck.sh
@ -267,23 +257,13 @@ pilnuje pola `visibility`, ale to człowiek decyduje, co dostaje `public`.
Po każdej zmianie w `kb/**/*.md`, która dotyczy dokumentów `public`:
```bash
set -a; . /opt/homelab/config/kb-site/.env; set +a # ACCESS_TOKEN
test -n "$ACCESS_TOKEN" \
&& python3 scripts/kb/gen_pages.py --base-url https://kb-e2a24af3.okit.pl \
&& python3 scripts/kb/gen_pages.py --check \
&& grep -q 'key=' build/kb-site/index.html \
&& tar -czf /tmp/kb-site.tgz -C build/kb-site . \
&& scp /tmp/kb-site.tgz piha:/tmp/kb-site.tgz \
&& ssh piha 'docker run --rm -v kb-site_kb-site_content:/content -v /tmp:/src:ro \
alpine sh -c "rm -rf /content/* /content/.[!.]* 2>/dev/null; tar -xzf /src/kb-site.tgz -C /content"' \
&& ssh piha 'rm /tmp/kb-site.tgz' \
&& rm /tmp/kb-site.tgz
bash scripts/kb/publish.sh
```
Łańcuch na `&&` jest celowy — każde ogniwo zatrzymuje publikację: pusty
`ACCESS_TOKEN` (build byłby nieklikalny), `--check` z exit 1 (wyciek), brak
`key=` w spisie (token był pusty albo nie dojechał do generatora).
To ten sam skrypt co w krokach 2-3 — rutynowa aktualizacja to po prostu
ponowne uruchomienie całej publikacji. Bramki (`ACCESS_TOKEN` pusty, `--check`
exit 1, brak `key=` w spisie) zatrzymują ją przed dotknięciem PIHA przy każdym
uruchomieniu, nie tylko przy pierwszym.
Kontener nie wymaga restartu — nginx czyta wolumen na bieżąco.

73
scripts/kb/publish.sh Executable file
View file

@ -0,0 +1,73 @@
#!/usr/bin/env bash
# scripts/kb/publish.sh — publikacja kb-site (kroki 2-4 z kb/runbooks/kb-site-deploy.md)
#
# Uruchamiane z węzła generującego (SATURN/SOLARIA), nie z PIHA.
# Kolejność bramek: token -> generacja -> --check -> sanity linków -> upload -> smoke test.
# Każda bramka zatrzymuje publikację przy porażce — patrz runbook, sekcja 2-3.
#
# Usage: scripts/kb/publish.sh
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
PIHA_HOST="${PIHA_HOST:-piha.local}"
BASE_URL="https://kb-e2a24af3.okit.pl"
TOKEN_FILE="/opt/homelab/config/kb-site/.env"
BUILD_DIR="$REPO_ROOT/build/kb-site"
TARBALL="/tmp/kb-site.tgz"
log() {
echo "[kb-publish] $*"
}
fail() {
echo "[kb-publish] BLAD: $*" >&2
exit 1
}
log "krok 1/6: wczytanie ACCESS_TOKEN z $TOKEN_FILE"
[[ -f "$TOKEN_FILE" ]] || fail "brak pliku $TOKEN_FILE — patrz kb/runbooks/kb-site-deploy.md sekcja 0"
set -a
# shellcheck disable=SC1090
. "$TOKEN_FILE"
set +a
[[ -n "${ACCESS_TOKEN:-}" ]] || fail "ACCESS_TOKEN pusty w $TOKEN_FILE — nie publikuje bez tokenu"
log "krok 2/6: generacja treści (gen_pages.py --base-url $BASE_URL)"
cd "$REPO_ROOT"
python3 scripts/kb/gen_pages.py --base-url "$BASE_URL"
log "krok 3/6: kontrola wyciekow (gen_pages.py --check)"
if ! python3 scripts/kb/gen_pages.py --check; then
fail "--check wykryl wyciek — publikacja WSTRZYMANA, popraw zrodlo i uruchom ponownie"
fi
log "krok 4/6: sanity check — token w linkach index.html"
KEY_COUNT=$(grep -c 'key=' "$BUILD_DIR/index.html" || true)
[[ "$KEY_COUNT" -gt 0 ]] || fail "0 wystapien 'key=' w $BUILD_DIR/index.html — token nie trafil do linkow"
log " -> $KEY_COUNT linkow z tokenem"
log "krok 5/6: pakowanie i wgranie na $PIHA_HOST"
tar -czf "$TARBALL" -C "$BUILD_DIR" .
log " -> scp $TARBALL do $PIHA_HOST:/tmp/kb-site.tgz"
scp "$TARBALL" "$PIHA_HOST:/tmp/kb-site.tgz"
log " -> helper alpine: podmiana wolumenu kb-site_kb-site_content"
ssh "$PIHA_HOST" 'docker run --rm \
-v kb-site_kb-site_content:/content \
-v /tmp:/src:ro \
alpine sh -c "rm -rf /content/* /content/.[!.]* 2>/dev/null; \
tar -xzf /src/kb-site.tgz -C /content"'
log " -> sprzatanie /tmp (lokalnie i na $PIHA_HOST)"
rm -f "$TARBALL"
ssh "$PIHA_HOST" 'rm -f /tmp/kb-site.tgz'
log "krok 6/6: smoke test na $PIHA_HOST:8250"
SMOKE_OUTPUT=$(ssh "$PIHA_HOST" 'curl -s localhost:8250' | head -c 300)
log " -> odpowiedz (pierwsze 300 znakow):"
echo "$SMOKE_OUTPUT"
log "publikacja zakonczona."