@onlineapps/conn-orch-validator 12.0.0 → 12.1.1
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 +149 -0
- package/README.md +47 -0
- package/jest.config.js +11 -0
- package/package.json +1 -1
- package/src/ValidationOrchestrator.js +28 -9
- package/src/cli/biz-ci-gate.js +14 -2
- package/src/manifest/checks/docsLintBridge.js +76 -17
- package/src/manifest/manifestShape.js +15 -4
- package/src/sync/readmeLocation.js +24 -2
- package/src/utils/dbAccountGrants.js +65 -6
- package/src/utils/libCompat.js +69 -22
- package/templates/business-service/init.sh +8 -2
- package/templates/business-service/scripts/verify-deploy-uniform.sh +21 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,155 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [12.1.1] — 2026-09-25
|
|
8
|
+
|
|
9
|
+
### Fixed — `verify-deploy-uniform.sh`: rada při checkoutu mimo `api_biz/` vede na cestu, kterou šablona skutečně deklaruje (d.899)
|
|
10
|
+
|
|
11
|
+
Hláška radila `GIT_CLONE_PATH: $CI_BUILDS_DIR/oa-uniform/api_biz/<service>` bez `$CI_CONCURRENT_ID`,
|
|
12
|
+
zatímco `.oa-uniform` v šabloně `.gitlab-ci.yml` deklaruje `$CI_BUILDS_DIR/oa-uniform/$CI_CONCURRENT_ID/api_biz/<service>`.
|
|
13
|
+
Nově radí „make this job extend .oa-uniform" a cituje cestu šablony; `biz-deploy-uniform-gate.bats` čte
|
|
14
|
+
očekávanou cestu ze šablony, takže se hláška a šablona nemohou rozejít.
|
|
15
|
+
|
|
16
|
+
### Fixed — `verify-deploy-uniform.sh` smaže jen klon, který sám vytvořil; cizí `api/` odmítne a nechá netknutý (d.885)
|
|
17
|
+
|
|
18
|
+
Šablonový skript brány nasazení odvozuje `API_CHECKOUT="<workspace>/api"` a existující adresář před
|
|
19
|
+
klonem maže, aby runner, který znovu používá svůj builds adresář, neměřil proti zastaralému SSOT. Za
|
|
20
|
+
„svůj" ale uznal každý adresář s `.git` a `config/services.json`, a ty nese KAŽDÝ checkout api
|
|
21
|
+
repozitáře. 2026-09-24 skript spustil vývojář z `api_biz/emailer` na pracovní stanici,
|
|
22
|
+
`<workspace>/api` byl skutečný pracovní strom a skript ho smazal (639 sledovaných souborů; obnoveno).
|
|
23
|
+
|
|
24
|
+
Skript teď po vlastním `git clone` zapíše do klonu značku `.verify-deploy-uniform.clone`
|
|
25
|
+
(`created-by verify-deploy-uniform.sh <ISO čas> <pid>`). Mazat smí jen adresář, který nese `.git`,
|
|
26
|
+
`config/services.json` A tu značku. Bez značky končí `die` (exit 2): „exists and carries no marker …
|
|
27
|
+
Expected: a clone this script made. Fix: remove it yourself, or run this script only from a CI
|
|
28
|
+
checkout where <workspace>/api is free" a na adresář nesáhne. Přepínač, který by kontrolu vypnul,
|
|
29
|
+
neexistuje (`automation-gates.md` §1 požadavky 3 a 5). URL `file://` se nezakazuje: ochranu nese
|
|
30
|
+
značka a druhé pravidlo pro tutéž věc by bylo druhou kolejí.
|
|
31
|
+
|
|
32
|
+
RED (`api/tests/scripts/biz-deploy-uniform-gate.bats`, tři nové případy): `not ok 1 an api checkout
|
|
33
|
+
the gate did not make is refused and left exactly as it was` (`[ "$status" -eq 2 ]` failed: skript
|
|
34
|
+
cizí checkout smazal a naklonoval znovu), `not ok 2` a `not ok 3` (značka neexistuje). GREEN: celý
|
|
35
|
+
soubor `1..20`, 20× `ok`, včetně případů se skutečným enginem, které čtou klon se značkou.
|
|
36
|
+
|
|
37
|
+
### Fixed — konfigurační řádek se severitou `deploy` už neshodí boot služby; start blokuje jen `boot` (d.870)
|
|
38
|
+
|
|
39
|
+
Krok 2 boot validace (`validateConfig`, řádky `C-SERVICE`, `C-OPS`, `C-CONTRACT`) soudil start podle
|
|
40
|
+
`blocking_severities` uniformy. Ten seznam je ale **verdikt nasaditelnosti**: `runManifest` z něj počítá
|
|
41
|
+
`ok`, tištěné jako DEPLOYABLE / NOT DEPLOYABLE, a pro službu zní `["boot", "deploy"]`. Krok 7 přitom
|
|
42
|
+
soudil start jen podle `boot`. Konfigurační řádek se severitou `deploy` by tedy odmítl start služby, kterou
|
|
43
|
+
konfirmace `biz-service-manifest` 001 §4 nechává běžet („`deploy` — the service **starts**") a jen ji označí
|
|
44
|
+
za nenasaditelnou. Dnes to nikdo neviděl, protože všechny tři řádky kroku 2 mají `boot`. Chyba by se ukázala
|
|
45
|
+
v den, kdy jednomu z nich manifest dá `deploy`.
|
|
46
|
+
|
|
47
|
+
Otázku „blokuje tento nález start?" teď oba kroky kladou jedné funkci `blocksBoot` nad konstantou
|
|
48
|
+
`BOOT_SEVERITY` (`src/manifest/manifestShape.js`, vedle `SEVERITIES`). `blocking_severities` zůstává tím,
|
|
49
|
+
čím je: seznamem verdiktu uniformy. Tvar výsledku kroků (`valid`, `errors`, `findings`, `deployable`,
|
|
50
|
+
`deployFindings`) se nemění.
|
|
51
|
+
|
|
52
|
+
RED (`tests/unit/ValidationOrchestrator.manifestStep.test.js`): nový případ „a config row of severity deploy
|
|
53
|
+
does not refuse the boot" `Expected: true / Received: false` na `step2.valid`, `1 failed, 15 passed`. Kontrolní
|
|
54
|
+
případ (týž řádek s `boot` → `valid: false` a přesně jedna chybová zpráva) byl zelený před opravou i po ní.
|
|
55
|
+
|
|
56
|
+
### Documentation — čísla kroků a citace zrušené hlášky v docblocích (d.870)
|
|
57
|
+
|
|
58
|
+
- Docblock `runCookbookTests()` říkal „Step 4", ale běh ho loguje jako `Step 5/7`; docblock
|
|
59
|
+
`validateOperations()` říkal „Step 3" (tedy číslo kroku prostředí), ale běh ho loguje jako `Step 4/7`.
|
|
60
|
+
- Odstavec o zrušeném kroku Readiness (a týž komentář v `tests/unit/ValidationOrchestrator.unit.test.js`)
|
|
61
|
+
citoval jako přítomnou hlášku kroku 2 `operations.json has no operations defined`, kterou žádný kód nevydává.
|
|
62
|
+
Teď cituje řádek uniformy `C-OPS`, který tu otázku zodpovídá. Odkaz na řádek nezestárne jako opsaný text.
|
|
63
|
+
|
|
64
|
+
### Fixed — R6 porovná se SSOT KAŽDÝ pin `@onlineapps/*`, i u knihoven, které infra neinstaluje (d.654, d.654b)
|
|
65
|
+
|
|
66
|
+
Zúžení na `infraConsumed` přeskakovalo i porovnání ROVNOSTI pinu se SSOT, ne jen otázku
|
|
67
|
+
kompatibility s infra vydáním. Pravidlo R6 přitom žádnou infra klauzuli nemá: služba pinuje
|
|
68
|
+
přesně to, co SSOT deklaruje (`.claude/rules/architecture-principles.md` § Version pinning).
|
|
69
|
+
`notGated` proto **už není výjimka z porovnání, jen informace** o tom, kterou VĚTU nález
|
|
70
|
+
dostane — infra balíček „biz pins X, infra ships Y", balíček bez infra konzumenta „service
|
|
71
|
+
<jméno> pins X, the platform library SSOT declares Y … Fix: npm install <pkg>@Y --save-exact".
|
|
72
|
+
Zelený řádek navíc říká, kolik pinů se doopravdy porovnalo, aby se coverage brány nedala
|
|
73
|
+
odhadnout z názvu druhého seznamu.
|
|
74
|
+
|
|
75
|
+
**Nahrazuje chování popsané u d.413** („`notGated` zůstává pro deklarovaný balíček, který
|
|
76
|
+
infra neinstaluje" — tam byl `notGated` výjimka z porovnání; od d.654 už není). Položka
|
|
77
|
+
vydané verze zůstává, jak byla: popisuje, co ta verze dělala.
|
|
78
|
+
|
|
79
|
+
Změřeno nad `api_biz/ingest` (13 pinů `@onlineapps/*`, SSOT deklaruje 28 knihoven a
|
|
80
|
+
`infraConsumed` 7) — PŘED (vydaný validátor 12.1.0): `not gated (no infra service installs
|
|
81
|
+
these): …8 balíčků…` + `OK verify-lib-compat — 5 pin(s) match the platform library set`;
|
|
82
|
+
PO: `no infra comparison (no infra service installs these; their SSOT pin is compared all
|
|
83
|
+
the same): …8 balíčků…` + `OK verify-lib-compat — 13 pin(s) match the platform library
|
|
84
|
+
SSOT, 5 of them also compared against the infra library set`. Falzifikace na kopii
|
|
85
|
+
`package.json` s `@onlineapps/service-wrapper` 10.1.2 proti SSOT 10.1.1 (balíček, který
|
|
86
|
+
žádná infra služba neinstaluje): 12.1.0 exit 0 „5 pin(s) match", nový validátor exit 1
|
|
87
|
+
s jmenovaným nálezem a opravou. Nedotčená kopie dál exit 0.
|
|
88
|
+
|
|
89
|
+
## [12.1.0] — 2026-09-18
|
|
90
|
+
|
|
91
|
+
### Fixed — lint, který se odmítl spustit, je NOT RUN, ne šest nálezů o dokumentaci (d.632, W632)
|
|
92
|
+
|
|
93
|
+
Řádky dokumentace (`docs-lint`, `docs-lint-clean` → `D-PORT`, `D-NPM`, `D-SCRIPT`, `D-RETIRED`, `D-HEADER`, `D-LINT`): lint
|
|
94
|
+
`lint-biz-docs`, který skončil bez verdiktu (exit 2, pád spawnu, nerozparsovatelný JSON), je NOT RUN na každém řádku, který
|
|
95
|
+
z něj čte, a signál nese `complete: false`; dosud z toho most dělal šest nálezů `deploy` s radami o obsahu dokumentace a
|
|
96
|
+
`complete: true` (změřeno 2026-09-17, CI joby 16572194080 a 16571115198: GitLab Runner zakládá `api_biz/<svc>.tmp` a lint ho
|
|
97
|
+
bral za službu). Důvod NOT RUN nese CELÝ stderr lintu, první řádek první (poslední řádek byla závorka gitu „Stopping at
|
|
98
|
+
filesystem boundary“); `Fix:` se doplní jen když ho hláška lintu nenese. Chybějící `config/biz-docs-lint.tree.json` zůstává
|
|
99
|
+
nálezem (`C-LINT`). Zároveň v api (`6f9d2f2b`): lint i `sync-biz-facts` berou sourozence ze SSOT `config/services.json`
|
|
100
|
+
(`directory` u `enabled`), ne z výpisu `api_biz/` — nedeklarovaný adresář se nečte a řekne se to jednou větou.
|
|
101
|
+
|
|
102
|
+
### Changed (testy) — třetí živá DB sada přes `liveDatabase.js`; sondy skládají dočasný adresář z `os.tmpdir()` (d.620b, W620b)
|
|
103
|
+
|
|
104
|
+
`bizCiGateCli.setupDbAccount.integration` (blok „how far the production grant reaches“) bere spojení z
|
|
105
|
+
`tests/helpers/liveDatabase.js`, práce je v `tests/helpers/grantReachProbe.js` a sada je třetí v jobu `test-validator-db`;
|
|
106
|
+
`setupDatabaseProbe.js` a `dbAccountProbe.js` už nepíší do doslovného `/tmp`.
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
### Changed (testy) — živé DB sady databázi deklarují, nehledají; „unwritable root“ se pod rootem předvede (d.628, d.623c, W623)
|
|
110
|
+
|
|
111
|
+
`setupDatabaseLive.integration` a `dbCiAccount.integration` čtou spojení z `DB_HOST`, `DB_PORT`, `CI_DB_ROOT_USER`,
|
|
112
|
+
`CI_DB_ROOT_PASSWORD` — jména, která čte `biz-ci-gate setup-db-account` — přes jednoho vlastníka `tests/helpers/liveDatabase.js`;
|
|
113
|
+
dosud každá nesla vlastní kopii s vývojovým kontejnerem `gen_mariadb10.5`, sítí `gendb-network` a heslem z gitignorovaného
|
|
114
|
+
`config/env-active/gen-db.env`, takže v CI checkoutu padaly. Bez deklarace se případy registrují jako přeskočené s vytištěným
|
|
115
|
+
důvodem (runner hlásí „27 skipped“, ne „27 passed“); s deklarací se měří a nedostupný server, chybějící klient i chybějící
|
|
116
|
+
`DB_DOCKER_NETWORK` jsou pád. V CI je pouští nový job `test-validator-db` se službou `mariadb:10.5.29` (vlastník 2026-09-18,
|
|
117
|
+
conf `ci-validator-db-job` 001). Lokálně: README § Running the live-database suites. `oaValidateCli.integration` — případ
|
|
118
|
+
„an unwritable service root fails fast“ se pod uid 0 předvádí pod neprivilegovaným účtem stroje místo NOT RUN.
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
### Changed — šablona `init.sh` staví strom z `package-lock.json` (`npm ci`), ne `npm install` (d.629, W629)
|
|
122
|
+
|
|
123
|
+
`templates/business-service/init.sh` `oa_npm_install()` = `npm ci --no-audit --no-fund`. `npm install --package-lock-only`
|
|
124
|
+
(re-pin v živém bind-mount stromu) přepíše lock i skrytý `node_modules/.package-lock.json`, složky balíčků nechá staré,
|
|
125
|
+
a `npm install` pak hlásí „up to date“ — služba tiše běží na jiné verzi, než pinuje lock a R6 (nález INFRA-monitoring,
|
|
126
|
+
INFRA B137 `28fef3dc` totéž v sedmi infra službách). Podmínka instalace (blok `oa-deps-guard v1`, marker
|
|
127
|
+
`.oa_drive_deps_hash`) ani znění bloku se nemění. Změřeno bez sítě nad lokálním tarballem: složka 1.0.0 × lock 2.0.0 →
|
|
128
|
+
`npm install` „up to date“ (1.0.0), `npm ci` „added 1 package“ (2.0.0). Řádek uniformy `F-INIT` roznáší jen blok, servisní
|
|
129
|
+
`oa_npm_install()` je řádek každé služby — běžících osm se nemění, dostanou ho pokynem. Služba oskládaná ze šablony
|
|
130
|
+
potřebuje `package-lock.json` už při prvním startu: šablona ho neveze, první boot bez něj skončí hlášeným `EUSAGE`.
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
### Fixed — hlavní grant databázového účtu escapuje `_` (d.620, W620)
|
|
134
|
+
|
|
135
|
+
`databaseAccountSql` escapuje `_` i v hlavním grantu (`` ON `oagen\_x`.* ``), ne jen v CI wildcardu: databázová část
|
|
136
|
+
`GRANT`u je LIKE vzor i bez `%`, takže grant, který dostane každá nasazená služba, otevíral i sousední schéma lišící se
|
|
137
|
+
v tom jednom znaku (`oagen_emailer` → `oagen5emailer`; nález INFRA-DOCS z falzifikace `INSTALL.md`). Escape je jedna
|
|
138
|
+
funkce `escapeSchemaPattern()` použitá na obě věty; `%` ve jméně schématu se odmítá s `Fix:` (žádné platformní jméno ho
|
|
139
|
+
nenese, `\` odmítal už `FORBIDDEN_IN_VALUE`). Změřeno proti živé MariaDB 10.5: před opravou účet přečetl sousedovy řádky,
|
|
140
|
+
po opravě `ERROR 1142 (42000) … SELECT command denied`; vlastní schéma dál čitelné, CI throwaway grant (`oagen\_x\_%`) drží.
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
### Fixed — pozice balíčku ve workspace nese konvenční prefix `api/`, ne jméno adresáře checkoutu (d.625, W625)
|
|
144
|
+
|
|
145
|
+
`sync/readmeLocation.js` odvozovalo `PACKAGE_IN_WORKSPACE` (a s ním `requiresSiblings` řádku `L-README-REGION`) z adresáře
|
|
146
|
+
NAD checkoutem. GitLab CI klonuje repozitář pod jménem projektu `infra-mono`, takže vznikalo
|
|
147
|
+
`infra-mono/shared/connector/conn-orch-validator`; `resolveWorkspacePath()` rozřeší přes marker jen hlavu `api` a zbytek
|
|
148
|
+
připojí doslova → řetězec neukazoval nikam a běh končil větou `Fix: run with --workspace pointing at a checkout that carries
|
|
149
|
+
infra-mono/…`, kterou nikdo nemůže splnit (`automation-gates.md` §1/4). Pravidlo stojí v hlavičce `manifest/workspaceRoot.js`:
|
|
150
|
+
`api/` na začátku cesty je konvence deklarujícího textu, nikdy jméno adresáře na disku — prefix se proto čte z
|
|
151
|
+
`WORKSPACE_MARKER` (jediný vlastník), ne píše podruhé. Změřeno v obrazu jobu nad jedním stromem pod dvěma jmény: 27 unit
|
|
152
|
+
testů a 6 bats červených pod `infra-mono`, zelené pod `api`; po opravě zelené pod oběma. Štítek souboru na stdout
|
|
153
|
+
`oa-sync-template` dál nese cestu na disku (tam soubor je) — srovnán test, ne výstup.
|
|
154
|
+
|
|
155
|
+
|
|
7
156
|
## [12.0.0] — 2026-09-17
|
|
8
157
|
|
|
9
158
|
### Fixed — BREAKING: R2 tvrdí pravidlo sekvence nasazení, ne včerejší text (d.614, W614)
|
package/README.md
CHANGED
|
@@ -269,6 +269,53 @@ namespace an integration tier builds into (`src/utils/throwawaySchema.js`). The
|
|
|
269
269
|
`_` is escaped, so the grant stays inside one service — unescaped it is a LIKE
|
|
270
270
|
wildcard, and `oagen_meta_%` would also match `oagen_metadata`.
|
|
271
271
|
|
|
272
|
+
**The escape belongs to BOTH grants.** The database-name position of a `GRANT`
|
|
273
|
+
is a LIKE pattern whether or not a `%` follows it, so the grant every installed
|
|
274
|
+
service gets is written `` `oagen\_meta`.* `` too: unescaped, it also grants
|
|
275
|
+
`oagen5meta` and every other schema differing in that one character. `%` in a
|
|
276
|
+
schema name is refused rather than escaped — no platform name carries one.
|
|
277
|
+
|
|
278
|
+
### Running the live-database suites
|
|
279
|
+
|
|
280
|
+
Two suites of this package measure what a **real** MariaDB does with the SQL the
|
|
281
|
+
package builds — `tests/unit/setupDatabaseLive.integration.test.js` (what
|
|
282
|
+
`ci:gate:setup` leaves in the schema) and `tests/unit/dbCiAccount.integration.test.js`
|
|
283
|
+
(whether the grants above are enough, and still not too much). They are part of
|
|
284
|
+
`npm run test:unit`, and in CI they have a job with a database of its own,
|
|
285
|
+
`test-validator-db` (owner decision
|
|
286
|
+
[`ci-validator-db-job` 001](../../../docs/governance/confirmations/ci-validator-db-job.md)).
|
|
287
|
+
|
|
288
|
+
Neither suite looks for a database. It is **declared**, under the names this
|
|
289
|
+
package's own CLI reads:
|
|
290
|
+
|
|
291
|
+
| Variable | What it is |
|
|
292
|
+
|---|---|
|
|
293
|
+
| `DB_HOST` / `DB_PORT` | the server the run measures |
|
|
294
|
+
| `CI_DB_ROOT_USER` / `CI_DB_ROOT_PASSWORD` | its administrative login — the suites create and drop schemas of their own |
|
|
295
|
+
| `DB_DOCKER_NETWORK` | only where no `mariadb` client is on PATH: the docker network the probe is carried in on |
|
|
296
|
+
|
|
297
|
+
Declare nothing and the suites report **NOT RUN** with that sentence and a `Fix:`
|
|
298
|
+
— they never stand down quietly. Declare a database and it is measured: an
|
|
299
|
+
unreachable server, a missing client and a missing network are failures, so a run
|
|
300
|
+
cannot go green having looked at nothing.
|
|
301
|
+
|
|
302
|
+
On a developer machine the dev server publishes no port and macOS carries no
|
|
303
|
+
client, so both the network and the credential come from the local environment —
|
|
304
|
+
the password is read out of the gitignored file that owns it, never written into
|
|
305
|
+
the repository:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
cd api/shared/connector/conn-orch-validator
|
|
309
|
+
DB_HOST=gen_mariadb10.5 DB_PORT=3306 \
|
|
310
|
+
CI_DB_ROOT_USER=root \
|
|
311
|
+
CI_DB_ROOT_PASSWORD="$(grep '^MARIADB_ROOT_PASSWORD=' ../../../config/env-active/gen-db.env | cut -d= -f2-)" \
|
|
312
|
+
DB_DOCKER_NETWORK=gendb-network \
|
|
313
|
+
npx jest tests/unit/setupDatabaseLive.integration.test.js tests/unit/dbCiAccount.integration.test.js
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
The run prints which server it measured, or why it measured none — that line is
|
|
317
|
+
what tells a green suite that looked from a green suite that did not.
|
|
318
|
+
|
|
272
319
|
---
|
|
273
320
|
|
|
274
321
|
## Environment contract (`env` block)
|
package/jest.config.js
CHANGED
|
@@ -13,6 +13,17 @@ module.exports = {
|
|
|
13
13
|
testMatch: [
|
|
14
14
|
'**/tests/**/*.test.js'
|
|
15
15
|
],
|
|
16
|
+
|
|
17
|
+
// tests/fixtures/manifest/** holds SIMULATED CHECKOUTS that the manifest
|
|
18
|
+
// suites read from disk as data, never require. Jest's haste map indexes every
|
|
19
|
+
// package.json it crawls, so the three fixture workspaces that each carry the
|
|
20
|
+
// service `biz-alpha` read as three modules of one name and the crawl prints
|
|
21
|
+
// "Haste module naming collision" on stdout. The reason the fixtures keep
|
|
22
|
+
// their names, and why the pattern stops at `manifest/` rather than covering
|
|
23
|
+
// tests/fixtures/ whole, is written once — in the repository's jest.config.js,
|
|
24
|
+
// beside the same string. This config owns the same exclusion for its own map,
|
|
25
|
+
// because rootDir here is the package.
|
|
26
|
+
modulePathIgnorePatterns: ['/tests/fixtures/manifest/'],
|
|
16
27
|
coverageThreshold: {
|
|
17
28
|
global: {
|
|
18
29
|
branches: 80,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/conn-orch-validator",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.1.1",
|
|
4
4
|
"description": "Validation orchestrator for OA Drive microservices - coordinates validation across all layers (base, infra, orch, business)",
|
|
5
5
|
"oa": {
|
|
6
6
|
"category": "orchestration"
|
|
@@ -24,6 +24,21 @@ const { loadManifest, DEFAULT_MANIFEST_PATH } = require('./manifest/loadManifest
|
|
|
24
24
|
const { runManifest } = require('./manifest/runManifest');
|
|
25
25
|
const { resolveWorkspaceRoot } = require('./manifest/workspaceRoot');
|
|
26
26
|
const { renderBanner, describeFinding } = require('./manifest/report');
|
|
27
|
+
const { BOOT_SEVERITY } = require('./manifest/manifestShape');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether a finding refuses the service's start — the one question steps 2 and
|
|
31
|
+
* 7 both ask, so they ask it here (`.claude/rules/change-discipline.md` § One
|
|
32
|
+
* rail per concern). Until d.870 step 2 answered it with the uniform's
|
|
33
|
+
* `blocking_severities`, which is the deployability verdict and names `deploy`
|
|
34
|
+
* as well, while step 7 answered `boot` alone: a config row given `deploy`
|
|
35
|
+
* would have refused a start that confirmation `biz-service-manifest` 001 §4
|
|
36
|
+
* says goes ahead.
|
|
37
|
+
*
|
|
38
|
+
* @param {{severity: string}} finding
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
const blocksBoot = (finding) => finding.severity === BOOT_SEVERITY;
|
|
27
42
|
const { buildDeployabilitySignal, writeDeployabilitySignal } = require('./manifest/deployabilitySignal');
|
|
28
43
|
const { isGitCheckout, NOT_A_CHECKOUT } = require('./manifest/gitCheckout');
|
|
29
44
|
|
|
@@ -392,8 +407,8 @@ class ValidationOrchestrator {
|
|
|
392
407
|
* (handler presence and shape, bundle_scope, input, output, the retired
|
|
393
408
|
* v2 fields) — the two loops were textual copies of one another.
|
|
394
409
|
* - Its only two extra guards, "operations must be an object" and "no
|
|
395
|
-
* operations defined", are step 2
|
|
396
|
-
*
|
|
410
|
+
* operations defined", are answered by step 2 through the uniform row
|
|
411
|
+
* `C-OPS`.
|
|
397
412
|
* - Run against a valid baseline plus 13 mutations covering every rule,
|
|
398
413
|
* its verdict equalled `step2 && step3` in all 14 cases. Against the
|
|
399
414
|
* eight live biz services it emitted one check, scored 80 out of a
|
|
@@ -651,9 +666,13 @@ class ValidationOrchestrator {
|
|
|
651
666
|
* leaves them out, because they are answered, not because they stopped
|
|
652
667
|
* mattering.
|
|
653
668
|
*
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
*
|
|
669
|
+
* Only a `boot` finding fails the start (`blocksBoot`, the same question
|
|
670
|
+
* step 7 asks). The uniform's `blocking_severities` is a different list: the
|
|
671
|
+
* deployability verdict, `["boot", "deploy"]` for a service — a `deploy` row
|
|
672
|
+
* here is reported, and the service starts undeployable (confirmation
|
|
673
|
+
* `biz-service-manifest` 001 §4; d.870). This step only ever runs the service
|
|
674
|
+
* uniform (`manifestRun()` loads `DEFAULT_MANIFEST_PATH`), so no library
|
|
675
|
+
* severity reaches it.
|
|
657
676
|
*
|
|
658
677
|
* @returns {{valid: boolean, errors: string[], findings: object[]}}
|
|
659
678
|
*/
|
|
@@ -661,7 +680,7 @@ class ValidationOrchestrator {
|
|
|
661
680
|
try {
|
|
662
681
|
const run = this.manifestRun();
|
|
663
682
|
const findings = run.findings.filter((finding) => CONFIG_STEP_ROWS.includes(finding.id));
|
|
664
|
-
const blocking = findings.filter(
|
|
683
|
+
const blocking = findings.filter(blocksBoot);
|
|
665
684
|
|
|
666
685
|
this.logger.info(`[ValidationOrchestrator] ✓ Config files: ${blocking.length === 0 ? 'PASS' : 'FAIL'}`);
|
|
667
686
|
return {
|
|
@@ -701,7 +720,7 @@ class ValidationOrchestrator {
|
|
|
701
720
|
}
|
|
702
721
|
|
|
703
722
|
/**
|
|
704
|
-
* Step
|
|
723
|
+
* Step 4: Validate operations compliance (v3 — handler registry dispatch).
|
|
705
724
|
* Required per operation: handler ('handlers/<path>#<export>'), bundle_scope, input, output.
|
|
706
725
|
* Forbidden (v2): endpoint, method, path.
|
|
707
726
|
* Warned about: a missing `description` — inherited from the removed
|
|
@@ -767,7 +786,7 @@ class ValidationOrchestrator {
|
|
|
767
786
|
}
|
|
768
787
|
|
|
769
788
|
/**
|
|
770
|
-
* Step
|
|
789
|
+
* Step 5: Run cookbook tests via CookbookTestRunner (v3 handler dispatch).
|
|
771
790
|
* Operations must declare a `handler` in operations.json; the runner
|
|
772
791
|
* loads the module via `require()` and calls the exported function
|
|
773
792
|
* in-process. Failures fail this validation step.
|
|
@@ -1003,7 +1022,7 @@ class ValidationOrchestrator {
|
|
|
1003
1022
|
});
|
|
1004
1023
|
|
|
1005
1024
|
const findings = result.findings.filter((finding) => !CONFIG_STEP_ROWS.includes(finding.id));
|
|
1006
|
-
const bootFindings = findings.filter(
|
|
1025
|
+
const bootFindings = findings.filter(blocksBoot);
|
|
1007
1026
|
|
|
1008
1027
|
return {
|
|
1009
1028
|
valid: bootFindings.length === 0,
|
package/src/cli/biz-ci-gate.js
CHANGED
|
@@ -467,8 +467,13 @@ async function runVerifyLibCompat(options) {
|
|
|
467
467
|
const librarySet = await loadLibrarySet(options.libraries);
|
|
468
468
|
const result = checkLibCompat(pkg, librarySet);
|
|
469
469
|
|
|
470
|
+
// What is skipped for these is the INFRA comparison, and nothing else: since
|
|
471
|
+
// d.654 their pin is held against the SSOT like every other. The line used to
|
|
472
|
+
// read "not gated", which after d.654 told the reader the opposite of what
|
|
473
|
+
// the run did.
|
|
470
474
|
if (result.notGated.length > 0) {
|
|
471
|
-
process.stdout.write(
|
|
475
|
+
process.stdout.write('[BizCiGate] no infra comparison (no infra service installs these; their SSOT pin '
|
|
476
|
+
+ `is compared all the same): ${result.notGated.join(', ')}\n`);
|
|
472
477
|
}
|
|
473
478
|
if (!result.ok) {
|
|
474
479
|
for (const violation of result.violations) {
|
|
@@ -476,7 +481,14 @@ async function runVerifyLibCompat(options) {
|
|
|
476
481
|
}
|
|
477
482
|
process.exit(1);
|
|
478
483
|
}
|
|
479
|
-
|
|
484
|
+
// Two numbers, because the run answers two questions and a single count
|
|
485
|
+
// could only ever be true of one of them: how many pins were held against the
|
|
486
|
+
// SSOT (all of them, since d.654), and how many of those infra also installs,
|
|
487
|
+
// which is the compatibility half. Both come from the rule module's own
|
|
488
|
+
// lists, never counted again here (`.claude/rules/automation-gates.md` §5 —
|
|
489
|
+
// a coverage sentence smaller than the coverage is a false signal).
|
|
490
|
+
process.stdout.write(`[BizCiGate] OK verify-lib-compat — ${result.compared.length} pin(s) match the platform `
|
|
491
|
+
+ `library SSOT, ${result.gated.length} of them also compared against the infra library set\n`);
|
|
480
492
|
}
|
|
481
493
|
|
|
482
494
|
async function runVerifyContract(options) {
|
|
@@ -107,6 +107,50 @@ const LINT_TIMEOUT_MS = 120000;
|
|
|
107
107
|
/** Room for the JSON of a large tree; the biggest measured today is 54 findings. */
|
|
108
108
|
const LINT_MAX_BUFFER = 32 * 1024 * 1024;
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* How much of the linter's own output the refusal sentence carries, in
|
|
112
|
+
* characters. A budget, not a line count: what makes a refusal actionable is the
|
|
113
|
+
* FIRST line, which is the linter's `[tool] ERROR:` message with its own `Fix:`
|
|
114
|
+
* in it, and the lines after it are context of falling value. So the first line
|
|
115
|
+
* is never cut and the rest fills what is left.
|
|
116
|
+
*/
|
|
117
|
+
const REFUSAL_BUDGET = 700;
|
|
118
|
+
|
|
119
|
+
/** The remedy for a refusal whose own message named none. */
|
|
120
|
+
const REFUSAL_FIX = `Fix: run ${LINT_SCRIPT} over this tree by hand and repair what it reports.`;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* What a run that produced no verdict says, from everything the linter printed.
|
|
124
|
+
*
|
|
125
|
+
* Until d.632 this read `…split('\n').filter(Boolean).pop()` — the LAST line —
|
|
126
|
+
* and the last line of the measured failure was git's own aside, `Stopping at
|
|
127
|
+
* filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set)`. The sentence
|
|
128
|
+
* that says WHAT the linter refused, and how to repair it, is the first one, so
|
|
129
|
+
* the reader was handed the footnote and not the message (CI jobs 16572194080,
|
|
130
|
+
* 16571115198; `.claude/rules/automation-gates.md` §1 requirement 4).
|
|
131
|
+
*
|
|
132
|
+
* @param {{status: number|null, stderr: string, stdout: string}} run the finished process
|
|
133
|
+
* @returns {string}
|
|
134
|
+
*/
|
|
135
|
+
function describeRefusal({ status, stderr, stdout }) {
|
|
136
|
+
const lines = String(stderr || stdout || '').split('\n').map((line) => line.trim()).filter(Boolean);
|
|
137
|
+
const exited = status === null || status === undefined ? 'on a signal' : String(status);
|
|
138
|
+
if (lines.length === 0) return `the lint exited ${exited} without a verdict and printed nothing. ${REFUSAL_FIX}`;
|
|
139
|
+
|
|
140
|
+
let said = lines[0];
|
|
141
|
+
for (const line of lines.slice(1)) {
|
|
142
|
+
if (said.length + line.length + 3 > REFUSAL_BUDGET) {
|
|
143
|
+
said += ' …';
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
said += ` | ${line}`;
|
|
147
|
+
}
|
|
148
|
+
// The linter writes its own `Fix:` into the message; a second one beside it
|
|
149
|
+
// would be this module restating the tool (docblock at the top of this file).
|
|
150
|
+
// Only a message that named none gets the command.
|
|
151
|
+
return `the lint exited ${exited} without a verdict — ${said}${said.includes('Fix:') ? '' : `. ${REFUSAL_FIX}`}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
110
154
|
/**
|
|
111
155
|
* The lint's answers, keyed by what the run was about. One service is linted
|
|
112
156
|
* ONCE however many rows cite it: five rows spawning five processes over the
|
|
@@ -296,7 +340,7 @@ function claimantOf(ruleId, rows) {
|
|
|
296
340
|
* Run the lint over one tree, once.
|
|
297
341
|
*
|
|
298
342
|
* @param {{ lintScript: string, docsDir: string, treeConfig: string }} params absolute paths
|
|
299
|
-
* @returns {{findings: Array<object>, skipped: Array<object>}|{
|
|
343
|
+
* @returns {{findings: Array<object>, skipped: Array<object>}|{refusal: string}}
|
|
300
344
|
*/
|
|
301
345
|
function lintOnce({ lintScript, docsDir, treeConfig }) {
|
|
302
346
|
// `\0` as the escape, never the raw byte: written literally it makes the
|
|
@@ -313,7 +357,7 @@ function lintOnce({ lintScript, docsDir, treeConfig }) {
|
|
|
313
357
|
|
|
314
358
|
/**
|
|
315
359
|
* @param {{ lintScript: string, docsDir: string, treeConfig: string }} params
|
|
316
|
-
* @returns {{findings: Array<object>, skipped: Array<object>}|{
|
|
360
|
+
* @returns {{findings: Array<object>, skipped: Array<object>}|{refusal: string}}
|
|
317
361
|
*/
|
|
318
362
|
function runLint({ lintScript, docsDir, treeConfig }) {
|
|
319
363
|
const run = spawnSync(process.execPath, [lintScript, ...lintArgumentsFor({ docsDir, treeConfig })], {
|
|
@@ -326,23 +370,20 @@ function runLint({ lintScript, docsDir, treeConfig }) {
|
|
|
326
370
|
maxBuffer: LINT_MAX_BUFFER
|
|
327
371
|
});
|
|
328
372
|
|
|
329
|
-
if (run.error) return {
|
|
373
|
+
if (run.error) return { refusal: `the lint could not be started: ${run.error.message} (${lintScript}). ${REFUSAL_FIX}` };
|
|
330
374
|
|
|
331
375
|
// 0 = clean, 1 = findings; anything else is the lint refusing to run, and its
|
|
332
376
|
// reason is on stderr. A run that ended there is NOT an empty finding list.
|
|
333
|
-
if (run.status !== 0 && run.status !== 1) {
|
|
334
|
-
const said = String(run.stderr || run.stdout || '').trim().split('\n').filter(Boolean).pop();
|
|
335
|
-
return { problem: `the lint exited ${run.status === null ? 'on a signal' : run.status}: ${said || 'no output'}` };
|
|
336
|
-
}
|
|
377
|
+
if (run.status !== 0 && run.status !== 1) return { refusal: describeRefusal(run) };
|
|
337
378
|
|
|
338
379
|
let parsed;
|
|
339
380
|
try {
|
|
340
381
|
parsed = JSON.parse(run.stdout);
|
|
341
382
|
} catch (error) {
|
|
342
|
-
return {
|
|
383
|
+
return { refusal: `the lint exited ${run.status} and its JSON output could not be read: ${error.message}. ${REFUSAL_FIX}` };
|
|
343
384
|
}
|
|
344
385
|
if (!Array.isArray(parsed.findings) || !Array.isArray(parsed.skipped)) {
|
|
345
|
-
return {
|
|
386
|
+
return { refusal: `the lint exited ${run.status} and its JSON output carries no "findings"/"skipped" arrays. ${REFUSAL_FIX}` };
|
|
346
387
|
}
|
|
347
388
|
return { findings: parsed.findings, skipped: parsed.skipped };
|
|
348
389
|
}
|
|
@@ -408,13 +449,23 @@ function treeConfigOf(block) {
|
|
|
408
449
|
/**
|
|
409
450
|
* Ask the lint about one service tree, once, for whichever row is asking.
|
|
410
451
|
*
|
|
411
|
-
*
|
|
412
|
-
* nothing to lint, the question could not be decided
|
|
413
|
-
* lint's own
|
|
452
|
+
* Four answers, and every caller words them in its own sentence: the tree has
|
|
453
|
+
* nothing to lint, the question could not be decided because a file with a fix
|
|
454
|
+
* of its own is missing, the lint REFUSED to run at all, or the lint's own
|
|
455
|
+
* payload.
|
|
456
|
+
*
|
|
457
|
+
* The last two were one answer until d.632, and both became findings. They are
|
|
458
|
+
* different facts. An absent tree config is a defect of this repository with a
|
|
459
|
+
* row that owns it (`C-LINT`) and a repair somebody can carry out here, so it
|
|
460
|
+
* stays a finding. A lint that refused to run measured NOTHING: it decided no
|
|
461
|
+
* rule, so a row reporting a `deploy` finding about documentation is asserting
|
|
462
|
+
* something nobody looked at — six of them, in the run this split comes from.
|
|
463
|
+
* That is NOT RUN (`.claude/rules/automation-gates.md` §5).
|
|
414
464
|
*
|
|
415
465
|
* @param {{ block: object, serviceRoot: string, workspaceRoot: string }} params
|
|
416
466
|
* @returns {{nothingToLint: true}
|
|
417
467
|
* |{undecidable: {relative: string, reason: string}}
|
|
468
|
+
* |{refused: string}
|
|
418
469
|
* |{answer: {findings: Array<object>, skipped: Array<object>}}}
|
|
419
470
|
*/
|
|
420
471
|
function askLint({ block, serviceRoot, workspaceRoot }) {
|
|
@@ -442,7 +493,7 @@ function askLint({ block, serviceRoot, workspaceRoot }) {
|
|
|
442
493
|
docsDir,
|
|
443
494
|
treeConfig
|
|
444
495
|
});
|
|
445
|
-
if (answer.
|
|
496
|
+
if (answer.refusal !== undefined) return { refused: answer.refusal };
|
|
446
497
|
|
|
447
498
|
return { answer };
|
|
448
499
|
}
|
|
@@ -475,7 +526,7 @@ const docsLint = Object.freeze({
|
|
|
475
526
|
|
|
476
527
|
/**
|
|
477
528
|
* @param {{ row: object, block: object, serviceRoot: string, workspaceRoot: string }} params
|
|
478
|
-
* @returns {Array<{where: string, what: string}
|
|
529
|
+
* @returns {Array<{where: string, what: string}>|{findings: Array<object>, notRun: string}}
|
|
479
530
|
*/
|
|
480
531
|
run({ row, block, serviceRoot, workspaceRoot }) {
|
|
481
532
|
const rows = rowsOf(block);
|
|
@@ -490,6 +541,9 @@ const docsLint = Object.freeze({
|
|
|
490
541
|
what: `${row.rule} could not be decided — ${asked.undecidable.reason}`
|
|
491
542
|
}];
|
|
492
543
|
}
|
|
544
|
+
if (asked.refused !== undefined) {
|
|
545
|
+
return { findings: [], notRun: `${row.rule} could not be decided — ${asked.refused}` };
|
|
546
|
+
}
|
|
493
547
|
const answer = asked.answer;
|
|
494
548
|
|
|
495
549
|
const found = answer.findings
|
|
@@ -511,9 +565,10 @@ const docsLint = Object.freeze({
|
|
|
511
565
|
// The mapping is the linter's own two channels onto this uniform's two: its
|
|
512
566
|
// `findings` are findings, its `skipped` list is the NOT RUN one (that is
|
|
513
567
|
// what `--allow-missing-siblings` fills, and what its own text output prints
|
|
514
|
-
// as NOT RUN lines). A tree
|
|
515
|
-
//
|
|
516
|
-
//
|
|
568
|
+
// as NOT RUN lines). A tree whose lint REFUSED to run is NOT RUN too, for
|
|
569
|
+
// the same reason and one branch above (`askLint` § refused); the absent
|
|
570
|
+
// tree config before it stays a finding, because `C-LINT` owns that file
|
|
571
|
+
// and the repair is here.
|
|
517
572
|
//
|
|
518
573
|
// The sentence itself is `describeUndecided` above — shared with `D-LINT`,
|
|
519
574
|
// which reports the same absence about the rules no row claims.
|
|
@@ -579,6 +634,9 @@ const docsLintClean = Object.freeze({
|
|
|
579
634
|
what: `no finding could be decided — ${asked.undecidable.reason}`
|
|
580
635
|
}];
|
|
581
636
|
}
|
|
637
|
+
if (asked.refused !== undefined) {
|
|
638
|
+
return { findings: [], notRun: `no finding could be decided — ${asked.refused}` };
|
|
639
|
+
}
|
|
582
640
|
const answer = asked.answer;
|
|
583
641
|
|
|
584
642
|
const found = errorGraded(answer.findings)
|
|
@@ -605,6 +663,7 @@ module.exports = {
|
|
|
605
663
|
claimantOf,
|
|
606
664
|
citationsOf,
|
|
607
665
|
covers,
|
|
666
|
+
describeRefusal,
|
|
608
667
|
errorGraded,
|
|
609
668
|
LINT_SCRIPT,
|
|
610
669
|
LINT_DIR
|
|
@@ -21,16 +21,26 @@
|
|
|
21
21
|
|
|
22
22
|
const { walkManifest, collectRows, isPlainObject } = require('./walk');
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The one severity that refuses a service's start (001 §4). Every other
|
|
26
|
+
* consequence leaves the service running: `deploy` makes it undeployable,
|
|
27
|
+
* `publish` belongs to a library, which has no start at all, and `warn` stops
|
|
28
|
+
* nothing. It is NOT the uniform's `blocking_severities` — that list is the
|
|
29
|
+
* VERDICT (DEPLOYABLE / PUBLISHABLE, `runManifest` turns it into `ok`), and for
|
|
30
|
+
* a service it names `deploy` too (d.870).
|
|
31
|
+
*/
|
|
32
|
+
const BOOT_SEVERITY = 'boot';
|
|
33
|
+
|
|
34
|
+
/** The one severity that stops nothing: it is printed in the table and nowhere else. */
|
|
35
|
+
const WARN_SEVERITY = 'warn';
|
|
36
|
+
|
|
24
37
|
/**
|
|
25
38
|
* The consequences a row may carry, and no others. `boot` and `deploy` are the
|
|
26
39
|
* service uniform's (001 §4); `publish` is the library uniform's — a library has
|
|
27
40
|
* no boot, and the choke point nothing reaches a service past is
|
|
28
41
|
* `scripts/publish-library.sh` (002 §10).
|
|
29
42
|
*/
|
|
30
|
-
const SEVERITIES = Object.freeze([
|
|
31
|
-
|
|
32
|
-
/** The one severity that stops nothing: it is printed in the table and nowhere else. */
|
|
33
|
-
const WARN_SEVERITY = 'warn';
|
|
43
|
+
const SEVERITIES = Object.freeze([BOOT_SEVERITY, 'deploy', 'publish', WARN_SEVERITY]);
|
|
34
44
|
|
|
35
45
|
/** Which severities a uniform may declare as blocking — every consequence except `warn`. */
|
|
36
46
|
const BLOCKING_CANDIDATES = Object.freeze(SEVERITIES.filter((severity) => severity !== WARN_SEVERITY));
|
|
@@ -435,6 +445,7 @@ module.exports = {
|
|
|
435
445
|
rowNeedsWorkspace,
|
|
436
446
|
describeFromProblem,
|
|
437
447
|
SEVERITIES,
|
|
448
|
+
BOOT_SEVERITY,
|
|
438
449
|
CHECK_SCOPES,
|
|
439
450
|
WORKSPACE_DEPENDENT_SCOPES,
|
|
440
451
|
BLOCKING_CANDIDATES,
|
|
@@ -29,7 +29,7 @@ const path = require('path');
|
|
|
29
29
|
|
|
30
30
|
const { loadManifest, DEFAULT_MANIFEST_PATH, LIBRARY_MANIFEST_PATH } = require('../manifest/loadManifest');
|
|
31
31
|
const {
|
|
32
|
-
API_CHECKOUT_ROOT, PACKAGE_ROOT: OWN_ROOT, resolveWorkspacePath
|
|
32
|
+
API_CHECKOUT_ROOT, PACKAGE_ROOT: OWN_ROOT, WORKSPACE_MARKER, resolveWorkspacePath
|
|
33
33
|
} = require('../manifest/workspaceRoot');
|
|
34
34
|
const { KINDS, renderUniformRegion } = require('./readmePointer');
|
|
35
35
|
|
|
@@ -74,6 +74,13 @@ function serviceManifestPath(serviceRoot) {
|
|
|
74
74
|
return path.join(serviceRoot, 'node_modules', ...selfName().split('/'), SERVICE_MANIFEST_IN_PACKAGE);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* The conventional prefix a workspace-relative path uses for the api checkout,
|
|
79
|
+
* read off the marker that declares it rather than typed a second time here
|
|
80
|
+
* (`../manifest/workspaceRoot.js`, which owns both the prefix and the marker).
|
|
81
|
+
*/
|
|
82
|
+
const [API_PREFIX] = WORKSPACE_MARKER.split('/');
|
|
83
|
+
|
|
77
84
|
/**
|
|
78
85
|
* Where this package lies inside its own workspace, workspace-relative — or null
|
|
79
86
|
* when it lies in no checkout at all (an installed copy, a service container).
|
|
@@ -84,10 +91,25 @@ function serviceManifestPath(serviceRoot) {
|
|
|
84
91
|
* at the manifest AS IT LIES IN THAT CHECKOUT, and where there is no copy there
|
|
85
92
|
* is no link — which is a question the run could not answer, never a package
|
|
86
93
|
* that passed (`.claude/rules/automation-gates.md` §5).
|
|
94
|
+
*
|
|
95
|
+
* The head of that path is the CONVENTION `api/`, never the name the checkout
|
|
96
|
+
* happens to wear on disk — the rule `workspaceRoot.js` states in its header and
|
|
97
|
+
* implements in `resolveWorkspacePath()`, which is the function this string is
|
|
98
|
+
* handed to. Deriving it as "the directory above the checkout, then the checkout"
|
|
99
|
+
* broke that rule wherever the checkout is named something else: GitLab CI checks
|
|
100
|
+
* this repository out as `infra-mono`, so the string became
|
|
101
|
+
* `infra-mono/shared/connector/conn-orch-validator`, which `resolveWorkspacePath`
|
|
102
|
+
* joins literally — pointing at nothing in every workspace, including the fixture
|
|
103
|
+
* and sandbox trees the suites build. Measured 2026-09-17 in the job's own image,
|
|
104
|
+
* one tree under two names: under `infra-mono` the run died with
|
|
105
|
+
* `Fix: run with --workspace pointing at a checkout that carries
|
|
106
|
+
* infra-mono/shared/connector/conn-orch-validator` — an instruction nobody can
|
|
107
|
+
* carry out (`.claude/rules/automation-gates.md` §1 requirement 4) — while the
|
|
108
|
+
* same tree named `api` was green.
|
|
87
109
|
*/
|
|
88
110
|
const PACKAGE_IN_WORKSPACE = API_CHECKOUT_ROOT === null
|
|
89
111
|
? null
|
|
90
|
-
: path.relative(
|
|
112
|
+
: [API_PREFIX, ...path.relative(API_CHECKOUT_ROOT, OWN_ROOT).split(path.sep)].join('/');
|
|
91
113
|
|
|
92
114
|
/**
|
|
93
115
|
* The manifest as it lies in the workspace being read, whichever of the two it
|
|
@@ -33,6 +33,24 @@
|
|
|
33
33
|
* what keeps the grant inside one service, and it is the difference
|
|
34
34
|
* `tests/unit/dbAccountGrants.test.js` measures.
|
|
35
35
|
*
|
|
36
|
+
* ## The escape belongs to BOTH grants, not only the wildcard one
|
|
37
|
+
*
|
|
38
|
+
* The database-name position of a `GRANT` is read as a LIKE pattern whether or
|
|
39
|
+
* not a `%` follows it, so `` ON `oagen_emailer`.* `` — the grant EVERY
|
|
40
|
+
* installed service gets — also grants `oagen5emailer` and every other schema
|
|
41
|
+
* differing in that one character. One escape function therefore runs on the
|
|
42
|
+
* schema name before either statement is built; the production runbook
|
|
43
|
+
* (`api/docs/setup/INSTALL.md` § the account and its grants, `schema_grant`)
|
|
44
|
+
* already escaped it, and a definition laxer than the runbook installing from it
|
|
45
|
+
* is the wrong way round. Measured against the dev server in
|
|
46
|
+
* `tests/unit/bizCiGateCli.setupDbAccount.integration.test.js`: with the
|
|
47
|
+
* unescaped grant the account reads the neighbour's rows; with it, `ERROR 1142`.
|
|
48
|
+
*
|
|
49
|
+
* `%` — the other wildcard of that position — is REFUSED rather than escaped,
|
|
50
|
+
* for the reason the comment above `FORBIDDEN_IN_VALUE` gives: no platform
|
|
51
|
+
* schema name carries one, so a value that does came from somewhere it should
|
|
52
|
+
* not have. `_` cannot be refused the same way: every platform name has one.
|
|
53
|
+
*
|
|
36
54
|
* Production never gets that grant: it installs one schema, and a wildcard there
|
|
37
55
|
* would be a standing privilege nobody asked for.
|
|
38
56
|
*
|
|
@@ -84,6 +102,43 @@ function requireValue(value, name, fix) {
|
|
|
84
102
|
return value;
|
|
85
103
|
}
|
|
86
104
|
|
|
105
|
+
/**
|
|
106
|
+
* The one escape of this file, used by every grant it builds.
|
|
107
|
+
*
|
|
108
|
+
* MariaDB reads the database-name position of a `GRANT` as a LIKE pattern, so
|
|
109
|
+
* `_` there matches any single character. Every platform schema is
|
|
110
|
+
* `oagen_<shortname>`, which means every unescaped grant this package could
|
|
111
|
+
* build would reach a schema nobody named.
|
|
112
|
+
*
|
|
113
|
+
* `%` is not escaped here — `requireGrantableSchema` refuses it before this
|
|
114
|
+
* runs, so the only wildcard left to neutralise is `_`.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} schema a schema name already checked by `requireGrantableSchema`
|
|
117
|
+
* @returns {string} the same name as a LIKE pattern matching only itself
|
|
118
|
+
*/
|
|
119
|
+
function escapeSchemaPattern(schema) {
|
|
120
|
+
return schema.replace(/_/g, '\\_');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A schema name this file can turn into a grant that reaches that schema ALONE.
|
|
125
|
+
*
|
|
126
|
+
* `requireValue` already refuses the backslash (an escape nobody wrote here) and
|
|
127
|
+
* the quoting characters. This adds the one character that is neither a quoting
|
|
128
|
+
* problem nor escapable without changing what the name means: `%`.
|
|
129
|
+
*/
|
|
130
|
+
function requireGrantableSchema(schema, fix) {
|
|
131
|
+
requireValue(schema, 'schema', fix);
|
|
132
|
+
if (schema.includes('%')) {
|
|
133
|
+
throw new Error('[DbAccountGrants] schema carries `%`, which the database-name position of a GRANT '
|
|
134
|
+
+ `reads as a wildcard: ${JSON.stringify(schema)}.\n`
|
|
135
|
+
+ ' A grant built from it would reach schemas nobody named, and escaping it would grant a '
|
|
136
|
+
+ 'schema whose name really contains `%` — neither is what a platform schema means.\n'
|
|
137
|
+
+ ` Fix: ${fix}`);
|
|
138
|
+
}
|
|
139
|
+
return schema;
|
|
140
|
+
}
|
|
141
|
+
|
|
87
142
|
/**
|
|
88
143
|
* Every statement that creates this service's account and grants it what it
|
|
89
144
|
* needs, in the order they must run.
|
|
@@ -95,26 +150,30 @@ function requireValue(value, name, fix) {
|
|
|
95
150
|
* (`DB_USER` of `config/env-templates/<service>.env`)
|
|
96
151
|
* @param {string} options.password the password the account is created with
|
|
97
152
|
* @param {boolean} [options.ciThrowaway] also grant the throwaway namespace
|
|
98
|
-
* `<schema>\_%`, which only an integration tier builds into
|
|
153
|
+
* `<escaped schema>\_%`, which only an integration tier builds into
|
|
99
154
|
* @returns {string[]} the statements, in order
|
|
100
155
|
*/
|
|
101
156
|
function databaseAccountSql({ schema, account, password, ciThrowaway = false } = {}) {
|
|
102
|
-
|
|
157
|
+
requireGrantableSchema(schema, 'pass database.schema from config/service/integration-contract.json.');
|
|
103
158
|
requireValue(account, 'account', 'pass DB_USER from this service\'s config/env-templates/<service>.env.');
|
|
104
159
|
requireValue(password, 'password', 'pass the password the account is created with; an account created '
|
|
105
160
|
+ 'with an empty password is one anybody on the network can use.');
|
|
106
161
|
|
|
107
162
|
const identity = `'${account}'@'${ACCOUNT_HOST}'`;
|
|
163
|
+
// Both grants below are built from this one value: the database-name position
|
|
164
|
+
// is a LIKE pattern in either of them (see the head of this file).
|
|
165
|
+
const pattern = escapeSchemaPattern(schema);
|
|
108
166
|
|
|
109
167
|
const statements = [
|
|
110
168
|
`CREATE USER IF NOT EXISTS ${identity} IDENTIFIED BY '${password}';`,
|
|
111
|
-
`GRANT ALL PRIVILEGES ON \`${
|
|
169
|
+
`GRANT ALL PRIVILEGES ON \`${pattern}\`.* TO ${identity};`
|
|
112
170
|
];
|
|
113
171
|
|
|
114
172
|
if (ciThrowaway === true) {
|
|
115
|
-
//
|
|
116
|
-
// the head of this file for the neighbour
|
|
117
|
-
|
|
173
|
+
// `\_%` is the throwaway SUFFIX — the one wildcard meant to be one — added
|
|
174
|
+
// to the escaped name; see the head of this file for the neighbour an
|
|
175
|
+
// unescaped name would otherwise reach.
|
|
176
|
+
statements.push(`GRANT ALL PRIVILEGES ON \`${pattern}\\_%\`.* TO ${identity};`);
|
|
118
177
|
}
|
|
119
178
|
|
|
120
179
|
// Last, always: a grant is not live for a connection opened before it.
|
package/src/utils/libCompat.js
CHANGED
|
@@ -7,26 +7,34 @@
|
|
|
7
7
|
* matches the version the platform declares. Mismatch means the service was
|
|
8
8
|
* built against a different platform contract than the one it will run on.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* it is by definition shipped and must match.
|
|
10
|
+
* EVERY package the SSOT declares and the service pins is compared against the
|
|
11
|
+
* declared version: R6's rule is that a service pins exactly what the SSOT
|
|
12
|
+
* declares (`.claude/rules/architecture-principles.md` § Version pinning), and
|
|
13
|
+
* that sentence carries no infra clause. What the `infraConsumed` list decides
|
|
14
|
+
* is which SENTENCE a stale pin gets, because the two say different things:
|
|
16
15
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* all — so it is refused, never narrowed away. Passing it would be a fallback
|
|
23
|
-
* to "not gated" (principle 3), and it would let a typo or a retired package
|
|
24
|
-
* name travel into an image with the gate reporting OK (measured 2026-09-14).
|
|
16
|
+
* - infra installs the package → biz and infra run different code against
|
|
17
|
+
* one contract, and the fix may be to deploy the matching infra release;
|
|
18
|
+
* - no infra service installs it → there is no infra version to diverge
|
|
19
|
+
* from, so the only statement left is the SSOT one: this pin is not what
|
|
20
|
+
* the platform declares, and the fix is to re-pin.
|
|
25
21
|
*
|
|
26
|
-
*
|
|
27
|
-
* infra
|
|
28
|
-
*
|
|
29
|
-
*
|
|
22
|
+
* Until 2026-09-18 (W651) `infraConsumed` decided more than that: a package no
|
|
23
|
+
* infra service installs skipped the version comparison altogether. Measured
|
|
24
|
+
* then: 20 of the 28 SSOT packages are biz-only, so for most of a service's
|
|
25
|
+
* pins a biz pipeline compared nothing at all, and a service could ship months
|
|
26
|
+
* behind the platform with a green gate. A bare version map has no such list —
|
|
27
|
+
* a platform-release entry records what was deployed, so everything in it is by
|
|
28
|
+
* definition shipped, and every mismatch is a compatibility one.
|
|
29
|
+
*
|
|
30
|
+
* A package the SSOT declares in NEITHER list is a different thing again: not
|
|
31
|
+
* biz-only, but unknown to the platform — there is no version to pin against at
|
|
32
|
+
* all, so it is refused, never narrowed away. Passing it would be a fallback to
|
|
33
|
+
* "not gated" (principle 3), and it would let a typo or a retired package name
|
|
34
|
+
* travel into an image with the gate reporting OK (measured 2026-09-14).
|
|
35
|
+
*
|
|
36
|
+
* The narrowing never relaxes the exact-pin rule either, which `^`, `~` and
|
|
37
|
+
* `latest` break in EVERY `@onlineapps/*` dependency, gated or not — a floating
|
|
30
38
|
* range makes the same commit install different code on different days whether
|
|
31
39
|
* or not infra happens to ship that package too. Until 2026-09-15 (W413) the
|
|
32
40
|
* exactness test sat inside the gated branch, so a biz-only pin could float
|
|
@@ -76,12 +84,39 @@ function normalizeLibrarySet(raw) {
|
|
|
76
84
|
return { versions: raw, infraConsumed: null };
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
/**
|
|
88
|
+
* The service a finding belongs to, taken from its package.json.
|
|
89
|
+
*
|
|
90
|
+
* The SSOT-pin finding names the service, because the cross-repository audit
|
|
91
|
+
* path (`api/scripts/ci/verify-lib-compat.mjs`) reports several repositories in
|
|
92
|
+
* one run. A package.json without a name is invalid input — npm requires the
|
|
93
|
+
* field — so it is refused with an actionable message rather than rendered as
|
|
94
|
+
* "service undefined".
|
|
95
|
+
*/
|
|
96
|
+
function serviceNameOf(pkg) {
|
|
97
|
+
const name = pkg?.name;
|
|
98
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
99
|
+
throw new Error('[LibCompat] Service package.json declares no "name" - a pin finding must say which '
|
|
100
|
+
+ 'service carries the pin. Fix: add a "name" field to the service package.json.');
|
|
101
|
+
}
|
|
102
|
+
return name;
|
|
103
|
+
}
|
|
104
|
+
|
|
79
105
|
/**
|
|
80
106
|
* Compare a package.json against the platform library set.
|
|
81
107
|
*
|
|
108
|
+
* `compared` is what the gate may claim as coverage: the packages whose
|
|
109
|
+
* version was actually held against the declared one. It is appended by the
|
|
110
|
+
* comparing loop itself, never recomputed from the other two lists by a
|
|
111
|
+
* renderer — a second derivation of one fact is the drift
|
|
112
|
+
* `.claude/rules/change-discipline.md` § One rail per concern forbids, and
|
|
113
|
+
* `gated ∪ notGated` is NOT the same set: a caret pin and a package the SSOT
|
|
114
|
+
* declares nowhere are classified and then refused before any comparison
|
|
115
|
+
* happens.
|
|
116
|
+
*
|
|
82
117
|
* @param {object} pkg parsed package.json of the biz service
|
|
83
118
|
* @param {object} librarySet committed SSOT shape or bare version map
|
|
84
|
-
* @returns {{ok: boolean, gated: string[], notGated: string[], violations: Array<{package: string, message: string}>}}
|
|
119
|
+
* @returns {{ok: boolean, gated: string[], notGated: string[], compared: string[], violations: Array<{package: string, message: string}>}}
|
|
85
120
|
*/
|
|
86
121
|
function checkLibCompat(pkg, librarySet) {
|
|
87
122
|
const { versions, infraConsumed } = normalizeLibrarySet(librarySet);
|
|
@@ -91,6 +126,7 @@ function checkLibCompat(pkg, librarySet) {
|
|
|
91
126
|
|
|
92
127
|
const gated = [];
|
|
93
128
|
const notGated = [];
|
|
129
|
+
const compared = [];
|
|
94
130
|
const violations = [];
|
|
95
131
|
|
|
96
132
|
for (const [name, version] of ours) {
|
|
@@ -133,13 +169,23 @@ function checkLibCompat(pkg, librarySet) {
|
|
|
133
169
|
continue;
|
|
134
170
|
}
|
|
135
171
|
|
|
136
|
-
|
|
172
|
+
// The equality comparison is unconditional, for the same reason the
|
|
173
|
+
// exactness test above is: R6 proves a service pins exactly what the SSOT
|
|
174
|
+
// declares, and the SSOT declares a version for the biz-only packages too.
|
|
175
|
+
// Only the sentence differs — there is no infra release to deploy for a
|
|
176
|
+
// package no infra service installs, so naming one would send the reader
|
|
177
|
+
// after a fix that does not exist.
|
|
178
|
+
compared.push(name);
|
|
137
179
|
|
|
138
180
|
if (declared !== version) {
|
|
139
181
|
violations.push({
|
|
140
182
|
package: name,
|
|
141
|
-
message:
|
|
142
|
-
|
|
183
|
+
message: bizOnly
|
|
184
|
+
? `${name}: service ${serviceNameOf(pkg)} pins ${version}, the platform library SSOT declares `
|
|
185
|
+
+ `${declared} — a service pins exactly what the SSOT declares, whether or not an infra service `
|
|
186
|
+
+ `installs the package. Fix: npm install ${name}@${declared} --save-exact.`
|
|
187
|
+
: `${name}: biz pins ${version}, infra ships ${declared} — `
|
|
188
|
+
+ 'rebuild the service against the current platform, or deploy the matching infra release first.'
|
|
143
189
|
});
|
|
144
190
|
}
|
|
145
191
|
}
|
|
@@ -148,6 +194,7 @@ function checkLibCompat(pkg, librarySet) {
|
|
|
148
194
|
ok: violations.length === 0,
|
|
149
195
|
gated,
|
|
150
196
|
notGated,
|
|
197
|
+
compared,
|
|
151
198
|
violations
|
|
152
199
|
};
|
|
153
200
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
set -e
|
|
3
3
|
|
|
4
|
+
# `npm ci`, not `npm install`: the tree is built from package-lock.json alone,
|
|
5
|
+
# never reconciled against npm's hidden node_modules/.package-lock.json, which
|
|
6
|
+
# a `--package-lock-only` re-pin rewrites while the package directories keep
|
|
7
|
+
# the old versions. A lock that disagrees with package.json stops the boot here
|
|
8
|
+
# instead of installing something nobody declared.
|
|
9
|
+
# @see api/tests/scripts/infra-init-scripts.bats
|
|
4
10
|
oa_npm_install() {
|
|
5
|
-
npm
|
|
11
|
+
npm ci --no-audit --no-fund
|
|
6
12
|
}
|
|
7
13
|
|
|
8
14
|
# --- oa-deps-guard v1 (identical in every OA Drive init.sh) ---
|
|
@@ -42,7 +48,7 @@ if [ "$CURRENT_HASH" != "$PREVIOUS_HASH" ] || [ "$NEEDS_INSTALL" = "1" ]; then
|
|
|
42
48
|
echo "$CURRENT_HASH" > "$MARKER_FILE"
|
|
43
49
|
echo "[init.sh] Dependencies installed, marker $MARKER_FILE updated."
|
|
44
50
|
else
|
|
45
|
-
echo "[init.sh] Dependency install failed - marker $MARKER_FILE left untouched, so the next start retries instead of trusting a half-installed tree. Fix: read the npm error above
|
|
51
|
+
echo "[init.sh] Dependency install failed - marker $MARKER_FILE left untouched, so the next start retries instead of trusting a half-installed tree. Fix: read the npm error above; a service that has a package-lock.json installs with 'npm ci --no-audit --no-fund' in this directory - 'npm install' is what let the folders and the lock drift apart; a service that has none yet needs 'npm install' once, to create it."
|
|
46
52
|
exit 1
|
|
47
53
|
fi
|
|
48
54
|
else
|
|
@@ -79,7 +79,7 @@ WORKSPACE_ROOT=$(dirname "$CONTAINER_DIR")
|
|
|
79
79
|
API_CHECKOUT="$WORKSPACE_ROOT/api"
|
|
80
80
|
|
|
81
81
|
if [ "$(basename "$CONTAINER_DIR")" != "api_biz" ]; then
|
|
82
|
-
die "The checkout is not in the layout the uniform needs - $SERVICE_ROOT lies in '$(basename "$CONTAINER_DIR")', and the engine answers the rows that read the platform SSOT only for a service under <root>/api_biz/ beside <root>/api. Expected: the job checks this project out there. Fix: in .gitlab-ci.yml
|
|
82
|
+
die "The checkout is not in the layout the uniform needs - $SERVICE_ROOT lies in '$(basename "$CONTAINER_DIR")', and the engine answers the rows that read the platform SSOT only for a service under <root>/api_biz/ beside <root>/api. Expected: the job checks this project out there. Fix: make this job extend .oa-uniform in .gitlab-ci.yml (it sets GIT_CLONE_PATH: \$CI_BUILDS_DIR/oa-uniform/\$CI_CONCURRENT_ID/api_biz/<service>), and confirm the runner allows a custom build directory ([runners.custom_build_dir] enabled)." 2
|
|
83
83
|
fi
|
|
84
84
|
|
|
85
85
|
command -v git >/dev/null 2>&1 || die "Missing tool - 'git' does not run, and the api checkout this run reads cannot be fetched without it. Fix: install git in the job image (apk add --no-cache git)." 2
|
|
@@ -90,12 +90,24 @@ command -v node >/dev/null 2>&1 || die "Missing tool - 'node' does not run, and
|
|
|
90
90
|
# Read-only and shallow: the uniform reads files, never history. A runner reuses
|
|
91
91
|
# its builds directory between pipelines, so a checkout left by an earlier run
|
|
92
92
|
# would silently measure this deploy against a stale SSOT — it is removed and
|
|
93
|
-
# fetched again, and
|
|
93
|
+
# fetched again, and ONLY when this script can prove it made it.
|
|
94
|
+
#
|
|
95
|
+
# The proof is the marker the clone step below writes. `.git` plus
|
|
96
|
+
# `config/services.json` is no proof at all: every checkout of the api repository
|
|
97
|
+
# carries both. On 2026-09-24 the script was run from api_biz/emailer on a
|
|
98
|
+
# workstation, <workspace>/api was the developer's real working tree, it passed
|
|
99
|
+
# that test and was deleted (639 tracked files; d.885). A directory without the
|
|
100
|
+
# marker is somebody's work, so the script refuses and touches nothing
|
|
101
|
+
# (`automation-gates.md` §1 requirement 3, Safe). There is no flag that skips
|
|
102
|
+
# the check: the one systemic path is a free place or a clone this script made.
|
|
103
|
+
CLONE_MARKER=".verify-deploy-uniform.clone"
|
|
94
104
|
if [ -e "$API_CHECKOUT" ]; then
|
|
95
|
-
if [ -d "$API_CHECKOUT/.git" ] && [ -f "$API_CHECKOUT/config/services.json" ]
|
|
105
|
+
if [ -d "$API_CHECKOUT/.git" ] && [ -f "$API_CHECKOUT/config/services.json" ] \
|
|
106
|
+
&& [ -f "$API_CHECKOUT/$CLONE_MARKER" ] \
|
|
107
|
+
&& head -n 1 "$API_CHECKOUT/$CLONE_MARKER" | grep -q '^created-by verify-deploy-uniform.sh '; then
|
|
96
108
|
rm -rf "$API_CHECKOUT"
|
|
97
109
|
else
|
|
98
|
-
die "Cannot place the api checkout - $API_CHECKOUT
|
|
110
|
+
die "Cannot place the api checkout - $API_CHECKOUT exists and carries no marker $CLONE_MARKER, so it is not a clone this script made, and deleting it could destroy somebody's work. Expected: a clone this script made. Fix: remove it yourself, or run this script only from a CI checkout where <workspace>/api is free (the job's GIT_CLONE_PATH points at a build directory this pipeline owns)." 2
|
|
99
111
|
fi
|
|
100
112
|
fi
|
|
101
113
|
|
|
@@ -106,6 +118,11 @@ if ! git clone --depth 1 --single-branch --branch "$API_REF" "$API_URL" "$API_CH
|
|
|
106
118
|
die "The api checkout could not be cloned - git clone --branch $API_REF $SAFE_URL failed, so api/config/services.json cannot be read and the uniform would answer a fraction of its rows. A partial answer is not a deploy permit, so this refuses rather than continues. Expected: read access for this pipeline's job token. Fix: in GitLab open the api project, Settings > CI/CD > Job token permissions, and add this project to the allowlist (owner's step, confirmation biz-service-manifest 008 point 2); check also that the ref '$API_REF' exists there." 2
|
|
107
119
|
fi
|
|
108
120
|
|
|
121
|
+
# The marker is what lets the NEXT run delete this clone, so it is written the
|
|
122
|
+
# moment the clone exists and before anything reads it.
|
|
123
|
+
printf 'created-by verify-deploy-uniform.sh %s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$$" > "$API_CHECKOUT/$CLONE_MARKER" \
|
|
124
|
+
|| die "Cannot mark the api checkout - writing $API_CHECKOUT/$CLONE_MARKER failed, and without it the next run would refuse to replace this clone. Expected: a writable build directory. Fix: check the permissions of $WORKSPACE_ROOT on the runner." 2
|
|
125
|
+
|
|
109
126
|
# --- the engine -------------------------------------------------------------
|
|
110
127
|
#
|
|
111
128
|
# The pin decides which uniform this service is measured against, so the engine
|