@onlineapps/conn-orch-validator 8.0.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [8.1.0] — 2026-09-15
8
+
9
+ ### Changed — deploy job šablony aplikuje migrace DB mezi `pull` a `up` (d.421)
10
+
11
+ Blok `oa-ci v1` v `templates/business-service/.gitlab-ci.yml` (řádek `G-CI`) má nový krok: po
12
+ `compose pull` a **před** `compose up` pustí na biz boxu běžec `api/scripts/lib/mariadb-migrations.sh`
13
+ z klonu api, který si krok uniformy vedle checkoutu stejně dělá — fáze 2 rozhodnutí vlastníka
14
+ `api/docs/governance/confirmations/db-migrations-first-deploy.md` 001. Pořadí je ta pojistka:
15
+ nový obraz je stažený, starý kontejner ještě obsluhuje, takže selhání migrace zastaví nasazení
16
+ s běžící předchozí verzí. Běžec se **nese**, nekopíruje: proud, kterým ssh krmí `bash -s`, je řádek
17
+ hesla, volby shellu, běžec z klonu api a teprve pak tělo skriptu — druhá implementace aplikátoru
18
+ nevzniká nikde (`change-discipline.md` § One rail per concern).
19
+
20
+ Co o schématu rozhoduje: `config/service/integration-contract.json`. Služba bez bloku `database`
21
+ vypíše `NOT APPLICABLE (no database block)` a nasazuje se dál; služba s blokem dostane
22
+ `database.schema` migrované z adresáře, který jmenuje `database.migrations`, s trackerem
23
+ `config/runtime/applied-migrations-<schema>.txt` ve vlastním checkoutu na boxu (tentýž plochý formát
24
+ jako fáze 1). Účet je účet služby (`MARIADB_MIGRATION_USER`/`_PASSWORD` z env souborů na boxu, nově
25
+ deklarované v `config/env-templates/__SERVICE_NAME__.env`), nikdy root, a krok **nic nezakládá**:
26
+ chybí-li klient `mysql`, klíč, env soubor nebo nemá-li účet na schéma právo, job zčervená s hláškou
27
+ na runbook `api/docs/setup/INSTALL.md`.
28
+
29
+ ## [8.0.0] — 2026-09-14
30
+
7
31
  ### Added — zahazovací testovací schéma je export balíčku (d.407)
8
32
 
9
33
  `createThrowawaySchema` (`src/utils/throwawaySchema.js`, na indexu balíčku) staví schéma, které
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-validator",
3
- "version": "8.0.0",
3
+ "version": "8.1.0",
4
4
  "description": "Validation orchestrator for OA Drive microservices - coordinates validation across all layers (base, infra, orch, business)",
5
5
  "oa": {
6
6
  "category": "orchestration"
@@ -28,7 +28,7 @@
28
28
  "author": "OnlineApps",
29
29
  "license": "PROPRIETARY",
30
30
  "dependencies": {
31
- "@onlineapps/logger-contract": "1.1.0",
31
+ "@onlineapps/logger-contract": "1.3.0",
32
32
  "@onlineapps/service-validator-core": "2.0.0"
33
33
  },
34
34
  "devDependencies": {
@@ -198,6 +198,39 @@ deploy-production:
198
198
  exit 1
199
199
  fi
200
200
  - echo "Deploying $BIZ_IMAGE_DIGEST to $DEPLOY_HOST ($CI_ENVIRONMENT_URL)"
201
+ # The read-only api clone the uniform step already made beside this checkout.
202
+ # Derived from CI_PROJECT_DIR rather than spelled out a second time: the
203
+ # layout is declared once, in GIT_CLONE_PATH, so the two cannot drift apart.
204
+ # TWO steps read it: the migration runner streamed to the box below, and the
205
+ # post-deploy gate at the end of this job.
206
+ - API_CHECKOUT="$(dirname "$(dirname "$CI_PROJECT_DIR")")/api"
207
+ # WHICH SCHEMA, IF ANY (confirmation db-migrations-first-deploy 001, phase 2).
208
+ # Read from the one declaration there is - this service's own installation
209
+ # contract - and read HERE, on the runner: the contract is this repository's,
210
+ # jq is in this image, and the box is not asked to carry a JSON parser for two
211
+ # values. A service that declares no `database` block has no schema to
212
+ # migrate, and the step below says that out loud rather than passing over it
213
+ # in silence (automation-gates.md 5: a step that checked nothing must never
214
+ # look like one that checked).
215
+ - |
216
+ CONTRACT="$CI_PROJECT_DIR/config/service/integration-contract.json"
217
+ if [ ! -f "$CONTRACT" ]; then
218
+ echo "[deploy] FATAL: no installation contract at $CONTRACT - this deploy cannot tell whether the service has a schema to migrate, and guessing is not an option. Fix: commit config/service/integration-contract.json (the uniform manifest requires it of every biz repository)." >&2
219
+ exit 1
220
+ fi
221
+ BIZ_DB_SCHEMA="$(jq -r '.database.schema // empty' "$CONTRACT")"
222
+ BIZ_DB_MIGRATIONS="$(jq -r '.database.migrations // empty' "$CONTRACT")"
223
+ if [ -n "$BIZ_DB_SCHEMA" ] && [ -z "$BIZ_DB_MIGRATIONS" ]; then
224
+ echo "[deploy] FATAL: $CONTRACT declares database.schema ($BIZ_DB_SCHEMA) and no database.migrations - the deploy would not know which files to apply. Fix: declare the glob of the directory that holds them (api/docs/biz/70-contracts/database-contract.md)." >&2
225
+ exit 1
226
+ fi
227
+ if [ -n "$BIZ_DB_MIGRATIONS" ]; then
228
+ # The runner applies a DIRECTORY and does not descend into it; the
229
+ # contract declares the glob (migrations/*.sql, migrations/BASELINE/*.sql),
230
+ # so the directory is its dirname - the same reading the runbook gives it
231
+ # (api/docs/setup/INSTALL.md, applying a business schema).
232
+ BIZ_DB_MIGRATIONS="$(dirname "$BIZ_DB_MIGRATIONS")"
233
+ fi
201
234
  # DEPLOY_PATH / DEPLOY_HOST / DEPLOY_USER are CI variables: the box decides
202
235
  # where its checkout lives, and no path is baked into this file.
203
236
  #
@@ -208,18 +241,41 @@ deploy-production:
208
241
  # first and the script after it: the remote reads that one line, exports it,
209
242
  # and hands the rest of the stream to `bash -s` - ssh's stdin is a socket
210
243
  # there, so `read` consumes exactly the line and no more.
244
+ #
245
+ # The shell the remote runs is ASSEMBLED from three parts, in this order: the
246
+ # password line, the shell options, and then the platform's own MariaDB
247
+ # migration runner followed by the body below. The runner is carried rather
248
+ # than copied - it is api/scripts/lib/mariadb-migrations.sh out of the clone
249
+ # the uniform step made, so the box applies the same shell the runbook applies
250
+ # by hand in phase 1, and no second implementation of it exists anywhere
251
+ # (change-discipline.md - One rail per concern). The options are set once, for
252
+ # the whole assembled stream, which is why they are no longer the first line
253
+ # of the body.
211
254
  - |
212
255
  {
213
256
  printf '%s\n' "$CI_REGISTRY_PASSWORD"
257
+ printf '%s\n' "set -euo pipefail"
258
+ cat "$API_CHECKOUT/scripts/lib/mariadb-migrations.sh"
214
259
  cat
215
- } <<'REMOTE' | ssh $DEPLOY_USER@$DEPLOY_HOST "IFS= read -r REGISTRY_PASSWORD; export REGISTRY_PASSWORD; bash -s -- '$DEPLOY_PATH' '$CI_REGISTRY' '$CI_REGISTRY_USER' '$CI_PROJECT_PATH_SLUG' '$BIZ_IMAGE_DIGEST' '$CI_PROJECT_PATH'"
216
- set -euo pipefail
260
+ } <<'REMOTE' | ssh $DEPLOY_USER@$DEPLOY_HOST "IFS= read -r REGISTRY_PASSWORD; export REGISTRY_PASSWORD; bash -s -- '$DEPLOY_PATH' '$CI_REGISTRY' '$CI_REGISTRY_USER' '$CI_PROJECT_PATH_SLUG' '$BIZ_IMAGE_DIGEST' '$CI_PROJECT_PATH' '$BIZ_DB_SCHEMA' '$BIZ_DB_MIGRATIONS'"
217
261
  DEPLOY_PATH="$1"
218
262
  REGISTRY="$2"
219
263
  REGISTRY_USER="$3"
220
264
  PROJECT_SLUG="$4"
221
265
  export BIZ_IMAGE_DIGEST="$5"
222
266
  PROJECT_PATH="$6"
267
+ # Empty when the installation contract declares no `database` block - the
268
+ # only thing that turns the migration step below on or off.
269
+ DB_SCHEMA="$7"
270
+ DB_MIGRATIONS_DIR="$8"
271
+ # This service's own env file on the box, the per-machine half of the two
272
+ # the compose file names (../shared.env is the host's). It carries the
273
+ # migration account - the SAME account the service connects as.
274
+ SERVICE_ENV="config/env-active/__SERVICE_NAME__.env"
275
+ # The host-level env file of the biz box, one per box and referenced rather
276
+ # than copied (server-topology 004) - the same path docker-compose.production.yml
277
+ # names.
278
+ HOST_ENV="../shared.env"
223
279
  # REGISTRY_PASSWORD arrives in the environment, from the first line of this
224
280
  # script's own stdin. Empty means the runner had no CI_REGISTRY_PASSWORD:
225
281
  # the pull would fail later with an authentication error that names nothing.
@@ -247,6 +303,72 @@ deploy-production:
247
303
  echo "$REGISTRY_PASSWORD" | docker --config "$DOCKER_CONFIG_DIR" login -u "$REGISTRY_USER" --password-stdin "$REGISTRY"
248
304
  echo "[deploy] pulling $BIZ_IMAGE_DIGEST"
249
305
  docker --config "$DOCKER_CONFIG_DIR" compose -f docker-compose.production.yml pull
306
+
307
+ # ─── Migrations: after the pull, before the switch ───────────────
308
+ # Owner decision api/docs/governance/confirmations/db-migrations-first-deploy.md
309
+ # 001, phase 2. The ordering IS the safety: at this point the new image is
310
+ # on the box and the OLD container is still serving, so a migration that
311
+ # fails stops the deploy with the previous version running and the new one
312
+ # never started. Applying after the switch would mean the new code meets a
313
+ # schema it does not have; applying before the pull would mean a schema
314
+ # change for an image that might not even be pullable.
315
+ #
316
+ # The account is the SERVICE's, never root: its grants cover its own schema
317
+ # and nothing else, so a migration that reaches sideways into a neighbouring
318
+ # database is refused by the server rather than politely applied
319
+ # (db-accounts-per-service 001). This step CREATES NOTHING - schema and
320
+ # account are an operator's step in the runbook, phase 1, and a deploy that
321
+ # provisioned what it found missing would both grant itself those privileges
322
+ # and hide a wrongly provisioned box.
323
+ if [ -z "$DB_SCHEMA" ]; then
324
+ echo "[deploy] migrations NOT APPLICABLE (no database block) - config/service/integration-contract.json declares no database, so this service has no schema to migrate."
325
+ else
326
+ # The two env files the compose names, in the order it names them: the
327
+ # host-level one first, this service's own second, so the same value wins
328
+ # here that wins inside the container. Reading only one of them would
329
+ # make this step disagree with the service it migrates for, depending on
330
+ # which file the box happens to carry DB_HOST in.
331
+ for env_file in "$HOST_ENV" "$SERVICE_ENV"; do
332
+ if [ ! -f "$env_file" ]; then
333
+ echo "[deploy] Missing env file - $env_file does not exist on this box, and docker-compose.production.yml names it: between the two of them they carry the endpoint and the migration account of $DB_SCHEMA. Fix: place it on the box (both are per-machine and never committed), then re-run this deploy." >&2
334
+ exit 1
335
+ fi
336
+ set -a
337
+ . "$env_file"
338
+ set +a
339
+ done
340
+ missing=""
341
+ [ -n "${MARIADB_MIGRATION_USER:-}" ] || missing="$missing MARIADB_MIGRATION_USER"
342
+ [ -n "${MARIADB_MIGRATION_PASSWORD:-}" ] || missing="$missing MARIADB_MIGRATION_PASSWORD"
343
+ [ -n "${DB_HOST:-}" ] || missing="$missing DB_HOST"
344
+ [ -n "${DB_PORT:-}" ] || missing="$missing DB_PORT"
345
+ if [ -n "$missing" ]; then
346
+ echo "[deploy] Missing environment variable -$missing in $DEPLOY_PATH/$SERVICE_ENV. Expected: the migration account of $DB_SCHEMA (the same account as DB_USER, granted on that schema only) and the endpoint it is reached at. Fix: add the keys to that file on this box; config/env-templates/ declares each of them with the reason it exists." >&2
347
+ exit 1
348
+ fi
349
+ # The precondition confirmation 001 demands before any migration: the
350
+ # account opens the schema, or this deploy stops and names the runbook.
351
+ if ! mariadb_migrations_can_open_over_tcp "$DB_HOST" "$DB_PORT" "$DB_SCHEMA"; then
352
+ echo "[deploy] Schema or account missing - '$MARIADB_MIGRATION_USER' cannot open '$DB_SCHEMA' at $DB_HOST:$DB_PORT. Expected: the schema and the account, with grants on ${DB_SCHEMA}.* and nothing else, provisioned once by an operator. Fix: run the business-schema step of api/docs/setup/INSTALL.md on the infra box, then re-run this deploy. This step never creates either: provisioning is a decision, not a deploy side effect." >&2
353
+ exit 1
354
+ fi
355
+ # The tracker is the caller's state and it lives in the checkout on this
356
+ # box, under the gitignored config/runtime/ - same file name and same flat
357
+ # format the runbook writes in phase 1, so a schema already carried
358
+ # forward is skipped rather than applied twice (confirmation 001:
359
+ # "fáze 2 nesmí změnit sémantiku trackeru").
360
+ mkdir -p config/runtime
361
+ TRACKER="config/runtime/applied-migrations-${DB_SCHEMA}.txt"
362
+ touch "$TRACKER"
363
+ apply_mariadb_migrations_over_tcp "$DB_MIGRATIONS_DIR" "$TRACKER" "$DB_HOST" "$DB_PORT" "$DB_SCHEMA"
364
+ # The runner logs each file it applies; what this line adds is the state
365
+ # that outlives the deploy. The runner's own counter of files it passed
366
+ # over is deliberately not echoed: no word that reads as a bypass belongs
367
+ # anywhere in a deploy job, and the gate that keeps this job free of one
368
+ # judges by the word, not by the intent.
369
+ echo "[deploy] migrations $DB_SCHEMA: $MARIADB_MIGRATIONS_APPLIED applied now, $(wc -l < "$TRACKER" | tr -d " ") recorded in $DEPLOY_PATH/$TRACKER"
370
+ fi
371
+
250
372
  docker --config "$DOCKER_CONFIG_DIR" compose -f docker-compose.production.yml up -d
251
373
  REMOTE
252
374
  # The deploy is not finished until the platform's own post-deploy gate says
@@ -257,11 +379,8 @@ deploy-production:
257
379
  # runs in remote mode — same script, same contract, same verdicts as a local
258
380
  # run, only reading over ssh.
259
381
  - |
260
- # The gate runs out of the read-only api clone the uniform step already
261
- # made beside this checkout. Derived from CI_PROJECT_DIR rather than
262
- # spelled out a second time: the layout is declared once, in
263
- # GIT_CLONE_PATH, so the two cannot drift apart.
264
- API_CHECKOUT="$(dirname "$(dirname "$CI_PROJECT_DIR")")/api"
382
+ # The gate runs out of the same read-only api clone the migration runner
383
+ # was streamed from; API_CHECKOUT is derived once, above.
265
384
  bash "$API_CHECKOUT/scripts/run-post-deploy-gate.sh" \
266
385
  --target "$DEPLOY_GATE_TARGETS" \
267
386
  --biz-host "$DEPLOY_USER@$DEPLOY_HOST" \
@@ -2,3 +2,21 @@
2
2
  # Copy to config/env-active/__SERVICE_NAME__.env and adjust values.
3
3
 
4
4
  SERVICE_NAME=__SERVICE_NAME__
5
+
6
+ # Schema migrations at deploy time. The deploy job (block `oa-ci v1` of
7
+ # .gitlab-ci.yml) runs api/scripts/lib/mariadb-migrations.sh between `compose
8
+ # pull` and `compose up`, and that runner REQUIRES both names - fail-fast, no
9
+ # default value (owner decision
10
+ # api/docs/governance/confirmations/db-migrations-first-deploy.md 001, phase 2).
11
+ # The account is the SAME one DB_USER names: its grants cover this service's
12
+ # schema and nothing else, so a migration that reached into a neighbouring
13
+ # database is refused by the server instead of quietly applied
14
+ # (db-accounts-per-service 001). Pattern: api/config/env-templates/auth.env.
15
+ #
16
+ # Commented out because this scaffold has no database: its
17
+ # config/service/integration-contract.json declares no `database` block, the
18
+ # deploy step prints NOT APPLICABLE and reads neither name. A service that
19
+ # declares that block uncomments both here and fills the values in
20
+ # config/env-active/ only - per machine, never in git.
21
+ # MARIADB_MIGRATION_USER=
22
+ # MARIADB_MIGRATION_PASSWORD=
@@ -40,7 +40,7 @@ MINIO_ACCESS_KEY=CHANGE_ME
40
40
  # The secret of that same object-store account; shares the compose-interpolation note of MINIO_ACCESS_KEY.
41
41
  MINIO_SECRET_KEY=CHANGE_ME
42
42
 
43
- # How often (ms) a business service publishes its MQ heartbeat, and the ONE source of truth for how fresh that signal has to be: every staleness threshold is a multiple of this cadence (BIZ_HEARTBEAT_STALE_FACTOR), never a second duration set by hand - owner decision docs/governance/confirmations/biz-health-freshness.md 001, which supersedes the absolute INFRASTRUCTURE_HEALTH_BUSINESS_STALE_AFTER of batch 111a. It is shared because the publisher and the judge are different processes: the registry derives its monitor and janitor thresholds from it today, and @onlineapps/service-wrapper is to read it in place of the wrapper.registry.heartbeatInterval runtime default, so a cadence change cannot reach one side only.
43
+ # How often (ms) a business service publishes its MQ heartbeat, and the ONE source of truth for how fresh that signal has to be: every staleness threshold is a multiple of this cadence (BIZ_HEARTBEAT_STALE_FACTOR), never a second duration set by hand - owner decision docs/governance/confirmations/biz-health-freshness.md 001, which supersedes the absolute INFRASTRUCTURE_HEALTH_BUSINESS_STALE_AFTER of batch 111a. It is shared because the publisher and the judge are different processes: the registry derives its monitor and janitor thresholds from it today, and @onlineapps/service-wrapper reads it as the only cadence there is (the wrapper.registry.heartbeatInterval config key is retired and refused by name), so a cadence change cannot reach one side only.
44
44
  BIZ_HEARTBEAT_INTERVAL_MS=30000
45
45
 
46
46
  # How many heartbeat cadences a business service may stay silent before it is judged stale; the only knob on top of BIZ_HEARTBEAT_INTERVAL_MS, and the reason no threshold is written as a duration anywhere (owner decision docs/governance/confirmations/biz-health-freshness.md 001: the factor is config, not doctrine - changing it is an ordinary config change). May be fractional; the derived threshold is whole milliseconds.
@@ -94,6 +94,28 @@ CI publishes that digest from the build stage and the deploy job refuses to run
94
94
  without it (requirement R1). A first deploy also clones the checkout on the box
95
95
  if it is missing, so onboarding a new service needs no manual step there.
96
96
 
97
+ ### Migrations at deploy time
98
+
99
+ Between that `pull` and that `up` the deploy job applies this service's pending
100
+ MariaDB migrations — the ordering is the safety: the new image is already on the
101
+ box, the previous container is still serving, and a migration that fails stops
102
+ the deploy before the switch. Two facts decide what happens:
103
+
104
+ - `config/service/integration-contract.json` → `database`. This scaffold declares
105
+ no such block, so the step prints `NOT APPLICABLE (no database block)` and the
106
+ deploy carries on. A service that declares one has its `database.schema`
107
+ migrated from the directory `database.migrations` names.
108
+ - `config/env-active/__SERVICE_NAME__.env` → `MARIADB_MIGRATION_USER` and
109
+ `MARIADB_MIGRATION_PASSWORD`, the service's own account. The step creates
110
+ neither the schema nor the account and refuses the deploy when that account
111
+ cannot open the schema, naming the runbook instead.
112
+
113
+ Who creates them, with which grants and which collation, is the runbook's step
114
+ and is not repeated here: `api/docs/setup/INSTALL.md`, the business-schema step.
115
+ The decision behind both halves — two phases, the service account as the
116
+ isolation guard, one runner and one tracker across them — is owner confirmation
117
+ `api/docs/governance/confirmations/db-migrations-first-deploy.md` 001.
118
+
97
119
  ## Status
98
120
 
99
121
  Current. One of the three files the installation contract requires by name —