From 02591466cc334e5823d3e7bbeac7872f6a5fe659 Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Wed, 13 May 2026 00:20:50 +0300 Subject: [PATCH 1/4] =?UTF-8?q?ci(release):=20auto-PR=20=D0=B2=20ordinis-i?= =?UTF-8?q?nfra=20=D1=81=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=BC=20image=20tag=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раньше release tag создавал v2.X.Y, build images, но values-prod.yaml в ordinis-infra оставался указывать на старый tag (либо unset → fall to :latest). Это создавало divergence: - Auto deploy через trigger-deploy-prod использовал runtime UPSTREAM_IMAGE_TAG = новый tag → правильный deploy. - Manual deploy через infra repo (helm upgrade -f values-prod.yaml) использовал static values → старый tag → split-brain ноды через :latest cache (см. ordinis-infra MR !10 fix). Новый job 'propose-prod-image-bump': - Triggers только на tag pipeline (semver v*.*.*). - Needs все docker-* jobs (images должны быть в registry). - Clones ordinis-infra, патчит image.tag в values-prod.yaml через awk с scope-tracking (только image: block, не writer/readApi/etc). - Commit + push branch + создаёт MR через GitLab API. - Reviewer approve → merge → infra pipeline auto-катит deploy-prod. Git становится единственным источником истины: HEAD ordinis-infra main всегда отражает что задеплоено на prod. Setup: новая CI variable INFRA_PROJECT_TOKEN (group-level access token scope: write_repository, api, role: Developer). Без неё job exits early с подсказкой — pipeline не блокируется (allow_failure: true). --- .gitlab-ci.yml | 184 +++++++++++++++++++++++++++++++++++++++++++++++++ RELEASE.md | 25 +++++++ 2 files changed, 209 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 750f43d..cc0f905 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -479,3 +479,187 @@ release: # Не блокирует следующие pipelines если release fail'нул (e.g. нет # релизных commits, нет GITLAB_TOKEN, transient network error). allow_failure: true + +# ============================================================= +# Auto-PR: bump prod image tag в ordinis-infra/values-prod.yaml. +# ============================================================= +# Зачем. До этого job'а prod deploys работали через две runtime механики: +# +# 1. Auto path. Ordinis tag pipeline → trigger-deploy-prod (manual gate) +# → downstream infra pipeline с UPSTREAM_IMAGE_TAG=v2-16-0- +# → helm upgrade --set writer.tag=$UPSTREAM ... → деплой. +# 2. Manual path. Помержить что-то в infra repo → infra pipeline → +# deploy-prod job (manual) → helm upgrade БЕЗ --set tag override → +# использует значения из values-prod.yaml. +# +# Проблема. (1) и (2) использовали разные источники истины для image tag: +# +# - (1) — runtime var UPSTREAM_IMAGE_TAG от triggering pipeline. +# - (2) — статика в values-prod.yaml (раньше unset → fall to global +# image.tag=latest → IfNotPresent кэш → split-brain pods, см. +# ordinis-infra MR !10 fix). +# +# После любого release manual path возвращался к старому tag'у который +# зафиксирован в values-prod.yaml — что divergировало с реальностью на +# кластере. Disaster-recovery `helm upgrade ... -f values-prod.yaml` +# rolled back prod на несколько версий назад. +# +# Решение. На каждый успешный release tag ordinis CI открывает MR в +# ordinis-infra обновляющий `image.tag` в values-prod.yaml на новый +# immutable tag (e.g. v2-16-0-). MR — single review point: после +# approve и merge infra pipeline auto-катит этот же tag через deploy-prod +# (rules: changes: charts/**/*). +# +# Git становится единственным источником истины: HEAD ordinis-infra +# main всегда отражает что задеплоено на prod. +# +# Setup ONE-TIME (см. RELEASE.md): +# 1. Создать group-level Access Token в 2-6/2-6-4/terravault group: +# - role: Developer на ordinis-infra (363) +# - scopes: write_repository, api +# 2. Settings → CI/CD → Variables в ORDINIS project: +# INFRA_PROJECT_TOKEN = (Masked, Protected) +# +# Trigger: только на semver tag (pipeline created semantic-release'ом). +# Allow_failure: true — manual deploy backup path остаётся если что. +propose-prod-image-bump: + stage: release + image: repo.nstart.cloud/library/alpine:3.20 + needs: + - job: docker-app + artifacts: false + - job: docker-admin-ui + artifacts: false + - job: docker-read-api + artifacts: false + - job: docker-projection-writer + artifacts: false + - job: docker-migrations + artifacts: false + rules: + # Только на tag pipeline (semantic-release завершил → новый tag → новый + # pipeline на ref=tag). Images должны быть собраны в этом pipeline'е + # (см. needs выше). На main pipeline tag ещё не существует — нечего + # bump'ать. + - if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/' + when: on_success + variables: + INFRA_PROJECT_ID: "368" + INFRA_REPO_PATH: "2-6/2-6-4/terravault/ordinis-infra" + before_script: + - | + if [ -z "$INFRA_PROJECT_TOKEN" ]; then + echo "═══════════════════════════════════════════════════════════════" + echo "INFRA_PROJECT_TOKEN is empty — skipping auto-PR creation." + echo "" + echo "Setup (one-time):" + echo " 1. group 2-6/2-6-4/terravault → Settings → Access Tokens" + echo " scopes: write_repository + api, role: Developer" + echo " 2. ordinis project (367) → Settings → CI/CD → Variables" + echo " add INFRA_PROJECT_TOKEN = " + echo " flags: Masked + Protected (tag pipelines is Protected ref)" + echo "" + echo "См. RELEASE.md для подробностей." + echo "═══════════════════════════════════════════════════════════════" + exit 0 + fi + - apk add --no-cache git curl jq + script: + - | + set -euo pipefail + + IMAGE_TAG="${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}" + VERSION="${CI_COMMIT_TAG}" + BRANCH="auto/bump-prod-image-${VERSION}" + INFRA_API="${CI_API_V4_URL}/projects/${INFRA_PROJECT_ID}" + + echo "Tag: $VERSION" + echo "Image tag: $IMAGE_TAG" + echo "Target branch: $BRANCH" + echo "" + + # 1. Clone infra (shallow для скорости). + git clone --depth=10 --branch=main \ + "https://oauth2:${INFRA_PROJECT_TOKEN}@${CI_SERVER_HOST}/${INFRA_REPO_PATH}.git" infra + cd infra + git config user.email "ci@ordinis.local" + git config user.name "ordinis-ci-bot" + + # Branch может остаться от прошлой неудачной попытки — clean delete. + git push origin --delete "$BRANCH" 2>/dev/null || true + git checkout -b "$BRANCH" + + # 2. Patch values-prod.yaml — заменяем image.tag. + # Используем awk c stateful scope-tracking: ищем `image:` block (top-level, + # без leading spaces), затем заменяем ` tag: ...` строку. Не трогаем + # writer.tag / readApi.tag / etc — у них другая indentation level. + awk -v new_tag="$IMAGE_TAG" ' + BEGIN { in_image_block = 0; replaced = 0 } + /^image:[[:space:]]*$/ { in_image_block = 1; print; next } + # Top-level keys (no indent, ends with `:`) reset scope. + /^[a-zA-Z][a-zA-Z0-9_-]*:/ && !/^image:/ { in_image_block = 0; print; next } + # Заменяем ` tag:` ТОЛЬКО внутри image: block. 2 spaces indent. + in_image_block && /^ tag:/ { + print " tag: \"" new_tag "\"" + replaced = 1 + next + } + { print } + END { if (!replaced) { print "ERROR: image.tag не найдена в values-prod.yaml" > "/dev/stderr"; exit 1 } } + ' charts/ordinis/values-prod.yaml > /tmp/values-prod.new + mv /tmp/values-prod.new charts/ordinis/values-prod.yaml + + # 3. Diff sanity: changed только эта строка. + if ! git diff --quiet charts/ordinis/values-prod.yaml; then + echo "=== Diff ===" + git diff charts/ordinis/values-prod.yaml + else + echo "INFO: tag уже $IMAGE_TAG в values-prod.yaml — no-op exit." + exit 0 + fi + + # 4. Commit + push branch. + git add charts/ordinis/values-prod.yaml + git commit -m "chore(release): bump prod image к ${IMAGE_TAG} + +Auto-generated ordinis-CI release pipeline. + +Source: ${CI_PROJECT_URL}/-/commit/${CI_COMMIT_SHA} +Pipeline: ${CI_PIPELINE_URL} +Tag: ${VERSION} +" + git push origin "$BRANCH" + + # 5. Create MR через GitLab API. + # description через --data-urlencode сохраняет переносы строк. + MR_DESC="Auto-generated ordinis CI release pipeline после semantic-release ${VERSION}. + +**Что меняется:** \`image.tag\` в \`charts/ordinis/values-prod.yaml\` → \`${IMAGE_TAG}\` (immutable SHA tag, гарантирует что ноды pull'нут именно этот digest). + +**Source:** +- Ordinis commit: ${CI_PROJECT_URL}/-/commit/${CI_COMMIT_SHA} +- Tag pipeline: ${CI_PIPELINE_URL} + +**Deploy после merge:** infra-pipeline auto-катит helm upgrade в \`ordinis-prod\` namespace через manual-gated \`deploy-prod\` job (как обычно). + +**Verify checklist:** +- [ ] CI 'helm-lint' pipeline зелёный (validate charts) +- [ ] После merge play 'deploy-prod' job +- [ ] \`curl /api/v1/version\` показывает \`commit: ${CI_COMMIT_SHORT_SHA}\` и \`tag: ${VERSION}\` +- [ ] Pods Running 2/2 на обоих writer replicas +- [ ] Все 5 deployment'ов на одном digest (no split-brain)" + + curl -sS --fail \ + --request POST \ + --header "PRIVATE-TOKEN: ${INFRA_PROJECT_TOKEN}" \ + --data-urlencode "source_branch=${BRANCH}" \ + --data-urlencode "target_branch=main" \ + --data-urlencode "title=chore(release): bump prod image к ${VERSION}" \ + --data-urlencode "description=${MR_DESC}" \ + --data-urlencode "remove_source_branch=true" \ + --data-urlencode "labels=release,auto-generated" \ + "${INFRA_API}/merge_requests" \ + | jq -r '"MR создан: " + .web_url' + # Не блокирует tag pipeline если auto-PR не получился (token отсутствует, + # transient API error и т.п.). Manual MR в infra остаётся всегда доступен. + allow_failure: true diff --git a/RELEASE.md b/RELEASE.md index 31b9c5c..914cae1 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -26,6 +26,31 @@ human кликает Deploy. После этого release job в `.gitlab-ci.yml` будет работать. +### Auto-PR в ordinis-infra (опционально) + +`propose-prod-image-bump` job открывает MR в `ordinis-infra` после каждого +release tag — bump'ит `image.tag` в `values-prod.yaml` на свежий immutable +SHA tag (e.g. `v2-16-0-abc12345`). Чинит historical bug когда manual deploys +через infra repo получали `:latest` (stale cache → split-brain pods). + +Setup: + +3. **Create Group Access Token** в `2-6/2-6-4/terravault`: + - `Group → Settings → Access Tokens → Add new token` + - Name: `ordinis-ci-infra-bump` + - Role: `Developer` + - Scopes: `write_repository`, `api` + - Expiry: 1 год + +4. **Add CI variable в ordinis project (367)**: + - Key: `INFRA_PROJECT_TOKEN` + - Value: (token из шага 3) + - Flags: `Masked` ✅, `Protected` ✅ + +Без этой переменной job exits early (`allow_failure: true`), tag pipeline +не блокируется. Manual MR в `ordinis-infra` остаётся всегда доступен как +fallback. + ## Commit conventions Conventional Commits (https://www.conventionalcommits.org/): From cac6b6a98180faab308594912248a26e01f2f97a Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Wed, 13 May 2026 00:24:33 +0300 Subject: [PATCH 2/4] =?UTF-8?q?fix(ci):=20use=20printf=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20commit/MR=20description=20avoiding=20YAML=20literal=20?= =?UTF-8?q?block=20break?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pipeline 6815 failed instantly с 'could not find expected : at line 625' — multi-line shell strings внутри script: - | литерального блока YAML ломали parser потому что continuation lines не были indented к уровню блока (lines 625, 627-629 в column 0 — YAML видел их как new key). Fix: собрать message строкой через printf '%b' с явными \n. Однострочные shell variables — YAML парсит без проблем независимо от содержимого внутри quotes. --- .gitlab-ci.yml | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cc0f905..419bbef 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -619,35 +619,16 @@ propose-prod-image-bump: fi # 4. Commit + push branch. + # printf '%b' разворачивает \n в реальные newlines внутри одной shell + # строки — избегаем multi-line YAML quoting issues (literal block scalar + # ломается на unindented continuation lines). git add charts/ordinis/values-prod.yaml - git commit -m "chore(release): bump prod image к ${IMAGE_TAG} - -Auto-generated ordinis-CI release pipeline. - -Source: ${CI_PROJECT_URL}/-/commit/${CI_COMMIT_SHA} -Pipeline: ${CI_PIPELINE_URL} -Tag: ${VERSION} -" + COMMIT_MSG=$(printf '%b' "chore(release): bump prod image к ${IMAGE_TAG}\n\nAuto-generated ordinis-CI release pipeline.\n\nSource: ${CI_PROJECT_URL}/-/commit/${CI_COMMIT_SHA}\nPipeline: ${CI_PIPELINE_URL}\nTag: ${VERSION}") + git commit -m "$COMMIT_MSG" git push origin "$BRANCH" # 5. Create MR через GitLab API. - # description через --data-urlencode сохраняет переносы строк. - MR_DESC="Auto-generated ordinis CI release pipeline после semantic-release ${VERSION}. - -**Что меняется:** \`image.tag\` в \`charts/ordinis/values-prod.yaml\` → \`${IMAGE_TAG}\` (immutable SHA tag, гарантирует что ноды pull'нут именно этот digest). - -**Source:** -- Ordinis commit: ${CI_PROJECT_URL}/-/commit/${CI_COMMIT_SHA} -- Tag pipeline: ${CI_PIPELINE_URL} - -**Deploy после merge:** infra-pipeline auto-катит helm upgrade в \`ordinis-prod\` namespace через manual-gated \`deploy-prod\` job (как обычно). - -**Verify checklist:** -- [ ] CI 'helm-lint' pipeline зелёный (validate charts) -- [ ] После merge play 'deploy-prod' job -- [ ] \`curl /api/v1/version\` показывает \`commit: ${CI_COMMIT_SHORT_SHA}\` и \`tag: ${VERSION}\` -- [ ] Pods Running 2/2 на обоих writer replicas -- [ ] Все 5 deployment'ов на одном digest (no split-brain)" + MR_DESC=$(printf '%b' "Auto-generated ordinis CI release pipeline после semantic-release ${VERSION}.\n\n**Что меняется:** \`image.tag\` в \`charts/ordinis/values-prod.yaml\` → \`${IMAGE_TAG}\` (immutable SHA tag, гарантирует что ноды pull'нут именно этот digest).\n\n**Source:**\n- Ordinis commit: ${CI_PROJECT_URL}/-/commit/${CI_COMMIT_SHA}\n- Tag pipeline: ${CI_PIPELINE_URL}\n\n**Deploy после merge:** infra-pipeline auto-катит helm upgrade в \`ordinis-prod\` namespace через manual-gated \`deploy-prod\` job (как обычно).\n\n**Verify checklist:**\n- [ ] CI 'helm-lint' pipeline зелёный (validate charts)\n- [ ] После merge play 'deploy-prod' job\n- [ ] \`curl /api/v1/version\` показывает \`commit: ${CI_COMMIT_SHORT_SHA}\` и \`tag: ${VERSION}\`\n- [ ] Pods Running 2/2 на обоих writer replicas\n- [ ] Все 5 deployment'ов на одном digest (no split-brain)") curl -sS --fail \ --request POST \ From b7e1f12bc47031f7a1b817a2d025d5472712220e Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Wed, 13 May 2026 00:25:48 +0300 Subject: [PATCH 3/4] =?UTF-8?q?fix(release):=20remove=20[skip=20ci]=20?= =?UTF-8?q?=D0=B8=D0=B7=20semantic-release=20commit=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] в commit message skip'ает GitLab pipeline для ВСЕХ ref'ов на этом SHA — включая tag pipeline. Эффект: semantic-release создаёт v*.*.* tag, но tag pipeline скипается → images с tag'ом v*-*-*- не собираются → propose-prod-image-bump (этот MR) не имеет images для bump. Confirmed на v2.16.0 (commit bf07d6b): tag pipeline 6814 = skipped, registry без 'v2-16-0-*' tags → невозможно deploy. Loop prevention для chore(release) commit'ов работает через releaseRules ('chore': release: false) — semantic-release на этом commit'е detect'ит 'no releasable commits since previous tag' → exit 0 → no new tag → no loop. Cost: docker-admin-ui ребилдит на каждом chore(release) commit (package.json + CHANGELOG.md match frontend-changes path-filter) — ~2 мин wasted CI time, приемлемо vs broken release flow. --- ordinis-admin-ui/.releaserc.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ordinis-admin-ui/.releaserc.json b/ordinis-admin-ui/.releaserc.json index d3101d5..1779cc3 100644 --- a/ordinis-admin-ui/.releaserc.json +++ b/ordinis-admin-ui/.releaserc.json @@ -63,7 +63,8 @@ "@semantic-release/git", { "assets": ["package.json", "CHANGELOG.md"], - "message": "chore(release): v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" + "_comment": "БЕЗ [skip ci]: tag pipeline на этом commit'е (через @semantic-release/gitlab) ДОЛЖЕН запускаться чтобы собрать images с tag'ом v*-*-*-. Без images = nothing для deploy на prod (см. ordinis-infra MR !10 + ordinis MR !154 auto-PR flow). Защита от loop: chore(release) тип имеет 'release: false' в releaseRules выше → semantic-release на этом chore-commit'е выйдет 0 (no releasable commits) → не создаст новый tag. Cost: docker-admin-ui ребилдит на каждом release commit (package.json + CHANGELOG.md changes match frontend-changes) — ~2 мин waste, приемлемо.", + "message": "chore(release): v${nextRelease.version}\n\n${nextRelease.notes}" } ], [ From 0c0478914c0b64e521368d05af5fc4d6ac47ef7e Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Wed, 13 May 2026 00:26:18 +0300 Subject: [PATCH 4/4] ci: re-trigger pipeline (previous commit had skip-ci marker literal in description)