flyway check
All checks were successful
CI and deploy / migration-check (push) Successful in 3s
CI and deploy / deploy (push) Successful in 14s

This commit is contained in:
Dusan Vojacek
2026-04-19 14:11:57 +02:00
parent 477e94f321
commit 906eeb1609
8 changed files with 200 additions and 68 deletions

View File

@@ -1,22 +1,73 @@
# Deploy na single server: deploy.sh volá hostovský Docker přes /var/run/docker.sock (bez DinD).
# CI: immutability + Flyway validate (JDBC na staging / sdílenou DB). Deploy na main až po úspěchu.
# Job bez container: — hostovský docker + git (stejně jako deploy).
# Gitea secrets: EMS_CI_FLYWAY_URL (jdbc:postgresql://…/ems). Volitelně EMS_CI_FLYWAY_USER, EMS_CI_FLYWAY_PASSWORD.
# Runner: container.valid_volumes pro /var/run/docker.sock (viz docs/deployment-self-hosted.md).
#
# Job běží v kontejneru — /opt/ems-deploy a sock musí být přimountované (viz container.volumes).
# V /opt/gitea-stack/runner/config.yaml nastav container.valid_volumes na stejné cesty.
# Sladit `runs-on` s labely registrace runneru (výchozí: self-hosted).
#
# Spuštění: push na větev main (včetně merge PR do main — merge v Gitea/Git je stále push na main).
# Nepřidávat paralelně pull_request:closed — při merge by běžel deploy dvakrát (push + PR).
# Spuštění deploye: push na main. Nepřidávat paralelně pull_request:closed — při merge by běžel deploy dvakrát.
name: deploy
name: CI and deploy
on:
push:
branches:
- main
- feature/**
pull_request:
workflow_dispatch:
jobs:
migration-check:
runs-on: self-hosted
steps:
- name: Checkout
env:
TOKEN: ${{ github.token }}
run: |
set -eu
su="${{ github.server_url }}"
case "$su" in
https://*) clone_url="https://oauth2:${TOKEN}@${su#https://}" ;;
http://*) clone_url="http://oauth2:${TOKEN}@${su#http://}" ;;
*) echo "unknown github.server_url: $su"; exit 1 ;;
esac
clone_url="${clone_url}/${{ github.repository }}.git"
git init
git remote add origin "$clone_url"
git fetch --depth=64 origin "${{ github.sha }}"
git checkout -qf FETCH_HEAD
git remote set-branches origin 'main' || true
git fetch --depth=64 origin main:refs/remotes/origin/main || true
- name: Repo layout
run: |
test -f docker-compose.yml
test -f deploy/docker-compose.yml
test -x deploy/deploy.sh
test -x scripts/ci_check_migration_immutability.sh
test -x scripts/ci_flyway_validate_remote.sh
- name: Migration immutability (vs PR base or main)
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -eu
BASE='origin/main'
if [ -n "${PR_BASE_SHA:-}" ]; then
BASE="$PR_BASE_SHA"
git fetch --no-tags --depth=256 origin "$BASE" || true
fi
./scripts/ci_check_migration_immutability.sh "$BASE"
- name: Flyway validate (remote DB)
env:
EMS_CI_FLYWAY_URL: ${{ secrets.EMS_CI_FLYWAY_URL }}
EMS_CI_FLYWAY_USER: ${{ secrets.EMS_CI_FLYWAY_USER }}
EMS_CI_FLYWAY_PASSWORD: ${{ secrets.EMS_CI_FLYWAY_PASSWORD }}
run: ./scripts/ci_flyway_validate_remote.sh
deploy:
needs: migration-check
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
runs-on: self-hosted
steps:
- name: Show execution context
@@ -27,8 +78,7 @@ jobs:
ls -ld /opt/ems-deploy
- name: Run deploy script
run: |
bash /opt/ems-deploy/deploy.sh
run: bash /opt/ems-deploy/deploy.sh
# Alternativa: runner v Dockeru bez přístupu k hostu — odkomentovat a upravit SERVER + secrets.
# deploy-ssh:

View File

@@ -1,46 +0,0 @@
name: test
on:
push:
branches:
- main
- feature/**
pull_request:
jobs:
smoke-test:
# Stejný label jako deploy.yml — výchozí act_runner má typicky jen `self-hosted`.
runs-on: self-hosted
# Výchozí job image často nemá Node → `actions/checkout@v4` padá na „Cannot find: node“.
# alpine/git je malý a stačí na shallow clone přes token (Gitea = GitHub-kompatibilní kontext).
container:
image: alpine/git:latest
steps:
- name: Checkout
env:
TOKEN: ${{ github.token }}
run: |
set -eu
su="${{ github.server_url }}"
case "$su" in
https://*) clone_url="https://oauth2:${TOKEN}@${su#https://}" ;;
http://*) clone_url="http://oauth2:${TOKEN}@${su#http://}" ;;
*) echo "unknown github.server_url: $su"; exit 1 ;;
esac
clone_url="${clone_url}/${{ github.repository }}.git"
git init
git remote add origin "$clone_url"
git fetch --depth=1 origin "${{ github.sha }}"
git checkout -qf FETCH_HEAD
- name: Repo layout
run: |
test -f docker-compose.yml
test -f deploy/docker-compose.yml
test -x deploy/deploy.sh
- name: Runner info
run: |
uname -a
pwd
ls -la

View File

@@ -192,3 +192,4 @@ Specifikace z `docs/02-architecture.md`, modulových docs a komentářů v `plan
- Výkon **W**, energie **Wh**, ceny **Kč/kWh**; čas v DB **`TIMESTAMPTZ` (UTC)**.
- NIKDY neupravuj existující V__ migrační soubory po jejich aplikaci na DB.
- Pokud je potřeba opravit chybu ve verzované migraci, vytvoř novou V{N+1} migraci.
- Deploy: `flyway validate` před `migrate` ([`deploy/deploy.sh`](deploy/deploy.sh)). Lokálně `./scripts/flyway_validate_local.sh`; CI viz [`docs/deployment-self-hosted.md`](docs/deployment-self-hosted.md) a `scripts/ci_check_migration_immutability.sh`.

View File

@@ -50,9 +50,12 @@ install -m 0644 "$COMPOSE_SRC" "$COMPOSE_DST"
log "docker compose config (validate)"
docker compose -f "$COMPOSE_DST" --env-file "$ENV_FILE" config >/dev/null
# Vždy spustit migrace z aktuálního ./app/db (mount ve flyway službě). Čisté `up -d` často
# znovu nespustí jednorázový kontejner flyway, takže změny jen v R__/*.sql by se neaplikovaly.
# Při chybě je v logu jobu celý Flyway výstup (konkrétní SQL / řádek).
# Flyway: nejdřív validate (soubory vs flyway_schema_history na DB), pak migrate.
# Čisté `up -d` často znovu nespustí jednorázový kontejner flyway změny jen v R__/*.sql přes migrate.
# Při chybě je v logu celý Flyway výstup (konkrétní SQL / řádek).
log "Flyway validate"
docker compose -f "$COMPOSE_DST" --env-file "$ENV_FILE" run --rm flyway validate
log "Flyway migrate (docker compose run --rm flyway)"
docker compose -f "$COMPOSE_DST" --env-file "$ENV_FILE" run --rm flyway migrate

View File

@@ -43,10 +43,12 @@ flowchart LR
dev[Dev laptop] -->|git push| gitea[Gitea main]
gitea --> act[Gitea Actions]
act --> runner[act_runner + Docker]
runner --> job[Job container Alpine + sock + /opt mount]
runner --> ci[migration-check validate]
ci -->|needs ok| job[deploy job + /opt mount]
job --> sh[deploy.sh]
sh --> git[git fetch / reset main]
sh --> sync[install compose.yml]
sh --> fly[flyway validate then migrate]
sh --> dc["docker compose build && up -d"]
dc --> hostd[Host Docker Engine]
hostd --> ems[EMS stack]
@@ -55,7 +57,7 @@ flowchart LR
1. Vývoj lokálně, merge do `main`, push na `git.vojacek.eu:2222`.
2. Workflow `.gitea/workflows/deploy.yml` (label `self-hosted`) spustí job.
3. Job kontejner má `docker.sock` a rw mount `/opt/ems-deploy`; nainstaluje `git`, `bash`, `docker-cli`, `docker-cli-compose` (Alpine).
4. `/opt/ems-deploy/deploy.sh`: `flock`, `git` v `app/`, sync `docker-compose.yml`, `docker compose config`, `build`, `up -d`, `image prune -f`.
4. `/opt/ems-deploy/deploy.sh`: `flock`, `git` v `app/`, sync `docker-compose.yml`, `docker compose config`, **`flyway validate`** (soubory vs `flyway_schema_history` na DB), **`flyway migrate`**, `build`, `up -d`, `image prune -f`.
Build probíhá na **hostovském** Dockeru (stejný daemon jako Gitea stack), bez DinD.
@@ -123,14 +125,26 @@ Produkční compose mapuje frontend na **`127.0.0.1:8080`**, backend na **`127.0
---
## 7. CI: test vs deploy
## 7. CI a deploy (jeden workflow)
| Workflow | Účel | Poznámka |
|----------|------|----------|
| `.gitea/workflows/test.yml` | Smoke (soubory, layout) | `runs-on: self-hosted` + job `container: alpine/git` a checkout přes `git fetch` (ne `actions/checkout@v4` — ta potřebuje **Node** v job kontejneru, výchozí image act_runneru ho nemá → „Cannot find: node in PATH“). |
| `.gitea/workflows/deploy.yml` | Deploy po pushi na `main` | Job kontejner + mounty + viz sekce 5. |
| Soubor | Účel |
|--------|------|
| `.gitea/workflows/deploy.yml` | **`migration-check`** na `pull_request` a `push` (`main`, `feature/**`): checkout přes `git fetch` (bez Node), skript **neměnnosti** `scripts/ci_check_migration_immutability.sh` (proti `origin/main` nebo base SHA u PR), **`flyway validate`** proti DB z JDBC (viz secrets). **`deploy`** jen při `push` na `main`, **`needs: migration-check`** — pak `/opt/ems-deploy/deploy.sh`. |
`workflow_dispatch` na deploy umožňuje ruční opakování bez prázdného commitu.
**Gitea secrets (repo):**
| Secret | Účel |
|--------|------|
| `EMS_CI_FLYWAY_URL` | JDBC na staging / sdílenou DB s aktuální `flyway_schema_history` (např. `jdbc:postgresql://host:5432/ems`). Pokud **není** nastaveno, krok remote validate se **přeskočí** (varování v logu); immutability check pořád běží. |
| `EMS_CI_FLYWAY_USER` / `EMS_CI_FLYWAY_PASSWORD` | Volitelně, pokud nejsou v URL. |
Job **nemá** `container:` — potřebuje hostovský `docker` + `git` (stejně jako deploy). Runner: `valid_volumes` pro `/var/run/docker.sock` (sekce 5).
**Lokálně před commitem:** z kořene repa s rozjetým Postgres z `docker-compose.yml` a aspoň jednou proběhlým migrate spusť `./scripts/flyway_validate_local.sh` (`flyway validate` vůči lokální DB).
**Flyway `validate` vs `migrate`:** `validate` nekontroluje „dry-run“ celého SQL nové pending migrace; ověří hlavně shodu **již zapsaných** řádků ve `flyway_schema_history` se soubory v repu (checksumy atd.) — proto musí běžet proti **běžící** DB. `migrate` teprve aplikuje pending skripty.
`workflow_dispatch` na větvi `main` spustí `migration-check` a potom i **`deploy`** (stejná podmínka jako u push na `main`).
---

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Fail if any versioned migration Vxxx already present on the base branch is modified
# in the current HEAD (Flyway checksum mismatch risk). See CLAUDE.md (never rewrite applied V__).
#
# Usage: ci_check_migration_immutability.sh [BASE]
# BASE — ref or full SHA (default: origin/main). For PRs pass base commit SHA.
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [[ -z "$ROOT" ]]; then
echo "ERROR: not inside a git repository"
exit 1
fi
cd "$ROOT"
BASE="${1:-origin/main}"
if [[ "$BASE" =~ ^[0-9a-f]{7,40}$ ]]; then
if ! git cat-file -e "${BASE}^{commit}" 2>/dev/null; then
git fetch --no-tags --depth=256 origin "$BASE"
fi
fi
if ! git rev-parse --verify -q "${BASE}^{commit}" >/dev/null; then
echo "ERROR: cannot resolve base revision: $BASE (fetch origin main?)"
exit 1
fi
max_ver_on_base() {
git ls-tree -r --name-only "$BASE" -- db/migration 2>/dev/null \
| grep -E '^db/migration/V[0-9]+__' \
| sed -n 's/^db\/migration\/V\([0-9][0-9]*\)__.*/\1/p' \
| sort -n \
| tail -1
}
MAX="$(max_ver_on_base || true)"
MAX="${MAX:-0}"
mapfile -t CHANGED < <(git diff --name-only "${BASE}...HEAD" -- 'db/migration/V*.sql' 2>/dev/null || true)
if [[ ${#CHANGED[@]} -eq 0 ]]; then
echo "ci_check_migration_immutability: no versioned migration changes vs $BASE (ok)"
exit 0
fi
err=0
for f in "${CHANGED[@]}"; do
[[ "$f" =~ ^db/migration/V([0-9]+)__ ]] || continue
ver="${BASH_REMATCH[1]}"
ver=$((10#$ver))
if (( ver <= MAX )); then
printf 'ERROR: do not modify historical migration %s (version %03d <= max on base %03d from %s). Add a new V* migration instead. See CLAUDE.md.\n' \
"$f" "$ver" "$MAX" "$BASE" >&2
err=1
fi
done
if (( err )); then
exit 1
fi
echo "ci_check_migration_immutability: ok (vs $BASE, max V on base=$MAX)"

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Flyway validate using migration files from repo root; JDBC from env (staging / CI DB).
# Env: EMS_CI_FLYWAY_URL — if unset, skips (warn). Optional: EMS_CI_FLYWAY_USER, EMS_CI_FLYWAY_PASSWORD, FLYWAY_IMAGE
set -euo pipefail
if [[ -z "${EMS_CI_FLYWAY_URL:-}" ]]; then
echo "WARN: EMS_CI_FLYWAY_URL not set — skipping remote Flyway validate (set Gitea secret for CI)."
exit 0
fi
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
IMG="${FLYWAY_IMAGE:-flyway/flyway:12}"
args=(
run --rm
-v "$ROOT/db/migration:/flyway/sql/migration"
-v "$ROOT/db/routines:/flyway/sql/routines"
-v "$ROOT/db/views:/flyway/sql/views"
-e "FLYWAY_URL=${EMS_CI_FLYWAY_URL}"
-e "FLYWAY_SCHEMAS=ems"
-e "FLYWAY_LOCATIONS=filesystem:/flyway/sql/migration,filesystem:/flyway/sql/routines,filesystem:/flyway/sql/views"
)
if [[ -n "${EMS_CI_FLYWAY_USER:-}" ]]; then
args+=(-e "FLYWAY_USER=${EMS_CI_FLYWAY_USER}")
fi
if [[ -n "${EMS_CI_FLYWAY_PASSWORD:-}" ]]; then
args+=(-e "FLYWAY_PASSWORD=${EMS_CI_FLYWAY_PASSWORD}")
fi
args+=("$IMG" validate)
echo "Running Flyway validate against remote DB (schema ems)…"
docker "${args[@]}"

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Flyway validate against local Postgres from repo-root docker-compose.
# Requires existing flyway_schema_history (run migrate at least once). Does not run migrate.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
if [[ ! -f .env ]]; then
echo "ERROR: missing .env (copy from .env.example)"
exit 1
fi
docker compose --env-file .env run --rm flyway validate