Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12cca5586b | ||
|
|
136f9d2fb0 | ||
|
|
119dd18d43 |
No files matched your search
@@ -6,6 +6,8 @@ data
|
||||
docs
|
||||
tools
|
||||
deploy
|
||||
!deploy/compose.yaml
|
||||
!deploy/update.sh
|
||||
.env
|
||||
.env.*
|
||||
*.log
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user