Spec for the first two sub-projects of the automated FaceKom release + e2e pipeline. Written 2026-08-11 against three simultaneous in-flight releases (NÚSZ 1.9.11.48, CIB 1.9.11.102, Generali 1.9.11.19).
Read these first — this extends them, it does not replace them
devel-update-and-release-flow (Phases 0–5) · release-workflow-design-handover (the delta and the four open design questions) ·
/Users/levander/coding/facekom/RELEASE-WORKFLOW-HANDOVER.md(the NÚSZ cut).
0. The pipeline this belongs to
P0 resolve → P1 devel update → P2 breaking-change sweep → P3 ticket collection
→ P4 test-plan generation → P5 execute (tiers 1→2→3) → P6 TjK → P7 cut
→ P8 back-merge → P9 close the loop
Everything is prepare-and-gate. No phase commits, pushes, opens a PR, or tags. Each write-shaped step produces the artifact plus the exact command, and stops. Decided 2026-08-11; consistent with the standing no-auto-commit / no-auto-PR rules.
P8 and P9 are in the pipeline because release-workflow-design-handover §7 and §10 establish that
nobody does them today — Raiffeisen .100 currently owes a back-merge, and its dev tickets sat
Pending a day after merge.
Sub-project decomposition and build order
| Sub-project | Size | Infra | Status | |
|---|---|---|---|---|
| A | Partner manifest | small | none | this spec |
| B | Breaking-change sweep | medium | none | this spec |
| C | Tier-2 boot harness | large | disposable stack | roadmap |
| D | Test-plan generator | medium | none | roadmap |
| E | Release driver (P0–P9 orchestrator) | medium | none | roadmap |
| F | Tier-3 browser suite | large | fk-dev | roadmap |
A and B are hermetic — they run on the Mac, need no box, and are the only pieces that pay off inside the three releases currently being cut.
The three test tiers (context for C–F, not built here)
Full treatment, with the per-runbook evidence and the not-automatable-in-principle list: facekom-test-tiers.
| Tier | Needs | Covers | Today |
|---|---|---|---|
| 1 static / hermetic | nothing | the clean-merge-broken-product class, lint, unit, config schema | B builds this |
| 2 booted, non-browser | app running | flow registration, RPC, cron CLI, emails actually sending, exports, DB | nothing — ~60% of the runbook |
| 3 browser | full stack + media | customer/operator UI flows | 12/45 k6 files ported |
Tier 2 dissolves the k6 seeding blocker
k6-e2e-harness-vuer-oss lists option (a) — “Node pre-seed step reusing
helper.ts, handing k6 a fixtures JSON viaopen()” — as the leanest fix for the 30 unportable tests. A tier-2 harness that boots the app to assert against it is that pre-seed step. One build, two problems. Argues for C before F.
A. Partner manifest
A.1 The problem
Partner facts live in four places that disagree, and none of them records which repos a partner ships:
| Source | Count | Holds | Gap |
|---|---|---|---|
.claude/scripts/tjk/partners.json | 14 | TjK shape/priorShape, YouTrack binding | no repos, no build path |
| client-registry | 22 | repo ↔ YT suffix ↔ build path, prose | not machine-readable, (confirm) entries |
vuer_build/partner/<name>/ | 39 dirs | legacy build contexts | build-only |
vuer-release/projects/<partner>/ | 14 | modern descriptors | 7 have zero release.json — defined, never built |
The repo-set gap is live: CIB ships portal_css (that is where FKITDEV-9197 broke) while NÚSZ and
Generali are vuer_oss + vuer_css only. Any sweep that assumes two repos misses CIB’s actual break.
A.2 Decision
Extend .claude/scripts/tjk/partners.json into the single partner manifest. Do not create a new file.
It is already executable JSON, already consumed by /fk-tjk, already carries the YouTrack binding, and
lives under .claude/scripts/ — which is where executable config belongs per the knowledge-placement rule.
client-registry stays as the human-readable companion and stops being read by tooling.
A.3 Fields
What is actually in the file today (14 keys + _meta): display, ytProject, defaultEnv,
priorShape, priorShapeVerified, fileStem; _meta.targetShape = "release-doc".
Two existing properties must survive the extension
- An unknown key HALTS
/fk-tjkon step one, by design. Adding partners is safe; loosening that fail-closed behaviour is not. NÚSZ was missing from the file entirely until 2026-08-11.ytProjectis pinned because the YouTrack shortName varies — MicroSec isMF, DÁP isASSDAP. Do not derive it, do not overwrite it. The newytProjectsmap is additional, for release scope.
Added per partner:
| Field | Meaning | Why it cannot be derived |
|---|---|---|
repo | customization branch suffix — customization/<repo> | variants: -instant, -v2, -f1 |
ytProjects | explicit {cr, bug, sla, ass} | Generali splits them: CR/BUG = GRALIA, SLA/ASS = GRALI |
repos[] | source repos this partner ships, each marked tagged or untagged | see below |
buildPath | vuer-release | vuer_build | esign | CIB is legacy, NÚSZ/Generali modern |
releaseProject | project dir name | ≠ repo, e.g. polgaribank → polgaribank-facekom |
changelogPath | customization/RELEASE.MD | RELEASE.md | null | varies per partner; several have none |
changelogStyle | heading form | NÚSZ ## 1.9.11.44, CIB ## [1.9.11.98] - 2026-03-10, MVM ### …, Raiffeisen # … |
tagPrefix | source-tag prefix | ≠ partner key: Generali’s is generali-atvilagitas-. release_tickets.py takes this |
topology | customization-branch | release-branch | the release-workflow-design-handover §1 ambiguity, made explicit per partner |
Not every repo a partner ships carries the partner tag
Verified 2026-08-11 by
git ls-remote:vuer_ossandvuer_csseach hold 102cib-1.9.11.*tags, andportal_cssholds none. CIB genuinely shipsportal_css— FKITDEV-9197 broke there — butportal_cssis on its own lifecycle (Travis, own audit gate) and is never tagged with the partner version. Sorepos[]is two facts, not one: which repos the sweep must scan (all of them — this is what catches the CIB break) versus which repos get the release tag (only thetaggedones). Conflating them makes A’s--selfcheckfail on everyportal_cssrow and makes P7 look for a tag that will never exist.
Seed
repos[]from the existing per-partner matrix
projects/facekom-v2/customization-clients.mdalready holds 22 clients × {repo, customization scope, bin scripts, CORE-override risk, scale, last activity}. It is the best existing input for A, and it records that CORE-override isYfor 21 of 22 partners — onlyinstacashis config-only.
The version is NOT a manifest field
Release trains are per partner and numbers get renumbered after the fact (NÚSZ .47 → .48 because another team took .47). A version is not stable until the tag exists. Resolve it only from that partner’s open
ASS<suffix>Assistance Type: Releaseissue, or fromgit ls-remote --tags. Never from the manifest, never from a sibling partner, never from conversation. This rule is already enforced in/fk-tjkand must not regress here.
A.4 Verification
partners.json --selfcheck (matching the existing convention on the TjK renderers) asserts every row
against real state, so the manifest is checked rather than trusted:
repos[]—git ls-remote --exit-code origin refs/heads/customization/<repo>in each named repobuildPath—vuer-release/projects/<releaseProject>/exists, elsevuer_build/partner/<repo>/changelogPath— the file exists at the customization branch tipytProjects— each project id resolves via the read-only YouTrack client
--selfcheck is read-only and hits no write path.
B. Breaking-change sweep
B.1 What it detects
One failure class, four shapes. From devel-dependency-removal-breaks-partner-customization:
Git reconciles text, and the two sides never touch the same line. The merge is clean and the product is broken.
| # | devel side | partner side | static? |
|---|---|---|---|
| B1 | dependency deleted from package.json as “unused” | customization/ requires it | yes |
| B2 | .js → .ts rename | extensionless require() | yes |
| B3 | core file deleted / function refactored away | override binds by path or calls the old name | mostly |
| B4 | dependency goes ESM-only (MJML v4→v5) | CommonJS require() in templates | no — emits a runtime check |
| B5 | config key restructured | partner prod config still on the old key | yes |
| B6 | new/changed migrations | partner has divergent migrations | yes |
Phase 4 gate 4 already seeds this — B is its automation, not a new idea
devel-update-and-release-flow’s fourth release gate is “breaking-change / config sweep” and names three commands:
git log <tag>..HEAD --grep='BREAKING' --grep='!:' -E,git diff --name-only <tag>..HEAD -- db/migrate/, andgit diff <tag>..HEAD -- config/. B turns that manual gate into structured findings and adds the four detectors the gate does not name. Conventional-commit!:/BREAKING CHANGE:scanning comes free — the repos enforce conventional commits by ruleset, so the marker is reliable.
B.2 Input and output
Input: a merge commit, or a base..head range, on a customization branch — i.e. what P1 just produced,
before it is pushed. That timing is the entire point.
Output: structured JSON findings, one record per hit:
detector B1|B2|B3|B4
severity break | drift | needs-runtime-check
repo vuer_oss | vuer_css | portal_css | …
develChange what devel did (path, symbol, version delta)
partnerUsage file:line under customization/ that depends on it
confirmed static-proven | needs-runtime-check
suggestedFix e.g. restore "multer": "1.4.5-lts.2"
D later consumes this JSON directly; needs-runtime-check records become mandatory items in the generated
runbook rather than pass/fail results.
B.3 The detectors
B1 — dependency removal. Removed names from
git diff <merge>^1..<merge> -- package.json, then per name git grep for require('<pkg>') and
from '<pkg>' under customization/. Two extra checks that are the actual value:
- hoisting probe — resolves in
node_modulesbut absent frompackage.json⇒ a finding, not a pass. Undeclared-but-hoisted is unowned. - major drift — CIB declared
^10.0.0, ran the hoisted14.0.1while “working”.
Must run per repo in repos[] — the CIB break was in portal_css, which a two-repo assumption skips.
B2 — .js → .ts renames. git diff --diff-filter=R for renames that change extension, then grep
extensionless requires of those basenames across the whole tree, not just customization/ — the NÚSZ
occurrences were in cron.js and bin/remove-old-video-files.js. n/no-missing-require catches this when
lint runs, but yarn lint ignores test/* and root *.ts is unlinted, so the grep is the backstop.
B3 — deleted / refactored core. git diff --diff-filter=D for deletions, then whether anything under
customization/ requires the deleted path. The harder half is refactor-away (FKITDEV-9194’s auth()):
symbols exported at base, absent at head, that customization/ calls by name.
B4 — module-system flips. A dependency whose major bumped and whose new package is ESM-only
("type": "module" or an ESM-only exports map). Then, across the whole tree — core and customization/
alike, since letter definitions live in both — which files require() it, and does any such file mix a
top-level import with a bare require(?
B4's failure mode is silence
That mix makes Node treat the file as ESM,
requireis undefined, the letter type never registers, and the email silently never sends — with green CI. Unit tests do not boot EmailService. B4 therefore never emits a pass; it emits a mandatory runtime check (“trigger each email, confirm it sends”) into the plan.
B5 — config key restructuring. git diff <tag>..HEAD -- config/ plus the reader side: a key path that
disappeared from the shipped configs while code reads a new path.
The canonical case is silent in every layer
devel restructured
browsers.showOldBrowserWarning→browsers.oldBrowserWarning.{show,delay}, andserver/web/Template.js:123reads the new path. Any partner whose prod config still carries the old key silently loses the old-browser warning — no error, no log line.docs/config/was not updated either, so the JSON schema does not warn. Nothing anywhere catches this.
Same range, list new-but-default-off keys separately (safe, but they belong in the changelog): sftp,
dataCleanupCron, transferRoomCron, documentRecognitionVersion, roomExportFilesExtendedName.
B6 — migrations. git diff --name-only <tag>..HEAD -- db/migrate/. Two outputs: new migrations in this
range (a deploy-ordering fact the runbook needs), and partner-vs-core divergence — the count and identity
of migrations the partner has that core does not.
Divergent migrations are not academic
nusz and cofidis carry 6 divergent migrations stamped 2017–2021 that back-fill out of order. Combined with
db.syncOnStart: true(§ open questions) this is how an environment gets silently corrupted by a branch switch. B6 is what makes that visible before P5 touches a box.
B.4 Gotchas to bake in
These are not hypotheticals — each has already cost a session
- rtk corrupts git output, and its failure mode is a false negative — which is fatal for a detector. It ate
:server/socket/fromgit show <rev>:<path>(bogus “unknown revision”), zeroes| wc -land| grep -c, and — worse, found 2026-08-11 — returned the WRONG commit forgit log -1and garbledgit show | grepinto a false “no match”. Every B detector is a git query whose negative result means “clean”. A garbled grep therefore reports a partner as safe when it is broken. Non-negotiable: drive every git call from a Pythonsubprocessargs list, never a shell pipeline, and treat any detector result as unverified if it came through plain rtk. (rtk-mangles-curl-and-pipes, rtk-pipe-count-mangling)- Narrowed fetch refspecs.
git fetch origin develwrites onlyFETCH_HEADand leavesorigin/develstale, exit 0, success message. Use an explicit+refs/heads/X:refs/remotes/origin/Xand cross-check withgit ls-remotebefore trusting anyorigin/<branch>. (narrowed-fetch-refspec-stale-devel-merge)- Worktree
node_modulesdrifts from the branch lock. Runyarn install --frozen-lockfilein the worktree first, or B1’s hoisting probe reports fiction. A base-commit worktree needs its own install — symlinkingnode_modulesfrom the merged tree voided a first attempt whenyarn.lockmoved 800 lines.- Node ≥ 24 is mandatory.
geoip-lite@2.0.3declaresengines.node >= 24.0.0, soyarn install --frozen-lockfilehard-fails on Node 22.package.jsonengines: >=22.18.0is stale; CI pins 24. Without a correct install, B1 cannot probe hoisting at all.yarn lintignorestest/*, root*.tsis unlinted, and nothing typechecks the k6 suite. Never treat a green lint as coverage.
B.5 Testing
Per the standing lazy-but-checked rule: one runnable check, no fixture authoring. Each detector must fire on its own known-historical merge:
| Detector | Historical case |
|---|---|
| B1 | FKITDEV-9197 — CIB / portal_css, multer hard-missing + uuid transitive |
| B2 | NÚSZ devel update — FFmpegService/JanusService .js→.ts stragglers |
| B3 | FKITDEV-9194 — deleted core file / auth() refactored away |
| B4 | FKITDEV-8727 — MJML v4→v5 ESM-only |
| B5 | browsers.showOldBrowserWarning → browsers.oldBrowserWarning.{show,delay} |
| B6 | nusz ↔ cofidis — 6 divergent migrations stamped 2017–2021 |
Real regressions, zero fixtures. A detector that stops firing on its historical case is broken.
Roadmap (C–F, not specified here)
-
C — tier-2 boot harness. The largest gap and the best value per hour. Boots the app, asserts flow registration, cron CLI completion, emails actually sending (Mailtrap), exports, DB state. Reuses
test/tests/support/helper.ts. Side effect: unblocks the 30 stalled k6 tests.More prior art exists than assumed — C is smaller than it looks
vuer_oss/bin/comptest.js(ondevel, documented bySYSTEM_CHECK.md) — a connectivity + diagnostics CLI: TCP/Redis/TLS probes, then RabbitMQ-RPC diagnostics from the running server, and it probes Janus + CV + CSS hosts directly. Flags--conn-only,--json,--verbose. The single best starting point for an automated smoke check —--jsonmeans it is already machine-readable.- The standalone cron-harness pattern, proven on FKITDEV-8959 TC-8959-02: a Node script in
/workspace/vuer_oss/bin/(so../server/…resolves) that bootstrapsprocess-settings, installs a consoleProxylogger, stubs services/crypto, authenticates sequelize, seeds by raw SQL, calls the real service methods, and SELECTs before/after. .dev-e2e/run-e2e.sh(untracked, FKITDEV-8827 worktree) — spins a throwawaypostgres:17-alpineon:5544via OrbStack and emits a real dated CSV./fk-tjkPhase 3 already looks for a.dev-e2e/.- Deterministic test credentials exist:
test.security.tempTokenSms(conventionally123456) andtempTokenEmail(mailToken) make the code fixed on every send. They ship only intest/testconfigs/*, not in a normalNODE_ENV=devdeploy — add toconfig/local.jsonand restart, since config caches at startup.
Sizing caveat: RabbitMQ is fail-fast — a closed AMQP connection triggers
process.exit(2)in both vuer_oss and vuer_css. There is no “app without a broker”. A disposable stack is Postgres + RabbitMQ at minimum, plus Redis andvuer_cvfor anything touching photo/liveness.A second detector belongs here rather than in B:
vuer_csshas no jsdom — jest runs in thenodeenvironment with all DOM behaviour hand-mocked, which is exactly how thedata-socketTokenregression shipped green through 1083 passing tests. Verifying DOM-coupled behaviour means rendering the real twig (autoescape: true, matchingserver/web/WebServer.js:257) and parsing with a real HTML parser. -
D — test-plan generator. B’s findings + P3’s ticket buckets + the manifest → a tiered runbook (
auto/manual/cannot-validate-in-UAT), shaped to feed/fk-tjk. Replaces hand-writing runbooks like nusz-1.9.11.48-test-runbook. -
E — release driver. Prepare-and-gate orchestration of P0–P9 over
release_tickets.py,/fk-tjk, vuer-release-cut-recipe and the legacy path. -
F — tier-3 browser. Finish the k6 port or reconsider the substrate. See the open blockers below.
Live state — verified by git ls-remote, 2026-08-11
All three in-flight releases are UNTAGGED. Latest source tags, identical across vuer_oss and vuer_css
(1530 / 1514 tags; the per-partner sets match, so the “tag both repos identically” rule does hold in practice):
| Partner | Latest live tag | Target | vuer-release |
|---|---|---|---|
| nusz | nusz-1.9.11.47 | .48 | nusz@16 → next @17 (@8 skipped, never created) |
| generali-atvilagitas | generali-atvilagitas-1.9.11.18 | .19 | 0 release.json — components only, never built |
| raiffeisen | raiffeisen-1.9.11.99 | .100 | raiffeisen@13 exists |
| cib | cib-1.9.11.101 (102 tags) | .102 | legacy vuer_build; portal_css has no partner tags |
Three environment facts that will bite P5 (fk-dev), recorded now
- fk-dev currently runs cofidis, repointed 2026-07-20 — not nusz. Isolation was verified by migration counts (
vuer_oss_cofidis, 158 migrations, cofidis-only marker present; nuszvuer_ossstill 153/0).config/dev.jsonsetsdb.syncOnStart: true→server/bootstrap/connection/db.jsrunsmigrate → sync → migrateon every boot. Restarting on a partner branch before repointing the DB silently migrates the current database. Change config before the checkout, with node processes stopped first, to close the crash-restart window.config/local.jsonis a single-file bind mount, so docker binds the inode. Restore withcat orig > file.sed -i,mv, andgit checkout --all swap the inode and the container silently keeps serving the stale file.And the reason a smoke check must assert more than a 200: container user
techteameris uid 1000 (hostubuntu), not hostops(1001). The first cofidis smoke test over-claimed — page 200 with the right partner markers, but stylesheets 404’d becauseweb/was owned by 1001 while the app builds as 1000, so every write hitEACCESand the build wrapper still exited 0. Verify referenced assets resolve, not just that the HTML returns 200.
Stale local state
The local
vuer-releaseclone is ~3 months behind (master@90319b0, 2026-05-14; nusz release dirs only to12against a live@16). Fetch before reasoning about it. Auto-memory recording NÚSZ at@14is stale.
Open questions carried forward
- How do legacy images reach Harbor? Verified:
vuer_build/build.shcontains nodocker pushand nodocker login— it builds, tags locally, exports.tar. Confirmed legacy sequence is./build.sh -b <partner>-1.9.11.NNthen./sign-partner.sh -t <tag> -k cosign.key, run native on fk-dev (github.key+cosign.keyare gitignored →git add -f). Signing is not publishing, and the publish step is in no file yet read. This sits directly on the CIB release. (vuer-build-never-pushes) - k6 has no camera on the docker path.
k6.ymlpasses onlyCI_DOMAIN, neverK6_BROWSER_ARGS, so no fake camera/mic and no--disable-web-security. For a video-identification product that is close to fatal for tier 3. (k6-e2e-harness-vuer-oss) - fk-dev tests source, never images. Deploy is a bind-mount branch swap, so nothing on fk-dev exercises the UBI/Node/nginx image layer. Whatever validates images has to be separate.
- Tag ↔ tested SHA. fk-dev runs a branch tip; production ships a Harbor image built from a tag. Assert the tag lands on exactly the SHA that was deployed and tested, or “we tested this build” is not true.
- Raiffeisen’s topology (§1 of release-workflow-design-handover) is still unresolved, and the
back-merge is still owed. The live
raiffeisen@13tag shows the modern pipeline is provisioned — that is evidence, not proof of how .100 ships. - Gate ordering is contradicted by practice. The documented order is tag → smoke test → TjK (Phase 4 gates 3/5/6), but the Raiffeisen .100 TjK sections were written 2026-08-11 while the tag still does not exist. Treat the documented ordering as aspirational until decided.
depcheck: blocking or warn-only? FKITDEV-8239 shipped itcontinue-on-errorand outsidebuild’sneeds; the NÚSZ .48 handover reports it now behaves as a failing check. Read the live workflow.release_tickets.pyonly sees tickets in commit subjects — so a release’s headline item can land in neither bucket. Generali .19’s headline is the pure-core commit6bdf66d16(ASSGRALI-63 / FKITDEV-8887). Any P3 automation must cross-check the parentASS*ticket’s changelog, and P4 must not treat the two buckets as complete.
Related
- devel-update-and-release-flow · release-workflow-design-handover · release-process
- devel-dependency-removal-breaks-partner-customization · breakage-risks · FKITDEV-9197
- k6-e2e-harness-vuer-oss · playwright-to-k6-translation-recipe · FKITDEV-9200
- vuer-release-cut-recipe · client-registry · dev-build-host
- tesztjegyzokonyv-generation-flow · nusz-1.9.11.48-test-runbook
- facekom-test-tiers — the tier model in full (what tier 2 covers, what nothing can reach)
- vuer-build-never-pushes — open question 1, standalone