@onlineapps/conn-orch-validator 11.0.0 → 12.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +119 -0
- package/README.md +47 -0
- package/jest.config.js +11 -0
- package/manifests/biz-service.manifest.json +1 -1
- package/package.json +1 -1
- package/src/manifest/checks/docsLintBridge.js +76 -17
- package/src/manifest/discovery.js +42 -3
- package/src/sync/readmeLocation.js +24 -2
- package/src/utils/dbAccountGrants.js +65 -6
- package/src/utils/deployContract.js +116 -6
- package/src/utils/testNamespace.js +30 -3
- package/templates/business-service/.gitlab-ci.yml +21 -9
- package/templates/business-service/init.sh +8 -2
- package/templates/business-service/scripts/verify-deploy-uniform.sh +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,125 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [12.1.0] — 2026-09-18
|
|
8
|
+
|
|
9
|
+
### Fixed — lint, který se odmítl spustit, je NOT RUN, ne šest nálezů o dokumentaci (d.632, W632)
|
|
10
|
+
|
|
11
|
+
Řádky dokumentace (`docs-lint`, `docs-lint-clean` → `D-PORT`, `D-NPM`, `D-SCRIPT`, `D-RETIRED`, `D-HEADER`, `D-LINT`): lint
|
|
12
|
+
`lint-biz-docs`, který skončil bez verdiktu (exit 2, pád spawnu, nerozparsovatelný JSON), je NOT RUN na každém řádku, který
|
|
13
|
+
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
|
|
14
|
+
`complete: true` (změřeno 2026-09-17, CI joby 16572194080 a 16571115198: GitLab Runner zakládá `api_biz/<svc>.tmp` a lint ho
|
|
15
|
+
bral za službu). Důvod NOT RUN nese CELÝ stderr lintu, první řádek první (poslední řádek byla závorka gitu „Stopping at
|
|
16
|
+
filesystem boundary“); `Fix:` se doplní jen když ho hláška lintu nenese. Chybějící `config/biz-docs-lint.tree.json` zůstává
|
|
17
|
+
nálezem (`C-LINT`). Zároveň v api (`6f9d2f2b`): lint i `sync-biz-facts` berou sourozence ze SSOT `config/services.json`
|
|
18
|
+
(`directory` u `enabled`), ne z výpisu `api_biz/` — nedeklarovaný adresář se nečte a řekne se to jednou větou.
|
|
19
|
+
|
|
20
|
+
### Changed (testy) — třetí živá DB sada přes `liveDatabase.js`; sondy skládají dočasný adresář z `os.tmpdir()` (d.620b, W620b)
|
|
21
|
+
|
|
22
|
+
`bizCiGateCli.setupDbAccount.integration` (blok „how far the production grant reaches“) bere spojení z
|
|
23
|
+
`tests/helpers/liveDatabase.js`, práce je v `tests/helpers/grantReachProbe.js` a sada je třetí v jobu `test-validator-db`;
|
|
24
|
+
`setupDatabaseProbe.js` a `dbAccountProbe.js` už nepíší do doslovného `/tmp`.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Changed (testy) — živé DB sady databázi deklarují, nehledají; „unwritable root“ se pod rootem předvede (d.628, d.623c, W623)
|
|
28
|
+
|
|
29
|
+
`setupDatabaseLive.integration` a `dbCiAccount.integration` čtou spojení z `DB_HOST`, `DB_PORT`, `CI_DB_ROOT_USER`,
|
|
30
|
+
`CI_DB_ROOT_PASSWORD` — jména, která čte `biz-ci-gate setup-db-account` — přes jednoho vlastníka `tests/helpers/liveDatabase.js`;
|
|
31
|
+
dosud každá nesla vlastní kopii s vývojovým kontejnerem `gen_mariadb10.5`, sítí `gendb-network` a heslem z gitignorovaného
|
|
32
|
+
`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
|
|
33
|
+
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í
|
|
34
|
+
`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,
|
|
35
|
+
conf `ci-validator-db-job` 001). Lokálně: README § Running the live-database suites. `oaValidateCli.integration` — případ
|
|
36
|
+
„an unwritable service root fails fast“ se pod uid 0 předvádí pod neprivilegovaným účtem stroje místo NOT RUN.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Changed — šablona `init.sh` staví strom z `package-lock.json` (`npm ci`), ne `npm install` (d.629, W629)
|
|
40
|
+
|
|
41
|
+
`templates/business-service/init.sh` `oa_npm_install()` = `npm ci --no-audit --no-fund`. `npm install --package-lock-only`
|
|
42
|
+
(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é,
|
|
43
|
+
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,
|
|
44
|
+
INFRA B137 `28fef3dc` totéž v sedmi infra službách). Podmínka instalace (blok `oa-deps-guard v1`, marker
|
|
45
|
+
`.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 →
|
|
46
|
+
`npm install` „up to date“ (1.0.0), `npm ci` „added 1 package“ (2.0.0). Řádek uniformy `F-INIT` roznáší jen blok, servisní
|
|
47
|
+
`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
|
|
48
|
+
potřebuje `package-lock.json` už při prvním startu: šablona ho neveze, první boot bez něj skončí hlášeným `EUSAGE`.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
### Fixed — hlavní grant databázového účtu escapuje `_` (d.620, W620)
|
|
52
|
+
|
|
53
|
+
`databaseAccountSql` escapuje `_` i v hlavním grantu (`` ON `oagen\_x`.* ``), ne jen v CI wildcardu: databázová část
|
|
54
|
+
`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
|
|
55
|
+
v tom jednom znaku (`oagen_emailer` → `oagen5emailer`; nález INFRA-DOCS z falzifikace `INSTALL.md`). Escape je jedna
|
|
56
|
+
funkce `escapeSchemaPattern()` použitá na obě věty; `%` ve jméně schématu se odmítá s `Fix:` (žádné platformní jméno ho
|
|
57
|
+
nenese, `\` odmítal už `FORBIDDEN_IN_VALUE`). Změřeno proti živé MariaDB 10.5: před opravou účet přečetl sousedovy řádky,
|
|
58
|
+
po opravě `ERROR 1142 (42000) … SELECT command denied`; vlastní schéma dál čitelné, CI throwaway grant (`oagen\_x\_%`) drží.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
### Fixed — pozice balíčku ve workspace nese konvenční prefix `api/`, ne jméno adresáře checkoutu (d.625, W625)
|
|
62
|
+
|
|
63
|
+
`sync/readmeLocation.js` odvozovalo `PACKAGE_IN_WORKSPACE` (a s ním `requiresSiblings` řádku `L-README-REGION`) z adresáře
|
|
64
|
+
NAD checkoutem. GitLab CI klonuje repozitář pod jménem projektu `infra-mono`, takže vznikalo
|
|
65
|
+
`infra-mono/shared/connector/conn-orch-validator`; `resolveWorkspacePath()` rozřeší přes marker jen hlavu `api` a zbytek
|
|
66
|
+
připojí doslova → řetězec neukazoval nikam a běh končil větou `Fix: run with --workspace pointing at a checkout that carries
|
|
67
|
+
infra-mono/…`, kterou nikdo nemůže splnit (`automation-gates.md` §1/4). Pravidlo stojí v hlavičce `manifest/workspaceRoot.js`:
|
|
68
|
+
`api/` na začátku cesty je konvence deklarujícího textu, nikdy jméno adresáře na disku — prefix se proto čte z
|
|
69
|
+
`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
|
|
70
|
+
testů a 6 bats červených pod `infra-mono`, zelené pod `api`; po opravě zelené pod oběma. Štítek souboru na stdout
|
|
71
|
+
`oa-sync-template` dál nese cestu na disku (tam soubor je) — srovnán test, ne výstup.
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## [12.0.0] — 2026-09-17
|
|
75
|
+
|
|
76
|
+
### Fixed — BREAKING: R2 tvrdí pravidlo sekvence nasazení, ne včerejší text (d.614, W614)
|
|
77
|
+
|
|
78
|
+
`src/utils/deployContract.js`: R2 vyžadovalo doslovné `git reset --hard origin/production`, což je OPAK toho, co blok
|
|
79
|
+
`oa-ci v1` nese od d.589b (`e138d1f9`) — `git merge-base --is-ancestor "$COMMIT_SHA" origin/production` a pak
|
|
80
|
+
`git reset --hard "$COMMIT_SHA"`. Brána `ci:gate:contract` je první krok `before_script` jobu `test`, takže služba se
|
|
81
|
+
syncnutým blokem 11.0.0 padala na R2 ještě před prvním testem (změřeno 17. 9. u pdfgen a hello-service; ostatních šest
|
|
82
|
+
neslo starý tvar, tedy jednu synchronizaci od téhož). R2 nově čte PRAVIDLO: nasazení nikdy nemerguje (`git pull` i
|
|
83
|
+
`git merge`, `merge-base` není merge), resetuje na commit, který pipeline měřila, a guard `merge-base --is-ancestor …
|
|
84
|
+
origin/production` musí stát PŘED tím resetem a jmenovat týž commit. Odolné vůči uvozovkám, mezerám a `; then`,
|
|
85
|
+
nikoli benevolentní: reset bez guardu, guard až za resetem, guard na jiný commit nebo jinou větev i samotný včerejší
|
|
86
|
+
`git reset --hard origin/production` jsou FAIL. Žádná přechodná tolerance (architecture-principles §11); hláška jmenuje
|
|
87
|
+
dnešní tvar a jediný příkaz, který ho zapíše — `npx oa-sync-template .gitlab-ci.yml --target .`. Všech osm služeb má
|
|
88
|
+
blok `oa-ci v1` rozejitý už dnes (`oa-sync-template .gitlab-ci.yml --check` = DRIFT u 8/8), takže R2 nepřidává povinnost,
|
|
89
|
+
kterou uniforma nenese.
|
|
90
|
+
|
|
91
|
+
### Added — sada „vlastní šablona projde vlastní bránou“ (d.614, W614)
|
|
92
|
+
|
|
93
|
+
`tests/unit/deployContractTemplate.test.js` renderuje `templates/business-service` týmiž helpery, kterými ji renderuje
|
|
94
|
+
`oa-sync-template --new` (`renderTree` + `deriveParams` + přejmenování env šablony), a pouští nad výsledkem
|
|
95
|
+
`verifyDeployContract`: 0 porušení. Chyběl právě tenhle spoj — každý případ R2 si dosud psal `.gitlab-ci.yml` ručně, takže
|
|
96
|
+
pravidlo i blok šlo měnit nezávisle a obě strany zůstaly zelené. Sada nese i tři případy, které dokazují, že umí zčervenat
|
|
97
|
+
(reset bez guardu, včerejší tvar, kontrolní případ mimo sekvenci). Ruční vstupy sad `deployContract`, `deployContractR7Declared`,
|
|
98
|
+
`bizCiGateCli.{envContract,installContract}` a helperu `conformantBizRepo` jsou srovnané na dnešní tvar.
|
|
99
|
+
|
|
100
|
+
### Changed — BREAKING: blok `G-CI` deklaruje DVA refy api místo jednoho (d.612, W612)
|
|
101
|
+
|
|
102
|
+
`templates/business-service/.gitlab-ci.yml`: `API_UNIFORM_REF_CONTINUOUS: main` čte průběžný job `validate-uniform`,
|
|
103
|
+
`API_UNIFORM_REF_DEPLOY: production` čte `deploy-production` — jeho krok uniformy i běžec migrací a seedů, kterého nese na
|
|
104
|
+
box. `API_UNIFORM_REF` zaniká bez aliasu a bez přechodného čtení. Průběžná brána (konfirmace `biz-service-manifest` 010)
|
|
105
|
+
měřila api `production`, která se pohne až na konci produkční cesty (push té větve je nasazení infra), takže byla červená
|
|
106
|
+
u všech osmi služeb z důvodu mimo kteroukoli z nich. Rozhodnutí vlastníka: `biz-service-manifest` 012. Služba srovná blok
|
|
107
|
+
jedním během `npx oa-sync-template .gitlab-ci.yml --target .`; do té doby její `validate-uniform` měří dál `production`.
|
|
108
|
+
`scripts/verify-deploy-uniform.sh` bere ref argumentem jako dosud; jeho hláška o prázdném refu jmenuje obě proměnné.
|
|
109
|
+
|
|
110
|
+
### Fixed — `ManifestDiscovery` „Referenced file not found“ radí to, co jde udělat (d.611, W612)
|
|
111
|
+
|
|
112
|
+
Soubor chybějící v api checkoutu, KTERÝ běh dosáhl, už neposílá CI job za `--workspace` (ten si job dosazuje sám): hláška
|
|
113
|
+
jmenuje checkout a `Fix:` říká aktualizovat ho lokálně, nebo posunout ref api, proti kterému job měří. Workspace bez api
|
|
114
|
+
checkoutu dostává dosavadní radu. V obou větvích zůstává tvrdý pád — chybějící SSOT v dosažitelném workspace je nález, ne
|
|
115
|
+
NOT RUN; říká to i `why` řádku `G-SHARED-ENV`.
|
|
116
|
+
|
|
117
|
+
### Fixed — workspace id ≤ 0 se odmítá na obou kolejích (d.606 + d.606b, W606)
|
|
118
|
+
|
|
119
|
+
`assertWorkspaceId` odmítá `0` i záporná čísla a prochází jím hodnota deklarovaná receptem (`defaults.workspace_id`,
|
|
120
|
+
per-step) i hodnota z prostředí (`TESTING_WORKSPACE_ID` v `getTestNamespace()`). Konfirmace `tier1-runner-workspace` 002
|
|
121
|
+
říká „kladné celé číslo“, kód kontroloval jen `Number.isInteger`: `TESTING_WORKSPACE_ID=0` vracelo `workspace_id: 0` a recept
|
|
122
|
+
s `workspace_id: 0` prošel Tier-1 cestou až k odeslání kroku. Odmítnutí jmenuje klíč, místo, kde se nastavuje, a normu
|
|
123
|
+
`api/docs/standards/tenant-allocation.md`. Tenantí kolej beze změny. Dopad na dnešní recepty a env služeb: 0 výskytů ≤ 0.
|
|
124
|
+
|
|
125
|
+
|
|
7
126
|
## [11.0.0] — 2026-09-17
|
|
8
127
|
|
|
9
128
|
### Changed — deploy job aplikuje seedy třídy PRODUCTION_LIKE (d.589, W589)
|
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,
|
|
@@ -446,7 +446,7 @@
|
|
|
446
446
|
"from": { "path": "api/config/shared-env.json", "text": true },
|
|
447
447
|
"severity": "deploy",
|
|
448
448
|
"owner": "BIZ-general",
|
|
449
|
-
"why": "the shared key set is one fact with one owner; measured on 2026-09-09, none of the nine copies matched the platform file and one key existed in no copy at all. The owner is api/config/shared-env.json and this row renders from it with the renderer the sync command writes with, so a service cannot be synced and reported drifted in the same hour; a run that cannot
|
|
449
|
+
"why": "the shared key set is one fact with one owner; measured on 2026-09-09, none of the nine copies matched the platform file and one key existed in no copy at all. The owner is api/config/shared-env.json and this row renders from it with the renderer the sync command writes with, so a service cannot be synced and reported drifted in the same hour; a run that cannot REACH the workspace says NOT RUN and names the command, because a render published with this package answers about the day it was published, not about the SSOT; a run that does reach it and finds no api/config/shared-env.json in the api checkout stops instead - a missing SSOT in a workspace the run can read is a finding and never a NOT RUN (automation-gates.md §5), and the message names the checkout to update and the api ref a CI job has to move",
|
|
450
450
|
"fix": "npx oa-sync-template shared-env --target .",
|
|
451
451
|
"doc": "api/docs/governance/confirmations/biz-service-manifest.md \u00a718"
|
|
452
452
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/conn-orch-validator",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.1.0",
|
|
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"
|
|
@@ -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,7 +21,7 @@
|
|
|
21
21
|
const fs = require('fs');
|
|
22
22
|
const path = require('path');
|
|
23
23
|
|
|
24
|
-
const { resolveWorkspacePath } = require('./workspaceRoot');
|
|
24
|
+
const { resolveWorkspacePath, apiCheckoutOf } = require('./workspaceRoot');
|
|
25
25
|
|
|
26
26
|
/** Never walked: neither is part of any repository's declared shape. */
|
|
27
27
|
const NEVER_WALKED = Object.freeze(['node_modules', '.git']);
|
|
@@ -211,11 +211,50 @@ function readReferencedFile({ from, workspaceRoot }) {
|
|
|
211
211
|
try {
|
|
212
212
|
return fs.readFileSync(file, 'utf8');
|
|
213
213
|
} catch (cause) {
|
|
214
|
-
throw new Error(
|
|
215
|
-
+ 'Fix: run with --workspace pointing at the directory that holds api/ and api_biz/.', { cause });
|
|
214
|
+
throw new Error(describeMissingReference({ from, workspaceRoot, file }), { cause });
|
|
216
215
|
}
|
|
217
216
|
}
|
|
218
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Why a `from:` file could not be read, said as the two different things it can
|
|
220
|
+
* be — because the reader's next move is a different one in each case.
|
|
221
|
+
*
|
|
222
|
+
* The run was pointed at a workspace that carries no api checkout: the layout is
|
|
223
|
+
* the problem, and `--workspace` is the answer it has always been.
|
|
224
|
+
*
|
|
225
|
+
* The run REACHED the checkout and the file is not in it: the layout is right
|
|
226
|
+
* and the advice to pass `--workspace` is one nobody can carry out — a CI job
|
|
227
|
+
* passes that flag itself. Measured on BIZ-invoicing 2026-09-17: the job reached
|
|
228
|
+
* `api/`, and the clone made at the api ref that job measures against did not
|
|
229
|
+
* carry `api/config/shared-env.json`, because that ref predates the file. What
|
|
230
|
+
* moves is the checkout (locally) or the ref (in CI), so the message names both,
|
|
231
|
+
* and it names the ref by variable — the two of confirmation
|
|
232
|
+
* `biz-service-manifest` 012.
|
|
233
|
+
*
|
|
234
|
+
* It stays a hard failure in both cases: a missing SSOT in a workspace this run
|
|
235
|
+
* CAN read is a finding, never a NOT RUN (`automation-gates.md` §5).
|
|
236
|
+
*
|
|
237
|
+
* @param {{ from: {path: string}, workspaceRoot: string, file: string }} params
|
|
238
|
+
* @returns {string}
|
|
239
|
+
*/
|
|
240
|
+
function describeMissingReference({ from, workspaceRoot, file }) {
|
|
241
|
+
const apiRoot = apiCheckoutOf(workspaceRoot);
|
|
242
|
+
const insideApiCheckout = apiRoot !== null
|
|
243
|
+
&& path.resolve(file).startsWith(apiRoot + path.sep);
|
|
244
|
+
|
|
245
|
+
if (!insideApiCheckout) {
|
|
246
|
+
return `[ManifestDiscovery] Referenced file not found - ${from.path} under ${workspaceRoot}. `
|
|
247
|
+
+ 'Fix: run with --workspace pointing at the directory that holds api/ and api_biz/.';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return `[ManifestDiscovery] Referenced file not found - ${from.path} is missing from the api checkout `
|
|
251
|
+
+ `${apiRoot}, which this run reached. The workspace is there and so is the checkout, so this is `
|
|
252
|
+
+ 'not a --workspace problem: the checkout does not carry the file at the state it stands on. '
|
|
253
|
+
+ `Fix: locally, update that checkout (git -C ${apiRoot} pull); in CI, move the api ref this job `
|
|
254
|
+
+ 'measures against to a commit that carries the file - API_UNIFORM_REF_CONTINUOUS for the '
|
|
255
|
+
+ 'validate-uniform job, API_UNIFORM_REF_DEPLOY for deploy-production.';
|
|
256
|
+
}
|
|
257
|
+
|
|
219
258
|
/**
|
|
220
259
|
* The whole referenced file as one value: `{ path, text: true }`.
|
|
221
260
|
*
|
|
@@ -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.
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* R8 permits and their reasons: api/docs/standards/tenant-allocation.md § Enforcement
|
|
9
9
|
*
|
|
10
10
|
* R1 the production compose pins its image immutably
|
|
11
|
-
* R2 the deploy resets to
|
|
11
|
+
* R2 the deploy never merges: it resets to the commit the pipeline measured,
|
|
12
|
+
* after verifying that commit is an ancestor of origin/production
|
|
12
13
|
* R3 CI Node major == Dockerfile Node major == engines.node major
|
|
13
14
|
* R4 no published ports — the only public entrypoint is doorman
|
|
14
15
|
* R5 images tagged with the full commit SHA, never the short one
|
|
@@ -138,15 +139,124 @@ function checkPublishedPorts(compose, add) {
|
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
// ── R2 — the deploy sequence ────────────────────────────────────────────────
|
|
143
|
+
//
|
|
144
|
+
// The rule, stated once and in full: a deploy on the box NEVER merges; it forces
|
|
145
|
+
// the checkout to the commit THIS PIPELINE MEASURED, and before it does, it
|
|
146
|
+
// verifies that commit is an ancestor of origin/production.
|
|
147
|
+
//
|
|
148
|
+
// Both halves are load-bearing, and each answers a different way a box ends up
|
|
149
|
+
// running something nobody measured:
|
|
150
|
+
//
|
|
151
|
+
// the measured commit — the runner read config/service/integration-contract.json
|
|
152
|
+
// at $CI_COMMIT_SHA and derived the migration directory and the seed list from
|
|
153
|
+
// it THERE. Resetting to origin/production instead lands the box on whatever
|
|
154
|
+
// the branch points at by the time the ssh step opens, so a push that arrives
|
|
155
|
+
// while the job runs leaves it applying a declaration no run ever measured.
|
|
156
|
+
//
|
|
157
|
+
// the ancestor check — a commit that is not on the branch this box follows is
|
|
158
|
+
// a pipeline for a different history, and resetting to it puts the box on code
|
|
159
|
+
// production never took. Fail-fast, before anything is touched.
|
|
160
|
+
//
|
|
161
|
+
// Until 2026-09-17 this function demanded the literal `git reset --hard
|
|
162
|
+
// origin/production`, which is the FIRST half's opposite. The packaged template
|
|
163
|
+
// had carried the two-step form since d.589b, so a repository that synced the
|
|
164
|
+
// block failed the gate byte-for-byte conformant — measured in pdfgen and
|
|
165
|
+
// hello-service, both of which run `ci:gate:contract` as the first before_script
|
|
166
|
+
// step of `test` and would therefore have failed before their first test. A rule
|
|
167
|
+
// that states yesterday's TEXT rather than the requirement is exactly what
|
|
168
|
+
// `.claude/rules/doc-code-binding.md` §1 calls a descriptive fact written by
|
|
169
|
+
// hand; `tests/unit/deployContractTemplate.test.js` is what now re-derives it,
|
|
170
|
+
// by running this gate over the template this package ships.
|
|
171
|
+
//
|
|
172
|
+
// There is no transition tolerance for the old form, and there must not be
|
|
173
|
+
// (`architecture-principles.md` §11): the six repositories still carrying it get
|
|
174
|
+
// a message naming today's form and the one command that writes it.
|
|
175
|
+
|
|
176
|
+
/** The branch a production box follows — the only ref an ancestor check may name. */
|
|
177
|
+
const PRODUCTION_BRANCH = 'origin/production';
|
|
178
|
+
|
|
179
|
+
/** `git reset --hard <ref>`, tolerant of the spacing a shell allows. */
|
|
180
|
+
const RESET_HARD = /(?:^|[^\w-])git\s+reset\s+--hard\s+(\S+)/g;
|
|
181
|
+
|
|
182
|
+
/** `git merge-base --is-ancestor <commit> <ref>` — the guard, same tolerance. */
|
|
183
|
+
const ANCESTOR_CHECK = /(?:^|[^\w-])git\s+merge-base\s+--is-ancestor\s+(\S+)\s+(\S+)/g;
|
|
184
|
+
|
|
185
|
+
/** `git merge …`. `git merge-base` is a different command, so it must not match. */
|
|
186
|
+
const GIT_MERGE = /(?:^|[^\w-])git\s+merge(?![-\w])/;
|
|
187
|
+
|
|
188
|
+
/** `git pull …` */
|
|
189
|
+
const GIT_PULL = /(?:^|[^\w-])git\s+pull(?:[^\w-]|$)/m;
|
|
190
|
+
|
|
191
|
+
/** The block this package ships, named in every fix so nobody has to retype it. */
|
|
192
|
+
const DEPLOY_SEQUENCE_FIX =
|
|
193
|
+
' Fix: npx oa-sync-template .gitlab-ci.yml --target . — the packaged block `oa-ci v1`\n'
|
|
194
|
+
+ ' carries the sequence: git fetch origin production, then\n'
|
|
195
|
+
+ ' `if ! git merge-base --is-ancestor "$COMMIT_SHA" origin/production; then exit 1; fi`,\n'
|
|
196
|
+
+ ' then `git reset --hard "$COMMIT_SHA"`.';
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* One shell word as this rule reads it: the separators a shell puts after an
|
|
200
|
+
* argument (`; then`, `&& …`) dropped, then the quotes.
|
|
201
|
+
*
|
|
202
|
+
* Tolerant, deliberately not lenient: what it normalises away is punctuation
|
|
203
|
+
* that cannot change which commit the command names.
|
|
204
|
+
*/
|
|
205
|
+
function shellWord(raw) {
|
|
206
|
+
return unquote(raw.replace(/[;&|)]+$/, ''));
|
|
207
|
+
}
|
|
208
|
+
|
|
141
209
|
function checkDeploySequence(ci, add) {
|
|
142
|
-
if (
|
|
210
|
+
if (GIT_PULL.test(ci)) {
|
|
143
211
|
add('R2', "'git pull' in the deploy path.\n"
|
|
144
212
|
+ ' A local modification on the server turns the merge into a conflict and aborts the deploy halfway.\n'
|
|
145
|
-
+
|
|
213
|
+
+ DEPLOY_SEQUENCE_FIX);
|
|
146
214
|
}
|
|
147
|
-
if (
|
|
148
|
-
add('R2', "
|
|
149
|
-
+ '
|
|
215
|
+
if (GIT_MERGE.test(ci)) {
|
|
216
|
+
add('R2', "'git merge' in the deploy path.\n"
|
|
217
|
+
+ ' A deploy that merges builds a commit that exists on no branch, so what the box runs\n'
|
|
218
|
+
+ ' is no longer any commit a pipeline measured.\n'
|
|
219
|
+
+ DEPLOY_SEQUENCE_FIX);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const resets = [...ci.matchAll(RESET_HARD)]
|
|
223
|
+
.map((match) => ({ at: match.index, target: shellWord(match[1]) }));
|
|
224
|
+
|
|
225
|
+
if (resets.length === 0) {
|
|
226
|
+
add('R2', "No 'git reset --hard' found in .gitlab-ci.yml.\n"
|
|
227
|
+
+ ' The deploy has to force the working tree to the commit this pipeline measured;\n'
|
|
228
|
+
+ ' nothing here does.\n'
|
|
229
|
+
+ DEPLOY_SEQUENCE_FIX);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Ordering is read over the whole file rather than per job: which YAML key a
|
|
234
|
+
// line sits under is the service's own half of the pipeline, and a gate that
|
|
235
|
+
// parsed jobs would be judging that half too. What it does measure is the
|
|
236
|
+
// thing that can go wrong — a guard that names another commit, another branch,
|
|
237
|
+
// or runs after the reset it is supposed to protect.
|
|
238
|
+
const guards = [...ci.matchAll(ANCESTOR_CHECK)]
|
|
239
|
+
.map((match) => ({ at: match.index, commit: shellWord(match[1]), branch: shellWord(match[2]) }))
|
|
240
|
+
.filter((guard) => guard.branch === PRODUCTION_BRANCH);
|
|
241
|
+
|
|
242
|
+
for (const reset of resets) {
|
|
243
|
+
if (reset.target === PRODUCTION_BRANCH) {
|
|
244
|
+
add('R2', `The deploy resets to ${PRODUCTION_BRANCH}, not to the commit this pipeline measured.\n`
|
|
245
|
+
+ ' origin/production is whatever the branch points at by the time the ssh step opens:\n'
|
|
246
|
+
+ ' a push landing while this job runs leaves the box applying a declaration no run\n'
|
|
247
|
+
+ ' ever measured, because the migration set was derived at the pipeline commit.\n'
|
|
248
|
+
+ DEPLOY_SEQUENCE_FIX);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (!guards.some((guard) => guard.at < reset.at && guard.commit === reset.target)) {
|
|
252
|
+
add('R2', `The deploy resets to ${reset.target}, which is not verified to be an ancestor of `
|
|
253
|
+
+ `${PRODUCTION_BRANCH} first.\n`
|
|
254
|
+
+ ' A commit that is not on the branch this box follows belongs to a different history,\n'
|
|
255
|
+
+ ' and resetting to it puts the box on code production never took.\n'
|
|
256
|
+
+ ` Expected: git merge-base --is-ancestor ${reset.target} ${PRODUCTION_BRANCH}, on a line\n`
|
|
257
|
+
+ ' BEFORE the reset, naming that same commit.\n'
|
|
258
|
+
+ DEPLOY_SEQUENCE_FIX);
|
|
259
|
+
}
|
|
150
260
|
}
|
|
151
261
|
}
|
|
152
262
|
|
|
@@ -202,6 +202,16 @@ function assertAllowedTenant(tenantId, options) {
|
|
|
202
202
|
* fallback there would hide exactly the misconfiguration the boundary exists to
|
|
203
203
|
* surface (architecture-principles.md §3).
|
|
204
204
|
*
|
|
205
|
+
* What it DOES hold is the shape of a workspace id: a POSITIVE integer. The
|
|
206
|
+
* recorded decision names it (api/docs/governance/confirmations/tier1-runner-workspace.md,
|
|
207
|
+
* entry 002) and `.claude/rules/workspace-architecture.md` says `workspace_id` 0
|
|
208
|
+
* is not a value; the guard checked `Number.isInteger` only, so `0` and `-5` came
|
|
209
|
+
* back normalized and a cookbook could send the boot probe into a workspace that
|
|
210
|
+
* cannot exist. The check lives here rather than in `normalizeNamespaceId()`
|
|
211
|
+
* because the tenant rail already refuses both — by the class allowlist, for a
|
|
212
|
+
* different reason and with a different fix — and one message per boundary is
|
|
213
|
+
* what keeps the refusal actionable.
|
|
214
|
+
*
|
|
205
215
|
* There is deliberately NO class allowlist here, and that is not an omission:
|
|
206
216
|
*
|
|
207
217
|
* - `api/docs/standards/tenant-allocation.md` allocates environment classes to
|
|
@@ -222,7 +232,7 @@ function assertAllowedTenant(tenantId, options) {
|
|
|
222
232
|
* `purpose` is the name the caller knows the id by (here, the cookbook key
|
|
223
233
|
* that carries it), `setBy` where that name is given a value.
|
|
224
234
|
* @returns {number} the normalized id.
|
|
225
|
-
* @throws {Error} when the id is absent
|
|
235
|
+
* @throws {Error} when the id is absent, not an integer, or not positive.
|
|
226
236
|
*/
|
|
227
237
|
function assertWorkspaceId(workspaceId, options) {
|
|
228
238
|
if (options === null || typeof options !== 'object') {
|
|
@@ -232,7 +242,14 @@ function assertWorkspaceId(workspaceId, options) {
|
|
|
232
242
|
requireOriginText(purpose, 'purpose');
|
|
233
243
|
requireOriginText(setBy, 'setBy');
|
|
234
244
|
|
|
235
|
-
|
|
245
|
+
const value = normalizeNamespaceId(workspaceId, { purpose, setBy });
|
|
246
|
+
if (value <= 0) {
|
|
247
|
+
throw new Error(`[TestNamespace] Invalid ${purpose}=${value} - Expected a positive integer `
|
|
248
|
+
+ 'workspace id (1 or greater); 0 identifies no workspace. '
|
|
249
|
+
+ `Fix: set ${purpose} to the workspace the run owns - ${setBy} - `
|
|
250
|
+
+ 'see api/docs/standards/tenant-allocation.md § The allocation.');
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
236
253
|
}
|
|
237
254
|
|
|
238
255
|
/**
|
|
@@ -275,9 +292,19 @@ function getTestNamespace() {
|
|
|
275
292
|
setBy: ENV_SET_BY
|
|
276
293
|
});
|
|
277
294
|
|
|
295
|
+
// The workspace takes the same shape check a cookbook's declared value takes,
|
|
296
|
+
// under the same function and the same sentence. It did not, and that made the
|
|
297
|
+
// boundary half a boundary: the positivity rule held for a value a cookbook
|
|
298
|
+
// declares and not for the variable every integration test and every boot
|
|
299
|
+
// probe actually runs under (measured: TESTING_WORKSPACE_ID=0 returned
|
|
300
|
+
// workspace_id 0). The tenant above shows the shape — the env is one caller of
|
|
301
|
+
// a boundary, never its owner.
|
|
278
302
|
return {
|
|
279
303
|
tenant_id: tenantId,
|
|
280
|
-
workspace_id: readNamespaceId('TESTING_WORKSPACE_ID')
|
|
304
|
+
workspace_id: assertWorkspaceId(readNamespaceId('TESTING_WORKSPACE_ID'), {
|
|
305
|
+
purpose: 'TESTING_WORKSPACE_ID',
|
|
306
|
+
setBy: ENV_SET_BY
|
|
307
|
+
})
|
|
281
308
|
};
|
|
282
309
|
}
|
|
283
310
|
|
|
@@ -68,12 +68,24 @@ variables:
|
|
|
68
68
|
# ABOVE api_biz, so the sibling api/ still lands beside this checkout, which
|
|
69
69
|
# is the layout verify-deploy-uniform.sh checks.
|
|
70
70
|
GIT_CLONE_PATH: $CI_BUILDS_DIR/oa-uniform/$CI_CONCURRENT_ID/api_biz/__SERVICE_NAME__
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
# biz-service-manifest 008 point 2).
|
|
71
|
+
# WHERE the platform SSOT is read from. Read access is granted by the api
|
|
72
|
+
# project's CI job-token allowlist - the owner's step, outside any
|
|
73
|
+
# repository (confirmation biz-service-manifest 008 point 2).
|
|
75
74
|
API_PROJECT_PATH: onlineapps/oadrive/infra-mono
|
|
76
|
-
|
|
75
|
+
# WHICH STATE of that SSOT a run is measured against - two facts, two names
|
|
76
|
+
# (confirmation biz-service-manifest 012; 010 for the continuous run, 008
|
|
77
|
+
# point 3 for the binding one).
|
|
78
|
+
#
|
|
79
|
+
# The continuous gate judges the commit in front of it, so it measures
|
|
80
|
+
# against the branch the platform develops on - the same one the libraries
|
|
81
|
+
# SSOT is read from. api production moves only at the END of a production
|
|
82
|
+
# path, and pushing it deploys infra, so a continuous gate pointed there
|
|
83
|
+
# measures a months-old ref and goes red for a reason outside this
|
|
84
|
+
# repository (automation-gates.md §5).
|
|
85
|
+
API_UNIFORM_REF_CONTINUOUS: main
|
|
86
|
+
# The deploy confirms against what is being deployed, and the migration
|
|
87
|
+
# runner this job carries onto the box comes from that same clone.
|
|
88
|
+
API_UNIFORM_REF_DEPLOY: production
|
|
77
89
|
|
|
78
90
|
# The uniform of THIS commit, on every pipeline - confirmation
|
|
79
91
|
# biz-service-manifest 010. Entry 008 places the BINDING run before the SSH step
|
|
@@ -102,7 +114,7 @@ validate-uniform:
|
|
|
102
114
|
# second place to keep in step (change-discipline.md - One rail per
|
|
103
115
|
# concern). It comes from the package this repository pins, beside the
|
|
104
116
|
# engine it runs.
|
|
105
|
-
- sh node_modules/@onlineapps/conn-orch-validator/templates/business-service/scripts/verify-deploy-uniform.sh "$CI_PROJECT_DIR" "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/${API_PROJECT_PATH}.git" "$
|
|
117
|
+
- sh node_modules/@onlineapps/conn-orch-validator/templates/business-service/scripts/verify-deploy-uniform.sh "$CI_PROJECT_DIR" "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/${API_PROJECT_PATH}.git" "$API_UNIFORM_REF_CONTINUOUS"
|
|
106
118
|
rules:
|
|
107
119
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
108
120
|
- if: $CI_COMMIT_BRANCH == "main"
|
|
@@ -247,7 +259,7 @@ deploy-production:
|
|
|
247
259
|
# not look everywhere, and nothing reaches the box. There is no flag and no
|
|
248
260
|
# variable that turns this off (automation-gates.md §1 requirement 5).
|
|
249
261
|
- npm ci
|
|
250
|
-
- sh node_modules/@onlineapps/conn-orch-validator/templates/business-service/scripts/verify-deploy-uniform.sh "$CI_PROJECT_DIR" "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/${API_PROJECT_PATH}.git" "$
|
|
262
|
+
- sh node_modules/@onlineapps/conn-orch-validator/templates/business-service/scripts/verify-deploy-uniform.sh "$CI_PROJECT_DIR" "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/${API_PROJECT_PATH}.git" "$API_UNIFORM_REF_DEPLOY"
|
|
251
263
|
# R1: refuse to deploy without the immutable target the build stage published.
|
|
252
264
|
- |
|
|
253
265
|
if [ -z "${BIZ_IMAGE_DIGEST:-}" ]; then
|
|
@@ -474,13 +486,13 @@ deploy-production:
|
|
|
474
486
|
echo "[deploy] Missing environment variable -$missing in $DEPLOY_PATH/$SERVICE_ENV. Expected: the migration account of $DB_SCHEMA (the same account as DB_USER, granted on that schema only) and the endpoint it is reached at. Fix: add the keys to that file on this box; config/env-templates/ declares each of them with the reason it exists." >&2
|
|
475
487
|
exit 1
|
|
476
488
|
fi
|
|
477
|
-
# The runner is carried from the api clone at
|
|
489
|
+
# The runner is carried from the api clone at API_UNIFORM_REF_DEPLOY, so a ref
|
|
478
490
|
# older than the seed entry point would reach `command not found` here -
|
|
479
491
|
# after the migrations and before the switch, in a production deploy and
|
|
480
492
|
# nowhere else. Checked before the first statement instead
|
|
481
493
|
# (automation-gates.md §1 requirement 4).
|
|
482
494
|
if [ -n "$DB_SEEDS" ] && ! declare -F apply_mariadb_seeds_over_tcp > /dev/null 2>&1; then
|
|
483
|
-
echo "[deploy] FATAL: the migration runner carried from the api clone has no apply_mariadb_seeds_over_tcp, and $DB_SCHEMA declares PRODUCTION_LIKE seeds this deploy has to apply. Expected: that entry point in api/scripts/lib/mariadb-migrations.sh. Fix: move
|
|
495
|
+
echo "[deploy] FATAL: the migration runner carried from the api clone has no apply_mariadb_seeds_over_tcp, and $DB_SCHEMA declares PRODUCTION_LIKE seeds this deploy has to apply. Expected: that entry point in api/scripts/lib/mariadb-migrations.sh. Fix: move API_UNIFORM_REF_DEPLOY to an api commit that carries it, and re-run this deploy." >&2
|
|
484
496
|
exit 1
|
|
485
497
|
fi
|
|
486
498
|
# The precondition confirmation 001 demands before any migration: the
|
|
@@ -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
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
# WHY THREE ARGUMENTS AND NOTHING FROM THE ENVIRONMENT. The script knows what to
|
|
33
33
|
# measure, never where the pieces come from (`architecture-principles.md` §1):
|
|
34
34
|
# the job passes the checkout GitLab made, the api repository URL carrying its
|
|
35
|
-
# job token, and the api ref
|
|
35
|
+
# job token, and the api ref THAT job measures against (the continuous job and
|
|
36
|
+
# the deploy job name different ones - confirmation biz-service-manifest 012). Nothing here reads
|
|
36
37
|
# `process.env`, so the same invocation is reproducible outside CI.
|
|
37
38
|
#
|
|
38
39
|
# THE LAYOUT IT NEEDS, AND WHY THE JOB HAS TO ASK FOR IT. The engine answers the
|
|
@@ -69,7 +70,7 @@ API_URL="$2"
|
|
|
69
70
|
API_REF="$3"
|
|
70
71
|
|
|
71
72
|
[ -n "$API_URL" ] || die "The api repository URL is empty - the uniform reads the platform SSOT from that checkout, and this script does not guess where it lives. Expected: the URL the job builds from CI_JOB_TOKEN. Fix: pass it as the second argument." 2
|
|
72
|
-
[ -n "$API_REF" ] || die "The api ref is empty - which state of the platform this deploy is measured against is a decision, not a default. Expected: the
|
|
73
|
+
[ -n "$API_REF" ] || die "The api ref is empty - which state of the platform this deploy is measured against is a decision, not a default. Expected: the ref the calling job declares (API_UNIFORM_REF_CONTINUOUS for validate-uniform, API_UNIFORM_REF_DEPLOY for deploy-production). Fix: pass it as the third argument." 2
|
|
73
74
|
[ -d "$SERVICE_ROOT" ] || die "Service root not found - '$SERVICE_ROOT' is not an existing directory. Expected: the checkout being deployed. Fix: pass \$CI_PROJECT_DIR." 2
|
|
74
75
|
|
|
75
76
|
SERVICE_ROOT=$(cd "$SERVICE_ROOT" && pwd)
|