@onlineapps/conn-orch-validator 9.0.0 → 11.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 +546 -0
- package/README.md +337 -19
- package/docs/DESIGN.md +32 -9
- package/manifests/biz-service.manifest.json +56 -6
- package/package.json +3 -2
- package/src/CookbookTestRunner.js +134 -22
- package/src/ValidationOrchestrator.js +312 -73
- package/src/cli/biz-ci-gate.js +191 -15
- package/src/cli/oa-sync-template.js +23 -8
- package/src/cli/oa-validate.js +70 -2
- package/src/index.js +21 -13
- package/src/lint/scripts/lintScripts.js +65 -18
- package/src/manifest/checks/composeRunnerBlock.js +37 -20
- package/src/manifest/checks/discoveryOrphan.js +2 -1
- package/src/manifest/checks/docsLintBridge.js +79 -21
- package/src/manifest/checks/gitTracked.js +14 -28
- package/src/manifest/checks/libraryPackage.js +3 -1
- package/src/manifest/checks/libraryWorkspace.js +18 -3
- package/src/manifest/checks/readmeRegion.js +9 -1
- package/src/manifest/checks/serviceConfig.js +29 -12
- package/src/manifest/checks/serviceDb.js +176 -7
- package/src/manifest/checks/serviceFiles.js +34 -7
- package/src/manifest/checks/serviceIdentityRows.js +3 -1
- package/src/manifest/checks/serviceRuntime.js +126 -1
- package/src/manifest/discovery.js +25 -7
- package/src/manifest/gitCheckout.js +84 -0
- package/src/manifest/runManifest.js +58 -7
- package/src/manifest/workspaceRoot.js +91 -5
- package/src/sync/serviceTemplate.js +76 -7
- package/src/sync/sharedEnv.js +11 -4
- package/src/sync/uniformFiles.js +91 -21
- package/src/utils/bizCiGateContract.js +25 -1
- package/src/utils/dbAccountGrants.js +126 -0
- package/src/utils/envContract.js +36 -6
- package/src/utils/envReads.js +102 -0
- package/src/utils/installContract.js +46 -5
- package/src/utils/libCompat.js +39 -19
- package/src/utils/preValidation.js +56 -11
- package/src/utils/stepFailure.js +106 -19
- package/src/utils/stepReferences.js +278 -0
- package/src/utils/testCoverageContract.js +60 -2
- package/src/utils/throwawaySchema.js +92 -7
- package/src/validatorIdentity.js +31 -0
- package/src/validators/ServiceStructureValidator.js +47 -15
- package/src/validators/ValidationProofGenerator.js +73 -34
- package/templates/business-service/.dockerignore +9 -1
- package/templates/business-service/.gitlab-ci.yml +199 -35
- package/templates/business-service/Dockerfile +49 -16
- package/templates/business-service/README.md +56 -9
- package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +17 -5
- package/templates/business-service/config/env-templates/shared.env +8 -2
- package/templates/business-service/docker-compose.production.yml +9 -0
- package/templates/business-service/docker-compose.yml +17 -0
- package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +1 -1
- package/templates/business-service/docs/80-setup/VALIDATION.md +1 -1
- package/templates/business-service/jest.config.js +9 -1
- package/templates/business-service/package.json.template +1 -1
- package/src/mocks/MockStorage.js +0 -188
|
@@ -2,19 +2,28 @@
|
|
|
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), the uniform gate that runs
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
5
|
+
# how the digest reaches the deploy (R1/R2), the uniform gate that runs on every
|
|
6
|
+
# pipeline (job validate-uniform, confirmation biz-service-manifest 010) and
|
|
7
|
+
# again before the SSH step as the binding instance (008), and the post-deploy
|
|
8
|
+
# gate that runs after it (confirmation deploy-gate-targets 003). The
|
|
9
|
+
# installation contract is checked by that uniform gate, in the manifest rows
|
|
10
|
+
# G-SETUP, D-DB-PACKAGE and D-DB-HEADERS, and no longer by a job of its own
|
|
11
|
+
# (d.470). npx oa-sync-template .gitlab-ci.yml --target . rewrites everything
|
|
12
|
+
# between these two markers and nothing outside them.
|
|
12
13
|
#
|
|
13
14
|
# Outside them is this repository's own: its test job - which database, which
|
|
14
15
|
# ci:gate:* steps and which artefacts its integration needs is a fact about the
|
|
15
16
|
# service - and whatever else it runs. The stages this block declares (test,
|
|
16
17
|
# build, secret-detection, deploy) cover those jobs too; measured over the eight
|
|
17
18
|
# biz repositories on 2026-09-11, none declares a fifth stage.
|
|
19
|
+
#
|
|
20
|
+
# One thing the platform can only RECOMMEND about that half, because it does not
|
|
21
|
+
# own it: give the service's own jobs the `devel` branch as well. The post-commit
|
|
22
|
+
# mirror pushes every commit there, and a `rules:` list naming only main,
|
|
23
|
+
# production and merge requests is why that branch produced no pipeline at all
|
|
24
|
+
# (010 point 2). The block does its side of it - validate-uniform and
|
|
25
|
+
# secret_detection run on devel; whether a service's own test job should too is
|
|
26
|
+
# the service's call.
|
|
18
27
|
include:
|
|
19
28
|
- template: Jobs/Secret-Detection.gitlab-ci.yml
|
|
20
29
|
|
|
@@ -33,6 +42,73 @@ variables:
|
|
|
33
42
|
IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
|
|
34
43
|
IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest
|
|
35
44
|
|
|
45
|
+
# The layout and the runtime every uniform run needs, declared ONCE for the two
|
|
46
|
+
# jobs that run it: the continuous check below and the binding one in
|
|
47
|
+
# deploy-production (confirmations biz-service-manifest 008 and 010). A hidden
|
|
48
|
+
# key is a declaration and never a job - GitLab runs nothing whose name starts
|
|
49
|
+
# with a dot - so this adds no gate, it gives the two that exist one source.
|
|
50
|
+
.oa-uniform:
|
|
51
|
+
# The uniform runs in these jobs, so they need the runtime the pinned engine
|
|
52
|
+
# declares - the same major the test stage and the Dockerfile carry, which is
|
|
53
|
+
# what R3 of the deploy contract compares.
|
|
54
|
+
image: node:24-alpine
|
|
55
|
+
variables:
|
|
56
|
+
# The engine answers the rows that read the platform SSOT only for a service
|
|
57
|
+
# lying at <root>/api_biz/<service> beside <root>/api. GitLab decides where a
|
|
58
|
+
# project is checked out, so the job ASKS for the one place that IS that
|
|
59
|
+
# layout - no symlink (api rule: No Symlinks) and no second copy of this
|
|
60
|
+
# repository, which would leave two trees and no way to tell which one the
|
|
61
|
+
# verdict was about. Needs [runners.custom_build_dir] enabled on the runner.
|
|
62
|
+
#
|
|
63
|
+
# $CI_CONCURRENT_ID is the runner's slot number, and it is what keeps two
|
|
64
|
+
# pipelines of this project off each other's tree. Since 010 the uniform runs
|
|
65
|
+
# on EVERY pipeline, so two of them on one runner is the ordinary case: with
|
|
66
|
+
# one shared path the second run either refuses on an api clone it did not
|
|
67
|
+
# make, or measures the one the first run is still replacing. The slot sits
|
|
68
|
+
# ABOVE api_biz, so the sibling api/ still lands beside this checkout, which
|
|
69
|
+
# is the layout verify-deploy-uniform.sh checks.
|
|
70
|
+
GIT_CLONE_PATH: $CI_BUILDS_DIR/oa-uniform/$CI_CONCURRENT_ID/api_biz/__SERVICE_NAME__
|
|
71
|
+
# Where the platform SSOT is read from, and which state of it a run is
|
|
72
|
+
# measured against. Read access is granted by the api project's CI job-token
|
|
73
|
+
# allowlist - the owner's step, outside any repository (confirmation
|
|
74
|
+
# biz-service-manifest 008 point 2).
|
|
75
|
+
API_PROJECT_PATH: onlineapps/oadrive/infra-mono
|
|
76
|
+
API_UNIFORM_REF: production
|
|
77
|
+
|
|
78
|
+
# The uniform of THIS commit, on every pipeline - confirmation
|
|
79
|
+
# biz-service-manifest 010. Entry 008 places the BINDING run before the SSH step
|
|
80
|
+
# of a production deploy; it never said that is the only place it runs, and read
|
|
81
|
+
# that way it left a service unmeasured between two deploys. Measured on
|
|
82
|
+
# biz-converter 2026-09-16: no job ran the uniform outside the deploy job, the
|
|
83
|
+
# post-commit mirror pushed every commit to origin/devel and pipelines?ref=devel
|
|
84
|
+
# answered 0, and the last pipeline on main was 13 days old. So the same gate,
|
|
85
|
+
# the same bytes and the same arguments run here too, on a merge request, on
|
|
86
|
+
# main, on the devel mirror and on production - and the deploy job keeps its own
|
|
87
|
+
# run as the last, binding instance.
|
|
88
|
+
validate-uniform:
|
|
89
|
+
stage: test
|
|
90
|
+
extends: .oa-uniform
|
|
91
|
+
before_script:
|
|
92
|
+
# git: the gate clones api read-only beside this checkout. npm ci: the gate
|
|
93
|
+
# and the engine it runs are BOTH the ones this repository pinned, so the
|
|
94
|
+
# version measured is the version this service proved - never a floating
|
|
95
|
+
# download.
|
|
96
|
+
- apk add --no-cache git
|
|
97
|
+
- npm ci
|
|
98
|
+
script:
|
|
99
|
+
# The same invocation deploy-production makes. The script is not this
|
|
100
|
+
# repository's own file: no biz repository carries it (measured over all
|
|
101
|
+
# eight, 2026-09-16), and a copy generated into eight of them would be a
|
|
102
|
+
# second place to keep in step (change-discipline.md - One rail per
|
|
103
|
+
# concern). It comes from the package this repository pins, beside the
|
|
104
|
+
# engine it runs.
|
|
105
|
+
- sh node_modules/@onlineapps/conn-orch-validator/templates/business-service/scripts/verify-deploy-uniform.sh "$CI_PROJECT_DIR" "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/${API_PROJECT_PATH}.git" "$API_UNIFORM_REF"
|
|
106
|
+
rules:
|
|
107
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
108
|
+
- if: $CI_COMMIT_BRANCH == "main"
|
|
109
|
+
- if: $CI_COMMIT_BRANCH == "devel"
|
|
110
|
+
- if: $CI_COMMIT_BRANCH == "production"
|
|
111
|
+
|
|
36
112
|
build:
|
|
37
113
|
stage: build
|
|
38
114
|
image: docker:24
|
|
@@ -72,28 +148,18 @@ secret_detection:
|
|
|
72
148
|
rules:
|
|
73
149
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
74
150
|
- if: $CI_COMMIT_BRANCH == "main"
|
|
151
|
+
# The mirror branch too (010 point 2): a secret pushed there is pushed.
|
|
152
|
+
- if: $CI_COMMIT_BRANCH == "devel"
|
|
75
153
|
- if: $CI_COMMIT_BRANCH == "production"
|
|
76
154
|
|
|
77
155
|
deploy-production:
|
|
78
156
|
stage: deploy
|
|
79
|
-
# The uniform
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
|
|
157
|
+
# The BINDING uniform run happens here, before the SSH step (confirmation
|
|
158
|
+
# biz-service-manifest 008). The runtime it needs and the layout it is
|
|
159
|
+
# measured in are the shared declaration above - the same one validate-uniform
|
|
160
|
+
# reads, so the two runs cannot be measuring different things.
|
|
161
|
+
extends: .oa-uniform
|
|
83
162
|
variables:
|
|
84
|
-
# The engine answers the rows that read the platform SSOT only for a service
|
|
85
|
-
# lying at <root>/api_biz/<service> beside <root>/api. GitLab decides where a
|
|
86
|
-
# project is checked out, so the job ASKS for the one place that IS that
|
|
87
|
-
# layout — no symlink (api rule: No Symlinks) and no second copy of this
|
|
88
|
-
# repository, which would leave two trees and no way to tell which one the
|
|
89
|
-
# verdict was about. Needs [runners.custom_build_dir] enabled on the runner.
|
|
90
|
-
GIT_CLONE_PATH: $CI_BUILDS_DIR/oa-uniform/api_biz/__SERVICE_NAME__
|
|
91
|
-
# Where the platform SSOT is read from, and which state of it a production
|
|
92
|
-
# deploy is measured against. Read access is granted by the api project's CI
|
|
93
|
-
# job-token allowlist — the owner's step, outside any repository
|
|
94
|
-
# (confirmation biz-service-manifest 008 point 2).
|
|
95
|
-
API_PROJECT_PATH: onlineapps/oadrive/infra-mono
|
|
96
|
-
API_UNIFORM_REF: production
|
|
97
163
|
# Which checks judge this deploy. The gate has no default target: the job
|
|
98
164
|
# declares what it is measured by (confirmation deploy-gate-targets 002),
|
|
99
165
|
# and the name is the service's registry identity — the one
|
|
@@ -181,7 +247,7 @@ deploy-production:
|
|
|
181
247
|
# not look everywhere, and nothing reaches the box. There is no flag and no
|
|
182
248
|
# variable that turns this off (automation-gates.md §1 requirement 5).
|
|
183
249
|
- npm ci
|
|
184
|
-
- scripts/verify-deploy-uniform.sh "$CI_PROJECT_DIR" "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/${API_PROJECT_PATH}.git" "$API_UNIFORM_REF"
|
|
250
|
+
- sh node_modules/@onlineapps/conn-orch-validator/templates/business-service/scripts/verify-deploy-uniform.sh "$CI_PROJECT_DIR" "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/${API_PROJECT_PATH}.git" "$API_UNIFORM_REF"
|
|
185
251
|
# R1: refuse to deploy without the immutable target the build stage published.
|
|
186
252
|
- |
|
|
187
253
|
if [ -z "${BIZ_IMAGE_DIGEST:-}" ]; then
|
|
@@ -222,6 +288,44 @@ deploy-production:
|
|
|
222
288
|
# (api/docs/setup/INSTALL.md, applying a business schema).
|
|
223
289
|
BIZ_DB_MIGRATIONS="$(dirname "$BIZ_DB_MIGRATIONS")"
|
|
224
290
|
fi
|
|
291
|
+
# WHICH SEEDS, IF ANY. The declaration says which files the installer
|
|
292
|
+
# applies; the file's own `-- Dataset-Class:` header says what class each
|
|
293
|
+
# one is (api/docs/standards/repository-installation-sql-contract.md §4).
|
|
294
|
+
# Neither fact is copied here, and the box is handed the ANSWER: it parses
|
|
295
|
+
# no JSON, reads no header and decides no class.
|
|
296
|
+
#
|
|
297
|
+
# Only PRODUCTION_LIKE reaches a production database. A service may declare
|
|
298
|
+
# a TEST_ONLY seed in the same list - converter does - and applying the
|
|
299
|
+
# declaration verbatim would write synthetic fixtures into a live schema.
|
|
300
|
+
BIZ_DB_SEEDS=""
|
|
301
|
+
for seed in $(jq -r '.database.seeds // [] | .[]' "$CONTRACT"); do
|
|
302
|
+
seed_file="$CI_PROJECT_DIR/$seed"
|
|
303
|
+
if [ ! -f "$seed_file" ]; then
|
|
304
|
+
echo "[deploy] FATAL: $CONTRACT declares database.seeds \"$seed\" and this checkout does not carry it. Expected: the file, at that path. Fix: add it, or correct the declaration (uniform row D-DB-PACKAGE reports the same gap)." >&2
|
|
305
|
+
exit 1
|
|
306
|
+
fi
|
|
307
|
+
seed_class="$(sed -n 's/^-- Dataset-Class:[[:space:]]*\([^[:space:]]*\).*/\1/p' "$seed_file" | head -n 1)"
|
|
308
|
+
case "$seed_class" in
|
|
309
|
+
PRODUCTION_LIKE)
|
|
310
|
+
# This deploy applies every PRODUCTION_LIKE seed on EVERY run and
|
|
311
|
+
# keeps no tracker (see the step on the box for why). A file that
|
|
312
|
+
# does not converge on a replay would duplicate rows or fail, so the
|
|
313
|
+
# header that licenses the replay is a precondition of the deploy,
|
|
314
|
+
# not a comment in the file.
|
|
315
|
+
seed_idem="$(sed -n 's/^-- Idempotency:[[:space:]]*\([^[:space:]]*\).*/\1/p' "$seed_file" | head -n 1)"
|
|
316
|
+
if [ "$seed_idem" != "yes" ]; then
|
|
317
|
+
echo "[deploy] FATAL: $seed declares \"-- Idempotency: $seed_idem\" and this deploy applies every PRODUCTION_LIKE seed on every run, with no tracker. Expected: Idempotency: yes. Fix: make the seed converge on a replay (INSERT ... ON DUPLICATE KEY UPDATE), or move the part that does not into a migration." >&2
|
|
318
|
+
exit 1
|
|
319
|
+
fi
|
|
320
|
+
BIZ_DB_SEEDS="$BIZ_DB_SEEDS $seed" ;;
|
|
321
|
+
TEST_ONLY)
|
|
322
|
+
echo "[deploy] seed $seed NOT APPLIED (Dataset-Class: TEST_ONLY) - test fixtures never reach a production database." ;;
|
|
323
|
+
*)
|
|
324
|
+
echo "[deploy] FATAL: $seed declares \"-- Dataset-Class: $seed_class\" and database.seeds admits PRODUCTION_LIKE and TEST_ONLY only. Fix: correct the header (uniform row D-DB-HEADERS owns it), or remove the file from database.seeds." >&2
|
|
325
|
+
exit 1 ;;
|
|
326
|
+
esac
|
|
327
|
+
done
|
|
328
|
+
BIZ_DB_SEEDS="${BIZ_DB_SEEDS# }"
|
|
225
329
|
# DEPLOY_PATH / DEPLOY_HOST / DEPLOY_USER are CI variables: the box decides
|
|
226
330
|
# where its checkout lives, and no path is baked into this file.
|
|
227
331
|
#
|
|
@@ -248,7 +352,7 @@ deploy-production:
|
|
|
248
352
|
printf '%s\n' "set -euo pipefail"
|
|
249
353
|
cat "$API_CHECKOUT/scripts/lib/mariadb-migrations.sh"
|
|
250
354
|
cat
|
|
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'"
|
|
355
|
+
} <<'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' '$BIZ_DB_SEEDS' '$CI_COMMIT_SHA'"
|
|
252
356
|
DEPLOY_PATH="$1"
|
|
253
357
|
REGISTRY="$2"
|
|
254
358
|
REGISTRY_USER="$3"
|
|
@@ -259,6 +363,14 @@ deploy-production:
|
|
|
259
363
|
# only thing that turns the migration step below on or off.
|
|
260
364
|
DB_SCHEMA="$7"
|
|
261
365
|
DB_MIGRATIONS_DIR="$8"
|
|
366
|
+
# Space-separated, repo-relative, and already filtered to PRODUCTION_LIKE on
|
|
367
|
+
# the runner - the box parses no JSON and decides no class. Empty when the
|
|
368
|
+
# contract declares no seeds, or declares only ones no production database
|
|
369
|
+
# may see.
|
|
370
|
+
DB_SEEDS="$9"
|
|
371
|
+
# The commit the runner MEASURED the contract at. The box is moved to it
|
|
372
|
+
# below, so both sides read one declaration rather than two.
|
|
373
|
+
COMMIT_SHA="${10}"
|
|
262
374
|
# This service's own env file on the box, the per-machine half of the two
|
|
263
375
|
# the compose file names (../shared.env is the host's). It carries the
|
|
264
376
|
# migration account - the SAME account the service connects as.
|
|
@@ -289,12 +401,39 @@ deploy-production:
|
|
|
289
401
|
# R2: reset, never merge. A local modification on the server would turn a
|
|
290
402
|
# merge into a conflict and abort the deploy halfway.
|
|
291
403
|
git fetch origin production
|
|
292
|
-
|
|
404
|
+
# ...and to the COMMIT THIS PIPELINE MEASURED, never to whatever
|
|
405
|
+
# origin/production points at by the time the ssh step opens. The runner
|
|
406
|
+
# read config/service/integration-contract.json at $CI_COMMIT_SHA and
|
|
407
|
+
# derived the migration directory and the seed list from it THERE; a push
|
|
408
|
+
# that lands while this job runs would otherwise leave the box applying a
|
|
409
|
+
# declaration nobody measured.
|
|
410
|
+
#
|
|
411
|
+
# It must still be ON the branch this box follows. A commit that is not is
|
|
412
|
+
# a pipeline for a different history, and resetting to it would put this
|
|
413
|
+
# box on code production never took.
|
|
414
|
+
if ! git merge-base --is-ancestor "$COMMIT_SHA" origin/production; then
|
|
415
|
+
echo "[deploy] FATAL: $COMMIT_SHA is not an ancestor of origin/production - this pipeline measured a commit that is not on the branch this box deploys, and resetting to it would put $PROJECT_PATH on a history production never took. Expected: the pipeline of a commit that is on production. Fix: re-run the pipeline of the current tip of production." >&2
|
|
416
|
+
exit 1
|
|
417
|
+
fi
|
|
418
|
+
git reset --hard "$COMMIT_SHA"
|
|
293
419
|
mkdir -p "$DOCKER_CONFIG_DIR"
|
|
294
420
|
echo "$REGISTRY_PASSWORD" | docker --config "$DOCKER_CONFIG_DIR" login -u "$REGISTRY_USER" --password-stdin "$REGISTRY"
|
|
295
421
|
echo "[deploy] pulling $BIZ_IMAGE_DIGEST"
|
|
296
422
|
docker --config "$DOCKER_CONFIG_DIR" compose -f docker-compose.production.yml pull
|
|
297
423
|
|
|
424
|
+
# Both env files, for EVERY service, BEFORE anything branches on a database.
|
|
425
|
+
# docker-compose.production.yml names them, so a missing one is fatal whether
|
|
426
|
+
# or not this service has a schema - and a service without one (pdfgen, hello)
|
|
427
|
+
# used to learn it from compose alone, as `env file ... not found`: a line that
|
|
428
|
+
# names no runbook and no owner. Neither file is committed, and neither is
|
|
429
|
+
# created by a script; the runbook step below is what puts them on the box.
|
|
430
|
+
for env_file in "$HOST_ENV" "$SERVICE_ENV"; do
|
|
431
|
+
if [ ! -f "$env_file" ]; then
|
|
432
|
+
echo "[deploy] Missing env file - $DEPLOY_PATH/$env_file does not exist on this box, and docker-compose.production.yml names it: the service could not start without it. Expected: both env files that compose names, on every box - the host-level $HOST_ENV and this service's own $SERVICE_ENV. Fix: place it on the box (both are per-machine and never committed) - api/docs/operations/production-box-provisioning.md § 3 The biz box, step 3." >&2
|
|
433
|
+
exit 1
|
|
434
|
+
fi
|
|
435
|
+
done
|
|
436
|
+
|
|
298
437
|
# ─── Migrations: after the pull, before the switch ───────────────
|
|
299
438
|
# Owner decision api/docs/governance/confirmations/db-migrations-first-deploy.md
|
|
300
439
|
# 001, phase 2. The ordering IS the safety: at this point the new image is
|
|
@@ -314,16 +453,14 @@ deploy-production:
|
|
|
314
453
|
if [ -z "$DB_SCHEMA" ]; then
|
|
315
454
|
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
455
|
else
|
|
317
|
-
# The two env files the compose names, in the order it names them:
|
|
318
|
-
# host-level one first, this service's own second, so the same value
|
|
319
|
-
# here that wins inside the container. Reading only one of them would
|
|
456
|
+
# The two env files the compose names, READ in the order it names them:
|
|
457
|
+
# the host-level one first, this service's own second, so the same value
|
|
458
|
+
# wins here that wins inside the container. Reading only one of them would
|
|
320
459
|
# make this step disagree with the service it migrates for, depending on
|
|
321
|
-
# which file the box happens to carry DB_HOST in.
|
|
460
|
+
# which file the box happens to carry DB_HOST in. That both exist was
|
|
461
|
+
# settled above, for every service - checking it twice would be two rails
|
|
462
|
+
# over one fact (.claude/rules/change-discipline.md § One rail per concern).
|
|
322
463
|
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
464
|
set -a
|
|
328
465
|
. "$env_file"
|
|
329
466
|
set +a
|
|
@@ -337,6 +474,15 @@ deploy-production:
|
|
|
337
474
|
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
475
|
exit 1
|
|
339
476
|
fi
|
|
477
|
+
# The runner is carried from the api clone at API_UNIFORM_REF, so a ref
|
|
478
|
+
# older than the seed entry point would reach `command not found` here -
|
|
479
|
+
# after the migrations and before the switch, in a production deploy and
|
|
480
|
+
# nowhere else. Checked before the first statement instead
|
|
481
|
+
# (automation-gates.md §1 requirement 4).
|
|
482
|
+
if [ -n "$DB_SEEDS" ] && ! declare -F apply_mariadb_seeds_over_tcp > /dev/null 2>&1; then
|
|
483
|
+
echo "[deploy] FATAL: the migration runner carried from the api clone has no apply_mariadb_seeds_over_tcp, and $DB_SCHEMA declares PRODUCTION_LIKE seeds this deploy has to apply. Expected: that entry point in api/scripts/lib/mariadb-migrations.sh. Fix: move API_UNIFORM_REF to an api commit that carries it, and re-run this deploy." >&2
|
|
484
|
+
exit 1
|
|
485
|
+
fi
|
|
340
486
|
# The precondition confirmation 001 demands before any migration: the
|
|
341
487
|
# account opens the schema, or this deploy stops and names the runbook.
|
|
342
488
|
if ! mariadb_migrations_can_open_over_tcp "$DB_HOST" "$DB_PORT" "$DB_SCHEMA"; then
|
|
@@ -369,6 +515,24 @@ deploy-production:
|
|
|
369
515
|
# anywhere in a deploy job, and the gate that keeps this job free of one
|
|
370
516
|
# judges by the word, not by the intent.
|
|
371
517
|
echo "[deploy] migrations $DB_SCHEMA: $MARIADB_MIGRATIONS_APPLIED applied now, $(wc -l < "$TRACKER" | tr -d " ") recorded in $DEPLOY_PATH/$TRACKER"
|
|
518
|
+
# Seeds: after the migrations, before the switch, for the same reason the
|
|
519
|
+
# migrations are there - the schema they write into is the one the step
|
|
520
|
+
# above just brought up to date, and the old container is still serving.
|
|
521
|
+
#
|
|
522
|
+
# They are NOT tracked. The tracker answers "has this file ever been
|
|
523
|
+
# applied", and a PRODUCTION_LIKE seed is a regenerated reference set
|
|
524
|
+
# whose name never changes and whose content does - converter's system
|
|
525
|
+
# catalogue is written by a generator in its own repository. Tracking it
|
|
526
|
+
# would pin production to the first catalogue ever installed, which is the
|
|
527
|
+
# drift this step exists to close. What licenses the replay is the file's own
|
|
528
|
+
# `-- Idempotency: yes`, and the runner side refused this deploy if it did
|
|
529
|
+
# not say so.
|
|
530
|
+
if [ -z "$DB_SEEDS" ]; then
|
|
531
|
+
echo "[deploy] seeds NOT APPLICABLE - config/service/integration-contract.json declares no PRODUCTION_LIKE seed for $DB_SCHEMA."
|
|
532
|
+
else
|
|
533
|
+
apply_mariadb_seeds_over_tcp "$DB_HOST" "$DB_PORT" "$DB_SCHEMA" $DB_SEEDS
|
|
534
|
+
echo "[deploy] seeds $DB_SCHEMA: $MARIADB_SEEDS_APPLIED applied (every deploy, no tracker)"
|
|
535
|
+
fi
|
|
372
536
|
fi
|
|
373
537
|
|
|
374
538
|
docker --config "$DOCKER_CONFIG_DIR" compose -f docker-compose.production.yml up -d
|
|
@@ -1,27 +1,60 @@
|
|
|
1
1
|
# === Production stage (build with: --target production) ===
|
|
2
2
|
FROM node:24-alpine AS production
|
|
3
3
|
WORKDIR /app
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
4
|
+
# The identity the PROCESS runs as, and it is the same one in every environment:
|
|
5
|
+
# "production is dev in the rights of the process". Until d.588 this stage
|
|
6
|
+
# declared no USER at all, so a deployed service ran as root while its dev
|
|
7
|
+
# container ran as 1000:1000 — measured 2026-09-17 on an image built from this
|
|
8
|
+
# template, `docker run --rm <image> id` → uid=0(root) gid=0(root). Nobody had
|
|
9
|
+
# decided that difference; it was what the file happened to say.
|
|
10
|
+
#
|
|
11
|
+
# `node` is the account node:24-alpine ships at uid 1000, gid 1000 (measured the
|
|
12
|
+
# same day: `docker run --rm node:24-alpine id node` → uid=1000(node)
|
|
13
|
+
# gid=1000(node)), so USER node here and `user: "1000:1000"` in both compose
|
|
14
|
+
# files are two spellings of ONE identity, not two decisions.
|
|
10
15
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
16
|
+
# The line below runs while /app is still root's, because WORKDIR created it so,
|
|
17
|
+
# and it does two things at once for that reason:
|
|
18
|
+
# * logs/ and conn-runtime/ are written at RUNTIME — @onlineapps/monitoring-core
|
|
19
|
+
# writes logs/app.<date>.log (its fileDirectory default) and ServiceWrapper
|
|
20
|
+
# writes conn-runtime/validation-proof.json on every boot. Both are excluded
|
|
21
|
+
# from the build context by .dockerignore, and in production /app is the
|
|
22
|
+
# image rather than a bind mount, so they exist only because this line
|
|
23
|
+
# creates them;
|
|
24
|
+
# * the handover of /app itself, after which every COPY carries --chown and
|
|
25
|
+
# nothing in the image belongs to uid 0.
|
|
26
|
+
RUN mkdir -p /app/logs /app/conn-runtime && chown -R node:node /app
|
|
27
|
+
USER node
|
|
28
|
+
COPY --chown=node:node package*.json ./
|
|
29
|
+
RUN npm ci --omit=dev
|
|
30
|
+
COPY --chown=node:node . .
|
|
31
|
+
# No conformance check here. From d.215b the stage ran `oa-validate`, on the
|
|
32
|
+
# sentence of confirmation biz-service-manifest 001 §3.1 ("an image with a
|
|
33
|
+
# finding is never built"); three later decisions took that away. 006 point 2
|
|
34
|
+
# calls the run inside an image "information, never a gate" — which a non-zero
|
|
35
|
+
# exit is not; 010 made the job `validate-uniform` run the COMPLETE uniform over
|
|
36
|
+
# a real checkout in every pipeline, and again before the deploy; 011 settled
|
|
37
|
+
# that a tree which is not a git checkout reports NOT RUN, never a verdict.
|
|
15
38
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
39
|
+
# What was left was a gate over a tree where the uniform cannot be answered: an
|
|
40
|
+
# image has no workspace above it, no `.git`, and — once `.dockerignore` does its
|
|
41
|
+
# job — no `README.md` and no `docker-compose*.yml` either. Measured 2026-09-17
|
|
42
|
+
# over exactly that shape: NOT DEPLOYABLE, 10 findings, every one of them
|
|
43
|
+
# "absent", exit 1. The deployability verdict is CI's; this stage builds.
|
|
21
44
|
CMD ["node", "index.js"]
|
|
22
45
|
|
|
23
46
|
# === Development stage (default) ===
|
|
24
47
|
FROM node:24-alpine
|
|
25
48
|
WORKDIR /app
|
|
26
|
-
|
|
49
|
+
# The same identity as the production stage — and here the directory is load
|
|
50
|
+
# bearing for a second reason. docker-compose.yml gives the one-shot test runner
|
|
51
|
+
# an anonymous volume at /app/conn-runtime so its validation proof never lands in
|
|
52
|
+
# the file the running service reads, and docker seeds such a volume from the
|
|
53
|
+
# IMAGE at that path. Measured 2026-09-17 with the path absent: docker created it
|
|
54
|
+
# root:root and the runner's write failed with "Permission denied"; with the
|
|
55
|
+
# directory present and owned by node, the volume inherits that ownership and the
|
|
56
|
+
# write succeeds.
|
|
57
|
+
RUN mkdir -p /app/logs /app/conn-runtime && chown -R node:node /app
|
|
58
|
+
USER node
|
|
59
|
+
COPY --chown=node:node package*.json ./
|
|
27
60
|
CMD ["npm", "start"]
|
|
@@ -6,27 +6,27 @@ Every `api_biz/*/package.json` that `api/config/services.json` lists wears it (`
|
|
|
6
6
|
Duty sections that apply:
|
|
7
7
|
|
|
8
8
|
- `files`: F-INIT, F-JEST, F-RUNNER, F-GITIGNORE, F-DOCKERIGNORE, G-PROD, G-CI, G-PROD-IMAGE, G-README, G-SETUP, G-SETUP-INSTALL, G-SETUP-MATRIX, G-SETUP-VALIDATION, X-IGNORED, X-HOOKS, X-PREVAL, X-DB-CONFIG
|
|
9
|
-
- `scripts`: S-TEST, S-ALL-C, S-INT-C, S-COOKBOOKS, S-HOST, S-HOOKS
|
|
9
|
+
- `scripts`: S-TEST, S-ALL-C, S-INT-C, S-UNIT-C, S-COOKBOOKS, S-COOK-C, S-HOST, S-HOOKS
|
|
10
10
|
- `tooling`: S-SCRIPTS
|
|
11
11
|
- `config`: C-IDENTITY, C-SERVICE, C-OPS, C-CONTRACT, C-CONNECTORS, C-SERVICE-DEAD, C-ENV, G-SHARED-ENV, C-ENV-READS
|
|
12
|
-
- `runtime`: R-NODE, R-MEM, R-PID1, R-PORTS-DEV, R-PORTS
|
|
13
|
-
- `db`: D-DB-CONSISTENT, D-DB-COLLATION, D-DB-PACKAGE, D-DB-NAMING, D-DB-README, D-DB-ACCOUNT, D-DB-HEADERS
|
|
12
|
+
- `runtime`: R-NODE, R-MEM, R-PID1, R-USER, R-PORTS-DEV, R-PORTS
|
|
13
|
+
- `db`: D-DB-CONSISTENT, D-DB-COLLATION, D-DB-PACKAGE, D-DB-NAMING, D-DB-README, D-DB-ACCOUNT, D-DB-CI-ACCOUNT, D-DB-HEADERS
|
|
14
14
|
- `docs`: C-LINT, D-PORT, D-NPM, D-SCRIPT, D-RETIRED, D-HEADER, D-LINT
|
|
15
15
|
|
|
16
16
|
Paths a duty owns:
|
|
17
17
|
|
|
18
18
|
- `.dockerignore`: F-DOCKERIGNORE (from `./node_modules/@onlineapps/conn-orch-validator/templates/business-service/gitignore`)
|
|
19
19
|
- `.gitignore`: F-GITIGNORE (from `./node_modules/@onlineapps/conn-orch-validator/templates/business-service/gitignore`)
|
|
20
|
-
- `.gitlab-ci.yml`: G-CI (from `./node_modules/@onlineapps/conn-orch-validator/templates/business-service/.gitlab-ci.yml`)
|
|
20
|
+
- `.gitlab-ci.yml`: G-CI (from `./node_modules/@onlineapps/conn-orch-validator/templates/business-service/.gitlab-ci.yml`), D-DB-CI-ACCOUNT
|
|
21
21
|
- `README.md`: G-README (from `./node_modules/@onlineapps/conn-orch-validator/manifests/biz-service.manifest.json`)
|
|
22
22
|
- `config/biz-docs-lint.tree.json`: C-LINT
|
|
23
23
|
- `config/env-templates`: C-ENV, D-DB-ACCOUNT
|
|
24
|
-
- `config/env-templates/shared.env`: G-SHARED-ENV (from
|
|
24
|
+
- `config/env-templates/shared.env`: G-SHARED-ENV (from `api/config/shared-env.json`)
|
|
25
25
|
- `config/service/config.json`: C-SERVICE
|
|
26
26
|
- `config/service/integration-contract.json`: C-CONTRACT, D-DB-COLLATION
|
|
27
27
|
- `config/service/operations.json`: C-OPS
|
|
28
28
|
- `docker-compose.production.yml`: G-PROD (from `./node_modules/@onlineapps/conn-orch-validator/templates/business-service/docker-compose.production.yml`), G-PROD-IMAGE, R-PORTS
|
|
29
|
-
- `docker-compose.yml`: F-RUNNER (from `./node_modules/@onlineapps/conn-orch-validator/templates/business-service/docker-compose.yml`), C-IDENTITY, R-MEM, R-PID1, R-PORTS-DEV
|
|
29
|
+
- `docker-compose.yml`: F-RUNNER (from `./node_modules/@onlineapps/conn-orch-validator/templates/business-service/docker-compose.yml`), C-IDENTITY, R-MEM, R-PID1, R-USER, R-PORTS-DEV
|
|
30
30
|
- `docs/80-setup/`: G-SETUP
|
|
31
31
|
- `docs/80-setup/INSTALL.md`: G-SETUP-INSTALL (from `./node_modules/@onlineapps/conn-orch-validator/templates/business-service/docs/80-setup/INSTALL.md`)
|
|
32
32
|
- `docs/80-setup/PLATFORM_MATRIX.md`: G-SETUP-MATRIX (from `./node_modules/@onlineapps/conn-orch-validator/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md`)
|
|
@@ -125,7 +125,7 @@ own memory budget, so a heavy suite can no longer be killed together with the
|
|
|
125
125
|
service, and the `test` profile keeps it out of a plain `docker compose up`.
|
|
126
126
|
|
|
127
127
|
```bash
|
|
128
|
-
npm run test:container
|
|
128
|
+
npm run test:unit:container # unit suite, in the runner
|
|
129
129
|
npm run test:all:container # everything test:all chains, in the runner
|
|
130
130
|
```
|
|
131
131
|
|
|
@@ -141,14 +141,23 @@ the `Dockerfile` or to the dependencies:
|
|
|
141
141
|
docker compose --profile test build
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
-
Without it the next `npm run test:container` runs the image the runner was last
|
|
144
|
+
Without it the next `npm run test:unit:container` runs the image the runner was last
|
|
145
145
|
built from, and a suite then passes or fails on code that is no longer in this
|
|
146
146
|
repository — a green run that says nothing about what is committed.
|
|
147
147
|
|
|
148
|
+
`jest.config.js` is held byte for byte by row `F-JEST`, so a local
|
|
149
|
+
`setupFiles` entry pointing at a `tests/setup-env.js` of your own does not
|
|
150
|
+
survive a sync — and does not need to. What a suite needs from the environment it
|
|
151
|
+
gets from the runner's `env_file`, the same files the service itself loads, which
|
|
152
|
+
is why the runner exists at all; a setup file that put those names into
|
|
153
|
+
`process.env` would be a second, quieter source for them
|
|
154
|
+
(`.claude/rules/change-discipline.md` § One rail per concern). A suite that needs
|
|
155
|
+
a value nothing declares says so by failing on the name, which is the point.
|
|
156
|
+
|
|
148
157
|
`npm test` and `npm run test:unit` are what runs INSIDE that container; the
|
|
149
158
|
runner is the one rail a suite runs on (owner decision
|
|
150
159
|
`api/docs/governance/confirmations/biz-test-container.md` 001, and manifest rows
|
|
151
|
-
`S-ALL-C` and `S-HOST`). Running them on the host is not a second, quicker way
|
|
160
|
+
`S-UNIT-C`, `S-ALL-C`, `S-COOK-C` and `S-HOST`). Running them on the host is not a second, quicker way
|
|
152
161
|
to the same answer: the host has neither the service's `env_file` nor its
|
|
153
162
|
network, so what fails there says nothing about the service — which is why no
|
|
154
163
|
`test:host` script exists and why the uniform forbids one.
|
|
@@ -211,3 +220,41 @@ cgroup.
|
|
|
211
220
|
Revise the service limit when this service exceeds 60% of it in operation,
|
|
212
221
|
measured the way the norm was: cgroup `memory.stat:anon`, sampled inside the
|
|
213
222
|
running container.
|
|
223
|
+
|
|
224
|
+
## Process identity
|
|
225
|
+
|
|
226
|
+
One identity, in every environment: the service process is **uid 1000, gid
|
|
227
|
+
1000** — the `node` account of `node:24-alpine` — and it is never root.
|
|
228
|
+
|
|
229
|
+
| Where | What says it |
|
|
230
|
+
|---|---|
|
|
231
|
+
| the image, both stages | `USER node` in `Dockerfile` |
|
|
232
|
+
| dev container and test runner | `user: "1000:1000"` in `docker-compose.yml` |
|
|
233
|
+
| production container | `user: "1000:1000"` in `docker-compose.production.yml` |
|
|
234
|
+
|
|
235
|
+
Row `R-USER` of the uniform holds the two halves nothing else reaches — the
|
|
236
|
+
production stage of the `Dockerfile` and the service node of `docker-compose.yml`.
|
|
237
|
+
The runner line is inside the block `F-RUNNER` holds, and the production compose is
|
|
238
|
+
`G-PROD`'s whole-file render, so neither is measured twice.
|
|
239
|
+
|
|
240
|
+
Both halves are stated on purpose. The `Dockerfile` decides what the IMAGE is,
|
|
241
|
+
the compose file decides what THIS deployment runs, and a rebuild that lost the
|
|
242
|
+
`USER` would otherwise reach the box unnoticed. Until 2026-09-17 neither half
|
|
243
|
+
existed for production: a `docker run --rm <image> id` on an image built from
|
|
244
|
+
this template answered `uid=0(root)`, while the dev container beside it ran as
|
|
245
|
+
1000 — a difference nobody had decided.
|
|
246
|
+
|
|
247
|
+
The two directories the process writes at runtime, `logs/` (the log files
|
|
248
|
+
`@onlineapps/monitoring-core` rotates) and `conn-runtime/` (the validation
|
|
249
|
+
proof), are created in the image and belong to that account. In dev they are
|
|
250
|
+
covered by the bind mount and belong to whoever owns the checkout; in production
|
|
251
|
+
`/app` IS the image, so a directory nothing created is a directory nothing can
|
|
252
|
+
write.
|
|
253
|
+
|
|
254
|
+
The one-shot test runner gets a `conn-runtime/` of its own — an anonymous volume
|
|
255
|
+
at `/app/conn-runtime` in its compose node. A validation proof is an assertion
|
|
256
|
+
about ONE running instance, and with the plain `.:/app` mount the runner wrote
|
|
257
|
+
its proof into the file the live service reads (measured in `api_biz/converter`,
|
|
258
|
+
2026-09-16). Nothing else is separated: the runner keeps the same build, the
|
|
259
|
+
same `env_file` and the same networks as the service, because that is what makes
|
|
260
|
+
a suite measure what the service sees.
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# __SERVICE_NAME__ environment
|
|
2
2
|
# Copy to config/env-active/__SERVICE_NAME__.env and adjust values.
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
#
|
|
4
|
+
# The service's own name is NOT here. config/service/config.json declares it and
|
|
5
|
+
# ServiceWrapper._serviceName() reads it there, passing it on to monitoring and
|
|
6
|
+
# to the MQ client; an env key repeating it would be a second bearer of one fact,
|
|
7
|
+
# and the copies measured on 2026-09-16 had already drifted from it (the template
|
|
8
|
+
# rendered the short name, every live file the registry name). Row C-IDENTITY
|
|
9
|
+
# holds every spelling in this repository to config/service/config.json.
|
|
5
10
|
|
|
6
11
|
# Schema migrations at deploy time. The deploy job (block `oa-ci v1` of
|
|
7
12
|
# .gitlab-ci.yml) runs api/scripts/lib/mariadb-migrations.sh between `compose
|
|
@@ -15,8 +20,15 @@ SERVICE_NAME=__SERVICE_NAME__
|
|
|
15
20
|
#
|
|
16
21
|
# Commented out because this scaffold has no database: its
|
|
17
22
|
# config/service/integration-contract.json declares no `database` block, the
|
|
18
|
-
# deploy step prints NOT APPLICABLE and reads neither name.
|
|
19
|
-
#
|
|
20
|
-
#
|
|
23
|
+
# deploy step prints NOT APPLICABLE and reads neither name.
|
|
24
|
+
#
|
|
25
|
+
# Where the values live is the host contract, not a choice: the deploy job reads
|
|
26
|
+
# config/env-active/<service>.env ON THE BOX, so a service that declares a
|
|
27
|
+
# `database` block uncomments both names HERE (the template, in git, values
|
|
28
|
+
# empty) and fills them in config/env-active/ on each machine - never in git. The
|
|
29
|
+
# account itself is not created by any of this: on a first deployment the owner
|
|
30
|
+
# of the box creates the schema and the service account by hand from the runbook,
|
|
31
|
+
# and the migration runner then runs under that account (db-migrations-first-deploy
|
|
32
|
+
# 001, phase 1).
|
|
21
33
|
# MARIADB_MIGRATION_USER=
|
|
22
34
|
# MARIADB_MIGRATION_PASSWORD=
|
|
@@ -4,10 +4,16 @@
|
|
|
4
4
|
# The runtime environment name every platform library resolves through its own runtime-config schema; without it a library cannot say which environment it is reporting from.
|
|
5
5
|
NODE_ENV=development
|
|
6
6
|
|
|
7
|
-
# The log level an
|
|
7
|
+
# The log level an infrastructure service's config/logging.json resolves through ${LOG_LEVEL}; one platform value so a stack does not log at a different level in every service. In the business chain the key is carried, not read: no library in the business chain reads it - @onlineapps/monitoring-core reads no environment variable at all (principle 1) and takes its level from the config the service hands it - so setting it on a business service changes nothing. It reaches every bearer because the shared key set is ONE file whose every copy is byte-identical to the platform template (confirmation biz-service-manifest 003 §18), never because each bearer reads every key.
|
|
8
8
|
LOG_LEVEL=info
|
|
9
9
|
|
|
10
|
-
#
|
|
10
|
+
# How large one log file may grow before @onlineapps/monitoring-core rotates it (src/logger.js REQUIRED_FILE_BOUNDS, src/config.js logMaxSize). From monitoring-core 3.0.0 the bound has no default in code, so a service that declares it nowhere fails at logger construction by design, and the value has to be declared - it is shared because the ceiling is a property of the box every service writes onto, not a preference of one service: the age bound LOG_MAX_DAYS alone let a single day's file fill the disk (hello wrote 2.4 GB/day before d.383). The platform value is 50 MB, the same in dev and production (owner decision docs/governance/confirmations/log-file-bounds.md 001).
|
|
11
|
+
LOG_MAX_SIZE_BYTES=52428800
|
|
12
|
+
|
|
13
|
+
# How many rotated files of one log row the directory keeps - the other half of the size bound, and equally without a default in @onlineapps/monitoring-core 3.0.0 (src/logger.js REQUIRED_FILE_BOUNDS, src/config.js logMaxFiles). Ten files of LOG_MAX_SIZE_BYTES is the 500 MB per service the platform declares as its ceiling, so the two keys are read together and travel together (owner decision docs/governance/confirmations/log-file-bounds.md 001).
|
|
14
|
+
LOG_MAX_FILES=10
|
|
15
|
+
|
|
16
|
+
# The HMAC-SHA256 secret the gateway and auth sign and verify access tokens with; it is shared because both ends of one token must hold the same value (docs/standards/JWT_AUTH.md). Four infrastructure services read the name, each at boot: api_gateway (infra/api_gateway/config/jwtSecret.js, assertJwtSecret), api_auth (infra/api_auth/config/bootEnv.js, assertJwtSecret), api_meta_reader and api_delivery_endpoint (both requireEnv('JWT_SECRET', ...) in src/config.js). In the business chain the key is carried, not read: no business service reads the name, and @onlineapps/service-common 3.0.0 takes the secret as an INPUT of verifyAccessToken(token, secret) and createJwtValidator({ secret }) and reads no environment variable at all (principle 1, CHANGELOG 3.0.0), so a business service could not use the value even if it were filled in. It reaches every bearer because the shared key set is ONE file whose every copy is byte-identical to the platform template (confirmation biz-service-manifest 003 §18), never because each bearer reads every key - which is why CHANGE_ME here is a real placeholder only on an infrastructure target.
|
|
11
17
|
JWT_SECRET=CHANGE_ME
|
|
12
18
|
|
|
13
19
|
# The ordered list of SecretBox master keys every process that resolves a secret reference decrypts with - entries of <key_id>:<base64 32-byte key> separated by commas, newest FIRST: the first entry seals every new write, every entry opens what it sealed (selected by the blob's own key_id), and a blob naming a key outside the list is refused rather than guessed at (owner decision docs/governance/confirmations/secretbox-master-keys.md 001). It is shared because one keyring has to reach every bearer at once - a rotation is a switch of this one list, and a service left on yesterday's list stops reading what its neighbour has already re-encrypted; the measured cost of the single key it replaces is six of ten rows of oagen_meta.secret unreadable for two weeks after the August 2026 rotation. The value is per-machine and lives only in config/env-active/shared.env. @onlineapps/conn-infra-secrets owns the name and the format and never reads the environment itself (principle 1): the bearer reads this key and hands the string to the connector.
|
|
@@ -33,6 +33,15 @@ services:
|
|
|
33
33
|
# carries a digest, and an image built here would in any case not be the one
|
|
34
34
|
# CI proved. Build locally through docker-compose.yml instead.
|
|
35
35
|
container_name: __CONTAINER_NAME__
|
|
36
|
+
# The same uid:gid the dev compose pins, and the Dockerfile's production
|
|
37
|
+
# stage declares the same identity as USER node (node:24-alpine ships that
|
|
38
|
+
# account at uid 1000, gid 1000). Until d.588 neither file said anything
|
|
39
|
+
# here, so the deployed process was the one on the platform running as root
|
|
40
|
+
# — measured 2026-09-17, `docker run --rm <image> id` → uid=0(root).
|
|
41
|
+
# Both halves are stated on purpose: the Dockerfile decides what the image
|
|
42
|
+
# is, this line decides what THIS deployment runs, and a rebuild that lost
|
|
43
|
+
# the USER would otherwise reach the box unnoticed.
|
|
44
|
+
user: "1000:1000"
|
|
36
45
|
deploy:
|
|
37
46
|
resources:
|
|
38
47
|
limits:
|
|
@@ -80,6 +80,23 @@ services:
|
|
|
80
80
|
mem_limit: 1g
|
|
81
81
|
volumes:
|
|
82
82
|
- .:/app
|
|
83
|
+
# The runner's OWN conn-runtime, and the reason it is a volume rather than
|
|
84
|
+
# a second bind mount. `conn-runtime/validation-proof.json` is an assertion
|
|
85
|
+
# about ONE running instance
|
|
86
|
+
# (docs/biz/50-lifecycle/validation-responsibilities.md § Data Flow), and
|
|
87
|
+
# with `.:/app` alone the runner writes it into the directory the SERVICE
|
|
88
|
+
# reads — measured in api_biz/converter on 2026-09-16, where a proof
|
|
89
|
+
# produced by a probe schema stood in the place of the service's own.
|
|
90
|
+
#
|
|
91
|
+
# ANONYMOUS on purpose: F-RUNNER holds this block and
|
|
92
|
+
# `oa-sync-template docker-compose.yml` writes this block and nothing else,
|
|
93
|
+
# so a named volume would reach every service without the top-level
|
|
94
|
+
# `volumes:` key that declares it and `docker compose config` would refuse
|
|
95
|
+
# the document. Docker seeds the volume from the IMAGE at that path, which
|
|
96
|
+
# is why the Dockerfile's development stage creates /app/conn-runtime and
|
|
97
|
+
# gives it to uid 1000 — without that, docker creates it root-owned and the
|
|
98
|
+
# runner cannot write it at all.
|
|
99
|
+
- /app/conn-runtime
|
|
83
100
|
env_file:
|
|
84
101
|
- ./config/env-active/shared.env
|
|
85
102
|
- ./config/env-active/__SERVICE_NAME__.env
|