Playbook: W4 v42-prod GitLab CI image-build pipeline (Faza 3C)
Created: 2026-07-11
Source issue: radieu/p24-infra#3768
Plan: docs/plans/w3-w4-cicd-full-deployment.md — Faza 3C
Artifact: docs/bms-1/gitlab-ci-w4.yml
Compose source of truth: docs/bms-1/docker-compose-w4.yml
Human-execution required. This pipeline builds and deploys the live Pinbox24 W4 backend (
v42-prod) on bms-1. p24-infra has push/MR rights but NO merge rights onpinbox24/p24-back-ts(seegitlab-repo-locations.md). An autonomous worker must never apply this to the GitLab repo — a human reviews and applies it.
What this is
docs/bms-1/gitlab-ci-w4.yml is the git-tracked source of truth for the .gitlab-ci.yml that
should live in the pinbox24/p24-back-ts GitLab repo (default branch development). It mirrors the
pattern established by docker-compose-w4.yml (#3542): land the artifact in p24-infra first, then
wire it into the live pipeline as a separate, human-applied step.
Two stages:
| Stage | Job | Trigger | What it does |
|---|---|---|---|
build | build:v42-prod | push to development / manual web trigger | Builds the plain v42-prod app image, pushes :<merged-tag> + :latest to ECR, retags the previous :latest as :old-<date> for rollback |
deploy | deploy:v42-prod | manual (run in the nightly window) | docker compose pull + up -d v42-prod, health-gates on :3000/health |
deploy | deploy:v42-prod:nightly | CI schedule (set to 20:00–06:00 UTC) | Same deploy, hands-off, for the nightly window |
All jobs run on the bms-1 shell runner — register the tag v42-bms1-autodeploy on it (same
pattern as mailgun-bms1-autodeploy / s3-v2-bms1-autodeploy from Faza 1).
Design decisions honored (issue #3768 scope, 2026-07-11)
Gap 8 — patches are NOT baked into the image
The persistent patches stay volume-mounted at runtime, exactly as in docker-compose-w4.yml.
Live mount state on bms-1 (reconfirmed 2026-08-09, #5992):
persistent-patches/postbookReport.helper.js→/app/dist/apps/postbook/postbookReport.helper.jspersistent-patches/uploadAwsS3.helper.js→/app/dist/globalHelpers/uploadAwsS3.helper.jspersistent-patches/socket.middleware.js→/app/dist/middleware/socket.middleware.js(#3966 socket-auth try/catch)persistent-patches/app.js→/app/dist/app.js(#3407 follow-up — disables the GUS-API startup code)ecosystem.config.js→/app/ecosystem.config.jscredentials.json→/app/credentials.json(SOPS-managed Google SA creds, #3737)
gus-api-regon-wsdl→/app/dist/wsdlmount was REMOVED (it never took effect — the realrequire()resolves fromnode_modules/@pobidowski/gus-api-regon/dist/wsdl/; dropped in the same change as theapp.jsGUS-code disable). It is no longer indocker-compose.yml.
uploadAwsS3.helper.jsandsocket.middleware.jsare still load-bearing even though their fixes are merged intop24-back-tsdevelopment: the deployedv42-prod:latestECR image’s bakeddist/still ships the pre-fix code (verified #5992). Do not drop these mounts on the strength of the merge alone — verify the baked image dist first. Full patch inventory + still-needed rationale:docs/pinbox24-w3-w4-architecture-spec.md §3.1.
The build:v42-prod job runs a plain docker build (no COPY of these files). This keeps the
image reproducible and the patches auditable in the builder dir. Do not change this — it was an
explicit decision on 2026-07-11.
Gap 5 — redis-v42 startup ordering
redis-v42 already has depends_on + a healthcheck in docker-compose-w4.yml. The deploy job uses
docker compose up -d so compose enforces condition: service_healthy — no ordering logic lives in
the pipeline. This prevents the RabbitMQ-consumer crash when v42-prod starts before redis-v42
(P1 Gap #6 in the plan).
D6 — WebSocket-safe restart window (nightly only, 20:00–06:00 UTC)
v42-prod serves WebSocket connections. A mid-day container recreate drops every live socket, so
restarts are confined to the low-traffic window 20:00–06:00 UTC. This is a documented operational
window, not an app-level graceful-drain — nothing fancier was requested (D6, 2026-07-11).
deploy:v42-prodiswhen: manual: the operator triggers it inside the window.deploy:v42-prod:nightlyruns on a CI schedule — set the schedule to fire within the window.- Do not run a deploy outside the window without an incident justification.
Out of scope
wkhtml-v42-prodPrometheus health check → tracked separately in #3739. Not duplicated here.
Prerequisites (one-time, before first apply)
- Runner tag
v42-bms1-autodeployregistered on the bms-1 shell runner. - AWS ECR creds as GitLab CI/CD variables (protected + masked), never in the file:
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY(regioneu-central-1). docker-compose-w4.ymlpresent on bms-1 at the builder path/root/builds/7N4sbbrB/0/pinbox24/p24-back-ts/docker-compose-w4.yml, synced fromdocs/bms-1/docker-compose-w4.yml. The autoheal step (#3611) keeps the env files it references populated from SOPS (pinbox24-w4.env.sops,pinbox24-backends.env.sops).- CI schedule (optional, for
deploy:v42-prod:nightly) created in the GitLab project set to a time inside 20:00–06:00 UTC.
Applying to GitLab — single-file PUT (NOT the Commits API)
The multi-action Commits API (POST /projects/:id/repository/commits) times out on this runner. Use
the single-file Repository Files endpoint, which commits one file in one call. This is a
credential operation (GITLAB_ADMIN_PAT) — a secret-manager / developer runs it, never an
autonomous dev-issue worker.
Source file (2026-08-01, #4796): read
GITLAB_ADMIN_PATfromsecrets/pinbox24-gitlab.env.sops, notsecrets/administration.env.sops. The canonical copy still lives inadministration.env.sops(developer-only, single recipient — never decryptable on any server), but the bms-4 infra-task worker’s age key was never a recipient of that file, so every command below silently had no path to the token when run on bms-4.pinbox24-gitlab.env.sopsis a narrow 2-recipient copy (developer + bms-4 worker key only) created specifically so this playbook’s commands work from bms-4. Both copies must be rotated together (seesecret-rotation-access-matrix.md).
Recommended: PUT to a NEW branch, then open an MR (p24-infra has no merge rights — a human on the
Pinbox24 side merges). Do not PUT directly to development.
# Read the token silently (never echo it). PROJECT_ID = URL-encoded path or numeric id.
PAT=$(sops -d --input-type dotenv --output-type dotenv \
/opt/p24-infra/secrets/pinbox24-gitlab.env.sops | grep '^GITLAB_ADMIN_PAT=' | cut -d= -f2-)
PROJECT_ID="pinbox24%2Fp24-back-ts" # URL-encoded namespace/project
FEATURE_BRANCH="faza-3c-v42-prod-ci"
# 1. Update .gitlab-ci.yml on a new branch (start_branch creates it from development).
# --data-urlencode reads the file content without shell-escaping issues.
curl -sf -X PUT "https://gitlab.com/api/v4/projects/${PROJECT_ID}/repository/files/.gitlab-ci.yml" \
-H "PRIVATE-TOKEN: ${PAT}" \
--data-urlencode "branch=${FEATURE_BRANCH}" \
--data-urlencode "start_branch=development" \
--data-urlencode "commit_message=ci(faza-3c): v42-prod image build+deploy pipeline (radieu/p24-infra#3768)" \
--data-urlencode "content@docs/bms-1/gitlab-ci-w4.yml"
# (If the branch already exists, drop start_branch. If .gitlab-ci.yml does not yet exist on the
# branch, use POST instead of PUT to create it.)
# 2. Open a merge request for human review — never merge it yourself.
curl -sf -X POST "https://gitlab.com/api/v4/projects/${PROJECT_ID}/merge_requests" \
-H "PRIVATE-TOKEN: ${PAT}" \
--data-urlencode "source_branch=${FEATURE_BRANCH}" \
--data-urlencode "target_branch=development" \
--data-urlencode "title=Faza 3C: v42-prod CI image-build pipeline (radieu/p24-infra#3768)"
unset PAT- Token goes in the
PRIVATE-TOKENheader only — never in the URL (leaks into history/logs). - Pass
-c http.extraHeader=...style only forgit, not needed for the REST calls above.
Before applying, diff against the live file — the artifact is proposed. Fetch and compare:
PAT=$(sops -d --input-type dotenv --output-type dotenv \
/opt/p24-infra/secrets/pinbox24-gitlab.env.sops | grep '^GITLAB_ADMIN_PAT=' | cut -d= -f2-)
curl -sf -H "PRIVATE-TOKEN: ${PAT}" \
"https://gitlab.com/api/v4/projects/pinbox24%2Fp24-back-ts/repository/files/.gitlab-ci.yml/raw?ref=development" \
-o /tmp/live-gitlab-ci-w4.yml
unset PAT
diff -u /tmp/live-gitlab-ci-w4.yml docs/bms-1/gitlab-ci-w4.yml || true
# Reconcile job names / stages / runner tag / ECR repo path with reality before applying.Rollback
The build job retags the previous :latest as :old-<date> before overwriting, so a rollback
target always exists (v42-prod has no automatic old- tag — same gap as s3-v2 in
pinbox24-ms-deploy.md).
# On bms-1 — repoint :latest at the last known-good old- tag, then redeploy in the nightly window.
docker tag "563740926945.dkr.ecr.eu-central-1.amazonaws.com/v42-prod:old-<date>" \
"563740926945.dkr.ecr.eu-central-1.amazonaws.com/v42-prod:latest"
docker compose -f /root/builds/7N4sbbrB/0/pinbox24/p24-back-ts/docker-compose-w4.yml up -d v42-prodGitLab pipeline history also records the exact image tag of every past deploy — use it to pin a rollback target.
Error reporting (repo standard)
If any apply/deploy step fails, report it — do not silently work around:
- Discord embed via
P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL. - GitHub issue in
radieu/p24-infralabelledbug.
Related
docs/bms-1/gitlab-ci-w4.yml— the pipeline artifactdocs/bms-1/docker-compose-w4.yml— deploy source of truth (#3542)docs/playbooks/v41-prod-ecr-rebuild.md— W4 frontend (v41-prod, Angular) ECR build/verify/cutover pipeline (#3733). The frontend is a separate pipeline family from this W4 backend (v42-prod) runbook — build+push runs automatically, deploy iswhen: manual.docs/pinbox24/gitlab-repo-locations.md— repo access + no-merge-rights ruledocs/playbooks/pinbox24-ms-deploy.md— Faza 1 mailgun/s3-v2 deploy patterndocs/plans/w3-w4-cicd-full-deployment.md— Faza 3C/3D- #3739 — wkhtml-v42-prod Prometheus health check (separate)
- #3542 — docker-compose-w4.yml committed to git (Faza 3A, unblocker)