The complete, followable procedure for taking a partner from “devel has moved on” to “this release is ready to tag” — merge, semantic sweep, evidence-grade validation, release-ticket collection for the TjK and author outreach, release gates. Supersedes the verification-only devel-update-verification-recipe (now a stub).

Extracted from the Generali 1.9.11.19 round (FKITDEV-9194, 2026-08-08 → 2026-08-11) and cross-checked against Cofidis (FKITDEV-9059) and NÚSZ (nusz-devel-update-2026-06-16-lint-merge-fix).

For Agents

Six phases, in order. 0 preconditions → 1 merge + semantic sweep → 2 evidence-grade validation → 3 collect release tickets (the TjK/outreach phase) → 4 release gates → 5 re-check devel before pushing. Deliverable is never “it merged”. It is: proof the merged devel was live, an enumerated list of semantic breaks found and fixed, every remaining failure classified pre-existing vs merge-caused against a base-commit worktree, an explicit statement of which CI gates will be red and why they already were, and a ticket list with authors for the test report. Tool: /Users/levander/coding/facekom/.claude/scripts/release_tickets.py (Phase 3).

The flow

graph TD
    P0["Phase 0 — preconditions<br/>which repos? dead lines? one ticket?<br/>CHECK remote.origin.fetch"] --> P1
    P1["Phase 1 — devel update<br/>worktree → chore/ branch → git merge origin/devel<br/>+ SEMANTIC sweep"] --> P2
    P2["Phase 2 — validation<br/>Node 24 · base-commit worktree · isolated suites<br/>remote run in the product image"] --> P3
    P3["Phase 3 — release tickets<br/>release_tickets.py → 2 buckets<br/>TjK entries + author outreach"] --> P4
    P4["Phase 4 — release gates<br/>merge to customization/ · RELEASE.md · tag<br/>config sweep · smoke test · TjK"] --> P5
    P5["Phase 5 — re-check devel<br/>right before push AND before release"]
    P5 -.->|upstream moved| P1

Phase 0 — Preconditions

Answer these three before touching a repo.

0.1 Which repos does this partner actually live in?

Grep the partner name across the whole fleet — do not assume. Generali is vuer_oss + vuer_css only: zero references in portal_css, esign_oss, esign_css.

Watch for dead branch lines

A partner can have more than one customization/* branch and only one of them is alive.

BranchStatusEvidence
customization/generali-atvilagitasLIVE — the release linetag generali-atvilagitas-1.9.11.18
customization/generali-karDEADlast commit 2022-01-24, 1754 commits behind devel

Never update a dead line. Merging 1754 commits of devel into a branch nobody deploys is pure risk with zero payoff. Confirm liveness by the newest tag, not by the branch existing.

Name glue (YouTrack suffix ≠ repo name) lives in client-registry.

0.2 One ticket or two?

Both conventions are in use and both are acceptable — just decide up front:

  • Dedicated devel-update ticket — e.g. FKITDEV-9073 Generali update 2026-07-15.
  • Folded into the release-prep ticket — Generali 1.9.11.19 ran the devel update on FKITDEV-9194 “Generáli Release 1.9.11.19 előkészítés” itself.

The branch name carries the ticket, so the choice locks in at Phase 1. It also determines where TjK evidence gets attached.

0.3 Check remote.origin.fetch — before anything else

git -C <repo> config --get remote.origin.fetch

If it is anything other than +refs/heads/*:refs/remotes/origin/*, you are in a narrowed clone and git fetch origin devel will lie to you. See the refspec trap below. vuer_css and esign_css are narrowed; vuer_oss is not.


Phase 1 — The devel update

1.1 Isolated worktree, one per repo

git -C /Users/levander/coding/facekom/vuer_oss worktree add \
  ../vuer_oss-FKITDEV-9194 -b chore/FKITDEV-9194-generali-update-2026-08-08 \
  customization/generali-atvilagitas

Sibling directory named <repo>-FKITDEV-NNNN. Never work in the main clone — it carries other people’s WIP and half-finished branches, and a devel update touches hundreds of files.

1.2 Branch naming is enforced

chore/FKITDEV-NNNN-<partner>-devel-update[-YYYY-MM-DD]

A GitHub ruleset rejects the legacy update/customization/<partner>/<date> form outright. Precedent: chore/FKITDEV-9073-generali-update-2026-07-15, chore/FKITDEV-9194-generali-update-2026-08-08. Commit messages are equally constrained — see techteamer-commit-message-ruleset.

1.3 Merge, never rebase

git fetch origin '+refs/heads/devel:refs/remotes/origin/devel'
git merge origin/devel        # a real merge commit
  • Merge commit, always. Rebasing a partner branch onto devel rewrites years of partner history.
  • The PR targets customization/<partner>. It never goes into devel. Devel is upstream of the partner branch, not downstream.

1.4 The semantic sweep — this is the actual work

A textually clean merge is not a working merge

Git reconciles text. devel deletes, renames and refactors core files while the customization/ tree keeps requiring them by path and calling them by name. Every one of those breaks merges conflict-free and fails at runtime.

Run these five checks, in this order.

(a) Deletions and renames first — the highest-risk class

git diff --diff-filter=D --name-only <merge>^1..<merge>
git diff --diff-filter=R --name-status <merge>^1..<merge>

Do this first because customization/ overrides bind to core by path and go stale silently.

  • vuer_css — caught exactly one: devel DELETED server/util/aiActHelper.ts (replaced by server/web/helper/getAiActData.js), still required at customization/server/web/routes/waiting-room.endpoint.js:5. Left as merged, the waiting-room route throws Cannot find module at request time.
  • vuer_oss0 deletions, 0 renames in the merge. That is not a shrug: it makes the entire bug class structurally impossible for this merge. Worth measuring precisely because it tells you how hard to look at everything else.

Same class as nusz-devel-update-2026-06-16-lint-merge-fix (core .js.ts rename vs extensionless require()), except deletion rather than rename.

(b) Programmatically resolve every relative require/import under customization/

Do not grep by eye. Resolve all of them and report unresolvable ones.

Skip comments, or you drown in false positives

Generali’s vuer_oss had 704 relative requires under customization/. The only unresolved hits were 4 commented-out placeholder examples — proven inert by showing they are byte-identical on the base commit.

Extension rules matter: devel converts .js.ts regularly and vuer repos require an explicit .ts (typescript-in-vuer-repos). An extensionless require that used to resolve will stop.

(c) Grep customization for identifiers devel REMOVED

Removals are the highest-risk change class, and they are invisible to git.

  • supportedSteps — removed by devel’s feat: [fkitdev-9119] — had 0 references in real source in either repo. Verify rather than assume in either direction.
  • Devel’s a6185aa41 fix: [fkitdev-8846] remove duplicated socket connections (#3130) replaced auth() with SocketService.getConnection('<page>.script') plus a “Missing layout socket connection provider” guard. Generali’s customization/ui/pages/gen-self-service-consent-{pep,ttny}/{*.script.js,*.ui.js} kept calling auth(...).

(d) Diff each customization/ override against its core counterpart

Look for drifted function signatures, renamed config keys, changed template variables. An override is a fork of a core file — it silently stops tracking the moment core moves (breakage-risks).

Fix direction

Mirror devel’s own core refactor inside the override, keeping the partner-specific identifiers and template paths (gen-*, cofidis-*, …). Do not revert core to suit the override.

(e) Diff the DEPENDENCY SETS — in both directions

The one class of break that neither git, nor lint, nor the test suite will show you

A removed dependency merges clean, lints clean, and dies at require-time on boot. git merge-tree reports the affected files as untouched, because the two sides never edit the same line: devel edits package.json, the partner’s require() sits in customization/. Full write-up: devel-dependency-removal-breaks-partner-customization.

depcheck cannot help. It runs on the branch it is invoked on, and partner customization/ code exists only on the partner branch — so it was never in the tree when devel decided the dependency was unused.

Do not diff package.json as text and move on. Compare the dependency sets of customization/<partner> and devel, and walk both differences.

# what did the merge REMOVE?
git diff <merge>^1..<merge> -- package.json | grep '^-' | grep -v '^---'
 
# for each removed package: does partner code still import it?
git grep -n "require(['\"]<pkg>" -- customization/
git grep -n "from ['\"]<pkg>"    -- customization/
 
# does it resolve ANYWAY? (present in node_modules, absent from package.json)
node -e "console.log(require('<pkg>/package.json').version)"

Then decide per package. There are three outcomes, and only one of them is “restore the line”:

SituationCorrect action
devel removed it deliberately — security retirement, EOL package — and partner code imports itPort the partner code off it. Restoring the dependency re-introduces precisely what devel retired.
devel removed it as merely unused in core, partner code imports itRestore the version the partner declared pre-merge.
Partner-only dependency devel never had, still importedKEEP it. Naively “taking devel’s package.json” at conflict resolution silently deletes it.

Verified instances, CIB 1.9.11.102 (FKITDEV-9197):

RepoPackagesOutcome
vuer_ossrequest, request-promise-nativedevel’s deliberate security removal — CIB’s own security/*.md had logged “Remove of request and request-promise-native scheduled” monthly since 2025-03 ⇒ ported to fetch (mTLS via undici, see vuer-oss-global-fetch-ignores-agent-mtls)
vuer_ossclamscan, soap, xml-formatter, short-uuid, uuid, zodpartner-only ⇒ KEPT
vuer_cssnode-fetch, passport, passport-oauth2partner-only — the CIB corporate-portal SSO path ⇒ KEPT
portal_cssmulter, uuiddevel’s 7a42894a chore(FKQA-304): remove unused libs (#675)restored ("multer": "1.4.5-lts.2", "uuid": "^10.0.0")

The third command is the one people skip, and “it still resolves” is not a pass. CIB’s multer failed require.resolve() outright — a real runtime break in customization/api/document-upload.js. Its uuid was worse: it resolved only transitively off devel’s hoisted uuid@14.0.1 against a declared ^10.0.0 — working by accident, at a major nobody chose, ready to break with zero partner-side changes the day the hoisting dependency moves. Undeclared-but-hoisted is unowned.

eslint gives you the quiet case for free

n/no-extraneous-require flags “you require something you did not declare”. Under --max-warnings 0 it fires on the transitive case at no cost — do not silence it.

Also check the merge for lint-config changes in the same pass: if devel migrated eslint, partner eslint-disable comments for now-disabled rules become “Unused eslint-disable directive” warnings, and --max-warnings 0 makes them failures — eslint9-flat-config-dead-disable-directives.


Phase 2 — Validation (evidence-grade)

2.1 Get Node right before you conclude anything

The repo default is too old, and engines lies

vuer_oss yarn install --frozen-lockfile hard-fails on Node 22geoip-lite requires >=24. package.json engines still says >=22.18.0, which is stale; CI pins Node 24. Jest also needs >=24.9 here (SLACOFI-14).

Locally: export PATH=/opt/homebrew/opt/node@24/bin:$PATH (24.18.0 used for Generali and CIB). A wrong-node failure tells you nothing and costs an hour.

Run the gate through its npm script — calling the tool directly drops flags

Invoking jest directly (a tempting way to dodge RTK’s yarn rewriting — see below) silently loses whatever the package.json script adds around it. On CIB (FKITDEV-9197), omitting --experimental-vm-modules manufactured 4 phantom failures in vuer_oss.

If you must bypass the script runner, copy the full flag list out of package.json first — and prefer rtk proxy yarn … over hand-rolling the invocation.

2.2 Classify every failure against a TRUE baseline

Never assert "pre-existing" without measuring it

Claiming a failure is pre-existing without a baseline run is the single easiest way to ship a regression on a partner branch.

git worktree add ../<repo>-base <pre-merge-partner-tip>
cd ../<repo>-base && yarn install --frozen-lockfile   # its OWN install

Do NOT symlink node_modules from the merged tree

The merge changes yarn.lock800 lines in the vuer_oss case. Symlinking the merged tree’s node_modules tests the base commit against the wrong dependency tree, and any conclusion drawn from it is void. That shortcut invalidated a first attempt on this very round. Pay the second yarn install.

It is worst exactly where you need the control most: when the merge changed dependencies, the symlink contaminates every suite that touches a removed one. On CIB (FKITDEV-9197) vuer-cv-service failed in the base worktree with Cannot find module 'request-promise-native' — a failure manufactured entirely by the shortcut, telling you nothing about the base commit.

Diff the failure sets, not the counts.

2.3 Run suspect suites isolated, and repeatedly

Full-suite vuer_oss runs showed a shifting failure set — test-order interference masks the signal. Isolated runs of the suspect suites were deterministic across 3 runs each. Take the isolated result as the truth, and say so in the write-up.

2.4 Real Generali numbers

Measured on fk-dev, inside the product image, Oracle Linux 9.7 / Node 24.12.0:

TreeResult
Base bfe85a4e683 suites / 9 tests failed
Merged2 suites / 3 tests failed

zero merge-caused failures; the merge NET-FIXES 6 tests. Survivors are converter and vuer-cv-service, both pre-existing and environmentalvuer-cv-service hardcodes the CI path /workspace/.... That is a far stronger statement than “3 failures, looks pre-existing”.

vuer_css on the same round: lint / build / depcheck PASS, test:unit 120 suites / 1083 passed / 0 failed (base 115/1043/0 ⇒ devel’s 5 new suites all pass against Generali customization), improved-yarn-audit --min-severity critical 0 vulnerabilities.

2.5 Remote validation on fk-dev, inside the product image

Unit tests that pass on macOS prove less than the same tests inside the image that ships. Run them on the fk-dev Tailscale VM (root@fk-dev, 100.91.108.61 — see dev-build-host) in a throwaway container off the real image (harbor.techteamer.com/facekom-devel/vuer_css:...) with the source mounted. No need to disturb the running stack or its DB.

Ship the tree with git archive, or:

COPYFILE_DISABLE=1 tar --exclude='._*' -czf /tmp/tree.tgz -C <worktree> .

macOS AppleDouble files become ~20 bogus failing suites

A plain tar on macOS emits ._* AppleDouble sidecars. They land in the tarball, jest picks them up as test files, and you get ~20 fabricated failing suites that look like merge damage. COPYFILE_DISABLE=1 + --exclude='._*', or just use git archive.

The product image sets NODE_ENV=dev — in-image results read GREENER than CI

jest defaults to NODE_ENV=test; the image runs as dev. Every failure that depends on which config file loads is therefore masked in-image and live in CI.

On CIB (FKITDEV-9197) portal-client.test.js passed in-image and failed in CI for precisely this reason — a module-scope require('../../../config') that finds dev.json under dev and nothing under test.

In-image validation is a strong check on native build/lint and on anything platform-dependent. It is a weak check on config loading. Do not report an in-image pass as a CI prediction.

Three environments, three answers — for the same tree

macOS local, fk-dev in-image, and CI disagreed on CIB, structurally rather than flakily: macOS has no /etc/hostname (see Gotchas), the image pins NODE_ENV=dev, CI runs NODE_ENV=test.

Treat “it passes locally” and “it passes in-image” as weak evidence about CI. The in-image pass is the more seductive of the two, because it looks like production.

Access — what actually works (tested 2026-08-12):

  • ops and root are both permitted. The canonical string stays command ssh ops@fk-dev.taild4189d.ts.net (dev-build-host). levander, ubuntu, admin and facekom are refused with tailnet policy does not permit you to SSH as user "<u>".
  • Connect by name, not by IP. fk-dev and fk-dev.taild4189d.ts.net are in known_hosts; 100.91.108.61 is not, and the IP form fails with Host key verification failed.
  • Use throwaway containers off the real image with the source mounted — the running partner stack stays untouched.

ssh is aliased

ssh is aliased to _kaku_wrapped_ssh, which is not loaded non-interactively — use command ssh from scripts.

2.6 Work out what CI NEWLY turns on

Three separate questions:

  1. Does the branch inherit new jobs? A legacy partner branch on the single lint-and-build job jumps to devel’s 6 jobs on its first devel merge (customization-branch-ci-pipeline-inheritance). Generali’s branches already carried the 6-job pipeline, so this did not fire. Cofidis’s did — it cost 5 failure clusters in FKITDEV-9059. CIB’s fired on all three repos, and on portal_css the inherited audit job took the branch from 25 CRITICAL advisories to 0 — the expansion is not always a tax (FKITDEV-9197).
  2. Does the branch enter a scheduled-workflow MATRIX? Check the matrix, not the file’s presence. vuer_oss gained long-lived-branches.yaml, but it is schedule-only and its matrix lists devel / mbh / raiffeisen / kh / unicredit — Generali is unaffected.
  3. What does the gate graph imply? build has needs: [lint, test, audit, sonar]a red audit also blocks build.

A red gate hides everything downstream — "skipped" is not "passed"

On the first CIB vuer_css run (FKITDEV-9197) Build and SonarQube did not fail, they were skipped, because both needs: the red test job. Reading that run as “4 green, 1 red” overstates what is known by two whole jobs. Re-read the needs: graph before reporting a run, and re-run after every fix rather than assuming the untouched jobs are fine.

vuer_oss audit is RED on every partner PR — and it is not yours

sequelize GHSA-v8fg-2rw7-q452 via sequelize@npm:@techteamer/sequelize 6.32.2. Pre-existing: byte-identical resolution on the base commit, which fails the same gate. This is the FKITDEV-8279 fork problem. Say so explicitly in the PR body, because it also reds out build and will otherwise be read as merge damage.

2.7 Local gate checklist

Run all of these, per repo, and record the numbers:


Phase 3 — Collect the release tickets (TjK + author outreach)

This is the phase that turns a green branch into a shippable release

The TjK needs one section per shipped ticket, and you cannot write those sections yourself — you need to reach out to the people who wrote the code. This phase produces both the section list and the contact list.

A verified, reusable tool now exists:

python3 /Users/levander/coding/facekom/.claude/scripts/release_tickets.py \
  <tag-prefix> <repo-path> [repo-path ...] [--head REF]
 
# Generali 1.9.11.19:
python3 .claude/scripts/release_tickets.py generali-atvilagitas- \
  ./vuer_oss-FKITDEV-9194 ./vuer_css-FKITDEV-9194

It resolves the latest release tag per repo from git ls-remote --tags (and fetches the tag explicitly — necessary in narrowed-refspec clones where the tag is not local), then emits two markdown tables with YouTrack links, GitHub commit links, GitHub PR links, state, assignee and commit author.

3.1 The two buckets, and why the split matters

#BucketQueryMeaning
1Landed DIRECTLY on the customization branchgit log <tag>..HEAD --no-merges --not origin/develPartner-specific work. Always needs a TjK entry — this is what the partner paid for.
2Came via devel but modified customization/git log <tag>..HEAD --no-merges -- customization/ minus bucket 1Core work that nonetheless altered partner behaviour. Usually needs an entry too — and is easy to miss entirely.

Bucket 2 is the whole reason the tool exists. Nobody reviewing a devel merge thinks of core commits as “release scope”, yet a core commit that touched customization/ changed the partner’s surface and has to be tested and reported.

3.2 Verified output — Generali 1.9.11.19

Baseline tag generali-atvilagitas-1.9.11.18.

Bucket 1 — landed directly on the customization branch

TicketSummaryStateAssigneeRepoCommit authorPR
FKITDEV-8567Generali - reduceRight átírásDoneFerenc Jurkiewiczvuer_ossSzecsődi Imre#7811
FKITDEV-9194devel-update fix (this round)Openandras.lederervuer_cssandras.lederer

Bucket 2 — came via devel, touched customization/

TicketSummaryStateAssigneeRepoPR
FKITDEV-9119EnvData supportedSteps törléseDoneMárton Szvetkóvuer_oss#8083
FKITDEV-9080WebSDK - AI ACTPendingKrisztián Makkaivuer_oss#8054

Flag anything not in a terminal state

FKITDEV-9080 is Pending and shipping anyway. The code is merged and on the branch, so it goes out with the release regardless of what the ticket says. Surface that to the release owner rather than letting the ticket state imply it is out of scope.

3.3 Two things the tool deliberately does NOT do

Assignee ≠ commit author — contact the right human

Both columns are emitted for exactly this reason. FKITDEV-8567 is assigned to Ferenc Jurkiewicz but was authored by Szecsődi Imre. When you need “what does this actually do, and how do I test it”, the author usually knows and the assignee sometimes does not. Pick per ticket.

The headline changelog item can be invisible to the tool

The tool only sees tickets referenced in commit subjects of commits reachable from the tag. A core commit that never touched customization/ is in neither bucket — yet it can be the release’s headline feature.

For 1.9.11.19 that is exactly what happened: the headline is ASSGRALI-63 / FKITDEV-8887 (iOS Safari audio recovery, 6bdf66d16), a pure-core commit. Always cross-check the parent ASS* release ticket’s changelog against the tool’s output and merge the two lists by hand.

3.4 What to do with the output

  1. Draft the TjK section list from bucket 1 + bucket 2 + the ASS* changelog.
  2. Message each author/assignee for the functional description and expected behaviour of their ticket.
  3. Convert to partner-side ticket ids for the customer document — FKITDEV-* never appears in a TjK (tesztjegyzokonyv-partner-release-document-structure).
  4. Feed into [[tesztjegyzokonyv-generation-flow|/fk-tjk]].

Phase 4 — Release gates (what “ready” actually means)

Green is not ready

A green pipeline on a chore/... branch means the code compiles and the tests pass. It does not mean anything is releasable. Six gates:

  1. The chore/... branch is MERGED into customization/<partner> via PR. The tag is cut from the customization branch, not from your working branch. Until the PR lands, there is nothing to tag.
  2. customization/RELEASE.md changelog entry. Historically its own ticket — FKITDEV-9153 did this for Generali .18. Format varies per partner (Release Notes Format).
  3. Tag <partner>-1.9.11.NN on both repos.

    package.json version stays 1.9.11 The .NN release counter lives only in the tag and the changelog. Do not bump package.json looking for it.

  4. Breaking-change / config sweep. Run over the full range:
    git log <tag>..HEAD --grep='BREAKING' --grep='!:' -E
    git diff --name-only <tag>..HEAD -- db/migrate/
    git diff <tag>..HEAD -- config/

    Real example from 1.9.11.19 — a silent config break browsers.showOldBrowserWarningbrowsers.oldBrowserWarning.{show,delay}, and server/web/Template.js:123 now reads the new path. Any partner production config still on the old key silently loses the old-browser warning — no error, no log line, the feature just stops. docs/config/ was not updated to match, so the schema does not warn either (typescript-in-vuer-repos sibling gotcha: new config fields need a JSON-schema entry).

    Devel restructured

    Also new in this range but default-off (so safe, but worth listing in the changelog): sftp, dataCleanupCron, transferRoomCron, documentRecognitionVersion, roomExportFilesExtendedName.

  5. Functional smoke test on a real deployment. Unit + lint + build in a container is not a smoke test. Boot the app and drive a real flow — see mjml-v5-esm-breaks-commonjs-email-templates for a green-CI/broken-runtime break that no gate above catches.
  6. TjK written with evidence — [[tesztjegyzokonyv-generation-flow|/fk-tjk]], using the Phase 3 ticket list.

Phase 5 — Re-check devel immediately before pushing, and again before release

The hard lesson of this round

I diagnosed a devel regression (FKITDEV-9199dataset.socketToken never resolving, because the twigs emitted camelCase data-socketToken), wrote a reader-side fix and pushed it — while the upstream fix cda6c80b97 had already landed ~2 hours earlier, taking the opposite approach (kebab-casing the twigs). Merging mine on top would have re-broken it.

Always git ls-remote for the upstream fix right before pushing to a fast-moving core branch — not just before merging. Hours matter on devel.

Practical form:

git ls-remote origin refs/heads/devel      # right before push
git log --oneline origin/devel -20 | grep -iE '<the thing you fixed>'

Re-run the same check again before cutting the release — the window between “PR merged” and “tag pushed” is often days.


Gotchas

git log -S does NOT follow renames — and that decides the fix side

A pathspec + -S silently stops at the rename, and you get a FALSE history

On CIB (FKITDEV-9197) git log -S "status(400)" -- <post-rename path> returned nothing, which was read — and asserted to the user — as “400 never existed in this middleware”. It had existed, under the file’s old name, and the whole question “was this test ever right?” turned on it.

git log --follow -S '<string>' -- <path>    # follows the rename
git log -L <start>,<end>:<file>             # better — one line's ENTIRE history, across renames

This bites specifically in devel-update work, where a stale-looking test forces the choice fix the test vs fix the code, and that choice is made almost entirely from history. A false negative sends you to the wrong side. Devel merges are full of renames by construction.

The general rule — when a negative result is about to carry weight, change instrument

An empty git log -S, a still-failing hypothesis test, a refused SSH user — none of these are evidence of absence until you have checked that the instrument could have detected the thing. In devel-update work this decides whether you fix the test or fix the code (FKITDEV-9197 §14.1), so getting it wrong sends you to the wrong side of the change.

Distinct from “read the actual error text” below, which covers a failure being misread. This covers a null result being over-trusted — three worked instances, and how each was caught, in §15.

macOS cannot load vuer_css’s config.js under NODE_ENV=dev

config.js:43 does fs.readFileSync('/etc/hostname') on the dev / travisci path. macOS has no /etc/hostnameENOENT, unless DEV_DOMAIN is set.

The real cost is the wrong conclusion, not the ENOENT

On CIB, an agent set NODE_ENV=dev to test a config-loading hypothesis, saw the suite still fail, concluded “NODE_ENV is not the variable”, and reattributed a genuine CI failure to a “nested worktree artefact”. The hypothesis was correct; the test of it was broken by an unrelated platform gap.

When a hypothesis test fails, read the actual error text. Never generalise from a failure you have not identified — a failed experiment only refutes the hypothesis if it failed for the reason you were testing. The move that resolved it was running the same tree in the Linux product image on fk-dev (Phase 2.5).

GitHub’s default PR title fails the branch-name ruleset

The branch name the rulesets require produces a PR title the rulesets reject:

Chore/fkitdev 9197 cib devel update                         ← GitHub's default from the branch
chore: [fkitdev-9197] merge devel into customization/cib    ← conforming

It misses on both counts — no lowercase type: prefix, no bracketed lowercase ticket. This is structural, so make it a checklist item: open the PR, then immediately fix the title (techteamer-commit-message-ruleset).

gh pr edit infers the repo from cwd — always pass -R

gh pr edit <n> -R TechTeamer/vuer_css --title 'chore: [fkitdev-NNNN] merge devel into customization/<partner>'

In a three-repo round with worktrees in unusual places, cwd inference is a coin flip.

Narrowed fetch refspec

git fetch origin devel silently leaves origin/devel STALE

In vuer_css and esign_css the clone has a narrowed remote.origin.fetch (e.g. +refs/heads/customization/raiffeisen:…). git fetch origin devel therefore writes only FETCH_HEAD, never refs/remotes/origin/devel — while printing a cheerful success line and exiting 0. The following git merge origin/devel merges a stale tree, conflict-free, with zero warning.

git fetch origin '+refs/heads/devel:refs/remotes/origin/devel'
git rev-parse origin/devel                    # must equal…
git ls-remote origin refs/heads/devel

This nearly shipped Generali 1.9.11.19 without its headline feature: the first css merge used devel @ 7d4f9956c8 (2026-07-28) against a live 3ace8872c3 (2026-08-07) — 6 commits dropped, including 6bdf66d16 = FKITDEV-8887 = ASSGRALI-63, the release’s main item. Full mechanism: narrowed-fetch-refspec-stale-devel-merge.

--force-with-lease fails with “stale info” in those same repos

Because git cannot infer the lease without a remote-tracking ref. Do not fall back to plain --force — keep the safety net by naming the expected sha:

git push '--force-with-lease=<branch>:<expected-sha>' origin <branch>

zsh mangles refspecs

git fetch origin "+refs/heads/$B:refs/remotes/origin/$B"   # BROKEN

$B:r is zsh’s remove-extension history modifier and eats the destination. Use fully literal refspec strings, or single-quote and interpolate outside the :.

Test files cannot be linted in vuer_css

The eslint config references a missing jest-formatting plugin and exits 2 on devel’s own test files. That is why yarn lint ignores test/*. Do not “fix” it as part of a devel update; do not read a lint pass as covering the test tree.

portal_css has the same residue: devel removed eslint-plugin-jest-formatting in 65ac214c feat: eslint 9 FKITDEV-6045 but eslint.config.mjs still references 'jest-formatting/padding-around-all': 'warn'. It is inert only because that block is scoped to files: ['test/*','test/**/*'] and yarn lint runs --ignore-pattern "test/*". Latent landmine if test linting is ever enabled.

An eslint-9 migration turns partner eslint-disable comments into build failures

Disabling a rule should make lint quieter. It does the opposite here. devel’s flat-config migration dropped compat.extends('standard','plugin:n/recommended','plugin:jest-formatting/strict') for n.configs['flat/recommended'] + js.configs.recommended and turned off no-empty / no-unused-vars / no-redeclare / no-useless-assignment. Every partner // eslint-disable-next-line <that rule> then reports “Unused eslint-disable directive” — a warning — and yarn lint --max-warnings 0 makes it red. CIB: 9 dead directives across 6 customization/ files (FKITDEV-9197).

eslint --fix does not delete them

It replaces each directive with a whitespace-only line. Delete the lines properly. Details: eslint9-flat-config-dead-disable-directives.

A green test job on vuer_oss is weaker evidence than it looks

One suite registers nothing; two others flake non-deterministically

All three are on devel, so they apply to every partner branch, and none of them is merge-caused. Full write-up: vuer-oss-unit-tests-green-is-weak-evidence.

DefectConsequence for validation
test/tests/unit/translations.test.js:181 misuses it.each — the callback is passed as each()’s second argument and the returned function is discarded ⇒ zero tests registeredNever cite this suite as coverage. Its green is structural. Every wrongKeys / missingLanguage finding it computes is discarded — a probe found 42 invisible findings on CIB alone
The same file’s scan() takes relative includedDirectories while cwd is absolute ⇒ loading depends on process.cwd() at module-eval timeActively flaky in CIDirectory not found: client/features on ~1 nightly run in 3, same commit, both runners, client/features being real and tracked. Does not reproduce on macOS
server/util/pdf.test.js › printImage › place sample PNG image does a byte-for-byte PDF comparisonDrifts by 3 bytes at random. A re-run with no content change goes green

Operational consequence: a single red run is not evidence of a regression, and a single green run is not evidence of its absence. Re-run, then classify against a base-commit worktree (Phase 2.2) and run suspect suites isolated and repeatedly (Phase 2.3).

If you are tempted to fix any of them — two traps

The it.each repair is a breaking change, not a cleanup: it converts those 42 findings into hard failures on CIB and an unknown number elsewhere. Budget the fallout, upstream, as its own ticket.

The obvious scan() one-liner silently breaks the suite. path.join(cwd, startPath) makes every returned path absolute, and three downstream consumers key off relative paths (require doubles the path, excludedFiles.includes stops matching, argMap[...] goes undefined). Keep the returns relative — the correct form is in the note.

portal_css has no test signal at all

yarn jest on pristine devel runs zero tests, and would still run zero if you wrote some — the custom test/lib/jest/test.sequencer.js has an empty CORE_TEST_ORDER and prepareTests filters with order.includes(...), and jest sorts before its “no tests found” check. CI is green only because the workflow appends --passWithNoTests. On this repo, validate with yarn lint + yarn build and do not claim a test gate — portal-css-jest-runs-zero-tests.

Portal versioning is a separate train

portal_css does not carry the vuer 1.9.11.NN numbers. CIB release 1.9.11.102 corresponds to portal tag cib-1.4.0.74. Resolve the Phase 3 baseline tag with the repo’s own prefix (cib-1.4.0.), not the release number quoted in the ticket.

No jsdom — jest runs in the node environment

All DOM behaviour in vuer_css is hand-mocked, so nothing exercises real HTML attribute parsing. This is exactly why the data-socketToken regression shipped green through 1083 passing tests.

To verify DOM-coupled behaviour, render for real

Render the actual twig with the real twig engine (autoescape: true, matching server/web/WebServer.js:257) and parse the output with a real HTML parser. Asserting against a hand-built mock object proves nothing about attribute casing, escaping, or serialization.

youtrack_guard hook blocks git operations

The facekom PreToolUse hook blocks git commit / git push / gh pr (and rm -rf) while /tmp/fk-ticket/.analyze-active exists, and it re-arms. When the user has authorized the action, clear it with a plain rm (not rm -rf, which the hook also blocks).

.gitignore has a trailing slash (node_modules/), which does not match a symlink. In any worktree where you symlinked or hand-placed deps, stage files explicitly — never git add ..

RTK mangles some git/jest invocations

Never trust RTK-rendered git output for a verification claim

RTK failures here are silent and confident, not error-shaped — you get a plausible wrong answer. Confirmed modes, all hit during real devel updates:

InvocationWhat RTK does
git show <rev>:<path>rewrites the <rev>:<path> argument
git show <ref>:<file> | grepgarbles output so real matches read as absent — nearly caused a wrong conflict resolution (FKITDEV-9197)
git log -1 right after committing a mergeshows the wrong commit (devel’s tip instead of the new merge commit)
diff -uunreadable line-offset view plus a wrong exit code
git diff --no-indexrenders as if the whole file were new
${PIPESTATUS[0]}blanked
jest --verboseper-test lines swallowed
find | wc -l, | grep -czeroed

Workarounds: rtk proxy <cmd>; git grep <pattern> <ref> -- <path> instead of git show \| grep; shasum -a 256 for same-vs-different; python3 subprocess.run([...]) with an argv list; python3 -c "import difflib…" to generate a diff; jest --json --outputFile=<f> when you need to assert on results.

rtk-mangles-curl-and-pipes · rtk-git-log-hides-merge-commits


Quick crib

export PATH=/opt/homebrew/opt/node@24/bin:$PATH
 
# 0 — preconditions
git -C <repo> config --get remote.origin.fetch
git -C <repo> log -1 --format='%ci' customization/<partner>          # dead line check
 
# 1 — merge
git -C <repo> worktree add ../<repo>-FKITDEV-NNNN \
    -b chore/FKITDEV-NNNN-<partner>-devel-update customization/<partner>
git fetch origin '+refs/heads/devel:refs/remotes/origin/devel'
[ "$(git rev-parse origin/devel)" = "$(git ls-remote origin refs/heads/devel | cut -f1)" ] || exit 1
git merge origin/devel
git diff --diff-filter=D --name-only HEAD^1..HEAD                    # deletions FIRST
git diff --diff-filter=R --name-status HEAD^1..HEAD
git diff HEAD^1..HEAD -- package.json | grep '^-' | grep -v '^---'   # REMOVED deps
git diff --name-only HEAD^1..HEAD | grep -E 'eslint'                 # lint-config churn
 
# 2 — validation
yarn install --frozen-lockfile && yarn lint && yarn test:unit && yarn build
improved-yarn-audit --min-severity critical
git worktree add ../<repo>-base <pre-merge-tip>                      # its OWN yarn install
 
# 2b — history archaeology (NEVER bare `git log -S <str> -- <path>`: it stops at renames)
git log --follow -S '<string>' -- <path>
git log -L <start>,<end>:<file>
 
# 2c — push + PR (fix the title immediately; the default fails the ruleset)
gh pr create -R TechTeamer/<repo> --base customization/<partner> \
    --title 'chore: [fkitdev-NNNN] merge devel into customization/<partner>'
gh pr edit <n> -R TechTeamer/<repo> --title '...'    # -R is mandatory, cwd inference is unreliable
 
# 3 — release tickets
python3 /Users/levander/coding/facekom/.claude/scripts/release_tickets.py \
    <partner>- ./<repo>-FKITDEV-NNNN [...]
 
# 4/5 — gates + upstream re-check
git log <tag>..HEAD --grep='BREAKING' --grep='!:' -E
git diff <tag>..HEAD -- config/ db/migrate/
git ls-remote origin refs/heads/devel                                # right before push