@onlineapps/conn-orch-validator 11.0.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,58 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [12.0.0] — 2026-09-17
8
+
9
+ ### Fixed — BREAKING: R2 tvrdí pravidlo sekvence nasazení, ne včerejší text (d.614, W614)
10
+
11
+ `src/utils/deployContract.js`: R2 vyžadovalo doslovné `git reset --hard origin/production`, což je OPAK toho, co blok
12
+ `oa-ci v1` nese od d.589b (`e138d1f9`) — `git merge-base --is-ancestor "$COMMIT_SHA" origin/production` a pak
13
+ `git reset --hard "$COMMIT_SHA"`. Brána `ci:gate:contract` je první krok `before_script` jobu `test`, takže služba se
14
+ 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
15
+ neslo starý tvar, tedy jednu synchronizaci od téhož). R2 nově čte PRAVIDLO: nasazení nikdy nemerguje (`git pull` i
16
+ `git merge`, `merge-base` není merge), resetuje na commit, který pipeline měřila, a guard `merge-base --is-ancestor …
17
+ origin/production` musí stát PŘED tím resetem a jmenovat týž commit. Odolné vůči uvozovkám, mezerám a `; then`,
18
+ nikoli benevolentní: reset bez guardu, guard až za resetem, guard na jiný commit nebo jinou větev i samotný včerejší
19
+ `git reset --hard origin/production` jsou FAIL. Žádná přechodná tolerance (architecture-principles §11); hláška jmenuje
20
+ 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á
21
+ 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,
22
+ kterou uniforma nenese.
23
+
24
+ ### Added — sada „vlastní šablona projde vlastní bránou“ (d.614, W614)
25
+
26
+ `tests/unit/deployContractTemplate.test.js` renderuje `templates/business-service` týmiž helpery, kterými ji renderuje
27
+ `oa-sync-template --new` (`renderTree` + `deriveParams` + přejmenování env šablony), a pouští nad výsledkem
28
+ `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
29
+ 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
30
+ (reset bez guardu, včerejší tvar, kontrolní případ mimo sekvenci). Ruční vstupy sad `deployContract`, `deployContractR7Declared`,
31
+ `bizCiGateCli.{envContract,installContract}` a helperu `conformantBizRepo` jsou srovnané na dnešní tvar.
32
+
33
+ ### Changed — BREAKING: blok `G-CI` deklaruje DVA refy api místo jednoho (d.612, W612)
34
+
35
+ `templates/business-service/.gitlab-ci.yml`: `API_UNIFORM_REF_CONTINUOUS: main` čte průběžný job `validate-uniform`,
36
+ `API_UNIFORM_REF_DEPLOY: production` čte `deploy-production` — jeho krok uniformy i běžec migrací a seedů, kterého nese na
37
+ box. `API_UNIFORM_REF` zaniká bez aliasu a bez přechodného čtení. Průběžná brána (konfirmace `biz-service-manifest` 010)
38
+ měřila api `production`, která se pohne až na konci produkční cesty (push té větve je nasazení infra), takže byla červená
39
+ u všech osmi služeb z důvodu mimo kteroukoli z nich. Rozhodnutí vlastníka: `biz-service-manifest` 012. Služba srovná blok
40
+ jedním během `npx oa-sync-template .gitlab-ci.yml --target .`; do té doby její `validate-uniform` měří dál `production`.
41
+ `scripts/verify-deploy-uniform.sh` bere ref argumentem jako dosud; jeho hláška o prázdném refu jmenuje obě proměnné.
42
+
43
+ ### Fixed — `ManifestDiscovery` „Referenced file not found“ radí to, co jde udělat (d.611, W612)
44
+
45
+ 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
46
+ jmenuje checkout a `Fix:` říká aktualizovat ho lokálně, nebo posunout ref api, proti kterému job měří. Workspace bez api
47
+ 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
48
+ NOT RUN; říká to i `why` řádku `G-SHARED-ENV`.
49
+
50
+ ### Fixed — workspace id ≤ 0 se odmítá na obou kolejích (d.606 + d.606b, W606)
51
+
52
+ `assertWorkspaceId` odmítá `0` i záporná čísla a prochází jím hodnota deklarovaná receptem (`defaults.workspace_id`,
53
+ per-step) i hodnota z prostředí (`TESTING_WORKSPACE_ID` v `getTestNamespace()`). Konfirmace `tier1-runner-workspace` 002
54
+ říká „kladné celé číslo“, kód kontroloval jen `Number.isInteger`: `TESTING_WORKSPACE_ID=0` vracelo `workspace_id: 0` a recept
55
+ 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
56
+ `api/docs/standards/tenant-allocation.md`. Tenantí kolej beze změny. Dopad na dnešní recepty a env služeb: 0 výskytů ≤ 0.
57
+
58
+
7
59
  ## [11.0.0] — 2026-09-17
8
60
 
9
61
  ### Changed — deploy job aplikuje seedy třídy PRODUCTION_LIKE (d.589, W589)
@@ -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 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",
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": "11.0.0",
3
+ "version": "12.0.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"
@@ -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(`[ManifestDiscovery] Referenced file not found - ${from.path} under ${workspaceRoot}. `
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
  *
@@ -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 origin/production rather than merging
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 (/(^|[^\w-])git\s+pull([^\w-]|$)/m.test(ci)) {
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
- + ' Fix: git fetch origin production && git reset --hard origin/production');
213
+ + DEPLOY_SEQUENCE_FIX);
146
214
  }
147
- if (!/git\s+reset\s+--hard\s+origin\/production/.test(ci)) {
148
- add('R2', "No 'git reset --hard origin/production' found in .gitlab-ci.yml.\n"
149
- + ' Fix: the deploy must force the working tree to the pushed commit, matching the infra contract.');
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 or not an integer.
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
- return normalizeNamespaceId(workspaceId, { purpose, setBy });
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
- # Where the platform SSOT is read from, and which state of it a run is
72
- # measured against. Read access is granted by the api project's CI job-token
73
- # allowlist - the owner's step, outside any repository (confirmation
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
- API_UNIFORM_REF: production
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" "$API_UNIFORM_REF"
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" "$API_UNIFORM_REF"
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 API_UNIFORM_REF, so a ref
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 API_UNIFORM_REF to an api commit that carries it, and re-run this deploy." >&2
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
@@ -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 that production follows. Nothing here reads
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 branch production follows. Fix: pass it as the third argument." 2
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)