Skip to content

Deploy pipeline — jak deploy.sh funguje

Technická dokumentace deploy mechanismu. Pravidla autorizace jsou v auto-memory (orthogonal feedback_deploy_authorization.md, feedback_hub_before_prod.md atd.).

Entry point

cd /root/projects/sentinel
bash deploy/deploy.sh <service> <env> [<commit>]
  • service: auth | pulse | s60-mail | n8n | badwolf | venom | billit (validováno v deploy.sh:80)
  • env: hub | prod
  • commit: volitelný — pokud nezadán, použije se master HEAD na cílovém serveru

Co deploy.sh dělá (v pořadí)

  1. Validace argumentů — service známé, env hub/prod
  2. SSH na cílový server — sentinel → hub-alfa nebo prod-alfa (Tailscale)
  3. Pre-deploy snapshot (ADR-007) — backup /opt/<service>/ a Docker volumes před deployem
  4. Read deploy.yml — manifest na cílovém serveru /opt/<service>/deploy.yml
  5. Migration check — pokud deploy.ymlmigration: yes na top-levelu, blokuje deploy a žádá explicitní povolení (od commit fd378b4)
  6. Git fetch + checkoutgit fetch origin master && git checkout <commit>
  7. Selektivní deploy (od commit c6f980c) — pokud git diff <last-deployed>..<commit> mění jen jeden subdir (např. billit-web/), spustí se pouze ten container s --no-deps. Tím se nerebuilduje zbytek monorepa.
  8. Build + updocker compose build <service> && docker compose up -d <service>
  9. DB migrace — pokud deploy.ymldatabase.migration: typeorm, spustí migration_command v migration_container
  10. Health checkcurl <health_url> (z deploy.yml)
  11. Append do deploy/deploys.log<timestamp> | <service> | <env> | <commit> | <branch> | <status> | qa=<pass/skip>
  12. Optional QA gate — pokud QA_GATE=on, spustí /root/projects/qa/qa-gate.sh (jen pro hub)

Deploy.yml — co tam musí být

service: <name>
version: "x.y.z"

containers:
  - name: <container-name>
    port: <internal-port>
    health: /health        # nebo /healthz
    env_file: .env
    build:
      context: <dir>
      dockerfile: Dockerfile
      args: [...]          # build-time env

dependencies:
  required: [postgresql, redis]
  optional: [...]

env_vars:
  required: [...]
  optional: [...]

database:
  type: postgresql
  migration: typeorm                     # auto | typeorm | none | manual
  migration_command: "..."
  migration_container: "<container>"

domains:
  staging: <hub-domain>
  production: <prod-domain>

nginx:
  routing:
    - { path: /api,  upstream: api:3200 }
    - { path: /,     upstream: web:3201 }

Selektivní deploy logika

LAST_DEPLOYED = $(grep "^<service>.*<env>" deploys.log | tail -1 | awk commit)
COMMIT = <target>

if LAST_DEPLOYED && LAST_DEPLOYED != COMMIT:
  DIFF_FILES = git diff --name-only LAST_DEPLOYED..COMMIT
  if DIFF_FILES match exactly one container's build context:
    SELECTIVE_SERVICES = "<that container>"
    use docker compose up -d --no-deps SELECTIVE_SERVICES
  else:
    full deploy (all containers)

Limitace: env-only změny (mimo git) vždy spustí full deploy (commit hash se nezmění, ale .env ano).

Pre-deploy snapshots (ADR-007)

Před každým deployem:

  1. Snapshot /opt/<service>//var/backups/deploy-snapshots/<service>/<timestamp>/
  2. Snapshot Docker volumes → tar.gz do stejné složky
  3. Retention: viz deploy/snapshot-cleanup.sh

Rollback: bash deploy/rollback.sh <service> <env> <snapshot-timestamp> (viz runbooks/rollback.md).

Deploy guard hook

/root/projects/sentinel/.claude/hooks/deploy-guard.sh — PreToolUse hook na Bash. Blokuje přímé docker compose up/down/restart přes SSH na hub/prod. Nutí deploy.sh.

Deploys log

/root/projects/sentinel/deploy/deploys.log — append-only chronologie:

2026-04-27T05:22:58Z | s60-pulse | prod | bcbca34 | master | done | qa=skipped

Sloupce: ISO timestamp, service, env, commit, branch, status, qa flag, optional note: ....

Co dělat při failure

  1. Build failure → IHNED poslat TODO příslušnému dev agentovi (feedback_deploy_build_errors.md)
  2. HC failure po deployi → rollback ze snapshotu, pak TODO agentovi
  3. Migration failure → rollback DB ze snapshotu, blokovat další deploy, TODO agentovi
  4. Container conflict → manual docker rm -f, znovu deploy.sh, do log přidat note: container conflict resolved manually

Známé limity

  • npm install běží jen když node_modules neexistuje (deploy.sh ~ř. 453). Když dev agent přidá novou dependency do package.json, stará node_modules ji nemá → tsc build fail. Workaround před deployem: cd /root/projects/<svc> && npm install --ignore-scripts. TODO: spouštět install i když diff $LAST_DEPLOYED..HEAD obsahuje package.json/package-lock.json (poprvé kouslo u pulse CR-141, @aws-sdk/client-s3).

Jak ověřit, co reálně běží (a čemu nevěřit)

⚠️ „✅ done" v changelogu není důkaz nasazení. 2026-08-01 se ukázalo, že s60-mail měl 36a41f6 zapsaný jako úspěšně nasazený 3× (21. 7. prod, 30. 7. hub + prod) a přitom na hubu i produkci běžel starší kód — /health vracel prázdné details, teprve po reálném deployi 1. 8. začal vracet database: up, redis: up. Ověřuj chováním běžícího kódu, ne záznamem.

Příčina: --skip-build + neověřovaný build hash (vyšetřeno 2026-08-01)

Zdroj se rsyncne správně, ale kontejner se recreatuje ze starého image — a nic to nezachytí:

Řádek Co dělá Proč to zakrývá problém
830 / 836 up -d --force-recreate bez --build kontejner běží z existujícího image
772 .deploy-commit se zapíše bezpodmínečně marker říká cílový commit, i když se nestavělo
967–968 source hash local ↔ remote obojí je ten rsyncnutý zdroj ⇒ vždy shoda
975 / 987 Build hash se vypíše a uloží, ale žádná podmínka ho netestuje jediný ukazatel skutečného artefaktu se ignoruje
1271 / 1282 deploys.log + changelog: commit + done flag --skip-build se nikam nezapíše

Důkaz.deploy-info u s60-mail/hub, stejný commit i zdroj, jiný artefakt:

30. 7. 17:40   commit=36a41f6  src_hash=eb412102…  dist_hash=9a89ff84…
 1. 8. 05:59   commit=36a41f6  src_hash=eb412102…  dist_hash=5d9ed069…

Historické .deploy-info jde vytáhnout ze snapshotu: tar -xzOf /var/backups/deploy-snapshots/<svc>/<env>/<id>/app.tar.gz --wildcards "*/.deploy-info"

⚠️ Pozor: --skip-build je zdokumentovaný workaround na docker race („removal already in progress"), takže se po něm sahá při re-runu rutinně — a přesně tím se vyrábějí falešná „done".

Opraveno 2026-08-01 — tři pojistky

# Pojistka Kde
1 Detekce stale artefaktu: zdroj se od posl. deploye změnil, ale dist_hash v kontejneru je bitově stejný → FAIL: STALE ARTEFAKT, VERIFY_OK=false fáze 7.9
2 Zámek na --skip-build: když se zdroj po rsyncu liší od posledního .deploy-info, deploy se zastaví ještě před docker fází (exit 1) před fází 6
3 Build mode v záznamech: deploys.log i changelog nově píšou build=full / build=skipped fáze 8

Nový flag --force-skip-build = vědomý override pojistky 2. Použij jen když build prokazatelně proběhl a selhal až recreate kontejneru (docker race). Pojistka 1 běží i tehdy, takže stale artefakt neprojde ani přes override.

Hláška VERIFIED nově říká, jestli se build změnil: VERIFIED (commit + source hash + build se změnil: <starý> → <nový>).

Chování ověřeno na 6 kombinacích hashů — poplach padne jen na kombinaci „zdroj změněn + build beze změny"; první deploy, nezměněný re-deploy i nezjistitelný dist_hash projdou.

Dvě nezávislé kontroly (druhá doplněna 2026-08-01 po auditu)

Kontrola build hashe má slepé místo: služby bez /app/dist (frontendy venom, billit-web) mají dist_hash prázdný, takže je neochrání. Proto přibyla kontrola stáří image.

Kontrola 1 — build hash Kontrola 2 — stáří image
Co porovnává dist_hash proti předchozímu .deploy-info čas vzniku image proti času předchozího deploye
Sepne když zdroj změněn + dist_hash beze změny zdroj změněn + image starší než předchozí deploy
Slepá u služeb bez /app/dist (frontendy) — funguje všude
Reakce FAIL FAIL, pokud je kontrola 1 slepá; jinak jen varování

Kontrola 2 je záměrně jen varování tam, kde kontrola 1 funguje: src_hash zahrnuje i soubory, které se do Docker build kontextu nekopírují (změna mimo COPY ⇒ image se legitimně nepřestaví), takže by jinak hlásila falešné poplachy.

Dvě pasti odhalené při venom deployi (2026-08-01)

1. Jméno kontejneru z manifestu ≠ běžící kontejner. deploy.yml uvádí logické jméno (containers[0].name), ale compose ho u některých služeb pojmenuje jinak — venom má v manifestu s60-venom, běží jako venom-venom-1. docker exec/inspect pak tiše selže a obě kontroly běží naprázdno (Build hash: (prázdný), Image vznikl: neznámo) — tedy přesně u frontendu, kvůli kterému kontrola stáří image vznikla. Fix: resolvovat přes docker compose ps -q v TARGET_PATH.

⚠️ Test existence musí být docker container inspect, ne docker inspect — to matchuje i image stejného jména (venom má image i manifest s60-venom), takže by test uspěl, fallback se nespustil a docker exec by stejně selhal.

2. Prázdný /app/dist nevrací prázdno, ale KONSTANTU. find /app/dist -name "*.js" | sort | xargs md5sum | md5sum na prázdném vstupu vrátí 886f4202f9e4fea2af611f1642f84a08 (md5 z md5 prázdného řetězce) — pro každou takovou službu stejnou. Po opravě jména kontejneru by tedy kontrola stale artefaktu u frontendu se změněným zdrojem viděla „dist beze změny" a shodila zdravý deploy. Fix: když find nenajde žádné .js, vrátit explicitně unknown → kontrola č. 1 se vypne a rozhoduje stáří image (kontrola č. 2), jak bylo zamýšleno.

Ověřeno: venom → unknown, badwolf → 350007f2…, auth → e1aaa43c… (shodné s hodnotami, které hlásily deploye téhož dne ⇒ u backendů beze změny chování).

⚠️ Časové zóny: Docker vrací u kontejnerů …Z, ale u lokálně stavěných image offset …+02:00. Porovnávat ty řetězce přímo je chyba (sentinel na to 1. 8. při auditu naletěl) — deploy.sh je normalizuje na epoch přes Python.

Ověřeno na 6 scénářích + kontrolně proti reálné flotile (auth, badwolf, pulse, venom) — dnes by žádná služba falešný poplach nespustila. Případ auth je hezká ukázka, proč podmínka vyžaduje i změnu zdroje: jeho image je o 9 dní starší než kontejner, protože se 31. 7. nasazoval týž commit a Docker legitimně použil cache.

📌 .deploy-commit leží na HOSTU v /opt/<služba>/.deploy-commit, ne uvnitř kontejneru (WORKDIR bývá /app a soubor se do image nekopíruje):

ssh root@<server> "cat /opt/<služba>/.deploy-commit"     # ✅ správně
docker exec <container> cat .deploy-commit               # ❌ hlásí „chybí" i u zdravé služby

Sentinel na to 1. 8. naletěl a chvíli mylně hlásil, že u s60-mail a auth chybí verifikace commitu.

Doporučené pořadí ověření po deployi:

  1. deploy.sh sám: Commit verification.deploy-commit + source hash local = remote
  2. chování — endpoint, který se novou verzí prokazatelně mění (u mailu /healthdatabase/redis)
  3. env se reálně propsala: md5 hodnoty v /opt/<svc>/.env proti /root/secrets/<svc>/.env.<env>
  4. u guardovaných endpointů obě strany: bez tokenu 401, s tokenem 200 — samotné 401 dokazuje jen zaregistrovanou routu, ne funkční handler

Související dokumenty