Author SHA1 Message Date
homelab-updaterandClaude Opus 5.5 12cca5586b docs: TASK_STATE with the deploy button
ci / publish (push) Successful in 1m17s
ci / check (push) Successful in 1m31s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:36:37 +02:00
homelab-updaterandClaude Opus 5.5 136f9d2fb0 feat(deploy): deploy a release by hand from Gitea Actions
ci / check (push) Successful in 1m30s
ci / publish (push) Successful in 1m22s
A deploy workflow, started only by hand (or through the API) and only
from master, pulls the chosen image (release tag, sha or latest) and
runs that image's own update.sh against VM103's compose directory
through the Docker socket. update.sh now copies the database through the
running server first, installs the image's compose.yaml and update.sh
with --install, and restores the previous compose file together with
the previous image when the new version does not come up. CI also
builds release tags v*.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:30:19 +02:00
homelab-updaterandClaude Opus 5.5 119dd18d43 docs: TASK_STATE after deploying phase G to VM103
ci / check (push) Successful in 1m29s
ci / publish (push) Successful in 1m18s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:24:46 +02:00
7 changed files with 165 additions and 20 deletions

No files matched your search

+2
View File
@@ -6,6 +6,8 @@ data
docs
tools
deploy
!deploy/compose.yaml
!deploy/update.sh
.env
.env.*
*.log
+3 -3
View File
@@ -1,5 +1,5 @@
# CI: `npm run check` on every push; on master/foundations also build the image and push it to the Gitea registry.
# It does not deploy: VM103 pulls the image with deploy/update.sh (docs/DEPLOY.md).
# CI: `npm run check` on every push; on master/foundations and on release tags (v…) also build the image and push it to
# the Gitea registry. It does not deploy: that is the deploy workflow, started by hand (docs/DEPLOY.md).
name: ci
on:
@@ -28,7 +28,7 @@ jobs:
publish:
needs: check
if: github.ref_name == 'master' || github.ref_name == 'foundations'
if: github.ref_name == 'master' || github.ref_name == 'foundations' || startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+61
View File
@@ -0,0 +1,61 @@
# Deploys one published image to VM103, only when started by hand: Actions → deploy → Run workflow, or the Gitea API.
# The new image's own deploy/update.sh does the work against /opt/homelab/compose/homelab-upgrade, so its guards stay:
# it refuses while a workflow run is in the middle of a step, copies the database first, installs the image's
# compose.yaml, waits for healthy and goes back to the previous image and compose file if the new version does not come up.
name: deploy
on:
workflow_dispatch:
inputs:
version:
description: "Image to deploy: a release tag (v2026.10.1), a full commit sha, or latest (master)"
required: true
default: latest
type: string
force:
description: "Restart even while a workflow run is in the middle of a step, or without a database copy"
required: false
default: false
type: boolean
env:
REGISTRY: git.mariuszpacek.pl
DEPLOY_DIR: /opt/homelab/compose/homelab-upgrade
jobs:
deploy:
# Only the reviewed definition on master may restart production.
if: github.ref_name == 'master'
runs-on: ubuntu-latest
steps:
- name: Check the version
env:
VERSION: ${{ inputs.version }}
run: |
[[ "$VERSION" =~ ^(latest|v[0-9][0-9A-Za-z._-]{0,40}|[0-9a-f]{40})$ ]] || { echo "version must be latest, a v… tag or a full commit sha, not: $VERSION"; exit 2; }
- name: Docker CLI available (talks to the VM daemon through the mounted socket)
run: command -v docker >/dev/null || (apt-get update -qq && apt-get install -y -qq docker.io >/dev/null); docker version --format '{{.Client.Version}} -> {{.Server.Version}}'
- name: Pull the image
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
image="${REGISTRY}/${GITHUB_REPOSITORY_OWNER}/homelab-upgrade:${VERSION}"
docker pull -q "$image"
echo "revision: $(docker inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$image")"
- name: Deploy
env:
VERSION: ${{ inputs.version }}
FORCE: ${{ inputs.force }}
run: |
set -euo pipefail
image="${REGISTRY}/${GITHUB_REPOSITORY_OWNER}/homelab-upgrade:${VERSION}"
args=("$VERSION" --install)
[ "$FORCE" = true ] && args+=(--force)
# root for the Docker socket and the root-owned compose directory; the same path inside, so compose resolves
# the project (name, .env, paths) exactly as on the VM.
docker run --rm --user 0 --entrypoint /app/deploy/update.sh \
-e DEPLOY_DIR="$DEPLOY_DIR" \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$DEPLOY_DIR:$DEPLOY_DIR" \
"$image" "${args[@]}"
+3 -1
View File
@@ -48,6 +48,8 @@ COPY --from=codex /opt/codex /opt/codex
COPY bin ./bin
COPY src ./src
COPY config ./config
RUN chmod -R a+rX ./src ./config ./bin
# The deploy workflow runs this image's own update.sh against VM103's compose directory (docs/DEPLOY.md).
COPY deploy/compose.yaml deploy/update.sh ./deploy/
RUN chmod -R a+rX ./src ./config ./bin ./deploy
USER node
CMD ["node", "src/main/server.ts"]
+5 -4
View File
@@ -13,18 +13,19 @@ Bardziej nowoczesny i graficzny frontend: okna/karty, wykresy z danych, które A
## Zrobione
- G0–G6 na gałęzi `frontend-refresh` (worktree `/root/homelab-upgrade-frontend`), każdy etap osobnym commitem, wypchnięte.
- G0–G7 scalone do `master` (`aec96b2`, fast-forward z `frontend-refresh`) i wdrożone 2026-09-30 na VM103: `./update.sh aec96b2…`, wszystkie usługi na obrazie CI (build 174), bez obrazów hotfix; `compose.yaml` na VM = `deploy/compose.yaml`. Kopie przed wdrożeniem: `/srv/appdata/homelab-upgrade/data/queue.sqlite.before-phase-g-*`, `/srv/appdata/homelab-upgrade/compose.yaml.before-phase-g-*`.
- Wdrożenie przyciskiem: workflow `deploy` (Actions → deploy → Run workflow z `master`, albo `dispatch_workflow` przez MCP) uruchamia `update.sh` z wdrażanego obrazu (`--install`: kopia bazy, `compose.yaml` z obrazu, powrót do poprzedniego obrazu i compose). Tagi `v*` budują obrazy wydań. Sprawdzone na VM103: run 206 wdrożył `136f9d2` (build 177), run 207 odrzucił niepoprawną wersję przed jakąkolwiek zmianą. Opis: `docs/DEPLOY.md`, „Aktualizacja”.
- Gitea (`homelab-compose`, `gitea/compose.yaml`): router `gitea-web-lan` bez limitu zapytań dla LAN, VPN i sieci Dockera (`ClientIP`, priorytet 200); limit Traefika zostaje dla internetu. Powód: CI dostawało 429 przy `docker login`.
- Graf Cytoscape nie ponawia nieudanego startu canvas przy każdym renderze (wcześniej czyścił listę zastępczą).
## Pozostałe
- Przegląd na żywo w przeglądarce (oba motywy, 375 px i desktop), poprawki wyglądu po przeglądzie.
- Merge `frontend-refresh` do `master` i wdrożenie na VM103 — decyzja właściciela.
- Ze strumienia Git/Arcane: sprawdzenie CI dla master i przejście z obrazów hotfix na obraz CI; migracja klientów Influx przed włączeniem aktualizacji.
- Ze strumienia Git/Arcane: migracja klientów Influx przed włączeniem aktualizacji.
## Następny krok
Właściciel ogląda UI (`npm run build:web` + `npm run start:server`, albo `npm run dev:web` z działającym serwerem) i zgłasza poprawki.
Właściciel ogląda nowe UI na https://upgrade.mariuszpacek.pl i zgłasza poprawki.
## Walidacja
+69 -4
View File
@@ -3,25 +3,36 @@
# Lives next to compose.yaml on VM103 (/opt/homelab/compose/homelab-upgrade). See docs/DEPLOY.md.
#
# ./update.sh deploy the tag from .env (TAG=..., default "latest")
# ./update.sh <tag> deploy <tag> (a branch name or a commit sha) and remember it in .env
# ./update.sh --force skip the "a workflow run is in the middle of a step" guard
# ./update.sh <tag> deploy <tag> (a release tag, a branch name or a commit sha) and remember it in .env
# ./update.sh --force skip the "a workflow run is in the middle of a step" guard and a failed database copy
# ./update.sh --install first put this script's compose.yaml and update.sh into DEPLOY_DIR: the deploy workflow
# runs the copy inside the new image (/app/deploy) against the VM's directory
set -euo pipefail
cd "$(dirname "$(readlink -f "$0")")"
script_dir="$(dirname "$(readlink -f "$0")")"
cd "${DEPLOY_DIR:-$script_dir}"
image_repo=git.mariuszpacek.pl/mariusz.pacek/homelab-upgrade
healthy_timeout=${HEALTHY_TIMEOUT:-90}
backups_kept=${BACKUPS_KEPT:-5}
tag_arg=""
force=0
install=0
for arg in "$@"; do
case "$arg" in
--force) force=1 ;;
--install) install=1 ;;
-*) echo "unknown option: $arg" >&2; exit 2 ;;
*) tag_arg="$arg" ;;
esac
done
if ((install == 1)) && [[ "$(pwd -P)" == "$(readlink -f "$script_dir")" ]]; then
echo "--install needs DEPLOY_DIR set to another directory: a script cannot replace itself while it runs" >&2
exit 2
fi
env_tag() {
[[ -f .env ]] && sed -n 's/^TAG=//p' .env | tail -n 1 || true
}
@@ -42,6 +53,44 @@ step_in_progress() {
esac
}
# A consistent online copy of the database through SQLite's backup API, taken by the running server (it owns /data);
# the newest $backups_kept copies stay.
backup_database() {
docker compose exec -T server node -e "
const { DatabaseSync, backup } = require('node:sqlite');
const { readdirSync, rmSync } = require('node:fs');
const prefix = 'queue.sqlite.before-deploy-';
const name = prefix + new Date().toISOString().replace(/[-:]/g, '').replace(/[.][0-9]+Z\$/, 'Z');
const db = new DatabaseSync('/data/queue.sqlite', { readOnly: true });
backup(db, '/data/' + name).then(() => {
db.close();
for (const old of readdirSync('/data').filter((file) => file.startsWith(prefix)).sort().reverse().slice($backups_kept)) rmSync('/data/' + old);
console.log('database copy: /data/' + name);
}, (error) => { console.error(error.message); process.exit(1); });
"
}
# The new compose.yaml and update.sh come from the image being deployed; the old compose file is kept, so a version
# that does not come up goes back together with the compose file it ran with.
install_files() {
cp -p compose.yaml compose.yaml.previous
install -m 600 "$script_dir/compose.yaml" compose.yaml
install -m 755 "$script_dir/update.sh" update.sh
if cmp -s compose.yaml.previous compose.yaml; then
echo "compose.yaml unchanged"
else
echo "compose.yaml changed:"
diff -u compose.yaml.previous compose.yaml || true
fi
}
restore_compose() {
if ((install == 1)) && [[ -f compose.yaml.previous ]]; then
echo "restoring the previous compose.yaml" >&2
cp -p compose.yaml.previous compose.yaml
fi
}
wait_until_up() {
local deadline=$((SECONDS + healthy_timeout)) status
while ((SECONDS < deadline)); do
@@ -71,10 +120,25 @@ if ((force == 0)) && [[ -n "$(service_container server)" ]] && step_in_progress;
exit 3
fi
if [[ -n "$(service_container server)" ]] && backup_database; then
:
elif ((force == 0)); then
echo "could not copy the database (is the server running?); fix it or use --force" >&2
exit 5
else
echo "warning: no database copy (--force)" >&2
fi
previous_image=$(docker inspect --format '{{.Image}}' "$(service_container server)" 2>/dev/null || true)
((install == 1)) && install_files
echo "pulling $image_repo:$tag"
TAG="$tag" docker compose pull
if ! TAG="$tag" docker compose pull; then
echo "could not pull $image_repo:$tag; nothing was restarted" >&2
restore_compose
exit 1
fi
TAG="$tag" docker compose up -d --remove-orphans
if wait_until_up; then
@@ -90,6 +154,7 @@ fi
echo "new version did not come up healthy" >&2
docker compose logs --tail 30 >&2 || true
restore_compose
if [[ -n "$previous_image" ]]; then
echo "rolling back to $previous_image" >&2
docker tag "$previous_image" "$image_repo:rollback"
+22 -8
View File
@@ -21,20 +21,34 @@ Port 8080 na hoście VM jest zajęty przez inną usługę, dlatego appka nie pub
## Aktualizacja
Obieg: `git push` → Gitea Actions (`.gitea/workflows/ci.yaml`): `npm run check`, a na `master` i `foundations` także build i push obrazu do rejestru Gitei → na VM103 jedna komenda:
Obieg: `git push` → Gitea Actions (`.gitea/workflows/ci.yaml`): `npm run check`, a na `master`, `foundations` i tagach wydań `v…` także build i push obrazu do rejestru Gitei → wdrożenie **tylko ręcznie**, przyciskiem albo przez API. Arcane nie zarządza tym projektem.
Wydanie: tag `v<rok>.<miesiąc>.<numer>` (np. `v2026.10.1`) na commicie z `master`, najlepiej jako release w Gitei; CI buduje obraz `:v2026.10.1` (i `:<sha>`). Stopka pokazuje wtedy `v2026.10.1 · build N`.
Wdrożenie: Gitea → repo → Actions → **deploy** → *Run workflow* (gałąź `master`), pole `version`: tag wydania, pełny sha albo `latest` (ostatni `master`); `force` tylko świadomie. To samo przez API/MCP: `dispatch_workflow` z `workflow_id: deploy.yaml`, `ref: master`, `inputs: { version, force }`; wynik i log w runie.
Workflow (`.gitea/workflows/deploy.yaml`) działa tylko z definicji na `master`: sprawdza format wersji, pobiera obraz i uruchamia **skrypt z tego obrazu** (`/app/deploy/update.sh --install`) na katalogu `/opt/homelab/compose/homelab-upgrade` przez socket Dockera. `update.sh`:
1. odmawia (kod 3), gdy instancja silnika jest w stanie `running` (run na bramce lub w kolejce restart przeżywa, przerwany węzeł wznawia się w UI przyciskiem „Retry the step”);
2. robi spójną kopię bazy przez działający serwer (`/data/queue.sqlite.before-deploy-<czas>`, zostaje 5 najnowszych); bez kopii odmawia (kod 5);
3. z `--install` wgrywa `compose.yaml` i `update.sh` z obrazu (poprzedni compose zostaje jako `compose.yaml.previous`, diff w logu);
4. `docker compose pull` (gdy obrazu nie ma: przywraca compose i kończy bez restartu) i `up -d`;
5. czeka do 90 s na `healthy` serwera i executora oraz na stabilne pozostałe usługi; gdy nowa wersja nie wstaje, wraca do poprzedniego obrazu (lokalny tag `:rollback`) i poprzedniego `compose.yaml`, nie zmienia `.env` i kończy się kodem 1.
`--force` pomija krok 1 i pozwala na brak kopii z kroku 2. Wycofanie to ponowne wdrożenie starszej wersji tym samym przyciskiem. Stare obrazy zostają na VM.
Awaryjnie, gdy Gitea lub runner nie działa, ten sam skrypt ręcznie na VM103:
```bash
cd /opt/homelab/compose/homelab-upgrade
./update.sh # tag z .env (domyślnie latest)
./update.sh foundations # inny tag (gałąź albo pełny sha) i zapamiętanie go w .env
./update.sh --force # pomija blokadę „trwa krok / węzeł runu”
./update.sh v2026.10.1 # inna wersja (tag, gałąź albo pełny sha) i zapamiętanie jej w .env
./update.sh --force # pomija blokadę „trwa krok / węzeł runu” i brak kopii bazy
```
`update.sh` (kopia `deploy/update.sh`): odmawia restartu, gdy driver ma krok starego łańcucha w stanie `running` albo instancja nowego silnika jest w stanie `running` (kod 3; run czekający na bramce lub w kolejce restart przeżywa, a przerwany węzeł wznawia się w UI przyciskiem „Retry the step”); robi `docker compose pull` i `up -d`; czeka do 90 s na `healthy` serwera i sprawdza, że driver i `docker-proxy` działają i się nie restartowały. Gdy nowa wersja nie wstaje, wraca do poprzedniego obrazu (lokalny tag `:rollback`), nie zmienia `.env` i kończy się kodem 1. Stare obrazy zostają na VM, więc wycofanie do dowolnego commita to `./update.sh <sha>`.
Ręczny `update.sh` nie wgrywa `compose.yaml`; po zmianie `deploy/compose.yaml` wdrażaj przyciskiem albo skopiuj plik.
CI niczego nie wdraża: deploy jest świadomym krokiem na VM (na razie bez timera, bo restart w trakcie kroku byłby groźny dopiero po dodaniu prawdziwych handlerów).
Przy zmianie schematu bazy nowa wersja migruje bazę przy starcie (migracje są transakcyjne; nowszej bazy niż kod odmawia otwarcia). Wycofanie do starszego obrazu po zmianie schematu nie zadziała, bo starszy kod odmówi otwarcia nowszej bazy: przed takim wdrożeniem zrób kopię `queue.sqlite*`.
Przy zmianie schematu bazy nowa wersja migruje bazę przy starcie (migracje są transakcyjne; nowszej bazy niż kod odmawia otwarcia). Wycofanie do starszego obrazu po zmianie schematu nie zadziała, bo starszy kod odmówi otwarcia nowszej bazy: wtedy przywraca się kopię `queue.sqlite.before-deploy-*` z kroku 2.
### Jednorazowa konfiguracja
@@ -43,7 +57,7 @@ Przy zmianie schematu bazy nowa wersja migruje bazę przy starcie (migracje są
3. **Sekret repo** `REGISTRY_TOKEN` (repo → Settings → Actions → Secrets): token `write:package`.
4. **Runner** (`deploy/runner/`, na VM103 w `/opt/homelab/compose/gitea-runner/`; dane w `/srv/appdata/gitea-runner/data`). Token rejestracyjny: repo → Settings → Actions → Runners → Create new runner (token na poziomie repo zamiast całej instancji). Runner ma `docker.sock`, czyli praktycznie root na VM: rejestruj go tylko dla tego repo.
5. **`docker login`** na VM103: pomijamy, bo pull jest anonimowy. Gdyby pakiet stał się prywatny: `docker login git.mariuszpacek.pl -u mariusz.pacek` tokenem `read:package`.
6. **Przełączenie z lokalnego obrazu**: skopiuj `deploy/compose.yaml` i `deploy/update.sh` do `/opt/homelab/compose/homelab-upgrade/`, po pierwszym zielonym buildzie na `foundations` uruchom `./update.sh foundations`. Kopie trzeba odświeżyć ręcznie, gdy te dwa pliki się zmienią.
6. **Przełączenie z lokalnego obrazu**: skopiuj `deploy/compose.yaml` i `deploy/update.sh` do `/opt/homelab/compose/homelab-upgrade/`, po pierwszym zielonym buildzie uruchom `./update.sh <sha>`. Później oba pliki wgrywa workflow deploy.
Ograniczenia, które warto znać: job dostaje repo z `http://192.168.0.139:13080` (port Gitei na VM), bo whitelist traefika nie obejmuje sieci jobów; `docker push` idzie z demona VM, więc źródłowy adres to 192.168.0.139 z whitelisty Gitei.