@onlineapps/conn-orch-validator 7.0.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2582 -2
- package/README.md +1038 -4
- package/docs/DESIGN.md +3 -1
- package/manifests/biz-service.manifest.json +658 -0
- package/manifests/library.manifest.json +324 -0
- package/package.json +12 -6
- package/src/CookbookTestRunner.js +408 -101
- package/src/CookbookTestUtils.js +7 -8
- package/src/ServiceReadinessValidator.js +10 -35
- package/src/ValidationOrchestrator.js +219 -71
- package/src/cli/biz-ci-gate.js +176 -33
- package/src/cli/oa-lint-scripts.js +221 -0
- package/src/cli/oa-sync-template.js +1020 -0
- package/src/cli/oa-validate.js +474 -0
- package/src/helpers/README.md +2 -1
- package/src/helpers/createServiceReadinessTests.js +60 -4
- package/src/index.js +33 -3
- package/src/lint/scripts/lintScripts.js +298 -0
- package/src/manifest/checks/composeRunnerBlock.js +222 -0
- package/src/manifest/checks/composeShape.js +165 -0
- package/src/manifest/checks/contractBridge.js +181 -0
- package/src/manifest/checks/discoveryOrphan.js +50 -0
- package/src/manifest/checks/docsLintBridge.js +553 -0
- package/src/manifest/checks/fileAbsent.js +35 -0
- package/src/manifest/checks/gitTracked.js +204 -0
- package/src/manifest/checks/index.js +111 -0
- package/src/manifest/checks/libraryContext.js +226 -0
- package/src/manifest/checks/libraryDocs.js +75 -0
- package/src/manifest/checks/libraryPackage.js +272 -0
- package/src/manifest/checks/librarySource.js +274 -0
- package/src/manifest/checks/libraryTests.js +121 -0
- package/src/manifest/checks/libraryWorkspace.js +293 -0
- package/src/manifest/checks/readmeRegion.js +135 -0
- package/src/manifest/checks/scriptHeaders.js +79 -0
- package/src/manifest/checks/serviceConfig.js +390 -0
- package/src/manifest/checks/serviceConnectors.js +81 -0
- package/src/manifest/checks/serviceDb.js +388 -0
- package/src/manifest/checks/serviceFiles.js +754 -0
- package/src/manifest/checks/serviceIdentityRows.js +351 -0
- package/src/manifest/checks/serviceRuntime.js +295 -0
- package/src/manifest/checks/serviceScripts.js +213 -0
- package/src/manifest/deployabilitySignal.js +121 -0
- package/src/manifest/discovery.js +386 -0
- package/src/manifest/loadManifest.js +62 -0
- package/src/manifest/manifestShape.js +446 -0
- package/src/manifest/report.js +245 -0
- package/src/manifest/runManifest.js +449 -0
- package/src/manifest/serviceIdentity.js +140 -0
- package/src/manifest/walk.js +74 -0
- package/src/manifest/workspaceRoot.js +242 -0
- package/src/mocks/MockMQClient.js +13 -30
- package/src/mocks/MockRegistry.js +4 -2
- package/src/mocks/MockStorage.js +4 -2
- package/src/sync/docsRegion.js +463 -0
- package/src/sync/generatedRegion.js +228 -0
- package/src/sync/readmeLocation.js +182 -0
- package/src/sync/readmePointer.js +477 -0
- package/src/sync/serviceTemplate.js +583 -0
- package/src/sync/sharedEnv.js +162 -0
- package/src/sync/uniformFiles.js +474 -0
- package/src/utils/bizCiGateContract.js +131 -7
- package/src/utils/connectorContract.js +97 -7
- package/src/utils/cookbookFormat.js +81 -40
- package/src/utils/deployContract.js +140 -9
- package/src/utils/envContract.js +57 -1
- package/src/utils/handlerRef.js +181 -0
- package/src/utils/installContract.js +287 -41
- package/src/utils/libCompat.js +29 -7
- package/src/utils/migrationOrder.js +163 -0
- package/src/utils/preValidation.js +20 -7
- package/src/utils/setupDatabase.js +194 -13
- package/src/utils/testCoverageContract.js +539 -0
- package/src/utils/testNamespace.js +247 -23
- package/src/utils/throwawaySchema.js +207 -0
- package/src/validators/ServiceStructureValidator.js +2 -1
- package/templates/business-service/.dockerignore +42 -0
- package/templates/business-service/.gitlab-ci.yml +409 -0
- package/templates/business-service/Dockerfile +27 -0
- package/templates/business-service/README.md +213 -0
- package/templates/business-service/config/biz-docs-lint.tree.json +10 -0
- package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +22 -0
- package/templates/business-service/config/env-templates/shared.env +65 -0
- package/templates/business-service/config/service/config.json +14 -0
- package/templates/business-service/config/service/integration-contract.json +12 -0
- package/templates/business-service/config/service/operations.json +41 -0
- package/templates/business-service/docker-compose.production.yml +60 -0
- package/templates/business-service/docker-compose.yml +93 -0
- package/templates/business-service/docs/80-setup/INSTALL.md +123 -0
- package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +65 -0
- package/templates/business-service/docs/80-setup/README.md +18 -0
- package/templates/business-service/docs/80-setup/VALIDATION.md +78 -0
- package/templates/business-service/docs/README.md +18 -0
- package/templates/business-service/gitignore +42 -0
- package/templates/business-service/index.js +10 -0
- package/templates/business-service/init.sh +54 -0
- package/templates/business-service/jest.config.js +6 -0
- package/templates/business-service/package.json.template +31 -0
- package/templates/business-service/scripts/verify-deploy-uniform.sh +180 -0
- package/templates/business-service/src/handlers/v3/echo.js +39 -0
- package/templates/business-service/tests/cookbooks/echo.json +36 -0
- package/templates/business-service/tests/unit/handler.test.js +78 -0
- package/src/WorkflowTestRunner.js +0 -402
|
@@ -0,0 +1,409 @@
|
|
|
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
|
+
# The read-only api clone the uniform step already made beside this checkout.
|
|
202
|
+
# Derived from CI_PROJECT_DIR rather than spelled out a second time: the
|
|
203
|
+
# layout is declared once, in GIT_CLONE_PATH, so the two cannot drift apart.
|
|
204
|
+
# TWO steps read it: the migration runner streamed to the box below, and the
|
|
205
|
+
# post-deploy gate at the end of this job.
|
|
206
|
+
- API_CHECKOUT="$(dirname "$(dirname "$CI_PROJECT_DIR")")/api"
|
|
207
|
+
# WHICH SCHEMA, IF ANY (confirmation db-migrations-first-deploy 001, phase 2).
|
|
208
|
+
# Read from the one declaration there is - this service's own installation
|
|
209
|
+
# contract - and read HERE, on the runner: the contract is this repository's,
|
|
210
|
+
# jq is in this image, and the box is not asked to carry a JSON parser for two
|
|
211
|
+
# values. A service that declares no `database` block has no schema to
|
|
212
|
+
# migrate, and the step below says that out loud rather than passing over it
|
|
213
|
+
# in silence (automation-gates.md 5: a step that checked nothing must never
|
|
214
|
+
# look like one that checked).
|
|
215
|
+
- |
|
|
216
|
+
CONTRACT="$CI_PROJECT_DIR/config/service/integration-contract.json"
|
|
217
|
+
if [ ! -f "$CONTRACT" ]; then
|
|
218
|
+
echo "[deploy] FATAL: no installation contract at $CONTRACT - this deploy cannot tell whether the service has a schema to migrate, and guessing is not an option. Fix: commit config/service/integration-contract.json (the uniform manifest requires it of every biz repository)." >&2
|
|
219
|
+
exit 1
|
|
220
|
+
fi
|
|
221
|
+
BIZ_DB_SCHEMA="$(jq -r '.database.schema // empty' "$CONTRACT")"
|
|
222
|
+
BIZ_DB_MIGRATIONS="$(jq -r '.database.migrations // empty' "$CONTRACT")"
|
|
223
|
+
if [ -n "$BIZ_DB_SCHEMA" ] && [ -z "$BIZ_DB_MIGRATIONS" ]; then
|
|
224
|
+
echo "[deploy] FATAL: $CONTRACT declares database.schema ($BIZ_DB_SCHEMA) and no database.migrations - the deploy would not know which files to apply. Fix: declare the glob of the directory that holds them (api/docs/biz/70-contracts/database-contract.md)." >&2
|
|
225
|
+
exit 1
|
|
226
|
+
fi
|
|
227
|
+
if [ -n "$BIZ_DB_MIGRATIONS" ]; then
|
|
228
|
+
# The runner applies a DIRECTORY and does not descend into it; the
|
|
229
|
+
# contract declares the glob (migrations/*.sql, migrations/BASELINE/*.sql),
|
|
230
|
+
# so the directory is its dirname - the same reading the runbook gives it
|
|
231
|
+
# (api/docs/setup/INSTALL.md, applying a business schema).
|
|
232
|
+
BIZ_DB_MIGRATIONS="$(dirname "$BIZ_DB_MIGRATIONS")"
|
|
233
|
+
fi
|
|
234
|
+
# DEPLOY_PATH / DEPLOY_HOST / DEPLOY_USER are CI variables: the box decides
|
|
235
|
+
# where its checkout lives, and no path is baked into this file.
|
|
236
|
+
#
|
|
237
|
+
# The registry password travels on STDIN and nowhere else. As an argument of
|
|
238
|
+
# the remote `bash -s` it stood in the deploy host's process table for the
|
|
239
|
+
# whole deploy, readable by every account on that box, and in this runner's
|
|
240
|
+
# argv besides. So the stream the remote shell reads is the password's line
|
|
241
|
+
# first and the script after it: the remote reads that one line, exports it,
|
|
242
|
+
# and hands the rest of the stream to `bash -s` - ssh's stdin is a socket
|
|
243
|
+
# there, so `read` consumes exactly the line and no more.
|
|
244
|
+
#
|
|
245
|
+
# The shell the remote runs is ASSEMBLED from three parts, in this order: the
|
|
246
|
+
# password line, the shell options, and then the platform's own MariaDB
|
|
247
|
+
# migration runner followed by the body below. The runner is carried rather
|
|
248
|
+
# than copied - it is api/scripts/lib/mariadb-migrations.sh out of the clone
|
|
249
|
+
# the uniform step made, so the box applies the same shell the runbook applies
|
|
250
|
+
# by hand in phase 1, and no second implementation of it exists anywhere
|
|
251
|
+
# (change-discipline.md - One rail per concern). The options are set once, for
|
|
252
|
+
# the whole assembled stream, which is why they are no longer the first line
|
|
253
|
+
# of the body.
|
|
254
|
+
- |
|
|
255
|
+
{
|
|
256
|
+
printf '%s\n' "$CI_REGISTRY_PASSWORD"
|
|
257
|
+
printf '%s\n' "set -euo pipefail"
|
|
258
|
+
cat "$API_CHECKOUT/scripts/lib/mariadb-migrations.sh"
|
|
259
|
+
cat
|
|
260
|
+
} <<'REMOTE' | ssh $DEPLOY_USER@$DEPLOY_HOST "IFS= read -r REGISTRY_PASSWORD; export REGISTRY_PASSWORD; bash -s -- '$DEPLOY_PATH' '$CI_REGISTRY' '$CI_REGISTRY_USER' '$CI_PROJECT_PATH_SLUG' '$BIZ_IMAGE_DIGEST' '$CI_PROJECT_PATH' '$BIZ_DB_SCHEMA' '$BIZ_DB_MIGRATIONS'"
|
|
261
|
+
DEPLOY_PATH="$1"
|
|
262
|
+
REGISTRY="$2"
|
|
263
|
+
REGISTRY_USER="$3"
|
|
264
|
+
PROJECT_SLUG="$4"
|
|
265
|
+
export BIZ_IMAGE_DIGEST="$5"
|
|
266
|
+
PROJECT_PATH="$6"
|
|
267
|
+
# Empty when the installation contract declares no `database` block - the
|
|
268
|
+
# only thing that turns the migration step below on or off.
|
|
269
|
+
DB_SCHEMA="$7"
|
|
270
|
+
DB_MIGRATIONS_DIR="$8"
|
|
271
|
+
# This service's own env file on the box, the per-machine half of the two
|
|
272
|
+
# the compose file names (../shared.env is the host's). It carries the
|
|
273
|
+
# migration account - the SAME account the service connects as.
|
|
274
|
+
SERVICE_ENV="config/env-active/__SERVICE_NAME__.env"
|
|
275
|
+
# The host-level env file of the biz box, one per box and referenced rather
|
|
276
|
+
# than copied (server-topology 004) - the same path docker-compose.production.yml
|
|
277
|
+
# names.
|
|
278
|
+
HOST_ENV="../shared.env"
|
|
279
|
+
# REGISTRY_PASSWORD arrives in the environment, from the first line of this
|
|
280
|
+
# script's own stdin. Empty means the runner had no CI_REGISTRY_PASSWORD:
|
|
281
|
+
# the pull would fail later with an authentication error that names nothing.
|
|
282
|
+
if [ -z "${REGISTRY_PASSWORD:-}" ]; then
|
|
283
|
+
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
|
|
284
|
+
exit 1
|
|
285
|
+
fi
|
|
286
|
+
DOCKER_CONFIG_DIR="$HOME/.docker-ci/${PROJECT_SLUG}"
|
|
287
|
+
|
|
288
|
+
# Onboarding a new biz service must not require a step on the server —
|
|
289
|
+
# owner decision api/docs/governance/confirmations/server-topology.md 004.
|
|
290
|
+
# The first deploy therefore creates the checkout itself; the deploy user
|
|
291
|
+
# holds a read key for every biz repository.
|
|
292
|
+
if [ ! -d "$DEPLOY_PATH/.git" ]; then
|
|
293
|
+
echo "[deploy] $DEPLOY_PATH holds no checkout - cloning git@gitlab.com:${PROJECT_PATH}.git"
|
|
294
|
+
git clone "git@gitlab.com:${PROJECT_PATH}.git" "$DEPLOY_PATH"
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
cd "$DEPLOY_PATH"
|
|
298
|
+
# R2: reset, never merge. A local modification on the server would turn a
|
|
299
|
+
# merge into a conflict and abort the deploy halfway.
|
|
300
|
+
git fetch origin production
|
|
301
|
+
git reset --hard origin/production
|
|
302
|
+
mkdir -p "$DOCKER_CONFIG_DIR"
|
|
303
|
+
echo "$REGISTRY_PASSWORD" | docker --config "$DOCKER_CONFIG_DIR" login -u "$REGISTRY_USER" --password-stdin "$REGISTRY"
|
|
304
|
+
echo "[deploy] pulling $BIZ_IMAGE_DIGEST"
|
|
305
|
+
docker --config "$DOCKER_CONFIG_DIR" compose -f docker-compose.production.yml pull
|
|
306
|
+
|
|
307
|
+
# ─── Migrations: after the pull, before the switch ───────────────
|
|
308
|
+
# Owner decision api/docs/governance/confirmations/db-migrations-first-deploy.md
|
|
309
|
+
# 001, phase 2. The ordering IS the safety: at this point the new image is
|
|
310
|
+
# on the box and the OLD container is still serving, so a migration that
|
|
311
|
+
# fails stops the deploy with the previous version running and the new one
|
|
312
|
+
# never started. Applying after the switch would mean the new code meets a
|
|
313
|
+
# schema it does not have; applying before the pull would mean a schema
|
|
314
|
+
# change for an image that might not even be pullable.
|
|
315
|
+
#
|
|
316
|
+
# The account is the SERVICE's, never root: its grants cover its own schema
|
|
317
|
+
# and nothing else, so a migration that reaches sideways into a neighbouring
|
|
318
|
+
# database is refused by the server rather than politely applied
|
|
319
|
+
# (db-accounts-per-service 001). This step CREATES NOTHING - schema and
|
|
320
|
+
# account are an operator's step in the runbook, phase 1, and a deploy that
|
|
321
|
+
# provisioned what it found missing would both grant itself those privileges
|
|
322
|
+
# and hide a wrongly provisioned box.
|
|
323
|
+
if [ -z "$DB_SCHEMA" ]; then
|
|
324
|
+
echo "[deploy] migrations NOT APPLICABLE (no database block) - config/service/integration-contract.json declares no database, so this service has no schema to migrate."
|
|
325
|
+
else
|
|
326
|
+
# The two env files the compose names, in the order it names them: the
|
|
327
|
+
# host-level one first, this service's own second, so the same value wins
|
|
328
|
+
# here that wins inside the container. Reading only one of them would
|
|
329
|
+
# make this step disagree with the service it migrates for, depending on
|
|
330
|
+
# which file the box happens to carry DB_HOST in.
|
|
331
|
+
for env_file in "$HOST_ENV" "$SERVICE_ENV"; do
|
|
332
|
+
if [ ! -f "$env_file" ]; then
|
|
333
|
+
echo "[deploy] Missing env file - $env_file does not exist on this box, and docker-compose.production.yml names it: between the two of them they carry the endpoint and the migration account of $DB_SCHEMA. Fix: place it on the box (both are per-machine and never committed), then re-run this deploy." >&2
|
|
334
|
+
exit 1
|
|
335
|
+
fi
|
|
336
|
+
set -a
|
|
337
|
+
. "$env_file"
|
|
338
|
+
set +a
|
|
339
|
+
done
|
|
340
|
+
missing=""
|
|
341
|
+
[ -n "${MARIADB_MIGRATION_USER:-}" ] || missing="$missing MARIADB_MIGRATION_USER"
|
|
342
|
+
[ -n "${MARIADB_MIGRATION_PASSWORD:-}" ] || missing="$missing MARIADB_MIGRATION_PASSWORD"
|
|
343
|
+
[ -n "${DB_HOST:-}" ] || missing="$missing DB_HOST"
|
|
344
|
+
[ -n "${DB_PORT:-}" ] || missing="$missing DB_PORT"
|
|
345
|
+
if [ -n "$missing" ]; then
|
|
346
|
+
echo "[deploy] Missing environment variable -$missing in $DEPLOY_PATH/$SERVICE_ENV. Expected: the migration account of $DB_SCHEMA (the same account as DB_USER, granted on that schema only) and the endpoint it is reached at. Fix: add the keys to that file on this box; config/env-templates/ declares each of them with the reason it exists." >&2
|
|
347
|
+
exit 1
|
|
348
|
+
fi
|
|
349
|
+
# The precondition confirmation 001 demands before any migration: the
|
|
350
|
+
# account opens the schema, or this deploy stops and names the runbook.
|
|
351
|
+
if ! mariadb_migrations_can_open_over_tcp "$DB_HOST" "$DB_PORT" "$DB_SCHEMA"; then
|
|
352
|
+
echo "[deploy] Schema or account missing - '$MARIADB_MIGRATION_USER' cannot open '$DB_SCHEMA' at $DB_HOST:$DB_PORT. Expected: the schema and the account, with grants on ${DB_SCHEMA}.* and nothing else, provisioned once by an operator. Fix: run the business-schema step of api/docs/setup/INSTALL.md on the infra box, then re-run this deploy. This step never creates either: provisioning is a decision, not a deploy side effect." >&2
|
|
353
|
+
exit 1
|
|
354
|
+
fi
|
|
355
|
+
# The tracker is the caller's state and it lives in the checkout on this
|
|
356
|
+
# box, under the gitignored config/runtime/ - same file name and same flat
|
|
357
|
+
# format the runbook writes in phase 1, so a schema already carried
|
|
358
|
+
# forward is skipped rather than applied twice (confirmation 001:
|
|
359
|
+
# "fáze 2 nesmí změnit sémantiku trackeru").
|
|
360
|
+
mkdir -p config/runtime
|
|
361
|
+
TRACKER="config/runtime/applied-migrations-${DB_SCHEMA}.txt"
|
|
362
|
+
touch "$TRACKER"
|
|
363
|
+
apply_mariadb_migrations_over_tcp "$DB_MIGRATIONS_DIR" "$TRACKER" "$DB_HOST" "$DB_PORT" "$DB_SCHEMA"
|
|
364
|
+
# The runner logs each file it applies; what this line adds is the state
|
|
365
|
+
# that outlives the deploy. The runner's own counter of files it passed
|
|
366
|
+
# over is deliberately not echoed: no word that reads as a bypass belongs
|
|
367
|
+
# anywhere in a deploy job, and the gate that keeps this job free of one
|
|
368
|
+
# judges by the word, not by the intent.
|
|
369
|
+
echo "[deploy] migrations $DB_SCHEMA: $MARIADB_MIGRATIONS_APPLIED applied now, $(wc -l < "$TRACKER" | tr -d " ") recorded in $DEPLOY_PATH/$TRACKER"
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
docker --config "$DOCKER_CONFIG_DIR" compose -f docker-compose.production.yml up -d
|
|
373
|
+
REMOTE
|
|
374
|
+
# The deploy is not finished until the platform's own post-deploy gate says
|
|
375
|
+
# the service is alive: the heartbeat projection is present, UP and fresh,
|
|
376
|
+
# and the container's RestartCount has not moved over the window
|
|
377
|
+
# (confirmations deploy-gate-targets 003, deploy-gate-restart-count 001).
|
|
378
|
+
# The two facts sit on two boxes and this runner is a third, so the gate
|
|
379
|
+
# runs in remote mode — same script, same contract, same verdicts as a local
|
|
380
|
+
# run, only reading over ssh.
|
|
381
|
+
- |
|
|
382
|
+
# The gate runs out of the same read-only api clone the migration runner
|
|
383
|
+
# was streamed from; API_CHECKOUT is derived once, above.
|
|
384
|
+
bash "$API_CHECKOUT/scripts/run-post-deploy-gate.sh" \
|
|
385
|
+
--target "$DEPLOY_GATE_TARGETS" \
|
|
386
|
+
--biz-host "$DEPLOY_USER@$DEPLOY_HOST" \
|
|
387
|
+
--infra-host "$GATE_INFRA_USER@$GATE_INFRA_HOST" \
|
|
388
|
+
--contract "$API_CHECKOUT/config/deploy-smoke-contract.json"
|
|
389
|
+
environment:
|
|
390
|
+
name: production
|
|
391
|
+
url: https://api.onlineapps.cz
|
|
392
|
+
rules:
|
|
393
|
+
- if: $CI_COMMIT_BRANCH == "production"
|
|
394
|
+
# --- end oa-ci v1
|
|
395
|
+
|
|
396
|
+
test:
|
|
397
|
+
stage: test
|
|
398
|
+
image: node:24-alpine
|
|
399
|
+
variables:
|
|
400
|
+
RABBITMQ_URL: "amqp://localhost:5672"
|
|
401
|
+
REGISTRY_URL: "http://localhost:33100"
|
|
402
|
+
NODE_ENV: "test"
|
|
403
|
+
script:
|
|
404
|
+
- npm ci
|
|
405
|
+
- npm run test:ci
|
|
406
|
+
rules:
|
|
407
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
408
|
+
- if: $CI_COMMIT_BRANCH == "main"
|
|
409
|
+
- 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.
|