@onlineapps/conn-orch-validator 8.0.0 → 9.0.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,69 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [9.0.0] — 2026-09-15
8
+
9
+ ### Removed — šablona už nenese job `verify-installation-contract` (d.470)
10
+
11
+ Blok `oa-ci v1` v `templates/business-service/.gitlab-ci.yml` deklaroval job, který pouštěl
12
+ `sh scripts/verify-installation-docs-sql-contract.sh`. Šablona ten skript **nikdy nenesla**
13
+ (`templates/business-service/scripts/` = jen `verify-deploy-uniform.sh`) — osm biz repozitářů mělo
14
+ každý vlastní kopii a mapa vydání d.320 ruší všech osm. Job v bloku tedy sliboval kontrolu, kterou
15
+ nově nascaffoldovaná služba nemůže provést (`automation-gates.md` §5).
16
+
17
+ Norma se nikam neztratila, jen má jednu kolej místo dvou: `install-contract` je ve validatoru
18
+ (`src/utils/installContract.js`) a manifest ji vymáhá řádky `G-SETUP`, `D-DB-PACKAGE` a
19
+ `D-DB-HEADERS`, které běží v uniformě před SSH krokem téhož deploy jobu (konfirmace
20
+ `biz-service-manifest` 008) — a jako podpříkaz `biz-ci-gate verify-install-contract`.
21
+
22
+ ### Changed — test job šablony nedělá `REGISTRY_URL` (d.470)
23
+
24
+ `test:` (mimo značky, výchozí job nové služby) deklaroval `REGISTRY_URL: "http://localhost:33100"`.
25
+ Nic, co ten job spouští, ho nečte: `test:ci` je `jest`, `jest.config.js` matchuje
26
+ `**/tests/**/*.test.js` a jediný test šablony požaduje `src/handlers/v3/echo.js`. Klíč byl HTTP
27
+ adresou registru pro `getService()`; konfirmace `biz-discovery-redis` 001 přesunula čtení na Redis
28
+ projekci a `725b25c2` HTTP volání odstranil. Čtyři otázky před rušením deklarace jsou zodpovězeny
29
+ v `api/docs/governance/confirmations/declaration-removal.md` 001. Deklarace klíče v
30
+ `config/env-templates/shared.env` se netýká — tu vlastní `api/config/shared-env.json`.
31
+
32
+ ### Changed — deploy job migruje jen tam, kde fáze 1 proběhla (d.477)
33
+
34
+ Krok migrací dělal `touch "$TRACKER"` a poté aplikoval vše, co tracker neznal — na boxu, kde
35
+ fáze 1 nikdy neběžela, by tak deploy aplikoval **celou řadu sám**. Konfirmace
36
+ `api/docs/governance/confirmations/db-migrations-first-deploy.md` 001 dělí práci na dvě fáze a
37
+ první z nich (schéma, účet a první běh téhož běžce ručně podle runbooku) nechává člověku.
38
+
39
+ Tracker `config/runtime/applied-migrations-<schema>.txt` je proto od teď **precondition**, ne
40
+ výstup: chybí-li, job končí fail-fast hláškou `[deploy] Migrations tracker <cesta> missing -
41
+ phase 1 … has not run on this box` a jmenuje runbook `api/docs/setup/INSTALL.md`. Žádný `touch`,
42
+ žádné `mkdir` — krok nezakládá nic, stejně jako nezakládá schéma ani účet. Prázdný tracker
43
+ zůstává platným stavem (fáze 1 bez přenesených souborů). `docs/80-setup/INSTALL.md` šablony
44
+ § Migrations at deploy time to říká třetí odrážkou.
45
+
46
+ ## [8.1.0] — 2026-09-15
47
+
48
+ ### Changed — deploy job šablony aplikuje migrace DB mezi `pull` a `up` (d.421)
49
+
50
+ Blok `oa-ci v1` v `templates/business-service/.gitlab-ci.yml` (řádek `G-CI`) má nový krok: po
51
+ `compose pull` a **před** `compose up` pustí na biz boxu běžec `api/scripts/lib/mariadb-migrations.sh`
52
+ z klonu api, který si krok uniformy vedle checkoutu stejně dělá — fáze 2 rozhodnutí vlastníka
53
+ `api/docs/governance/confirmations/db-migrations-first-deploy.md` 001. Pořadí je ta pojistka:
54
+ nový obraz je stažený, starý kontejner ještě obsluhuje, takže selhání migrace zastaví nasazení
55
+ s běžící předchozí verzí. Běžec se **nese**, nekopíruje: proud, kterým ssh krmí `bash -s`, je řádek
56
+ hesla, volby shellu, běžec z klonu api a teprve pak tělo skriptu — druhá implementace aplikátoru
57
+ nevzniká nikde (`change-discipline.md` § One rail per concern).
58
+
59
+ Co o schématu rozhoduje: `config/service/integration-contract.json`. Služba bez bloku `database`
60
+ vypíše `NOT APPLICABLE (no database block)` a nasazuje se dál; služba s blokem dostane
61
+ `database.schema` migrované z adresáře, který jmenuje `database.migrations`, s trackerem
62
+ `config/runtime/applied-migrations-<schema>.txt` ve vlastním checkoutu na boxu (tentýž plochý formát
63
+ jako fáze 1). Účet je účet služby (`MARIADB_MIGRATION_USER`/`_PASSWORD` z env souborů na boxu, nově
64
+ deklarované v `config/env-templates/__SERVICE_NAME__.env`), nikdy root, a krok **nic nezakládá**:
65
+ chybí-li klient `mysql`, klíč, env soubor nebo nemá-li účet na schéma právo, job zčervená s hláškou
66
+ na runbook `api/docs/setup/INSTALL.md`.
67
+
68
+ ## [8.0.0] — 2026-09-14
69
+
7
70
  ### Added — zahazovací testovací schéma je export balíčku (d.407)
8
71
 
9
72
  `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": "9.0.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,8 +28,8 @@
28
28
  "author": "OnlineApps",
29
29
  "license": "PROPRIETARY",
30
30
  "dependencies": {
31
- "@onlineapps/logger-contract": "1.1.0",
32
- "@onlineapps/service-validator-core": "2.0.0"
31
+ "@onlineapps/logger-contract": "2.0.0",
32
+ "@onlineapps/service-validator-core": "2.0.1"
33
33
  },
34
34
  "devDependencies": {
35
35
  "jest": "^29.5.0"
@@ -2,12 +2,13 @@
2
2
  # Written by @onlineapps/conn-orch-validator (row G-CI of
3
3
  # manifests/biz-service.manifest.json). The PLATFORM half of this pipeline: which
4
4
  # pipelines run at all, how a service is built, how its image is identified (R5),
5
- # how the digest reaches the deploy (R1/R2), that the installation document and
6
- # the migrations still agree, the uniform gate that runs before the SSH step
7
- # (confirmation biz-service-manifest 008) and the post-deploy gate that runs
8
- # after it (confirmation deploy-gate-targets 003). npx oa-sync-template .gitlab-ci.yml
9
- # --target . rewrites everything between these two markers and nothing outside
10
- # them.
5
+ # how the digest reaches the deploy (R1/R2), the uniform gate that runs before
6
+ # the SSH step (confirmation biz-service-manifest 008) and the post-deploy gate
7
+ # that runs after it (confirmation deploy-gate-targets 003). The installation
8
+ # contract is checked by that uniform gate, in the manifest rows G-SETUP,
9
+ # D-DB-PACKAGE and D-DB-HEADERS, and no longer by a job of its own (d.470).
10
+ # npx oa-sync-template .gitlab-ci.yml --target . rewrites everything between
11
+ # these two markers and nothing outside them.
11
12
  #
12
13
  # Outside them is this repository's own: its test job - which database, which
13
14
  # ci:gate:* steps and which artefacts its integration needs is a fact about the
@@ -32,16 +33,6 @@ variables:
32
33
  IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
33
34
  IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest
34
35
 
35
- verify-installation-contract:
36
- stage: test
37
- image: alpine:latest
38
- script:
39
- - sh scripts/verify-installation-docs-sql-contract.sh
40
- rules:
41
- - if: $CI_PIPELINE_SOURCE == "merge_request_event"
42
- - if: $CI_COMMIT_BRANCH == "main"
43
- - if: $CI_COMMIT_BRANCH == "production"
44
-
45
36
  build:
46
37
  stage: build
47
38
  image: docker:24
@@ -198,6 +189,39 @@ deploy-production:
198
189
  exit 1
199
190
  fi
200
191
  - echo "Deploying $BIZ_IMAGE_DIGEST to $DEPLOY_HOST ($CI_ENVIRONMENT_URL)"
192
+ # The read-only api clone the uniform step already made beside this checkout.
193
+ # Derived from CI_PROJECT_DIR rather than spelled out a second time: the
194
+ # layout is declared once, in GIT_CLONE_PATH, so the two cannot drift apart.
195
+ # TWO steps read it: the migration runner streamed to the box below, and the
196
+ # post-deploy gate at the end of this job.
197
+ - API_CHECKOUT="$(dirname "$(dirname "$CI_PROJECT_DIR")")/api"
198
+ # WHICH SCHEMA, IF ANY (confirmation db-migrations-first-deploy 001, phase 2).
199
+ # Read from the one declaration there is - this service's own installation
200
+ # contract - and read HERE, on the runner: the contract is this repository's,
201
+ # jq is in this image, and the box is not asked to carry a JSON parser for two
202
+ # values. A service that declares no `database` block has no schema to
203
+ # migrate, and the step below says that out loud rather than passing over it
204
+ # in silence (automation-gates.md 5: a step that checked nothing must never
205
+ # look like one that checked).
206
+ - |
207
+ CONTRACT="$CI_PROJECT_DIR/config/service/integration-contract.json"
208
+ if [ ! -f "$CONTRACT" ]; then
209
+ 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
210
+ exit 1
211
+ fi
212
+ BIZ_DB_SCHEMA="$(jq -r '.database.schema // empty' "$CONTRACT")"
213
+ BIZ_DB_MIGRATIONS="$(jq -r '.database.migrations // empty' "$CONTRACT")"
214
+ if [ -n "$BIZ_DB_SCHEMA" ] && [ -z "$BIZ_DB_MIGRATIONS" ]; then
215
+ 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
216
+ exit 1
217
+ fi
218
+ if [ -n "$BIZ_DB_MIGRATIONS" ]; then
219
+ # The runner applies a DIRECTORY and does not descend into it; the
220
+ # contract declares the glob (migrations/*.sql, migrations/BASELINE/*.sql),
221
+ # so the directory is its dirname - the same reading the runbook gives it
222
+ # (api/docs/setup/INSTALL.md, applying a business schema).
223
+ BIZ_DB_MIGRATIONS="$(dirname "$BIZ_DB_MIGRATIONS")"
224
+ fi
201
225
  # DEPLOY_PATH / DEPLOY_HOST / DEPLOY_USER are CI variables: the box decides
202
226
  # where its checkout lives, and no path is baked into this file.
203
227
  #
@@ -208,18 +232,41 @@ deploy-production:
208
232
  # first and the script after it: the remote reads that one line, exports it,
209
233
  # and hands the rest of the stream to `bash -s` - ssh's stdin is a socket
210
234
  # there, so `read` consumes exactly the line and no more.
235
+ #
236
+ # The shell the remote runs is ASSEMBLED from three parts, in this order: the
237
+ # password line, the shell options, and then the platform's own MariaDB
238
+ # migration runner followed by the body below. The runner is carried rather
239
+ # than copied - it is api/scripts/lib/mariadb-migrations.sh out of the clone
240
+ # the uniform step made, so the box applies the same shell the runbook applies
241
+ # by hand in phase 1, and no second implementation of it exists anywhere
242
+ # (change-discipline.md - One rail per concern). The options are set once, for
243
+ # the whole assembled stream, which is why they are no longer the first line
244
+ # of the body.
211
245
  - |
212
246
  {
213
247
  printf '%s\n' "$CI_REGISTRY_PASSWORD"
248
+ printf '%s\n' "set -euo pipefail"
249
+ cat "$API_CHECKOUT/scripts/lib/mariadb-migrations.sh"
214
250
  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
251
+ } <<'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
252
  DEPLOY_PATH="$1"
218
253
  REGISTRY="$2"
219
254
  REGISTRY_USER="$3"
220
255
  PROJECT_SLUG="$4"
221
256
  export BIZ_IMAGE_DIGEST="$5"
222
257
  PROJECT_PATH="$6"
258
+ # Empty when the installation contract declares no `database` block - the
259
+ # only thing that turns the migration step below on or off.
260
+ DB_SCHEMA="$7"
261
+ DB_MIGRATIONS_DIR="$8"
262
+ # This service's own env file on the box, the per-machine half of the two
263
+ # the compose file names (../shared.env is the host's). It carries the
264
+ # migration account - the SAME account the service connects as.
265
+ SERVICE_ENV="config/env-active/__SERVICE_NAME__.env"
266
+ # The host-level env file of the biz box, one per box and referenced rather
267
+ # than copied (server-topology 004) - the same path docker-compose.production.yml
268
+ # names.
269
+ HOST_ENV="../shared.env"
223
270
  # REGISTRY_PASSWORD arrives in the environment, from the first line of this
224
271
  # script's own stdin. Empty means the runner had no CI_REGISTRY_PASSWORD:
225
272
  # the pull would fail later with an authentication error that names nothing.
@@ -247,6 +294,83 @@ deploy-production:
247
294
  echo "$REGISTRY_PASSWORD" | docker --config "$DOCKER_CONFIG_DIR" login -u "$REGISTRY_USER" --password-stdin "$REGISTRY"
248
295
  echo "[deploy] pulling $BIZ_IMAGE_DIGEST"
249
296
  docker --config "$DOCKER_CONFIG_DIR" compose -f docker-compose.production.yml pull
297
+
298
+ # ─── Migrations: after the pull, before the switch ───────────────
299
+ # Owner decision api/docs/governance/confirmations/db-migrations-first-deploy.md
300
+ # 001, phase 2. The ordering IS the safety: at this point the new image is
301
+ # on the box and the OLD container is still serving, so a migration that
302
+ # fails stops the deploy with the previous version running and the new one
303
+ # never started. Applying after the switch would mean the new code meets a
304
+ # schema it does not have; applying before the pull would mean a schema
305
+ # change for an image that might not even be pullable.
306
+ #
307
+ # The account is the SERVICE's, never root: its grants cover its own schema
308
+ # and nothing else, so a migration that reaches sideways into a neighbouring
309
+ # database is refused by the server rather than politely applied
310
+ # (db-accounts-per-service 001). This step CREATES NOTHING - schema and
311
+ # account are an operator's step in the runbook, phase 1, and a deploy that
312
+ # provisioned what it found missing would both grant itself those privileges
313
+ # and hide a wrongly provisioned box.
314
+ if [ -z "$DB_SCHEMA" ]; then
315
+ echo "[deploy] migrations NOT APPLICABLE (no database block) - config/service/integration-contract.json declares no database, so this service has no schema to migrate."
316
+ else
317
+ # The two env files the compose names, in the order it names them: the
318
+ # host-level one first, this service's own second, so the same value wins
319
+ # here that wins inside the container. Reading only one of them would
320
+ # make this step disagree with the service it migrates for, depending on
321
+ # which file the box happens to carry DB_HOST in.
322
+ for env_file in "$HOST_ENV" "$SERVICE_ENV"; do
323
+ if [ ! -f "$env_file" ]; then
324
+ 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
325
+ exit 1
326
+ fi
327
+ set -a
328
+ . "$env_file"
329
+ set +a
330
+ done
331
+ missing=""
332
+ [ -n "${MARIADB_MIGRATION_USER:-}" ] || missing="$missing MARIADB_MIGRATION_USER"
333
+ [ -n "${MARIADB_MIGRATION_PASSWORD:-}" ] || missing="$missing MARIADB_MIGRATION_PASSWORD"
334
+ [ -n "${DB_HOST:-}" ] || missing="$missing DB_HOST"
335
+ [ -n "${DB_PORT:-}" ] || missing="$missing DB_PORT"
336
+ if [ -n "$missing" ]; then
337
+ 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
338
+ exit 1
339
+ fi
340
+ # The precondition confirmation 001 demands before any migration: the
341
+ # account opens the schema, or this deploy stops and names the runbook.
342
+ if ! mariadb_migrations_can_open_over_tcp "$DB_HOST" "$DB_PORT" "$DB_SCHEMA"; then
343
+ 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
344
+ exit 1
345
+ fi
346
+ # The tracker is the caller's state and it lives in the checkout on this
347
+ # box, under the gitignored config/runtime/ - same file name and same flat
348
+ # format the runbook writes in phase 1, so a schema already carried
349
+ # forward is skipped rather than applied twice (confirmation 001:
350
+ # "fáze 2 nesmí změnit sémantiku trackeru").
351
+ #
352
+ # Its EXISTENCE is the precondition, and the step never creates it.
353
+ # Confirmation 001 splits the work in two: phase 1 - schema, account and
354
+ # the first run of this same runner - is an operator's step in the
355
+ # runbook, and phase 2 is this job carrying that state forward. On a box
356
+ # phase 1 never touched there is no tracker, and a job that created one
357
+ # would apply the WHOLE series unattended on the first deploy, which is
358
+ # precisely the phase the owner kept manual. So a missing tracker stops
359
+ # the deploy and names the runbook, exactly as a missing schema does.
360
+ TRACKER="config/runtime/applied-migrations-${DB_SCHEMA}.txt"
361
+ if [ ! -f "$TRACKER" ]; then
362
+ echo "[deploy] Migrations tracker $DEPLOY_PATH/$TRACKER missing - phase 1 (the migrations of $DB_SCHEMA applied by hand with the same runner) has not run on this box. Expected: the flat tracker phase 1 leaves behind, carried forward by this deploy. Fix: run the business-schema migrations step of api/docs/setup/INSTALL.md for $DB_SCHEMA, then re-run this deploy. This step never creates the tracker: a deploy that did would apply the whole series unattended on a box nobody had provisioned." >&2
363
+ exit 1
364
+ fi
365
+ apply_mariadb_migrations_over_tcp "$DB_MIGRATIONS_DIR" "$TRACKER" "$DB_HOST" "$DB_PORT" "$DB_SCHEMA"
366
+ # The runner logs each file it applies; what this line adds is the state
367
+ # that outlives the deploy. The runner's own counter of files it passed
368
+ # over is deliberately not echoed: no word that reads as a bypass belongs
369
+ # anywhere in a deploy job, and the gate that keeps this job free of one
370
+ # judges by the word, not by the intent.
371
+ echo "[deploy] migrations $DB_SCHEMA: $MARIADB_MIGRATIONS_APPLIED applied now, $(wc -l < "$TRACKER" | tr -d " ") recorded in $DEPLOY_PATH/$TRACKER"
372
+ fi
373
+
250
374
  docker --config "$DOCKER_CONFIG_DIR" compose -f docker-compose.production.yml up -d
251
375
  REMOTE
252
376
  # The deploy is not finished until the platform's own post-deploy gate says
@@ -257,11 +381,8 @@ deploy-production:
257
381
  # runs in remote mode — same script, same contract, same verdicts as a local
258
382
  # run, only reading over ssh.
259
383
  - |
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"
384
+ # The gate runs out of the same read-only api clone the migration runner
385
+ # was streamed from; API_CHECKOUT is derived once, above.
265
386
  bash "$API_CHECKOUT/scripts/run-post-deploy-gate.sh" \
266
387
  --target "$DEPLOY_GATE_TARGETS" \
267
388
  --biz-host "$DEPLOY_USER@$DEPLOY_HOST" \
@@ -279,7 +400,6 @@ test:
279
400
  image: node:24-alpine
280
401
  variables:
281
402
  RABBITMQ_URL: "amqp://localhost:5672"
282
- REGISTRY_URL: "http://localhost:33100"
283
403
  NODE_ENV: "test"
284
404
  script:
285
405
  - npm ci
@@ -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,35 @@ 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. Three 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
+ - `config/runtime/applied-migrations-<schema>.txt` on the box — the tracker.
113
+ **It must already be there**, and this job never creates it. The file is what
114
+ phase 1 leaves behind: the operator who created the schema and the account also
115
+ ran the first pass of the same runner by hand, from the runbook. A deploy onto a
116
+ box phase 1 never touched therefore stops with `Migrations tracker … missing`
117
+ rather than applying the whole series unattended.
118
+
119
+ Who creates the schema, the account and that first tracker — with which grants and
120
+ which collation — is the runbook's step and is not repeated here:
121
+ `api/docs/setup/INSTALL.md`, the business-schema step. The decision behind both
122
+ halves — two phases, the service account as the isolation guard, one runner and
123
+ one tracker across them — is owner confirmation
124
+ `api/docs/governance/confirmations/db-migrations-first-deploy.md` 001.
125
+
97
126
  ## Status
98
127
 
99
128
  Current. One of the three files the installation contract requires by name —