@onlineapps/conn-orch-validator 7.0.0 → 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.
- package/CHANGELOG.md +2558 -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 +290 -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 +4 -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 +101 -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,474 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `npx oa-validate [serviceRoot]` — the uniform of a biz service, and
|
|
6
|
+
* `npx oa-validate --library [packageDir]` — the uniform of a shared library,
|
|
7
|
+
* checked at any moment in any repository (confirmation `biz-service-manifest`
|
|
8
|
+
* 001 §3.1, 002 §13.1). One table, exit 0 or 1.
|
|
9
|
+
*
|
|
10
|
+
* There is deliberately NO flag that points the run at another manifest and no
|
|
11
|
+
* flag that skips a row: the manifest ships inside this package and its version
|
|
12
|
+
* is the pin (`.claude/rules/automation-gates.md` §1 requirement 5 — one path,
|
|
13
|
+
* unbypassable). `--workspace` is not such a flag: it says WHERE the SSOTs the
|
|
14
|
+
* manifest references live, and where they cannot be found the rows that need
|
|
15
|
+
* them are printed NOT RUN, never passed.
|
|
16
|
+
*
|
|
17
|
+
* A service run also RECORDS its verdict, in `<serviceRoot>/ci/deployability.json`
|
|
18
|
+
* — the machine half a deploy gate reads (`deployable && complete`). Until d.232
|
|
19
|
+
* the only writer was step 7 of the boot validation, which runs inside the
|
|
20
|
+
* service container, where the workspace SSOTs are absent: every signal that
|
|
21
|
+
* existed therefore said `complete: false`, so the gate could never see a proven
|
|
22
|
+
* tree, while the run that IS complete — this CLI, from the api checkout — left
|
|
23
|
+
* nothing behind. Both callers share one builder and one writer
|
|
24
|
+
* (`manifest/deployabilitySignal.js`): one file shape, not two.
|
|
25
|
+
*
|
|
26
|
+
* @see api/docs/governance/confirmations/biz-service-manifest.md §3, §4
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
|
|
32
|
+
const { loadManifest, DEFAULT_MANIFEST_PATH, LIBRARY_MANIFEST_PATH } = require('../manifest/loadManifest');
|
|
33
|
+
const { runManifest } = require('../manifest/runManifest');
|
|
34
|
+
const {
|
|
35
|
+
resolveWorkspaceRoot, canonicalRoot, API_MARKER, PACKAGE_ROOT
|
|
36
|
+
} = require('../manifest/workspaceRoot');
|
|
37
|
+
const {
|
|
38
|
+
renderReport, renderJson, exitCodeFor, blockingOf, verdictOf, describeClearOutcome
|
|
39
|
+
} = require('../manifest/report');
|
|
40
|
+
const { buildDeployabilitySignal, writeDeployabilitySignal } = require('../manifest/deployabilitySignal');
|
|
41
|
+
const { discoverBearers } = require('../manifest/discovery');
|
|
42
|
+
const { collectRows } = require('../manifest/walk');
|
|
43
|
+
const { CHECK_REGISTRY } = require('../manifest/checks');
|
|
44
|
+
const { declaredCategory, readPackage } = require('../manifest/checks/libraryContext');
|
|
45
|
+
|
|
46
|
+
/** Exit code for a run that could not start at all — distinct from a finding. */
|
|
47
|
+
const USAGE_EXIT = 2;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* This package's own version — the manifest ships inside it, so the manifest
|
|
51
|
+
* version IS this version (confirmation 004 § point 2).
|
|
52
|
+
*
|
|
53
|
+
* Read when the signal is written, not when the module loads: a `require` at the
|
|
54
|
+
* top would turn a copy of the engine planted without its `package.json` into a
|
|
55
|
+
* MODULE_NOT_FOUND stack trace before a single row ran, and the reader would be
|
|
56
|
+
* told nothing about what is missing (`architecture-principles.md` §5). Same
|
|
57
|
+
* shape, same reason, as `sync/readmeLocation.js` § selfName.
|
|
58
|
+
*
|
|
59
|
+
* @returns {string} the version string
|
|
60
|
+
*/
|
|
61
|
+
function validatorVersion() {
|
|
62
|
+
const pkgPath = path.join(__dirname, '..', '..', 'package.json');
|
|
63
|
+
if (!fs.existsSync(pkgPath)) {
|
|
64
|
+
throw new Error(`[oa-validate] This copy of the engine carries no package.json - ${pkgPath} is missing, `
|
|
65
|
+
+ 'so the deployability signal cannot say which manifest version measured the service. '
|
|
66
|
+
+ 'Fix: run the engine from an installed package or a full checkout of '
|
|
67
|
+
+ 'api/shared/connector/conn-orch-validator.');
|
|
68
|
+
}
|
|
69
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The discovery block of the library uniform — one uniform, one block. */
|
|
73
|
+
const LIBRARY_BLOCK = 'library';
|
|
74
|
+
|
|
75
|
+
/** The one scope whose `where` is read from the package root rather than the workspace. */
|
|
76
|
+
const PACKAGE_RELATIVE_SCOPE = 'service';
|
|
77
|
+
|
|
78
|
+
/** What the category column shows for a package that declares none. */
|
|
79
|
+
const NO_CATEGORY = '-';
|
|
80
|
+
|
|
81
|
+
const USAGE = `
|
|
82
|
+
Usage:
|
|
83
|
+
oa-validate [serviceRoot] [--workspace <root>] [--json]
|
|
84
|
+
oa-validate --workspace <root> [--json]
|
|
85
|
+
oa-validate --library <packageDir> [--workspace <root>] [--json]
|
|
86
|
+
oa-validate --library --all [--workspace <root>] [--json]
|
|
87
|
+
|
|
88
|
+
Checks a repository against the uniform manifest shipped in
|
|
89
|
+
@onlineapps/conn-orch-validator. The manifest version IS this package's
|
|
90
|
+
version, so the pin decides which shape the bearer is measured against.
|
|
91
|
+
|
|
92
|
+
Three modes, decided by what the run is pointed at:
|
|
93
|
+
service — a service root (given, or the current directory) is checked
|
|
94
|
+
against the biz-service uniform. Rows that look at the whole
|
|
95
|
+
workspace report only what lies under that service; a
|
|
96
|
+
neighbour's finding belongs in the neighbour's table.
|
|
97
|
+
workspace — NO service root and --workspace <root>: the workspace-wide rows
|
|
98
|
+
report in full, and every per-service row is printed NOT RUN,
|
|
99
|
+
because there is no repository to look in.
|
|
100
|
+
library — --library: the library uniform instead, over one package
|
|
101
|
+
directory or, with --all, over every package its discovery
|
|
102
|
+
pattern finds.
|
|
103
|
+
|
|
104
|
+
Arguments:
|
|
105
|
+
serviceRoot Biz service repository to check. Default: the current
|
|
106
|
+
directory, unless --workspace is given without a service
|
|
107
|
+
root, which selects the workspace mode above.
|
|
108
|
+
|
|
109
|
+
Options:
|
|
110
|
+
--library [packageDir] Check a shared library against the library uniform instead.
|
|
111
|
+
Give exactly one of: a package directory, or --all.
|
|
112
|
+
--all With --library: every package the uniform's discovery
|
|
113
|
+
pattern finds under the workspace root.
|
|
114
|
+
--workspace <root> Directory holding the api checkout and its siblings
|
|
115
|
+
(api_biz/*), where the manifest's "from" references are
|
|
116
|
+
resolved. Default: the parent of the checkout THIS package
|
|
117
|
+
lies in — the checkout is found from the package's own
|
|
118
|
+
location and carries ${API_MARKER}, whatever it is named.
|
|
119
|
+
No such checkout (an installed copy, a service container)
|
|
120
|
+
→ the rows that need it are reported NOT RUN, never as
|
|
121
|
+
passing.
|
|
122
|
+
--json Print the run as JSON instead of the table.
|
|
123
|
+
--help Print this and exit 0.
|
|
124
|
+
|
|
125
|
+
Output:
|
|
126
|
+
A service run writes <serviceRoot>/ci/deployability.json — the verdict of the
|
|
127
|
+
table above in the shape a deploy gate reads (deployable && complete). The
|
|
128
|
+
workspace and library modes write no such file: the signal is a verdict about
|
|
129
|
+
one service tree, and neither run has one.
|
|
130
|
+
|
|
131
|
+
Exit codes:
|
|
132
|
+
0 no blocking finding (service: boot, deploy | library: publish)
|
|
133
|
+
1 at least one such finding — the service is not deployable, the library is not publishable
|
|
134
|
+
2 the run could not start (bad path, broken manifest, no target), or a service
|
|
135
|
+
run could not record its verdict
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
function parseArgs(argv) {
|
|
139
|
+
const parsed = { serviceRoot: null, workspace: null, json: false, help: false, library: false, all: false };
|
|
140
|
+
const args = [...argv];
|
|
141
|
+
|
|
142
|
+
while (args.length > 0) {
|
|
143
|
+
const token = args.shift();
|
|
144
|
+
if (token === '--help' || token === '-h') {
|
|
145
|
+
parsed.help = true;
|
|
146
|
+
} else if (token === '--json') {
|
|
147
|
+
parsed.json = true;
|
|
148
|
+
} else if (token === '--library') {
|
|
149
|
+
parsed.library = true;
|
|
150
|
+
} else if (token === '--all') {
|
|
151
|
+
parsed.all = true;
|
|
152
|
+
} else if (token === '--workspace') {
|
|
153
|
+
const value = args.shift();
|
|
154
|
+
if (!value || value.startsWith('--')) {
|
|
155
|
+
throw new Error('[oa-validate] Option --workspace has no value - it names the directory holding '
|
|
156
|
+
+ 'api/ and api_biz/. Fix: oa-validate [serviceRoot] --workspace <root>');
|
|
157
|
+
}
|
|
158
|
+
parsed.workspace = value;
|
|
159
|
+
} else if (token.startsWith('--')) {
|
|
160
|
+
throw new Error(`[oa-validate] Unknown option - ${token}. Fix: run oa-validate --help for the options.`);
|
|
161
|
+
} else if (parsed.serviceRoot === null) {
|
|
162
|
+
parsed.serviceRoot = token;
|
|
163
|
+
} else {
|
|
164
|
+
throw new Error(`[oa-validate] Unexpected argument - ${token}. `
|
|
165
|
+
+ 'Fix: exactly one service root is checked per run.');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (parsed.all && !parsed.library) {
|
|
170
|
+
throw new Error('[oa-validate] Option --all needs --library - a service is checked one repository at a time. '
|
|
171
|
+
+ 'Fix: oa-validate --library --all');
|
|
172
|
+
}
|
|
173
|
+
if (parsed.library && parsed.all && parsed.serviceRoot !== null) {
|
|
174
|
+
throw new Error(`[oa-validate] --library --all takes no package directory - got ${parsed.serviceRoot}. `
|
|
175
|
+
+ 'Fix: drop the argument, or run oa-validate --library <packageDir> for that one package.');
|
|
176
|
+
}
|
|
177
|
+
if (parsed.library && !parsed.all && parsed.serviceRoot === null && !parsed.help) {
|
|
178
|
+
throw new Error('[oa-validate] --library needs a target - a package directory, or --all for every package '
|
|
179
|
+
+ 'the uniform finds. Fix: oa-validate --library <packageDir> | oa-validate --library --all');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return parsed;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Which rows report a path relative to the PACKAGE root, by row id.
|
|
187
|
+
*
|
|
188
|
+
* Of the three scopes (`manifestShape.js` § CHECK_SCOPES) only `service` does:
|
|
189
|
+
* a `scope: workspace` or `scope: bearer` check already writes the path from the
|
|
190
|
+
* workspace root, precisely so that two packages failing the same row are told
|
|
191
|
+
* apart (`libraryContext.js` § whereOf). The aggregate run below therefore puts
|
|
192
|
+
* the package back in front of a `service` row's path and leaves the other two
|
|
193
|
+
* alone. Asking the registry rather than listing ids keeps this true when a row
|
|
194
|
+
* changes scope — `L-CONSUMER` and `L-TOOLING` became `bearer` in d.223, and
|
|
195
|
+
* nothing here had to move.
|
|
196
|
+
*
|
|
197
|
+
* @param {object} manifest parsed manifest
|
|
198
|
+
* @returns {Set<string>} the ids whose `where` is package-relative
|
|
199
|
+
*/
|
|
200
|
+
function packageRelativeRowIds(manifest) {
|
|
201
|
+
const ids = new Set();
|
|
202
|
+
for (const { row } of collectRows(manifest)) {
|
|
203
|
+
if (CHECK_REGISTRY[row.check].scope === PACKAGE_RELATIVE_SCOPE) ids.add(row.id);
|
|
204
|
+
}
|
|
205
|
+
return ids;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* One package, checked. `runManifest` already places every finding for this
|
|
210
|
+
* package: a `service` row's path is package-relative, a `bearer` row runs
|
|
211
|
+
* against this package alone, and a `workspace` row's answer is filtered down to
|
|
212
|
+
* what lies under this package's root. Nothing is filtered here.
|
|
213
|
+
*
|
|
214
|
+
* @param {{ manifest: object, packageRoot: string, workspaceRoot: string|null }} params
|
|
215
|
+
* @returns {{ findings: Array<object>, notRun: Array<object> }}
|
|
216
|
+
*/
|
|
217
|
+
function checkOnePackage({ manifest, packageRoot, workspaceRoot }) {
|
|
218
|
+
const result = runManifest({ manifest, serviceRoot: packageRoot, workspaceRoot });
|
|
219
|
+
const category = declaredCategory(readPackage(packageRoot).json) || NO_CATEGORY;
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
findings: result.findings.map((finding) => ({ ...finding, category })),
|
|
223
|
+
notRun: result.notRun
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Every package of the library uniform, checked. Two kinds of run, because the
|
|
229
|
+
* manifest asks two kinds of question:
|
|
230
|
+
*
|
|
231
|
+
* - one run PER PACKAGE answers the per-package rows, and its `scope: service`
|
|
232
|
+
* findings get the package put back in front of their path;
|
|
233
|
+
* - one WHOLE-SET run answers the rows that are about the set — `U-ORPHAN`
|
|
234
|
+
* reports a name in the SSOT with no package on disk, and that finding
|
|
235
|
+
* belongs to no package's root, so no package's run would ever show it.
|
|
236
|
+
*
|
|
237
|
+
* The two overlap on the whole-set rows, which every package's run answers
|
|
238
|
+
* identically; a finding is kept once, keyed by its full identity, so nothing
|
|
239
|
+
* but an exact repetition collapses. The whole-set run goes last so that a
|
|
240
|
+
* finding a package already claimed keeps that package's category.
|
|
241
|
+
*
|
|
242
|
+
* Discovery is the manifest's, never a second walk: the same pattern and the
|
|
243
|
+
* same exclusions that decide who wears the uniform decide who is measured
|
|
244
|
+
* against it.
|
|
245
|
+
*
|
|
246
|
+
* @param {{ manifest: object, workspaceRoot: string }} params
|
|
247
|
+
* @returns {{ findings: Array<object>, notRun: Array<object>, packages: number }}
|
|
248
|
+
*/
|
|
249
|
+
function checkEveryPackage({ manifest, workspaceRoot }) {
|
|
250
|
+
const block = manifest.discovery[LIBRARY_BLOCK];
|
|
251
|
+
const { bearers } = discoverBearers({ block, workspaceRoot });
|
|
252
|
+
const packageRelative = packageRelativeRowIds(manifest);
|
|
253
|
+
|
|
254
|
+
const findings = [];
|
|
255
|
+
const seen = new Set();
|
|
256
|
+
const notRun = [];
|
|
257
|
+
|
|
258
|
+
const keep = (finding) => {
|
|
259
|
+
const key = `${finding.id}|${finding.where}|${finding.what}`;
|
|
260
|
+
if (seen.has(key)) return;
|
|
261
|
+
seen.add(key);
|
|
262
|
+
findings.push(finding);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
for (const bearer of bearers) {
|
|
266
|
+
const one = checkOnePackage({ manifest, packageRoot: bearer.dir, workspaceRoot });
|
|
267
|
+
for (const finding of one.findings) {
|
|
268
|
+
const where = packageRelative.has(finding.id)
|
|
269
|
+
? `${bearer.relativeDir}/${finding.where}`
|
|
270
|
+
: finding.where;
|
|
271
|
+
keep({ ...finding, where });
|
|
272
|
+
}
|
|
273
|
+
for (const skipped of one.notRun) {
|
|
274
|
+
if (!notRun.some((already) => already.id === skipped.id)) notRun.push(skipped);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// The whole-set run has no package root, so its `scope: service` rows are NOT
|
|
279
|
+
// RUN by construction — an artefact of this aggregate, not a gap in coverage
|
|
280
|
+
// (every bearer above ran them). Its `bearer` rows repeat, package by package,
|
|
281
|
+
// exactly what the loop above already collected, and `keep` drops the
|
|
282
|
+
// repetition. Only its findings are taken.
|
|
283
|
+
for (const finding of runManifest({ manifest, workspaceRoot }).findings) {
|
|
284
|
+
keep({ ...finding, category: NO_CATEGORY });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return { findings, notRun, packages: bearers.length };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** The columns of the library table (002 §13.1). */
|
|
291
|
+
const LIBRARY_COLUMNS = Object.freeze(['id', 'category', 'where', 'what', 'fix']);
|
|
292
|
+
|
|
293
|
+
function renderLibraryTable(findings) {
|
|
294
|
+
const widths = LIBRARY_COLUMNS.map((column) => Math.max(
|
|
295
|
+
column.length,
|
|
296
|
+
...findings.map((finding) => String(finding[column]).length)
|
|
297
|
+
));
|
|
298
|
+
const line = (cells) => cells
|
|
299
|
+
.map((cell, index) => (index === cells.length - 1 ? String(cell) : String(cell).padEnd(widths[index])))
|
|
300
|
+
.join(' ')
|
|
301
|
+
.trimEnd();
|
|
302
|
+
|
|
303
|
+
return [line(LIBRARY_COLUMNS), ...findings.map((f) => line(LIBRARY_COLUMNS.map((column) => f[column])))];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* The library report. Which severities block and which two words the verdict is
|
|
308
|
+
* printed with come from the LIBRARY manifest, exactly as the service report
|
|
309
|
+
* takes them from the service one: a library has no boot and no deploy of its
|
|
310
|
+
* own (002 §10), and a second list in this file was the duplicate that made a
|
|
311
|
+
* service verdict offer `publish` (`api/shared/TODO.md` §0.2b-15).
|
|
312
|
+
*
|
|
313
|
+
* @param {object} run what checkOnePackage or checkEveryPackage returned, plus its heading,
|
|
314
|
+
* the uniform's blockingSeverities and its verdict words
|
|
315
|
+
* @returns {string} the whole report, newline-terminated
|
|
316
|
+
*/
|
|
317
|
+
function renderLibraryReport(run) {
|
|
318
|
+
const blocking = blockingOf(run);
|
|
319
|
+
const verdict = verdictOf(run);
|
|
320
|
+
|
|
321
|
+
const lines = [`Library conformance — uniform ${run.uniform}`, ...run.heading, ''];
|
|
322
|
+
|
|
323
|
+
for (const skipped of run.notRun) lines.push(`NOT RUN ${skipped.id} — ${skipped.reason}`);
|
|
324
|
+
if (run.notRun.length > 0) lines.push('');
|
|
325
|
+
|
|
326
|
+
if (run.findings.length > 0) {
|
|
327
|
+
lines.push(...renderLibraryTable(run.findings), '');
|
|
328
|
+
lines.push('Doc:');
|
|
329
|
+
const seen = new Set();
|
|
330
|
+
for (const finding of run.findings) {
|
|
331
|
+
if (seen.has(finding.id)) continue;
|
|
332
|
+
seen.add(finding.id);
|
|
333
|
+
lines.push(` ${finding.id} ${finding.doc}`);
|
|
334
|
+
}
|
|
335
|
+
lines.push('');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (blocking.length > 0) {
|
|
339
|
+
lines.push(`${verdict.blocked} — ${blocking.length} finding(s) of severity `
|
|
340
|
+
+ `${run.blockingSeverities.join('|')}`);
|
|
341
|
+
} else {
|
|
342
|
+
// The same sentence the service report prints, from the same owner: a clean
|
|
343
|
+
// verdict names what did not run (`report.js` § describeClearOutcome).
|
|
344
|
+
lines.push(`${verdict.clear} — ${describeClearOutcome(run)}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return `${lines.join('\n')}\n`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The library modes. Returns the exit code; printing happens here because the
|
|
352
|
+
* library table has its own columns (002 §13.1) and the service report has its
|
|
353
|
+
* own owner.
|
|
354
|
+
*
|
|
355
|
+
* @param {object} options parsed arguments
|
|
356
|
+
* @returns {number} exit code
|
|
357
|
+
*/
|
|
358
|
+
function runLibrary(options) {
|
|
359
|
+
const manifest = loadManifest(LIBRARY_MANIFEST_PATH);
|
|
360
|
+
|
|
361
|
+
if (options.all) {
|
|
362
|
+
const workspaceRoot = resolveWorkspaceRoot({ explicit: options.workspace });
|
|
363
|
+
if (workspaceRoot === null) {
|
|
364
|
+
throw new Error(`[oa-validate] --library --all needs a workspace root - this package lies at ${PACKAGE_ROOT}, `
|
|
365
|
+
+ `which is not shared/connector/<package> of a checkout carrying ${API_MARKER}. `
|
|
366
|
+
+ 'Fix: pass --workspace <root>.');
|
|
367
|
+
}
|
|
368
|
+
const run = checkEveryPackage({ manifest, workspaceRoot });
|
|
369
|
+
const report = {
|
|
370
|
+
uniform: manifest.uniform,
|
|
371
|
+
blockingSeverities: manifest.blocking_severities,
|
|
372
|
+
verdict: manifest.verdict,
|
|
373
|
+
heading: [`Workspace root: ${workspaceRoot}`, `Packages: ${run.packages}`],
|
|
374
|
+
...run
|
|
375
|
+
};
|
|
376
|
+
process.stdout.write(options.json ? `${JSON.stringify(report, null, 2)}\n` : renderLibraryReport(report));
|
|
377
|
+
return blockingOf(report).length > 0 ? 1 : 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const given = path.resolve(options.serviceRoot);
|
|
381
|
+
if (!fs.existsSync(given) || !fs.statSync(given).isDirectory()) {
|
|
382
|
+
throw new Error(`[oa-validate] Package directory not found - ${given}. `
|
|
383
|
+
+ 'Fix: pass an existing package directory.');
|
|
384
|
+
}
|
|
385
|
+
// The root the run REPORTS is the canonical one, the same spelling the
|
|
386
|
+
// workspace root is resolved in — so the heading and the engine's own
|
|
387
|
+
// comparisons speak about one directory (`workspaceRoot.js` § canonicalRoot).
|
|
388
|
+
const packageRoot = canonicalRoot(given);
|
|
389
|
+
|
|
390
|
+
const workspaceRoot = resolveWorkspaceRoot({ explicit: options.workspace });
|
|
391
|
+
const run = checkOnePackage({ manifest, packageRoot, workspaceRoot });
|
|
392
|
+
const report = {
|
|
393
|
+
uniform: manifest.uniform,
|
|
394
|
+
blockingSeverities: manifest.blocking_severities,
|
|
395
|
+
verdict: manifest.verdict,
|
|
396
|
+
heading: [
|
|
397
|
+
`Package root: ${packageRoot}`,
|
|
398
|
+
`Workspace root: ${workspaceRoot === null ? 'NOT RESOLVED' : workspaceRoot}`
|
|
399
|
+
],
|
|
400
|
+
...run
|
|
401
|
+
};
|
|
402
|
+
process.stdout.write(options.json ? `${JSON.stringify(report, null, 2)}\n` : renderLibraryReport(report));
|
|
403
|
+
return blockingOf(report).length > 0 ? 1 : 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function main(argv) {
|
|
407
|
+
const options = parseArgs(argv);
|
|
408
|
+
|
|
409
|
+
if (options.help) {
|
|
410
|
+
process.stdout.write(`${USAGE.trim()}\n`);
|
|
411
|
+
return 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (options.library) return runLibrary(options);
|
|
415
|
+
|
|
416
|
+
const manifest = loadManifest(DEFAULT_MANIFEST_PATH);
|
|
417
|
+
|
|
418
|
+
// `--workspace` with no service root IS the workspace mode (lead decision,
|
|
419
|
+
// api/shared/TODO.md §0.2b-8 point 2): the home of the workspace-wide rows,
|
|
420
|
+
// the run that sees every orphan rather than only this service's own.
|
|
421
|
+
if (options.serviceRoot === null && options.workspace !== null) {
|
|
422
|
+
const workspaceRoot = resolveWorkspaceRoot({ explicit: options.workspace });
|
|
423
|
+
const workspaceResult = runManifest({ manifest, workspaceRoot });
|
|
424
|
+
|
|
425
|
+
process.stdout.write(options.json ? renderJson(workspaceResult) : renderReport(workspaceResult));
|
|
426
|
+
return exitCodeFor(workspaceResult);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const given = path.resolve(options.serviceRoot === null ? process.cwd() : options.serviceRoot);
|
|
430
|
+
if (!fs.existsSync(given) || !fs.statSync(given).isDirectory()) {
|
|
431
|
+
throw new Error(`[oa-validate] Service root not found - ${given}. `
|
|
432
|
+
+ 'Fix: pass an existing repository directory, or run the command inside the service repository.');
|
|
433
|
+
}
|
|
434
|
+
const serviceRoot = canonicalRoot(given);
|
|
435
|
+
|
|
436
|
+
const workspaceRoot = resolveWorkspaceRoot({ explicit: options.workspace });
|
|
437
|
+
const result = runManifest({ manifest, serviceRoot, workspaceRoot });
|
|
438
|
+
|
|
439
|
+
process.stdout.write(options.json ? renderJson(result) : renderReport(result));
|
|
440
|
+
|
|
441
|
+
// Order as in step 7 (`ValidationOrchestrator.js` § validateManifestConformance):
|
|
442
|
+
// the report is printed BEFORE the file is written, so a service root that
|
|
443
|
+
// cannot be written to still leaves the human-readable verdict behind. The
|
|
444
|
+
// write has no switch — the run that produces the verdict is the run that
|
|
445
|
+
// records it (`automation-gates.md` §1 requirement 5) — so an unwritable root
|
|
446
|
+
// ends the run with the writer's own message and the usage exit code, never
|
|
447
|
+
// with an exit 0 whose signal nobody wrote.
|
|
448
|
+
writeDeployabilitySignal({
|
|
449
|
+
serviceRoot,
|
|
450
|
+
signal: buildDeployabilitySignal({ result, validatorVersion: validatorVersion() })
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return exitCodeFor(result);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (require.main === module) {
|
|
457
|
+
try {
|
|
458
|
+
process.exitCode = main(process.argv.slice(2));
|
|
459
|
+
} catch (error) {
|
|
460
|
+
process.stderr.write(`${error.message}\n`);
|
|
461
|
+
process.exitCode = USAGE_EXIT;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
module.exports = {
|
|
466
|
+
main,
|
|
467
|
+
parseArgs,
|
|
468
|
+
runLibrary,
|
|
469
|
+
checkOnePackage,
|
|
470
|
+
checkEveryPackage,
|
|
471
|
+
renderLibraryReport,
|
|
472
|
+
LIBRARY_COLUMNS,
|
|
473
|
+
USAGE_EXIT
|
|
474
|
+
};
|
package/src/helpers/README.md
CHANGED
|
@@ -173,7 +173,8 @@ function createMyTests(testsDir, options = {}) {
|
|
|
173
173
|
console.log(ServiceStructureValidator.formatResult(result));
|
|
174
174
|
|
|
175
175
|
if (!result.valid) {
|
|
176
|
-
throw new Error('
|
|
176
|
+
throw new Error('[createMyTests] Service structure validation failed - Expected the layout '
|
|
177
|
+
+ 'ServiceStructureValidator requires. Fix: correct the errors printed above, then rerun.');
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
// 2. Load config
|
|
@@ -32,6 +32,48 @@ const ServiceReadinessValidator = require('../ServiceReadinessValidator');
|
|
|
32
32
|
const MockRegistry = require('../mocks/MockRegistry');
|
|
33
33
|
const { ServiceStructureValidator } = require('../validators/ServiceStructureValidator');
|
|
34
34
|
const { MIN_COOKBOOK_FORMAT_VERSION } = require('../utils/cookbookFormat');
|
|
35
|
+
const { HANDLER_REF_PATTERN, parseHandlerRef, resolveHandlerModule } = require('../utils/handlerRef');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve every handler an `operations.json` declares, the way the runtime does.
|
|
39
|
+
*
|
|
40
|
+
* The readiness suite used to match the ref against a regex and stop there, so
|
|
41
|
+
* an operation naming a module nobody shipped — or an export that does not
|
|
42
|
+
* exist — passed readiness in CI and failed at boot, where
|
|
43
|
+
* `HandlerRegistry.validate()` resolves every declared operation and fail-fasts
|
|
44
|
+
* (api/docs/biz/10-invocation/handler-dispatch.md § `HandlerRegistry` —
|
|
45
|
+
* resolution rules). The verdict was right in the end; it arrived one
|
|
46
|
+
* environment too late.
|
|
47
|
+
*
|
|
48
|
+
* Fail-fast on the first unusable handler, naming the operation: a service with
|
|
49
|
+
* six operations must not report "something is wrong" and leave the reader to
|
|
50
|
+
* find which.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} serviceRoot - absolute path to the service root (holds `src/`)
|
|
53
|
+
* @param {Object} operationsFlat - the flat `{ name: spec }` map
|
|
54
|
+
* @returns {Array<{operation: string, modulePath: string, exportName: string, handler: Function}>}
|
|
55
|
+
* @throws {Error} naming the operation and the reason its handler is unusable
|
|
56
|
+
*/
|
|
57
|
+
function resolveDeclaredHandlers(serviceRoot, operationsFlat) {
|
|
58
|
+
const resolved = [];
|
|
59
|
+
|
|
60
|
+
for (const [operation, spec] of Object.entries(operationsFlat || {})) {
|
|
61
|
+
try {
|
|
62
|
+
parseHandlerRef(spec && spec.handler);
|
|
63
|
+
const { modulePath, exportName, handler } = resolveHandlerModule({
|
|
64
|
+
serviceRoot,
|
|
65
|
+
handlerRef: spec.handler
|
|
66
|
+
});
|
|
67
|
+
resolved.push({ operation, modulePath, exportName, handler });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`[ServiceReadiness] Operation '${operation}' has an unusable handler - ${error.message}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
35
77
|
|
|
36
78
|
/**
|
|
37
79
|
* Create service readiness integration test suite
|
|
@@ -71,7 +113,8 @@ function createServiceReadinessTests(testsDir, options = {}) {
|
|
|
71
113
|
// FAIL FAST if structure is invalid
|
|
72
114
|
if (!structureResult.valid) {
|
|
73
115
|
throw new Error(
|
|
74
|
-
|
|
116
|
+
'[createServiceReadinessTests] Service structure validation failed - Expected the layout '
|
|
117
|
+
+ 'ServiceStructureValidator requires. Fix: correct the errors it printed above, then rerun.'
|
|
75
118
|
);
|
|
76
119
|
}
|
|
77
120
|
|
|
@@ -188,17 +231,30 @@ function createServiceReadinessTests(testsDir, options = {}) {
|
|
|
188
231
|
expect(Object.keys(operationsFlat).length).toBeGreaterThan(0);
|
|
189
232
|
|
|
190
233
|
const validScopes = ['platform', 'tenant', 'workspace'];
|
|
191
|
-
const handlerPattern = /^handlers\/[a-zA-Z0-9_/-]+#[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
192
234
|
|
|
193
235
|
for (const [, operationSpec] of Object.entries(operationsFlat)) {
|
|
194
236
|
expect(operationSpec).toHaveProperty('handler');
|
|
195
237
|
expect(operationSpec).toHaveProperty('bundle_scope');
|
|
196
|
-
expect(operationSpec.handler).toMatch(
|
|
238
|
+
expect(operationSpec.handler).toMatch(HANDLER_REF_PATTERN);
|
|
197
239
|
expect(validScopes).toContain(operationSpec.bundle_scope);
|
|
198
240
|
expect(operationSpec).not.toHaveProperty('endpoint');
|
|
199
241
|
expect(operationSpec).not.toHaveProperty('method');
|
|
200
242
|
}
|
|
201
243
|
});
|
|
244
|
+
|
|
245
|
+
test('every declared handler resolves to an exported function', () => {
|
|
246
|
+
// The shape of the ref says nothing about whether the module is in the
|
|
247
|
+
// image. Boot resolves every one of them and refuses to start; this test
|
|
248
|
+
// asks the same question one environment earlier, by the same rule.
|
|
249
|
+
const operationsFlat = operations.operations || operations;
|
|
250
|
+
|
|
251
|
+
const resolved = resolveDeclaredHandlers(serviceRoot, operationsFlat);
|
|
252
|
+
|
|
253
|
+
expect(resolved).toHaveLength(Object.keys(operationsFlat).length);
|
|
254
|
+
for (const entry of resolved) {
|
|
255
|
+
expect(typeof entry.handler).toBe('function');
|
|
256
|
+
}
|
|
257
|
+
});
|
|
202
258
|
});
|
|
203
259
|
}
|
|
204
260
|
|
|
@@ -231,4 +287,4 @@ function generateMockInput(inputSchema) {
|
|
|
231
287
|
return mockData;
|
|
232
288
|
}
|
|
233
289
|
|
|
234
|
-
module.exports = { createServiceReadinessTests };
|
|
290
|
+
module.exports = { createServiceReadinessTests, resolveDeclaredHandlers };
|
package/src/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module @onlineapps/conn-orch-validator
|
|
5
5
|
* @description Service validation framework using the operations.json contract.
|
|
6
6
|
*
|
|
7
|
-
* Production entry point: {@link ValidationOrchestrator} (
|
|
7
|
+
* Production entry point: {@link ValidationOrchestrator} (7-step pre-validation
|
|
8
8
|
* driven by operations.json — OpenAPI path iteration is NOT supported).
|
|
9
9
|
*
|
|
10
10
|
* @see /api/docs/biz/30-operations/registration-wire.md §3 (operations.json)
|
|
@@ -27,7 +27,6 @@ const ValidationProofGenerator = require('./validators/ValidationProofGenerator'
|
|
|
27
27
|
// ./ValidationOrchestrator below requires the same module unguarded and took
|
|
28
28
|
// the load down anyway. All it added was a misleading line before the real one.
|
|
29
29
|
const CookbookTestRunner = require('./CookbookTestRunner');
|
|
30
|
-
const WorkflowTestRunner = require('./WorkflowTestRunner');
|
|
31
30
|
const BizCiGateContract = require('./utils/bizCiGateContract');
|
|
32
31
|
|
|
33
32
|
const ServiceReadinessValidator = require('./ServiceReadinessValidator');
|
|
@@ -40,7 +39,6 @@ module.exports = {
|
|
|
40
39
|
get MockRegistry() { return MockRegistry; },
|
|
41
40
|
get MockStorage() { return MockStorage; },
|
|
42
41
|
|
|
43
|
-
get WorkflowTestRunner() { return WorkflowTestRunner; },
|
|
44
42
|
get CookbookTestUtils() { return CookbookTestUtils; },
|
|
45
43
|
get ServiceReadinessValidator() { return ServiceReadinessValidator; },
|
|
46
44
|
get CookbookTestRunner() { return CookbookTestRunner; },
|
|
@@ -52,6 +50,38 @@ module.exports = {
|
|
|
52
50
|
// env, never chosen per test. Enforced by deploy-contract R8.
|
|
53
51
|
get getTestNamespace() { return require('./utils/testNamespace').getTestNamespace; },
|
|
54
52
|
|
|
53
|
+
// The isolation half of the same boundary: a namespace the test must NOT see
|
|
54
|
+
// rows from, taken from the allowed classes rather than from a literal or
|
|
55
|
+
// from `tenant + 1` (99 + 1 = 100 is the LIVE tenant). Accepted by R8 exactly
|
|
56
|
+
// like getTestNamespace().
|
|
57
|
+
get getForeignTestNamespace() { return require('./utils/testNamespace').getForeignTestNamespace; },
|
|
58
|
+
|
|
59
|
+
// The same boundary asked about an id the caller already holds: an
|
|
60
|
+
// operational script is handed a tenant on the command line and must know
|
|
61
|
+
// whether it may touch it at all. Throws the refusal getTestNamespace()
|
|
62
|
+
// throws, rendered from one place with the caller's own name for the value,
|
|
63
|
+
// and returns the id normalized. The allowed classes are NOT exported — a
|
|
64
|
+
// copy of the boundary is a second boundary, and the refusal names them.
|
|
65
|
+
get assertAllowedTenant() { return require('./utils/testNamespace').assertAllowedTenant; },
|
|
66
|
+
|
|
67
|
+
// The throwaway schema an integration suite builds from the service's own
|
|
68
|
+
// declaration: one build for every DB-owning service, sharing every decision
|
|
69
|
+
// with the CI gate's `buildSchema` except the one that differs — the test's
|
|
70
|
+
// schema is dropped and recreated, the service's is never touched. A file
|
|
71
|
+
// that imports it is what deploy-contract R8 permits, in place of the
|
|
72
|
+
// `CREATE DATABASE` text heuristic it used to read.
|
|
73
|
+
get createThrowawaySchema() { return require('./utils/throwawaySchema').createThrowawaySchema; },
|
|
74
|
+
|
|
75
|
+
// What a v3 handler reference is, and what resolving one means — one
|
|
76
|
+
// definition for the package, and the one `service-wrapper`'s HandlerLoader
|
|
77
|
+
// can take over as an internal swap: `resolveHandlerModule` states exactly
|
|
78
|
+
// the rule it implements today (containment, require, export is a function),
|
|
79
|
+
// while the stricter declared form stays in `parseHandlerRef`, so adopting it
|
|
80
|
+
// narrows nothing the wrapper accepts.
|
|
81
|
+
get HANDLER_REF_PATTERN() { return require('./utils/handlerRef').HANDLER_REF_PATTERN; },
|
|
82
|
+
get parseHandlerRef() { return require('./utils/handlerRef').parseHandlerRef; },
|
|
83
|
+
get resolveHandlerModule() { return require('./utils/handlerRef').resolveHandlerModule; },
|
|
84
|
+
|
|
55
85
|
get createServiceReadinessTests() { return createServiceReadinessTests; },
|
|
56
86
|
get BizCiGateContract() { return BizCiGateContract; },
|
|
57
87
|
get IntegrationRun() { return require('./utils/integrationRun'); },
|