name: upgrade-2.13-to-2.14
description: >
Upgrade a single eRegistrations instance under
Conf-<UPPER_ENV>/compose/<country>/docker-stack.yml from 2.13 to 2.14, where
<env> is one of dev/test/preview/prelive/live. This is the largest mechanical
step in the chain: bumps every unctad/<service>:$<VAR>_VER (or pinned
semver) image to :RC (the platform tag introduced in 2.14), drops the legacy
Keycloak /auth path from internal and public URLs across bpa-frontend,
bpa-backend, camunda, mule, ereg-cms-frontend, statistics-backend,
statistics-frontend, renames MONGO_URI → RH_MONGO_URI on restheart,
bumps EREGISTRATIONS_VERSION and BUILD_TYPE to the values the rest of the
chain expects (non-dev: 2.14 / RC; dev: DEV / DEV). Keycloak Quarkus
migration is mandatory — when Wildfly env vars are detected on the keycloak
service block, the skill replaces them with the Quarkus block (KC_DB_*,
KC_HOSTNAME*) without prompting; this is a hard requirement of the 2.14
baseline, not an optional cleanup. Conditionally adds an opensearch-node1
service block plus repoints GRAYLOG_ELASTICSEARCH_HOSTS from
$SERVICE_HOST:9200 to opensearch-node1:9200 when the graylog service
still references the legacy Elasticsearch endpoint. Strict mode — aborts on anything unexpected.
Env-aware anomaly thresholds. LIVE invocations require a retype-country
confirmation rail before commit (skipped in chain mode — the orchestrator
does it once up front). Two invocation modes: standalone (creates branch,
pushes, opens PR) and chain mode (CHAIN_MODE=1 CHAIN_BRANCH=<name>,
commits to orchestrator-managed branch, no push, no PR). Swarm-stack
(docker-stack.yml) shape only — instances still on docker-compose.yml must
run /docker-swarm-migration first.
license: UNCTAD-Internal
compatibility: Run from the eregistrations-v4 working tree on master (standalone) or on the orchestrator-supplied chain branch (chain mode), with a clean tracked tree. Requires an authenticated CLI for the host VCS in standalone mode (gh for GitHub origins; Bitbucket origins skip CLI PR creation and print a manual link).
allowed-tools: Read, Write, Edit, Grep, Glob, Bash(git *), Bash(gh *), Bash(grep *), Bash(test *), Bash(ls *), Bash(basename *), Bash(dirname *), AskUserQuestion
metadata:
version: "1.0.0"
version-date: "2026-05-04"
author: "UNCTAD Trade Facilitation Section"
argument-hint: "[] [] [BACKUP_CONFIRMED=1] [CHAIN_MODE=1 CHAIN_BRANCH=]"
jira: "TOBE-17814"
Upgrade an eRegistrations instance from 2.13 to 2.14
You are performing the eRegistrations 2.13 → 2.14 upgrade of a single instance. The target file is Conf-<UPPER_ENV>/compose/<country>/docker-stack.yml, where <env> ∈ {dev, test, preview, prelive, live}. This is the largest mechanical step in the chain: 2.14 introduced the :RC tag channel for every unctad/* image (replacing per-service $<VAR>_VER env-var pinning) and changed how Keycloak is integrated (drop /auth path; optionally migrate the keycloak service from Wildfly to Quarkus; optionally add opensearch-node1 and repoint Graylog).
The historical 2.13 → 2.14 commits varied across countries — Lomas (c99340f9) did the full Keycloak Quarkus + Opensearch migration in one shot; Mali (9f900cc8) bumped images and URLs but kept Wildfly Keycloak; DEV instances (0b39d556) only bumped EREGISTRATIONS_VERSION. This skill encodes the superset with conditional rules: required transformations apply everywhere, conditional blocks (Keycloak Quarkus, Opensearch) apply only when their respective preconditions are detected.
Operate in strict mode: any anomaly pauses for explicit user input, with abort as the default.
The skill is invoked as /upgrade-2.13-to-2.14 with optional positional args (see Arguments below). It is also routed to by the upgrade-eregistrations-instance orchestrator when it detects a swarm-stack instance whose unctad/* images use $<VAR>_VER interpolation or pinned semver tags and whose EREGISTRATIONS_VERSION is 2.13 (or $EREGISTRATIONS_VERSION env-var-interpolated).
When the upgrade is approved, standalone mode commits on a fresh branch chore/upgrade-<env>-<country>-2.13-to-2.14, pushes it, and opens a pull request. Chain mode (orchestrator-invoked) skips branch creation, push, and PR — it commits a single step-scoped commit on the orchestrator-managed branch and returns.
Arguments
The skill accepts up to four positional/flag tokens, whitespace-separated, in any order:
<env>— one ofdev,test,preview,prelive,live(lowercase).<country>— the folder name underConf-<UPPER_ENV>/compose/, e.g.lomasdezamora,mali,cuba.BACKUP_CONFIRMED=1— flag. Suppresses the STEP 1.5 backup prompt.CHAIN_MODE=1— flag. Switches to chain mode: orchestrator owns branch/push/PR. RequiresCHAIN_BRANCHto also be set.CHAIN_BRANCH=<branch>— the branch the orchestrator already created and switched to. Sub-skill commits here.
Tokenizer rules:
- Whitespace-split.
- For each token: if it matches
^[A-Z_]+=.+$, treat as aKEY=VALUEflag and store; if lowercased it equals one of the env keywords, set<env>; otherwise it's<country>. - Unknown
KEY=VALUEflags warn ("Unknown flag<token>, ignoring.") but do not abort.
Missing positional values trigger AskUserQuestion prompts in STEP 1. If <country> was supplied via args, validation is single-shot (no retry loop).
Env → directory mapping:
<env> |
<UPPER_ENV> |
Directory |
|---|---|---|
| dev | DEV |
Conf-DEV/compose/ |
| test | TEST |
Conf-TEST/compose/ |
| preview | PREVIEW |
Conf-PREVIEW/compose/ |
| prelive | PRELIVE |
Conf-PRELIVE/compose/ |
| live | LIVE |
Conf-LIVE/compose/ |
Scope (intentionally narrow)
- In scope: a single
Conf-<UPPER_ENV>/compose/<country>/docker-stack.ymlwhoseunctad/*images are pinned via$<SERVICE>_VERenv-var interpolation or pinned semver tags (e.g.:5.17.7-0,:1.260.10) and whoseEREGISTRATIONS_VERSIONis2.13or$EREGISTRATIONS_VERSION. - Out of scope: instances still on
docker-compose.yml(refuse and point at/docker-swarm-migration), Coolify-managed instances, simultaneous upgrades of multiple instances, version pairs other than 2.13 → 2.14.
If the target instance has only docker-compose.yml (no docker-stack.yml), abort with: "<country> is still on docker-compose.yml. Run /docker-swarm-migration first to convert the instance to swarm, then re-run this skill."
STEP 0: Pre-flight git checks
Before doing anything else, verify the repository is in a state where the upgrade can proceed.
Standalone mode (no CHAIN_MODE=1):
- Working tree is a git repo at the repo root. Run
git rev-parse --show-toplevel. If it errors, abort: "Not in a git working tree." - Current branch is
master. Rungit rev-parse --abbrev-ref HEAD. If notmaster, abort: "Refusing to run on branch. Switch to master first." - No staged or modified tracked files. Run
git status --porcelain --untracked-files=no. If non-empty, abort and print: "There are staged or modified tracked files. Resolve the changes below first." followed by the same output. - Origin host detected, CLI authenticated. If the orchestrator already set
HOSTin conversation state, reuse it. Otherwise resolve it now:git remote get-url origin.- URL contains
github.com→ setHOST=github. Rungh auth status. If it errors, abort: "GitHub CLI (gh) is not installed or not authenticated. Install gh and rungh auth loginbefore re-running this skill." - URL contains
bitbucket.org→ setHOST=bitbucket. The skill will skip CLI-based PR creation and print a manual Bitbucket URL after push. - Otherwise abort: "Unsupported origin host:
."
- URL contains
masteris in sync with origin. Rungit pull --ff-only origin master. On failure, abort and print the git error verbatim.
Chain mode (CHAIN_MODE=1):
- Working tree is a git repo at the repo root. Same as standalone.
- Currently on the orchestrator-supplied chain branch. Run
git rev-parse --abbrev-ref HEAD. If it doesn't equal<CHAIN_BRANCH>, abort: "Chain mode expected branch<CHAIN_BRANCH>but on<actual>. Orchestrator state inconsistent." - No staged or modified tracked files. Same as standalone.
- Skip host detection and pull — orchestrator did both already.
When pre-flight passes, proceed to STEP 1.
STEP 1: Resolve env, country, target
Resolve
<env>. If supplied via args, use it. Otherwise AskUserQuestion: "Which environment? dev / test / preview / prelive / live." Lowercase, validate. Two-strikes invalid → abort.Compute
<UPPER_ENV>from the table above.Verify eregistrations-v4 shape. Run
test -d "Conf-<UPPER_ENV>/compose". If missing, abort: "Conf-<UPPER_ENV>/compose/does not exist."Find candidates. Candidates are docker-stack.yml files that have
EREGISTRATIONS_VERSION=2.13(literal or$EREGISTRATIONS_VERSIONinterpolated) andunctad/*images using$<VAR>_VERor pinned semver tags (i.e. not:RC/:BETA/:2.17/:2.18):for f in Conf-<UPPER_ENV>/compose/*/docker-stack.yml; do if grep -qE 'EREGISTRATIONS_VERSION=["'"'"']?(2\.13|\$EREGISTRATIONS_VERSION)' "$f" \ && grep -qE 'image:[[:space:]]*unctad/[^:[:space:]]+:(\$[A-Z_]+_VER|[0-9]+\.[0-9]+)' "$f"; then echo "$(basename "$(dirname "$f")")" fi done | sortNo candidates found. If zero lines: "Nothing to upgrade — no
Conf-<UPPER_ENV>swarm-stack instance hasEREGISTRATIONS_VERSION=2.13onunctad/*:$VAR_VER-style images. Note: instances still ondocker-compose.ymlmust run/docker-swarm-migrationfirst." Exit 0.Resolve
<country>.- If supplied via args: validate against candidates list. Invalid → single-shot abort.
- If not supplied: list candidates, ask "Which
<env>instance? Type the country folder name." Two-strikes invalid → abort.
Confirm target file exists. Compute
TARGET=Conf-<UPPER_ENV>/compose/<country>/docker-stack.yml. Runtest -f "$TARGET". If missing, abort.Save state for the rest of the run:
<env>,<UPPER_ENV>,<country>,TARGET.
STEP 1.5: Backup confirmation
If BACKUP_CONFIRMED=1 was passed (orchestrator-routed and chain-mode invocations always set this), skip this step.
Otherwise AskUserQuestion: "Is the current state of <env>/<country> recoverable (snapshot, prior tag, manual export)? (y/N)"
y(case-insensitive) → STEP 2.Nor empty → abort: "Resolve backups before re-running."
STEP 2: Pre-transformation strict scan
Compute env-aware anomaly thresholds. The 2.13 baseline used inconsistent values for these env vars (DEV instances often had BUILD_TYPE=$BPA_FRONTEND_VER and EREGISTRATIONS_VERSION=$EREGISTRATIONS_VERSION interpolation, non-dev had literal LIVE / 2.13). The skill normalizes the post-state to the values the rest of the chain (2.14 → 2.15 onward) expects:
<env> |
accepted source BUILD_TYPE (2.13) |
accepted source EREGISTRATIONS_VERSION (2.13) |
post-state (2.14) |
|---|---|---|---|
| dev | any of DEV, $BPA_FRONTEND_VER, LIVE |
any of DEV, $EREGISTRATIONS_VERSION, 2.13 |
BUILD_TYPE=DEV, EREGISTRATIONS_VERSION=DEV |
| test, preview, prelive, live | LIVE |
2.13 or $EREGISTRATIONS_VERSION |
BUILD_TYPE=RC, EREGISTRATIONS_VERSION=2.14 |
Print: "Env: <env>. Accepted source BUILD_TYPE ∈ EREGISTRATIONS_VERSION ∈ BUILD_TYPE=<post_BT>, EREGISTRATIONS_VERSION=<post_EV>."
Scan <TARGET> for anomalies. Each pauses for (c)ontinue / (s)kip / (a)bort (default abort, empty = abort). c applies the relevant rule to this occurrence; s leaves untouched (remembered for the kind); a exits without edits.
Anomaly kinds:
Already-2.14+ unctad image tags. Any
unctad/*:RC,:BETA,:2.17,:2.18line. Suggests partial prior upgrade. Default abort.Already-2.14
EREGISTRATIONS_VERSION. AnyEREGISTRATIONS_VERSION=2.14line. Default abort.Unexpected
EREGISTRATIONS_VERSIONvalue. AnyEREGISTRATIONS_VERSION=line whose RHS (after stripping quotes) is not in the accepted-source set for<env>and not2.14(covered above).Unexpected
BUILD_TYPEvalue. AnyBUILD_TYPE=line whose RHS (after stripping quotes) is not in the accepted-source set for<env>and not the post-state value.Unexpected unctad image tag. A line matching
image:\s*unctad/[^:]+:[^ ]+whose tag is not in{$<VAR>_VER, pinned-semver, DEV}. Country-specific images on:DEV(e.g.unctad/mule3-mali:DEV) are expected — typical answers.Missing expected service blocks. If the file lacks
bpa-frontend:,restheart:, orkeycloak:service blocks. Print missing names and pause — typicalsfor unusual variants.
If no anomalies: "No anomalies. Detecting conditional preconditions." and proceed.
Precondition detection
Before applying transformations, detect which Keycloak shape and which Graylog shape the file is in:
KEYCLOAK_QUARKUS_NEEDED: true if thekeycloak:service block contains any of:PROXY_ADDRESS_FORWARDINGDB_VENDOR=POSTGRESDB_ADDR=DB_DATABASE=DB_USER=DB_PASSWORD=
False only if all of those are absent and the block already contains Quarkus markers (
KC_DB,KC_HOSTNAME, etc.) — meaning Keycloak was migrated independently before this skill ran. Mixed (Wildfly and Quarkus markers present) → raise as anomaly, default abort.Important: Quarkus migration on the keycloak service block is a hard requirement of the 2.14 baseline — the next sub-skills in the chain (
/upgrade-2.14-to-2.15onward) and the runtimeunctad/keycloak:RCimage at 2.14+ both assume Quarkus. The skill applies the migration without prompting whenKEYCLOAK_QUARKUS_NEEDED=true. If you don't want it applied, abort the whole skill and migrate Keycloak manually first.OPENSEARCH_NEEDED: true if thegraylog:service block containsGRAYLOG_ELASTICSEARCH_HOSTS:referencing$SERVICE_HOST:9200(or any host that is notopensearch-node1:9200) AND the file does not contain a top-levelopensearch-node1:service. Otherwise false.
Print the booleans: "Keycloak Quarkus migration: <yes|no>. Opensearch addition: <yes|no>."
STEP 3: Apply the required transformations
Edit <TARGET> in place. Preserve indentation and line endings exactly. Apply rules in order; each operates on the file produced by the previous.
Image tag rules
Rule 1 — Bump unctad/<service>:$<SERVICE>_VER to unctad/<service>:RC.
For every line matching ^(\s*)image:\s*unctad/([^:\s]+):\$[A-Z_]+_VER\s*$, replace the tag portion with :RC. Keep leading whitespace and image name verbatim.
Rule 2 — Bump unctad/<service>:<pinned-semver> to unctad/<service>:RC.
For every line matching ^(\s*)image:\s*unctad/([^:\s]+):([0-9]+\.[0-9]+(\.[0-9]+)?(-[0-9]+)?)\s*$ where <service> is not in the country-image deny-list (mule3-<country>, mule4-<country>, cashier-<country>), replace the tag with :RC.
Rule 3 — License-registry / GDB special case.
If a line matches image:\s*unctad/license-registry: (any tag), the canonical 2.14 RC tag is :RC (same as other services). Apply Rule 1 / Rule 2. Note that 2.15 → 2.16 will later move this specific service to :DEV — that is out of scope for this skill.
Rule 4 — Country-image and floating-tag services left alone.
Skip lines matching unctad/(mule3-|mule4-|cashier-). Their :DEV (or other) tags follow a country-specific lifecycle. Skip unctad/statistics-backend:DEV and unctad/ds-frontend:DEV if present (these floating-tag services are introduced post-2.14 and shouldn't appear on a 2.13 baseline; if they do, raise an anomaly via STEP 2).
Keycloak URL rules (always apply)
These rules drop the /auth path segment that Wildfly Keycloak required and Quarkus Keycloak doesn't. Apply across all services that talk to Keycloak.
Rule 5 — Internal KEYCLOAK_URL drop /auth.
For every env-list item matching KEYCLOAK_URL=http://keycloak:8080/auth (with or without quotes, in any service's environment: list — typically camunda, bpa-backend, bpa-frontend (internal variant), mule, js-assistant), strip the /auth suffix → KEYCLOAK_URL=http://keycloak:8080.
Rule 6 — Internal AUTH_SERVICE_URL drop /auth.
Same shape as Rule 5 but for AUTH_SERVICE_URL=http://keycloak:8080/auth (typically on mule).
Rule 7 — Internal AUTH_SERVICE_BACKEND_URL drop /auth.
For AUTH_SERVICE_BACKEND_URL=http://keycloak:8080/auth (typically on ereg-cms-frontend, statistics-backend), strip /auth.
Rule 8 — Public KEYCLOAK_URL drop /auth/.
For public-facing variants matching KEYCLOAK_URL=https://login.$YOUR_DOMAIN_NAME/auth/ (with the trailing slash; typically on bpa-frontend, statistics-frontend), strip /auth/ → KEYCLOAK_URL=https://login.$YOUR_DOMAIN_NAME/.
Rule 9 — Public AUTH_SERVICE_PUBLIC_URL drop /auth.
For AUTH_SERVICE_PUBLIC_URL=https://login.$YOUR_DOMAIN_NAME/auth (typically on ereg-cms-frontend), strip /auth (no trailing slash variant).
For each rule, log how many lines matched. If a rule expected matches in a service block that is present but matches zero, raise it as an anomaly (the service may have been customized).
RestHeart rule
Rule 10 — Rename MONGO_URI to RH_MONGO_URI on restheart.
Locate the restheart: service block. For the env-list item matching MONGO_URI=<value> (with or without quotes), replace just the var name with RH_MONGO_URI. Keep <value> and quoting style intact. If both MONGO_URI and RH_MONGO_URI exist already, raise an anomaly.
Env-var rules on bpa-frontend
The post-state values are env-aware (per the table at the top of STEP 2):
- Non-dev envs (
test,preview,prelive,live): post-stateEREGISTRATIONS_VERSION=2.14,BUILD_TYPE=RC. - Dev env: post-state
EREGISTRATIONS_VERSION=DEV,BUILD_TYPE=DEV.
Rule 11 — Normalize EREGISTRATIONS_VERSION on bpa-frontend.
Replace any list item whose stripped content is - EREGISTRATIONS_VERSION=<accepted_source> (where <accepted_source> is any value in the accepted-source set for <env> — 2.13 / $EREGISTRATIONS_VERSION for non-dev, DEV / $EREGISTRATIONS_VERSION / 2.13 for dev), or quoted variants, with - EREGISTRATIONS_VERSION=<post_EV> (the env-specific post-state value). Preserve original indentation, dash, and quoting style.
Rule 12 — Normalize BUILD_TYPE on bpa-frontend.
Replace any list item whose stripped content is - BUILD_TYPE=<accepted_source> (where <accepted_source> is any value in the accepted-source set for <env> — LIVE for non-dev, DEV / $BPA_FRONTEND_VER / LIVE for dev), or quoted variants, with - BUILD_TYPE=<post_BT>.
Env-var rules on ereg-cms-frontend (DS-side service)
Rule 13 — Normalize EREGISTRATIONS_VERSION and BUILD_TYPE on ereg-cms-frontend.
Locate the ereg-cms-frontend: service block. Apply the same replacements as Rule 11 and Rule 12 inside its environment: list. If neither var is present, append - "EREGISTRATIONS_VERSION=<post_EV>" and - "BUILD_TYPE=<post_BT>" (matching the surrounding double-quoted style).
STEP 3.5: Apply Keycloak Quarkus migration (mandatory when KEYCLOAK_QUARKUS_NEEDED)
Keycloak Quarkus migration is a hard requirement of the 2.14 baseline — it is not optional and there is no opt-out prompt.
If KEYCLOAK_QUARKUS_NEEDED=false (Keycloak was already migrated to Quarkus before this skill ran), skip this entire step.
Otherwise:
Locate the
keycloak:service block at top-level indent (2 spaces).Replace the entire
environment:list with the canonical Quarkus block. The before/after shapes:Before (Wildfly):
keycloak: restart: always container_name: keycloak image: unctad/keycloak:RC # already bumped by Rule 1 by this point ports: - "8180:8080" - "9990:9990" environment: - "PROXY_ADDRESS_FORWARDING=true" - "DB_VENDOR=POSTGRES" - "DB_ADDR=$SERVICE_HOST" - "DB_DATABASE=$KEYCLOAK_POSTGRES_DB_NAME" - "DB_USER=$KEYCLOAK_POSTGRES_DB_USER" - "DB_PASSWORD=$KEYCLOAK_POSTGRES_DB_PASSWORD" - "KEYCLOAK_STATISTICS=all"After (Quarkus):
keycloak: restart: always container_name: keycloak image: unctad/keycloak:RC ports: - "8180:8080" - "9990:9990" environment: - "HTTP_ADDRESS_FORWARDING=true" - "KC_DB=postgres" - "KC_DB_URL=jdbc:postgresql://postgres_host:5432/keycloak" - "KC_DB_USERNAME=$KEYCLOAK_POSTGRES_DB_USER" - "KC_DB_PASSWORD=$KEYCLOAK_POSTGRES_DB_PASSWORD" - "KC_DB_SCHEMA=public" - "KC_HOSTNAME=login.$YOUR_DOMAIN_NAME" - "KC_HOSTNAME_STRICT_HTTPS=true" - "KC_HOSTNAME_STRICT=true" - "KEYCLOAK_STATISTICS=all" - "KC_LOG_LEVEL=INFO" extra_hosts: - "postgres_host:$SERVICE_HOST"Country-specific risks to surface before applying. Print:
Applying mandatory Keycloak Quarkus migration. The template hardcodes: - DB name: keycloak (your instance may use a different name via $KEYCLOAK_POSTGRES_DB_NAME) - Hostname: login.$YOUR_DOMAIN_NAME (template; expanded at deploy time) - Log level: INFO - Strict HTTPS hostname: enabled Existing $KEYCLOAK_POSTGRES_DB_NAME usage will be lost — the JDBC URL hard-codes /keycloak. If your instance uses a non-default DB name, abort now (Ctrl-C / answer "a" to the next anomaly), migrate Keycloak manually, and re-run this skill.No
(y/N)prompt — the migration is required for the 2.14 baseline. The user-facing escape hatch is to abort the whole skill (e.g. by raising any anomaly in STEP 2 witha, or by Ctrl-C) and migrate Keycloak manually before re-running.Apply the replacement. Replace the entire
environment:list of thekeycloak:block with the Quarkus list above. If anextra_hosts:block does not already exist onkeycloak:, append it after the newenvironment:block. Ifextra_hosts:exists withpostgres_hostalready in it, leave it alone. If it exists withoutpostgres_host, add thepostgres_host:$SERVICE_HOSTentry to the existing list.
STEP 3.6: Apply Opensearch migration (only if OPENSEARCH_NEEDED)
If OPENSEARCH_NEEDED=false, skip this entire step.
Otherwise:
Repoint Graylog at Opensearch. In the
graylog:service block:- Replace
GRAYLOG_ELASTICSEARCH_HOSTS: http://$SERVICE_HOST:9200withGRAYLOG_ELASTICSEARCH_HOSTS: http://opensearch-node1:9200. Preserve the mapping-style key (KEY: value) or list-style (- KEY=value) as found. - Remove the
extra_hosts:entry- "elastic_host:$SERVICE_HOST"if present. Ifextra_hosts:becomes empty, remove the emptyextra_hosts:key too.
- Replace
Insert the
opensearch-node1service block. Add the following block immediately after thegraylog:service (before the next service definition), matching the prevailing 2-space top-level indent:opensearch-node1: image: opensearchproject/opensearch:2.12.0 container_name: opensearch-node1 environment: - plugins.security.disabled=true - cluster.name=opensearch-cluster - node.name=opensearch-node1 - discovery.seed_hosts=opensearch-node1 - cluster.initial_cluster_manager_nodes=opensearch-node1 - bootstrap.memory_lock=true - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" - OPENSEARCH_INITIAL_ADMIN_PASSWORD=$OPENSEARCH_ADMIN_PASSWORD - path.repo=/opt/os_backup ulimits: memlock: soft: -1 hard: -1 nofile: soft: 65536 hard: 65536 volumes: - /opt/volumes/opensearch/data:/usr/share/opensearch/data - /opt/volumes/opensearch/os_backup:/opt/os_backup networks: - graylog_networkCountry-specific risks to surface. Before writing the block, print:
Opensearch service block is about to be added. The template hardcodes: - Image: opensearchproject/opensearch:2.12.0 - Volume paths: /opt/volumes/opensearch/{data,os_backup} on the host - Network: graylog_network (must already exist in the file's `networks:` section) - Required env: $OPENSEARCH_ADMIN_PASSWORD (must be set in the deployment env) Verify the volume paths and the graylog_network reference for <country>. After deploy, you will need to backfill Graylog indices into Opensearch via a one-time data migration outside this skill's scope.AskUserQuestion: "Add Opensearch block as-is? (y/N — N skips this step and you do it manually)".
y→ insert the block; ensuregraylog_networkis referenced in the file's top-levelnetworks:section (raise an anomaly if not).Nor empty → skip. Leave Graylog pointing at the legacy ES endpoint. Print: "Opensearch block not added. Graylog will continue to write to$SERVICE_HOST:9200. Manual data migration required separately." Continue with STEP 4.
STEP 3.7: Emit Keycloak realm patch artifacts (standalone mode only)
Upgraded instances on a pre-filled (2.13-era) Keycloak realm are missing pieces the 2.14+ runtime depends on. Emit a patch bundle alongside docker-stack.yml so the operator can apply it against the live realm before redeploy. Chain mode skips this — the orchestrator emits once at chain end.
Resolve starter-conf path (source of canonical realm template):
STARTER_CONF_PATHenv var, OR../eregistrations-starter-conf/scripts/keycloak-realm.template.jsonrelative to eregistrations-v4 worktree, OR- Prompt operator; on two-strikes invalid, skip this step and print "Realm patch artifacts skipped; apply manually."
Emit under
Conf-<UPPER_ENV>/compose/<country>/keycloak-patch/:partial-import.json— body forPOST /admin/realms/<realm>/partialImportclient-scope.json— body forPOST /admin/realms/<realm>/client-scopesuser-profile.json— body forPUT /admin/realms/<realm>/users/profileapply.sh— applies all four via curl + admin token
Use
templates/extract-keycloak-patch.pyto produce the artifacts. It reads the starter-conf template, substitutes placeholders from this country's docker-stack.yml (realm name, domain, OAuth client IDs), generates fresh client-secret UUIDs, strips internal IDs from the client-scope so Keycloak generates fresh ones, and writes the four files.Add the
keycloak-patch/directory to git ignore (or warn operator) — the JSON files contain freshly-minted client-secret UUIDs that must not land in version control.
See LESSONS.md for the why, the operator workflow, the rationale for not committing the artifacts, and the post-handoff KEYCLOAK_CLIENT_SCOPE_ID step that the operator handles manually (swarm stack files can't carry $VAR placeholders, so the env var insertion is a deploy-time edit after the realm patch resolves the scope UUID).
STEP 4: Post-transformation safety scan
After applying the rules, scan the modified <TARGET> for any remaining surprises that would suggest the upgrade is incomplete:
grep -nE 'image:[[:space:]]*unctad/[^:]+:\$[A-Z_]+_VER' "$TARGET" || true— should be empty (Rule 1 catches all$VAR_VERimages).grep -nE 'image:[[:space:]]*unctad/[^:]+:[0-9]+\.[0-9]+' "$TARGET" || true— should match only country-image services (mule3-/mule4-/cashier-) if any. Any other match is a missed bump.grep -n 'EREGISTRATIONS_VERSION=2\.13' "$TARGET" || true— should be empty.grep -n 'EREGISTRATIONS_VERSION=\$EREGISTRATIONS_VERSION' "$TARGET" || true— should be empty.grep -nE 'KEYCLOAK_URL=http://keycloak:8080/auth' "$TARGET" || true— should be empty.grep -nE 'AUTH_SERVICE_(URL|BACKEND_URL)=http://keycloak:8080/auth' "$TARGET" || true— should be empty.grep -n 'login\.\$YOUR_DOMAIN_NAME/auth' "$TARGET" || true— should be empty.grep -n '^\s*-\s*"\?MONGO_URI=' "$TARGET" || true— should be empty (Rule 10 renamed it).- If
KEYCLOAK_QUARKUS_NEEDED=trueand the user saidy, also check:grep -nE '"?\b(PROXY_ADDRESS_FORWARDING|DB_VENDOR|DB_ADDR|DB_DATABASE|DB_USER|DB_PASSWORD)=' "$TARGET" || true— should be empty. Word-boundary\bis important; without it, the QuarkusKC_DB_PASSWORD=matches as a false positive.
For every match, present it as an anomaly with (c)ontinue / (s)kip / (a)bort. a rolls back via git restore -- "$TARGET" and exits.
STEP 5: Diff review
Show diff: git --no-pager diff --no-color -- "$TARGET". Print verbatim. Note the diff will be large — typically 60–150 lines depending on conditional rules — much bigger than later step skills.
Standalone mode: AskUserQuestion: "Commit, push, and open PR? (y/N)". y → STEP 5.5 (LIVE only) → STEP 6. Anything else → git restore -- "$TARGET" and exit cleanly.
Chain mode: skip the y/N prompt — the orchestrator already gathered intent. Proceed straight to STEP 6 (commit only). The orchestrator handles the between-step pause and the squash + PR at the end of the chain.
STEP 5.5: LIVE confirmation rail (standalone mode only when <env>=live)
In chain mode, this step is skipped — the orchestrator does the LIVE retype-country rail once before the first step and threads BACKUP_CONFIRMED=1 plus the chain branch through.
In standalone mode for live envs:
- Print: "This will upgrade a LIVE production instance:
<country>. Type the country name exactly to confirm." - Read trimmed answer. Compare to
<country>exactly (case-sensitive). - Mismatch →
git restore -- "$TARGET"and exit cleanly: "Country name mismatch. Aborted."
STEP 6: Commit (and push/PR in standalone mode)
Chain mode
Stage and commit on the chain branch.
git add "$TARGET" git commit -m "Step 2.13→2.14 on <env>.<country> TOBE-17814"Print: "Step 2.13→2.14 committed on
<CHAIN_BRANCH>." Return control to the orchestrator. Do not push, do not open a PR.
Standalone mode
Compute branch name.
BRANCH=chore/upgrade-<env>-<country>-2.13-to-2.14.Check branch doesn't exist (locally and on origin). If it does, abort and
git restore -- "$TARGET".Create branch and commit.
git checkout -b "$BRANCH" git add "$TARGET" git commit -m "Upgrade <env>.<country> from 2.13 to 2.14 TOBE-17814"Push.
git push -u origin "$BRANCH". On rejection: leave the local commit, print recovery hint.Open PR.
- GitHub:
gh pr create --base master --head "$BRANCH" --title "Upgrade <env>.<country> from 2.13 to 2.14" --body "<body>" - Bitbucket: skip CLI; print the manual link in the format
https://bitbucket.org/<workspace>/<repo>/pull-requests/new?source=$BRANCH&dest=master.
- GitHub:
Print the PR URL.
Switch back to master.
git checkout master.
Reference: failure modes
| Class | Examples | Outcome |
|---|---|---|
| Hard abort (no edits) | not in git repo; not on master (standalone) / not on chain branch (chain mode); dirty tree; gh missing on GitHub origin in standalone mode; pull fails; Conf-<UPPER_ENV>/compose/ missing; user mistypes country twice (interactive); country supplied via args is invalid; target file missing; branch already exists locally or on origin (standalone) |
Print failure reason, exit non-zero. |
| Clean exit (no edits) | candidate scan finds zero files; selected file has zero unctad/*:$VAR_VER lines; user said "N" to backup confirmation |
Print "Nothing to upgrade" / " |
| Soft pause | any anomaly (pre-scan, post-scan, precondition detection); diff-review answered N (standalone only); LIVE retype-country mismatch (standalone only); user said N to the Opensearch confirmation |
Wait for input; on abort/restore/mismatch, run git restore -- "$TARGET" and exit cleanly. The Opensearch addition has an opt-out (the data backfill is a separate operation). The Keycloak Quarkus migration has no opt-out — to skip it, abort the whole skill and migrate Keycloak manually first. |
Reference: PR body template (standalone mode)
## Summary
Mechanical upgrade of `Conf-<UPPER_ENV>/compose/<country>/docker-stack.yml`
from eRegistrations 2.13 to 2.14.
## Required transformations applied
- Bumped every `unctad/<service>:$<VAR>_VER` (and pinned-semver) image tag
to `:RC` (the platform tag introduced in 2.14).
- Dropped `/auth` path suffix from `KEYCLOAK_URL` (internal + public),
`AUTH_SERVICE_URL`, `AUTH_SERVICE_BACKEND_URL`, `AUTH_SERVICE_PUBLIC_URL`.
- Renamed `MONGO_URI` → `RH_MONGO_URI` on `restheart`.
- Bumped `EREGISTRATIONS_VERSION` from `2.13` (or `$EREGISTRATIONS_VERSION`)
to `2.14` on `bpa-frontend` and `ereg-cms-frontend`.
- Bumped `BUILD_TYPE=LIVE` → `BUILD_TYPE=RC` on `bpa-frontend` and
`ereg-cms-frontend` (non-dev envs only).
## Keycloak realm patch artifacts
<one of:>
- Emitted under `Conf-<UPPER_ENV>/compose/<country>/keycloak-patch/`
(not committed — see LESSONS.md). Apply locally against the live Keycloak
before redeploying.
- Skipped (operator declined / starter-conf path not resolved).
## Mandatory Keycloak Quarkus migration
<one of:>
- Applied: replaced Wildfly env vars (`PROXY_ADDRESS_FORWARDING`, `DB_*`,
`KEYCLOAK_STATISTICS`) with the Quarkus block (`HTTP_ADDRESS_FORWARDING`,
`KC_DB*`, `KC_HOSTNAME*`, `KC_LOG_LEVEL=INFO`) and added
`extra_hosts: postgres_host:$SERVICE_HOST`.
- Not needed (Keycloak was already on Quarkus before this run).
## Optional Opensearch addition
<one of:>
- Opensearch migration applied: added `opensearch-node1` service block
(`opensearchproject/opensearch:2.12.0`) and repointed
`GRAYLOG_ELASTICSEARCH_HOSTS` from `$SERVICE_HOST:9200` to
`opensearch-node1:9200`. Note: a one-time data backfill from the legacy
Elasticsearch into Opensearch is required separately.
- Opensearch migration skipped (operator chose to handle manually, or
Graylog was already pointing at Opensearch).
## Anomalies skipped
<skipped>
## Test plan
- [ ] CI passes.
- [ ] Reviewer eyeballs the diff against the rules in this skill, paying
particular attention to the Keycloak Quarkus block (DB name, hostname)
and the Opensearch block (volume paths, network reference).
- [ ] After deploy: keycloak service comes up healthy on Quarkus,
Graylog ingestion lands in opensearch-node1 (if applied),
`bpa-frontend`, `bpa-backend`, `restheart`, `camunda` all reachable.
- [ ] Smoke-test BPA login flow end-to-end (auth path drop is the riskiest
change beyond the Keycloak block).