@onlineapps/conn-orch-validator 6.0.1 → 8.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +2591 -2
  2. package/README.md +1075 -7
  3. package/docs/DESIGN.md +3 -1
  4. package/manifests/biz-service.manifest.json +658 -0
  5. package/manifests/library.manifest.json +324 -0
  6. package/package.json +12 -6
  7. package/src/CookbookTestRunner.js +422 -104
  8. package/src/CookbookTestUtils.js +7 -8
  9. package/src/ServiceReadinessValidator.js +78 -42
  10. package/src/ValidationOrchestrator.js +298 -75
  11. package/src/cli/biz-ci-gate.js +176 -33
  12. package/src/cli/oa-lint-scripts.js +221 -0
  13. package/src/cli/oa-sync-template.js +1020 -0
  14. package/src/cli/oa-validate.js +474 -0
  15. package/src/helpers/README.md +12 -2
  16. package/src/helpers/createServiceReadinessTests.js +75 -6
  17. package/src/index.js +33 -3
  18. package/src/lint/scripts/lintScripts.js +298 -0
  19. package/src/manifest/checks/composeRunnerBlock.js +222 -0
  20. package/src/manifest/checks/composeShape.js +165 -0
  21. package/src/manifest/checks/contractBridge.js +181 -0
  22. package/src/manifest/checks/discoveryOrphan.js +50 -0
  23. package/src/manifest/checks/docsLintBridge.js +553 -0
  24. package/src/manifest/checks/fileAbsent.js +35 -0
  25. package/src/manifest/checks/gitTracked.js +204 -0
  26. package/src/manifest/checks/index.js +111 -0
  27. package/src/manifest/checks/libraryContext.js +226 -0
  28. package/src/manifest/checks/libraryDocs.js +75 -0
  29. package/src/manifest/checks/libraryPackage.js +272 -0
  30. package/src/manifest/checks/librarySource.js +274 -0
  31. package/src/manifest/checks/libraryTests.js +121 -0
  32. package/src/manifest/checks/libraryWorkspace.js +293 -0
  33. package/src/manifest/checks/readmeRegion.js +135 -0
  34. package/src/manifest/checks/scriptHeaders.js +79 -0
  35. package/src/manifest/checks/serviceConfig.js +390 -0
  36. package/src/manifest/checks/serviceConnectors.js +81 -0
  37. package/src/manifest/checks/serviceDb.js +388 -0
  38. package/src/manifest/checks/serviceFiles.js +754 -0
  39. package/src/manifest/checks/serviceIdentityRows.js +351 -0
  40. package/src/manifest/checks/serviceRuntime.js +295 -0
  41. package/src/manifest/checks/serviceScripts.js +213 -0
  42. package/src/manifest/deployabilitySignal.js +121 -0
  43. package/src/manifest/discovery.js +386 -0
  44. package/src/manifest/loadManifest.js +62 -0
  45. package/src/manifest/manifestShape.js +446 -0
  46. package/src/manifest/report.js +245 -0
  47. package/src/manifest/runManifest.js +449 -0
  48. package/src/manifest/serviceIdentity.js +140 -0
  49. package/src/manifest/walk.js +74 -0
  50. package/src/manifest/workspaceRoot.js +242 -0
  51. package/src/mocks/MockMQClient.js +13 -30
  52. package/src/mocks/MockRegistry.js +4 -2
  53. package/src/mocks/MockStorage.js +4 -2
  54. package/src/sync/docsRegion.js +463 -0
  55. package/src/sync/generatedRegion.js +228 -0
  56. package/src/sync/readmeLocation.js +182 -0
  57. package/src/sync/readmePointer.js +477 -0
  58. package/src/sync/serviceTemplate.js +583 -0
  59. package/src/sync/sharedEnv.js +162 -0
  60. package/src/sync/uniformFiles.js +474 -0
  61. package/src/utils/bizCiGateContract.js +131 -7
  62. package/src/utils/connectorContract.js +97 -7
  63. package/src/utils/cookbookFormat.js +81 -40
  64. package/src/utils/deployContract.js +213 -13
  65. package/src/utils/envContract.js +57 -1
  66. package/src/utils/handlerRef.js +181 -0
  67. package/src/utils/installContract.js +287 -41
  68. package/src/utils/libCompat.js +29 -7
  69. package/src/utils/migrationOrder.js +163 -0
  70. package/src/utils/preValidation.js +20 -7
  71. package/src/utils/setupDatabase.js +194 -13
  72. package/src/utils/testCoverageContract.js +539 -0
  73. package/src/utils/testNamespace.js +247 -23
  74. package/src/utils/throwawaySchema.js +207 -0
  75. package/src/validators/ServiceStructureValidator.js +21 -20
  76. package/templates/business-service/.dockerignore +42 -0
  77. package/templates/business-service/.gitlab-ci.yml +290 -0
  78. package/templates/business-service/Dockerfile +27 -0
  79. package/templates/business-service/README.md +213 -0
  80. package/templates/business-service/config/biz-docs-lint.tree.json +10 -0
  81. package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +4 -0
  82. package/templates/business-service/config/env-templates/shared.env +65 -0
  83. package/templates/business-service/config/service/config.json +14 -0
  84. package/templates/business-service/config/service/integration-contract.json +12 -0
  85. package/templates/business-service/config/service/operations.json +41 -0
  86. package/templates/business-service/docker-compose.production.yml +60 -0
  87. package/templates/business-service/docker-compose.yml +93 -0
  88. package/templates/business-service/docs/80-setup/INSTALL.md +101 -0
  89. package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +65 -0
  90. package/templates/business-service/docs/80-setup/README.md +18 -0
  91. package/templates/business-service/docs/80-setup/VALIDATION.md +78 -0
  92. package/templates/business-service/docs/README.md +18 -0
  93. package/templates/business-service/gitignore +42 -0
  94. package/templates/business-service/index.js +10 -0
  95. package/templates/business-service/init.sh +54 -0
  96. package/templates/business-service/jest.config.js +6 -0
  97. package/templates/business-service/package.json.template +31 -0
  98. package/templates/business-service/scripts/verify-deploy-uniform.sh +180 -0
  99. package/templates/business-service/src/handlers/v3/echo.js +39 -0
  100. package/templates/business-service/tests/cookbooks/echo.json +36 -0
  101. package/templates/business-service/tests/unit/handler.test.js +78 -0
  102. package/src/WorkflowTestRunner.js +0 -402
@@ -0,0 +1,290 @@
1
+ # --- oa-ci v1
2
+ # Written by @onlineapps/conn-orch-validator (row G-CI of
3
+ # manifests/biz-service.manifest.json). The PLATFORM half of this pipeline: which
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.
11
+ #
12
+ # Outside them is this repository's own: its test job - which database, which
13
+ # ci:gate:* steps and which artefacts its integration needs is a fact about the
14
+ # service - and whatever else it runs. The stages this block declares (test,
15
+ # build, secret-detection, deploy) cover those jobs too; measured over the eight
16
+ # biz repositories on 2026-09-11, none declares a fifth stage.
17
+ include:
18
+ - template: Jobs/Secret-Detection.gitlab-ci.yml
19
+
20
+ workflow:
21
+ name: "$CI_PROJECT_NAME — $CI_COMMIT_BRANCH [$CI_PIPELINE_SOURCE]"
22
+
23
+ stages:
24
+ - test
25
+ - build
26
+ - secret-detection
27
+ - deploy
28
+
29
+ variables:
30
+ # R5: one commit-sha scheme across the platform — the FULL sha, the way infra
31
+ # tags. Two lengths would mean two schemes and the release manifest needs one.
32
+ IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
33
+ IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest
34
+
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
+ build:
46
+ stage: build
47
+ image: docker:24
48
+ services:
49
+ - docker:24-dind
50
+ script:
51
+ # A password on a command line stands in the runner's process table for the
52
+ # length of the build and in every trace that echoes the command. The deploy
53
+ # host's own login has read it from stdin since it was written; both ends of
54
+ # one login now agree.
55
+ - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
56
+ - docker build --target production -t $IMAGE -t $IMAGE_LATEST .
57
+ - docker push $IMAGE
58
+ - docker push $IMAGE_LATEST
59
+ # R1: the deploy target is the digest, and it exists only after the push.
60
+ - |
61
+ # --format is not honoured by the buildx on the runner: it prints the whole
62
+ # descriptor (Name/MediaType/Digest), so the digest would be captured
63
+ # multi-line and build.env rejected as an invalid dotenv artifact. Parse
64
+ # the Digest field.
65
+ BIZ_IMAGE_DIGEST=$(docker buildx imagetools inspect "$IMAGE" | awk '/^Digest:/ { print $2; exit }')
66
+ if ! printf '%s' "$BIZ_IMAGE_DIGEST" | grep -qE '^sha256:[0-9a-f]{64}$'; then
67
+ echo "[build] FATAL: no valid digest resolved for $IMAGE - got '$BIZ_IMAGE_DIGEST', expected sha256:<64 hex>. The deploy would have no immutable target. Fix: confirm the push succeeded and that buildx can read $CI_REGISTRY." >&2
68
+ exit 1
69
+ fi
70
+ echo "[build] $IMAGE -> $BIZ_IMAGE_DIGEST"
71
+ echo "BIZ_IMAGE_DIGEST=$BIZ_IMAGE_DIGEST" > build.env
72
+ artifacts:
73
+ reports:
74
+ dotenv: build.env
75
+ rules:
76
+ - if: $CI_COMMIT_BRANCH == "main"
77
+ - if: $CI_COMMIT_BRANCH == "production"
78
+
79
+ secret_detection:
80
+ stage: secret-detection
81
+ rules:
82
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
83
+ - if: $CI_COMMIT_BRANCH == "main"
84
+ - if: $CI_COMMIT_BRANCH == "production"
85
+
86
+ deploy-production:
87
+ stage: deploy
88
+ # The uniform runs here (confirmation biz-service-manifest 008), so the job
89
+ # needs the runtime the pinned engine declares — the same major the test stage
90
+ # and the Dockerfile carry, which is what R3 of the deploy contract compares.
91
+ image: node:24-alpine
92
+ variables:
93
+ # The engine answers the rows that read the platform SSOT only for a service
94
+ # lying at <root>/api_biz/<service> beside <root>/api. GitLab decides where a
95
+ # project is checked out, so the job ASKS for the one place that IS that
96
+ # layout — no symlink (api rule: No Symlinks) and no second copy of this
97
+ # repository, which would leave two trees and no way to tell which one the
98
+ # verdict was about. Needs [runners.custom_build_dir] enabled on the runner.
99
+ GIT_CLONE_PATH: $CI_BUILDS_DIR/oa-uniform/api_biz/__SERVICE_NAME__
100
+ # Where the platform SSOT is read from, and which state of it a production
101
+ # deploy is measured against. Read access is granted by the api project's CI
102
+ # job-token allowlist — the owner's step, outside any repository
103
+ # (confirmation biz-service-manifest 008 point 2).
104
+ API_PROJECT_PATH: onlineapps/oadrive/infra-mono
105
+ API_UNIFORM_REF: production
106
+ # Which checks judge this deploy. The gate has no default target: the job
107
+ # declares what it is measured by (confirmation deploy-gate-targets 002),
108
+ # and the name is the service's registry identity — the one
109
+ # api/config/services.json carries and the contract files its target under.
110
+ DEPLOY_GATE_TARGETS: "biz:__REGISTRY_NAME__"
111
+ before_script:
112
+ # Preconditions first, before anything is installed or deployed
113
+ # (automation-gates.md §1 requirement 4). The post-deploy gate reads the
114
+ # heartbeat projection on the INFRA box as its own read-only account
115
+ # (confirmation deploy-gate-targets 003 point 3); without that identity this
116
+ # job could deploy a service and then not verify it, which is exactly the
117
+ # false guarantee §5 of the same rule names. So it refuses instead, and
118
+ # there is no branch on which the gate is skipped (§1 requirement 5).
119
+ - |
120
+ # gate-identity-check
121
+ missing=""
122
+ [ -n "${GATE_INFRA_HOST:-}" ] || missing="$missing GATE_INFRA_HOST"
123
+ [ -n "${GATE_INFRA_USER:-}" ] || missing="$missing GATE_INFRA_USER"
124
+ [ -n "${GATE_INFRA_SSH_PRIVATE_KEY:-}" ] || missing="$missing GATE_INFRA_SSH_PRIVATE_KEY"
125
+ if [ -n "$missing" ]; then
126
+ echo "[deploy] FATAL: the post-deploy gate has no identity on the infra box - missing CI variable(s):$missing. Expected: the group-level variables GATE_INFRA_HOST, GATE_INFRA_USER and GATE_INFRA_SSH_PRIVATE_KEY (base64 ed25519 private key of the gate-reader account). Fix: set them in GitLab > Group > Settings > CI/CD > Variables, and install the account on the infra box with api/scripts/provision/gate-reader-install.sh (confirmation deploy-gate-targets 003 point 3)." >&2
127
+ exit 1
128
+ fi
129
+ # end gate-identity-check
130
+ - |
131
+ # host-key-check
132
+ # An ssh that accepts whatever key answers on an address hands the deploy
133
+ # key, the registry password and the gate-reader key to whoever answered.
134
+ # Both boxes are therefore pinned from a CI variable, and a missing pin
135
+ # stops the job HERE, before anything is installed and before the first
136
+ # connection - never a fallback to accepting the unknown
137
+ # (automation-gates.md §1 requirements 4 and 5, §2).
138
+ if [ -z "${DEPLOY_HOST_KEY:-}" ]; then
139
+ echo "[deploy] Missing CI variable - DEPLOY_HOST_KEY is the ssh-keyscan line of the biz box DEPLOY_HOST ($DEPLOY_HOST), which this job deploys to over ssh. Fix: on a machine that already trusts that box run 'ssh-keyscan -t ed25519 $DEPLOY_HOST' and set its whole output line as DEPLOY_HOST_KEY in GitLab > Group > Settings > CI/CD > Variables." >&2
140
+ exit 1
141
+ fi
142
+ if [ -z "${GATE_INFRA_HOST_KEY:-}" ]; then
143
+ echo "[deploy] Missing CI variable - GATE_INFRA_HOST_KEY is the ssh-keyscan line of the infra box GATE_INFRA_HOST ($GATE_INFRA_HOST), which the post-deploy gate reads the heartbeat projection from. Fix: on a machine that already trusts that box run 'ssh-keyscan -t ed25519 $GATE_INFRA_HOST' and set its whole output line as GATE_INFRA_HOST_KEY in GitLab > Group > Settings > CI/CD > Variables." >&2
144
+ exit 1
145
+ fi
146
+ # end host-key-check
147
+ # openssh-client and git for the two SSH steps and the api checkout the
148
+ # uniform reads; bash, jq and curl for the post-deploy gate — it declares
149
+ # bash>=4 (declare -A), reads the contract with jq and submits a workflow
150
+ # smoke with curl. No docker: in remote mode every docker call is carried to
151
+ # the box that holds the fact, over ssh.
152
+ - apk add --no-cache openssh-client git bash jq curl
153
+ - mkdir -p ~/.ssh
154
+ - echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/deploy_key
155
+ - chmod 600 ~/.ssh/deploy_key
156
+ - echo "$GATE_INFRA_SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/gate_infra_key
157
+ - chmod 600 ~/.ssh/gate_infra_key
158
+ # The identity of the two boxes, pinned. Written before the config that
159
+ # demands it, so no ordering makes the demand unsatisfiable.
160
+ - echo "$DEPLOY_HOST_KEY" > ~/.ssh/known_hosts
161
+ - echo "$GATE_INFRA_HOST_KEY" >> ~/.ssh/known_hosts
162
+ - chmod 644 ~/.ssh/known_hosts
163
+ # Two boxes, two identities, and neither key is ever offered to the other
164
+ # box: the deploy key opens the biz box, the gate-reader key the infra box.
165
+ # ssh_config keeps the FIRST value it finds for a keyword, so the host
166
+ # blocks come before the catch-all and the catch-all carries only the
167
+ # host-key policy - which is where it belongs rather than on a command line:
168
+ # the post-deploy gate this job runs at the end (see its invocation below)
169
+ # opens its own ssh connections inside the script, and takes no ssh flags
170
+ # from here, so a policy written per-command would cover the deploy step and
171
+ # leave the gate connecting to anything that answered.
172
+ - |
173
+ {
174
+ echo "Host $DEPLOY_HOST"
175
+ echo " IdentityFile ~/.ssh/deploy_key"
176
+ echo " IdentitiesOnly yes"
177
+ echo "Host $GATE_INFRA_HOST"
178
+ echo " IdentityFile ~/.ssh/gate_infra_key"
179
+ echo " IdentitiesOnly yes"
180
+ echo "Host *"
181
+ echo " StrictHostKeyChecking yes"
182
+ echo " UserKnownHostsFile ~/.ssh/known_hosts"
183
+ } > ~/.ssh/config
184
+ dependencies:
185
+ - build
186
+ script:
187
+ # The uniform of this checkout, complete, BEFORE anything is deployed
188
+ # (confirmation biz-service-manifest 006 point 3, refined by 008): deploy is
189
+ # confirmation, never discovery. One blocking finding, or a run that could
190
+ # not look everywhere, and nothing reaches the box. There is no flag and no
191
+ # variable that turns this off (automation-gates.md §1 requirement 5).
192
+ - npm ci
193
+ - scripts/verify-deploy-uniform.sh "$CI_PROJECT_DIR" "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/${API_PROJECT_PATH}.git" "$API_UNIFORM_REF"
194
+ # R1: refuse to deploy without the immutable target the build stage published.
195
+ - |
196
+ if [ -z "${BIZ_IMAGE_DIGEST:-}" ]; then
197
+ echo "[deploy] FATAL: BIZ_IMAGE_DIGEST is empty - the build stage publishes it as a dotenv artifact. Refusing to deploy without an immutable image target." >&2
198
+ exit 1
199
+ fi
200
+ - echo "Deploying $BIZ_IMAGE_DIGEST to $DEPLOY_HOST ($CI_ENVIRONMENT_URL)"
201
+ # DEPLOY_PATH / DEPLOY_HOST / DEPLOY_USER are CI variables: the box decides
202
+ # where its checkout lives, and no path is baked into this file.
203
+ #
204
+ # The registry password travels on STDIN and nowhere else. As an argument of
205
+ # the remote `bash -s` it stood in the deploy host's process table for the
206
+ # whole deploy, readable by every account on that box, and in this runner's
207
+ # argv besides. So the stream the remote shell reads is the password's line
208
+ # first and the script after it: the remote reads that one line, exports it,
209
+ # and hands the rest of the stream to `bash -s` - ssh's stdin is a socket
210
+ # there, so `read` consumes exactly the line and no more.
211
+ - |
212
+ {
213
+ printf '%s\n' "$CI_REGISTRY_PASSWORD"
214
+ 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
217
+ DEPLOY_PATH="$1"
218
+ REGISTRY="$2"
219
+ REGISTRY_USER="$3"
220
+ PROJECT_SLUG="$4"
221
+ export BIZ_IMAGE_DIGEST="$5"
222
+ PROJECT_PATH="$6"
223
+ # REGISTRY_PASSWORD arrives in the environment, from the first line of this
224
+ # script's own stdin. Empty means the runner had no CI_REGISTRY_PASSWORD:
225
+ # the pull would fail later with an authentication error that names nothing.
226
+ if [ -z "${REGISTRY_PASSWORD:-}" ]; then
227
+ echo "[deploy] FATAL: REGISTRY_PASSWORD is empty - it is read from the first line of this script's own stdin, before bash -s. Expected: CI_REGISTRY_PASSWORD set on the runner. Fix: check the job's CI variables; refusing to deploy without a registry login." >&2
228
+ exit 1
229
+ fi
230
+ DOCKER_CONFIG_DIR="$HOME/.docker-ci/${PROJECT_SLUG}"
231
+
232
+ # Onboarding a new biz service must not require a step on the server —
233
+ # owner decision api/docs/governance/confirmations/server-topology.md 004.
234
+ # The first deploy therefore creates the checkout itself; the deploy user
235
+ # holds a read key for every biz repository.
236
+ if [ ! -d "$DEPLOY_PATH/.git" ]; then
237
+ echo "[deploy] $DEPLOY_PATH holds no checkout - cloning git@gitlab.com:${PROJECT_PATH}.git"
238
+ git clone "git@gitlab.com:${PROJECT_PATH}.git" "$DEPLOY_PATH"
239
+ fi
240
+
241
+ cd "$DEPLOY_PATH"
242
+ # R2: reset, never merge. A local modification on the server would turn a
243
+ # merge into a conflict and abort the deploy halfway.
244
+ git fetch origin production
245
+ git reset --hard origin/production
246
+ mkdir -p "$DOCKER_CONFIG_DIR"
247
+ echo "$REGISTRY_PASSWORD" | docker --config "$DOCKER_CONFIG_DIR" login -u "$REGISTRY_USER" --password-stdin "$REGISTRY"
248
+ echo "[deploy] pulling $BIZ_IMAGE_DIGEST"
249
+ docker --config "$DOCKER_CONFIG_DIR" compose -f docker-compose.production.yml pull
250
+ docker --config "$DOCKER_CONFIG_DIR" compose -f docker-compose.production.yml up -d
251
+ REMOTE
252
+ # The deploy is not finished until the platform's own post-deploy gate says
253
+ # the service is alive: the heartbeat projection is present, UP and fresh,
254
+ # and the container's RestartCount has not moved over the window
255
+ # (confirmations deploy-gate-targets 003, deploy-gate-restart-count 001).
256
+ # The two facts sit on two boxes and this runner is a third, so the gate
257
+ # runs in remote mode — same script, same contract, same verdicts as a local
258
+ # run, only reading over ssh.
259
+ - |
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"
265
+ bash "$API_CHECKOUT/scripts/run-post-deploy-gate.sh" \
266
+ --target "$DEPLOY_GATE_TARGETS" \
267
+ --biz-host "$DEPLOY_USER@$DEPLOY_HOST" \
268
+ --infra-host "$GATE_INFRA_USER@$GATE_INFRA_HOST" \
269
+ --contract "$API_CHECKOUT/config/deploy-smoke-contract.json"
270
+ environment:
271
+ name: production
272
+ url: https://api.onlineapps.cz
273
+ rules:
274
+ - if: $CI_COMMIT_BRANCH == "production"
275
+ # --- end oa-ci v1
276
+
277
+ test:
278
+ stage: test
279
+ image: node:24-alpine
280
+ variables:
281
+ RABBITMQ_URL: "amqp://localhost:5672"
282
+ REGISTRY_URL: "http://localhost:33100"
283
+ NODE_ENV: "test"
284
+ script:
285
+ - npm ci
286
+ - npm run test:ci
287
+ rules:
288
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
289
+ - if: $CI_COMMIT_BRANCH == "main"
290
+ - if: $CI_COMMIT_BRANCH == "production"
@@ -0,0 +1,27 @@
1
+ # === Production stage (build with: --target production) ===
2
+ FROM node:24-alpine AS production
3
+ WORKDIR /app
4
+ COPY package*.json ./
5
+ RUN npm ci --omit=dev
6
+ COPY . .
7
+ # The manifest conformance check, inside the image that would be deployed
8
+ # (confirmation biz-service-manifest 001 §3.1: "The same check runs in the
9
+ # Dockerfile production stage, so an image with a finding is never built").
10
+ #
11
+ # It is the CLI of @onlineapps/conn-orch-validator, which every service declares
12
+ # under `dependencies` — measured across all eight biz repositories on
13
+ # 2026-09-09 — so `npm ci --omit=dev` above installs it and this line needs
14
+ # nothing the image does not already carry.
15
+ #
16
+ # The rows that read a platform file (the template, api/config/shared-env.json,
17
+ # api/.nvmrc) report NOT RUN here and say so by name: an image has no workspace
18
+ # above it. What runs is every row the repository itself answers, and one
19
+ # finding of severity boot or deploy exits non-zero and fails the build.
20
+ RUN node node_modules/@onlineapps/conn-orch-validator/src/cli/oa-validate.js .
21
+ CMD ["node", "index.js"]
22
+
23
+ # === Development stage (default) ===
24
+ FROM node:24-alpine
25
+ WORKDIR /app
26
+ COPY package*.json ./
27
+ CMD ["npm", "start"]
@@ -0,0 +1,213 @@
1
+ <!-- BEGIN GENERATED: biz-service-uniform — regenerate: npx oa-sync-template readme-uniform --target . -->
2
+ Uniform: [biz-service](./node_modules/@onlineapps/conn-orch-validator/manifests/biz-service.manifest.json)
3
+
4
+ Every `api_biz/*/package.json` that `api/config/services.json` lists wears it (`U-ORPHAN`, `U-IDENTITY`).
5
+
6
+ Duty sections that apply:
7
+
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
10
+ - `tooling`: S-SCRIPTS
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
14
+ - `docs`: C-LINT, D-PORT, D-NPM, D-SCRIPT, D-RETIRED, D-HEADER, D-LINT
15
+
16
+ Paths a duty owns:
17
+
18
+ - `.dockerignore`: F-DOCKERIGNORE (from `./node_modules/@onlineapps/conn-orch-validator/templates/business-service/gitignore`)
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`)
21
+ - `README.md`: G-README (from `./node_modules/@onlineapps/conn-orch-validator/manifests/biz-service.manifest.json`)
22
+ - `config/biz-docs-lint.tree.json`: C-LINT
23
+ - `config/env-templates`: C-ENV, D-DB-ACCOUNT
24
+ - `config/env-templates/shared.env`: G-SHARED-ENV (from `./node_modules/@onlineapps/conn-orch-validator/templates/business-service/config/env-templates/shared.env`)
25
+ - `config/service/config.json`: C-SERVICE
26
+ - `config/service/integration-contract.json`: C-CONTRACT, D-DB-COLLATION
27
+ - `config/service/operations.json`: C-OPS
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
30
+ - `docs/80-setup/`: G-SETUP
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
+ - `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`)
33
+ - `docs/80-setup/VALIDATION.md`: G-SETUP-VALIDATION (from `./node_modules/@onlineapps/conn-orch-validator/templates/business-service/docs/80-setup/VALIDATION.md`)
34
+ - `hooks/pre-commit`: X-HOOKS
35
+ - `init.sh`: F-INIT (from `./node_modules/@onlineapps/conn-orch-validator/templates/business-service/init.sh`)
36
+ - `jest.config.js`: F-JEST (from `./node_modules/@onlineapps/conn-orch-validator/templates/business-service/jest.config.js`)
37
+ - `migrations/`: D-DB-PACKAGE, D-DB-NAMING, D-DB-HEADERS
38
+ - `migrations/README.md`: D-DB-README
39
+ - `scripts/`: S-SCRIPTS
40
+ - `scripts/run-pre-validation.js`: X-PREVAL
41
+ - `src/config/database.js`: X-DB-CONFIG
42
+ <!-- END GENERATED: biz-service-uniform -->
43
+
44
+ # __SERVICE_NAME__
45
+
46
+ __SERVICE_DESCRIPTION__
47
+
48
+ ## Quick Start
49
+
50
+ ```bash
51
+ # 1. Copy env templates
52
+ cp config/env-templates/shared.env config/env-active/shared.env
53
+ cp config/env-templates/service.env config/env-active/__SERVICE_NAME__.env
54
+
55
+ # 2. Edit config/env-active/ files with actual values
56
+
57
+ # 3. Start (dev)
58
+ docker compose up -d --build
59
+
60
+ # 4. Start (production) — the image is pinned by digest, so it is PULLED, never
61
+ # built here: `docker build` refuses a tag that carries a digest. CI publishes
62
+ # the value; a manual run has to name the digest it wants.
63
+ BIZ_IMAGE_DIGEST=sha256:… docker compose -f docker-compose.production.yml pull
64
+ BIZ_IMAGE_DIGEST=sha256:… docker compose -f docker-compose.production.yml up -d
65
+ ```
66
+
67
+ On the production biz box the second env file the compose references is
68
+ `../shared.env` — ONE host-level file per box, at
69
+ `/var/www/html/oadrive/api_biz/shared.env`. It is rendered on the infra box by
70
+ `api/scripts/provision/render-biz-shared-env.sh` and carried over by the
71
+ operator; this compose references it and never copies it (owner decision
72
+ `api/docs/governance/confirmations/server-topology.md` 004 and 005).
73
+ The network there is the external `biz_network`, also created by role-biz. The
74
+ dev arrangement above is a single host and keeps its own
75
+ `config/env-active/shared.env`.
76
+
77
+ ## Structure
78
+
79
+ ```
80
+ config/
81
+ ├── service/ # Service metadata (config.json, operations.json)
82
+ ├── env-templates/ # Versioned env templates
83
+ └── env-active/ # Runtime env (gitignored)
84
+ src/
85
+ └── handlers/v3/ # One module per operation; the handler: field points here
86
+ tests/
87
+ ├── unit/
88
+ └── cookbooks/ # Tier-1 cookbooks, one per operation
89
+ index.js # Entry point (bootstrap)
90
+ ```
91
+
92
+ There is **no** `src/app.js`, **no** `src/routes/` and no HTTP framework in
93
+ `package.json`: a biz container binds no port, and an operation is reached
94
+ through the `handler:` field in `operations.json` — ADR 0005,
95
+ `api/docs/biz/80-decisions/0005-no-http-in-biz-containers.md`, with the testable
96
+ form of it in `api/docs/biz/00-model/service-shape.md` § No-HTTP tests. Business
97
+ logic goes into `src/handlers/v3/`, the layout `api_biz/hello-service/src/` uses
98
+ — see `api/docs/biz/60-templates/service-template.md` § Expected Structure.
99
+ (Paths are relative to the workspace root, because this README is copied into a
100
+ sibling repository where a link relative to `api/` would not resolve.)
101
+
102
+ ## The example operation
103
+
104
+ The template ships one working operation, `echo`, so a new service starts from a
105
+ shape that already passes. Three files describe it, and replacing the operation
106
+ means replacing all three together:
107
+
108
+ | File | States |
109
+ |---|---|
110
+ | `config/service/operations.json` | the contract — `handler`, `bundle_scope`, `mutates`, `input`/`output` schemas |
111
+ | `src/handlers/v3/echo.js` | the behaviour — `echo(input, ctx)`, returning a value |
112
+ | `tests/cookbooks/echo.json` | the Tier-1 assertion — calls the operation and asserts the output BY VALUE |
113
+
114
+ `echo` is deliberately total and deterministic, which is what lets the cookbook
115
+ assert `length` equals `8` instead of merely asserting that something came back.
116
+ Keep that property when you replace it: a cookbook that asserts only truthiness
117
+ proves the dispatch worked and nothing about the result
118
+ (`.claude/rules/architecture-principles.md` §10a).
119
+
120
+ ## Testing
121
+
122
+ Suites run **in a container**, in the one-shot test runner this service ships
123
+ beside itself (`__CONTAINER_NAME___tests` in `docker-compose.yml`). It has its
124
+ own memory budget, so a heavy suite can no longer be killed together with the
125
+ service, and the `test` profile keeps it out of a plain `docker compose up`.
126
+
127
+ ```bash
128
+ npm run test:container # unit suite, in the runner
129
+ npm run test:all:container # everything test:all chains, in the runner
130
+ ```
131
+
132
+ Each wraps one command — `docker compose run --rm __CONTAINER_NAME___tests npm run <suite>`.
133
+
134
+ **Rebuilding the runner takes the profile with it.** `profiles: [test]` keeps the
135
+ runner out of a plain `docker compose up`, and out of a plain build for the same
136
+ reason — measured 2026-09-14: `docker compose build --dry-run` builds one image,
137
+ `docker compose --profile test build --dry-run` builds two. So after a change to
138
+ the `Dockerfile` or to the dependencies:
139
+
140
+ ```bash
141
+ docker compose --profile test build
142
+ ```
143
+
144
+ Without it the next `npm run test:container` runs the image the runner was last
145
+ built from, and a suite then passes or fails on code that is no longer in this
146
+ repository — a green run that says nothing about what is committed.
147
+
148
+ `npm test` and `npm run test:unit` are what runs INSIDE that container; the
149
+ runner is the one rail a suite runs on (owner decision
150
+ `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
152
+ to the same answer: the host has neither the service's `env_file` nor its
153
+ network, so what fails there says nothing about the service — which is why no
154
+ `test:host` script exists and why the uniform forbids one.
155
+
156
+ ### Cookbooks (Tier-1)
157
+
158
+ ```bash
159
+ npm run test:cookbooks:container # in the runner, which has the env
160
+ ```
161
+
162
+ It wraps `npm run test:cookbooks`, the library command
163
+ (`oa-biz-ci-gate run-prevalidation`, manifest row `S-COOKBOOKS`), which is what
164
+ runs inside the container. The cookbook run dispatches real handlers under the
165
+ testing namespace, so it reads `TESTING_TENANT_ID` and `TESTING_WORKSPACE_ID`;
166
+ the runner has both from `env_file`, and the run fails by name if they are
167
+ missing. It writes `conn-runtime/validation-proof.json` — generated,
168
+ gitignored, and **not** what the platform trusts: the proof that counts is the
169
+ one ServiceWrapper writes on every boot
170
+ (`api/docs/biz/50-lifecycle/validation-responsibilities.md` § Data Flow).
171
+
172
+ This template ships **no** `tests/integration/`, and no script declaring one:
173
+ there is no behaviour here to exercise, and a declaration with nothing behind it
174
+ is the defect `.claude/rules/change-discipline.md` § "Removing something removes
175
+ its declaration" describes. Add the directory together with the first real
176
+ integration test — `.claude/rules/architecture-principles.md` §10a requires one
177
+ for every behavioural change, so the first handler brings its own.
178
+
179
+ ## Contract gates
180
+
181
+ A fresh scaffold passes `biz-ci-gate verify-contract` — deploy, installation,
182
+ env, test coverage and the library pins — with nothing added by hand:
183
+
184
+ ```bash
185
+ node node_modules/@onlineapps/conn-orch-validator/src/cli/biz-ci-gate.js verify-contract
186
+ ```
187
+
188
+ Two files are what makes that true, and both are copied and substituted with the
189
+ rest of the template: `config/service/integration-contract.json` (the connectors,
190
+ the integration minimum, and — once a handler reads one — the `env` block), and
191
+ `docs/80-setup/{INSTALL,PLATFORM_MATRIX,VALIDATION}.md`, the three installation
192
+ documents the contract requires of every repository
193
+ (`api/docs/standards/repository-installation-sql-contract.md`).
194
+
195
+ ## Memory limits
196
+
197
+ Two numbers, and they measure different things — the platform norm, recorded in
198
+ `api/docs/governance/confirmations/biz-memory-limits.md` 001:
199
+
200
+ | Where | Key | Value | Covers |
201
+ |---|---|---|---|
202
+ | service, dev **and** production | `deploy.resources.limits.memory` | `512M` | the resident service process, nothing else |
203
+ | test runner, dev/CI only | `mem_limit` | `1g` | one suite at a time (`--maxWorkers=1`) |
204
+
205
+ They are prescribed values, not figures derived for this service. The split is
206
+ the point: while one budget covered both, a test run was killed by the kernel
207
+ and took the service down with it — so raising the service limit is never the
208
+ fix for a suite that runs out of memory, and no suite may share the service's
209
+ cgroup.
210
+
211
+ Revise the service limit when this service exceeds 60% of it in operation,
212
+ measured the way the norm was: cgroup `memory.stat:anon`, sampled inside the
213
+ running container.
@@ -0,0 +1,10 @@
1
+ {
2
+ "_note": "Tree-bound overlay for linting api_biz/__SERVICE_NAME__/docs with api/scripts/ci/lint-biz-docs.mjs. Run from api/: node scripts/ci/lint-biz-docs.mjs --root ../api_biz/__SERVICE_NAME__/docs --tree-config ../api_biz/__SERVICE_NAME__/config/biz-docs-lint.tree.json --skip-code-rules --format text. Only the tree-bound keys belong here; every other rule is a platform rule inherited from api/config/biz-docs-lint.json. Manifest row C-LINT requires the file: without it the lint refuses to run over the tree, and a tree nothing lints is a tree nothing checks.",
3
+ "canonical_facts": [],
4
+ "_canonical_facts_note": "Empty on purpose, and it stays empty until this service owns a cross-cutting fact of its own. canonical_facts names the node that OWNS such a fact; everything a new service says is owned by the platform tree api/docs/biz/** and this tree only links to it.",
5
+ "default_max_words": 3687,
6
+ "_default_max_words_note": "The platform per-node norm (api/config/biz-docs-lint.json). A service tree does not get a laxer node cap than the platform tree it hangs off.",
7
+ "tree_max_words": 4000,
8
+ "_tree_max_words_note": "The scaffolded tree measures 1666 words in five nodes (INSTALL 544, VALIDATION 524, PLATFORM_MATRIX 412, 80-setup/README 94, README 92), counted with node api/scripts/ci/lib/countWords.js on 2026-09-14 \u2014 the two maps joined with D-LINT, which reports the orphan nodes a tree without them leaves behind. The budget is deliberately larger than that measurement, because a new service writes its own nodes into an otherwise empty tree; RAISE IT WITH A MEASUREMENT once the tree is real, the way api_biz/converter and api_biz/pdfgen state theirs, and never to make a failing run pass.",
9
+ "max_words_overrides": {}
10
+ }
@@ -0,0 +1,4 @@
1
+ # __SERVICE_NAME__ environment
2
+ # Copy to config/env-active/__SERVICE_NAME__.env and adjust values.
3
+
4
+ SERVICE_NAME=__SERVICE_NAME__
@@ -0,0 +1,65 @@
1
+ # GENERATED FILE - do not edit. Edit the manifest instead:
2
+ # api/config/shared-env.json, then run `npx oa-sync-template shared-env --target <dir>`.
3
+
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
+ NODE_ENV=development
6
+
7
+ # The log level an infra service's config/logging.json resolves through ${LOG_LEVEL}; one platform value so a stack does not log at four different levels.
8
+ LOG_LEVEL=info
9
+
10
+ # 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).
11
+ JWT_SECRET=CHANGE_ME
12
+
13
+ # 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.
14
+ SECRETS_MASTER_KEYS=CHANGE_ME
15
+
16
+ # The address of the platform cache on the docker network; declared once because every service reaches the same instance. The instance demands a password (server-topology 004: Redis publishes on wg0 with authentication), so the URL carries it: the value is per-machine and lives only in config/env-active/shared.env, and it is the same password as NODE_CACHE_PASSWORD in config/env-active/compose-infra.env - one fact reaching the container that enforces it and the services that authenticate to it, the same two-delivery split MINIO_ACCESS_KEY and MINIO_SECRET_KEY already have.
17
+ REDIS_URL=redis://:CHANGE_ME@api_node_cache:6379
18
+
19
+ # The broker every service opens its MQ connection with; the credentials are per-machine and live only in config/env-active/shared.env - the platform account is oa_dev, declared by infra/api_services_queuer/definitions.json.
20
+ RABBITMQ_URL=amqp://CHANGE_ME:CHANGE_ME@api_services_queuer:5672
21
+
22
+ # HTTP address of the registry for administrative operations from the host - scripts/bump-infra-version.sh announces an infrastructure version bump through it. No service reads it: registration travels over MQ to registry.register and discovery reads the Redis projection (confirmation biz-discovery-redis 002).
23
+ REGISTRY_URL=http://api_services_registry:33100
24
+
25
+ # The host AWS SigV4 signatures are computed against - deliberately the loopback name, not the reachable one, because MinIO validates the Host header (infra/api_minio_proxy/README.md).
26
+ MINIO_ENDPOINT=127.0.0.1
27
+
28
+ # Where the bytes actually go once the signature is computed against MINIO_ENDPOINT; the other half of the host-header split (infra/api_minio_proxy/README.md).
29
+ MINIO_ACTUAL_HOST=api_minio_proxy
30
+
31
+ # The S3 API port of the object store, shared with the endpoint it belongs to.
32
+ MINIO_PORT=9000
33
+
34
+ # Whether the object-store client opens its connection with TLS. It is declared here because until d.411 it was declared nowhere and every reader carried its own default, so one platform value had as many owners as it had readers (.claude/rules/change-discipline.md - One rail per concern). The platform value is false: MinIO publishes its S3 port on the private wg0 overlay only and never on a public interface (docs/governance/confirmations/server-topology.md 002 and 004), so the hop that would carry TLS is an encrypted one already; a deployment that puts TLS in front of the object store changes this key, and no library.
35
+ MINIO_USE_SSL=false
36
+
37
+ # The object-store account every service reads and writes under; the same pair is also interpolated into compose for the MinIO container itself (config/env-templates/compose-infra.env), two delivery mechanisms that must carry one value.
38
+ MINIO_ACCESS_KEY=CHANGE_ME
39
+
40
+ # The secret of that same object-store account; shares the compose-interpolation note of MINIO_ACCESS_KEY.
41
+ MINIO_SECRET_KEY=CHANGE_ME
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.
44
+ BIZ_HEARTBEAT_INTERVAL_MS=30000
45
+
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.
47
+ BIZ_HEARTBEAT_STALE_FACTOR=2.5
48
+
49
+ # The tenant a validation or test run writes into; it is platform-wide and never overridden per service, because a service that picks its own namespace can pick a live one. The classes and their meaning are owned by docs/standards/tenant-allocation.md.
50
+ TESTING_TENANT_ID=99
51
+
52
+ # The workspace half of that same namespace; it travels with TESTING_TENANT_ID because a run that knows one without the other addresses a namespace it cannot name.
53
+ TESTING_WORKSPACE_ID=200
54
+
55
+ # Process locale. No code in the platform reads it; the same three values are also baked into every infra Dockerfile as ENV, so this declaration has no measured consumer of its own.
56
+ LANG=en_US.UTF-8
57
+
58
+ # Process locale. No code in the platform reads it; the same three values are also baked into every infra Dockerfile as ENV, so this declaration has no measured consumer of its own.
59
+ LC_ALL=en_US.UTF-8
60
+
61
+ # Process locale. No code in the platform reads it; the same three values are also baked into every infra Dockerfile as ENV, so this declaration has no measured consumer of its own.
62
+ LANGUAGE=en_US.UTF-8
63
+
64
+ # The timezone every container renders local time in, declared ONCE here and inherited by every bearer of shared.env rather than as a per-service key (owner decision docs/governance/confirmations/container-timezone.md 001, on the BIZ-invoicing finding of 2026-08-29 that a document issued after local midnight carried the PREVIOUS day's date, because every container ran UTC). Timestamps stored in databases and in message envelopes stay UTC; this key changes only what local-time rendering resolves to - an invoice date, a log line a human reads - and code that derives a business date must still say which zone it means. Node honours it from its own ICU data, so a container needs no tzdata package for Date/Intl; libc tools (date, shell) follow it only where the image ships tzdata.
65
+ TZ=Europe/Prague
@@ -0,0 +1,14 @@
1
+ {
2
+ "service": {
3
+ "name": "__REGISTRY_NAME__",
4
+ "description": "__SERVICE_DESCRIPTION__",
5
+ "specificationEndpoint": "/specification",
6
+ "workspaceScoped": false
7
+ },
8
+ "wrapper": {
9
+ "mq": {
10
+ "url": "${RABBITMQ_URL}",
11
+ "prefetch": 10
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "serviceName": "__REGISTRY_NAME__",
3
+ "requiredConnectors": {
4
+ "db": false,
5
+ "redis": false,
6
+ "mq": true,
7
+ "minio": false
8
+ },
9
+ "integrationMinimum": {
10
+ "minTestFiles": 1
11
+ }
12
+ }