Files
mdm-ordinis/.gitlab-ci.yml
T
Zimin A.N. cac6b6a981 fix(ci): use printf для commit/MR description avoiding YAML literal block break
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.
2026-05-13 00:24:33 +03:00

647 lines
31 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================
# Ordinis — build & push images.
# Path-aware CI: backend и frontend jobs запускаются только когда меняются
# соответствующие пути. Trigger downstream передаёт BACKEND_IMAGE_TAG и/или
# FRONTEND_IMAGE_TAG; infra-pipeline применяет helm upgrade с per-service
# overrides — сервисы которые не пересобирались не катятся.
# =============================================================
stages:
- test
- build
- publish
- trigger-deploy
- release
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn"
MAVEN_CLI_OPTS: "-B -ntp --batch-mode"
IMAGE_TAG: $CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA
REGISTRY: repo.nstart.cloud
IMAGE_REPO: terravault
DOCKER_BUILDKIT: "1"
cache:
key:
files: ["pom.xml"]
paths:
- .m2/repository
default:
# Pin builds to nstart-tagged runners. Без тегов job'ы попадали в shared
# pool и в часы пик висели по 20+ мин в waiting_for_resource (см.
# pipeline 6553 docker-app — 1464s до cancel). Tag nstart берёт наши
# выделенные runners.
tags:
- nstart
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
- script_failure
# Backend changes триггерят maven + docker для всех Java-сервисов + миграции.
.backend-changes: &backend-changes
- "ordinis-app/**/*"
- "ordinis-read-api/**/*"
- "ordinis-projection-writer/**/*"
- "ordinis-rest-api/**/*"
- "ordinis-domain/**/*"
- "ordinis-validation/**/*"
- "ordinis-events-api/**/*"
- "ordinis-outbox/**/*"
- "ordinis-auth/**/*"
- "ordinis-cuod-bundle/**/*"
- "ordinis-migrations/**/*"
- "pom.xml"
- ".gitlab-ci.yml"
# Frontend changes — только admin-ui.
.frontend-changes: &frontend-changes
- "ordinis-admin-ui/**/*"
# Documentation changes — Diplodoc-built integrator guide.
# docs-internal/ исключено явно: оно НЕ build'ится Diplodoc'ом и не должно
# триггерить docs job.
.docs-changes: &docs-changes
- "docs/**/*"
# ─── TEST ──────────────────────────────────────
maven-test:
stage: test
image: repo.nstart.cloud/library/maven:3.9.9-eclipse-temurin-21
script:
- mvn $MAVEN_CLI_OPTS verify
artifacts:
when: always
reports:
junit: '**/target/surefire-reports/TEST-*.xml'
# 3 days — junit reports нужны для debugging recent failures; longer keep
# съедает storage (см. pipeline 6502 500 на artifact upload — storage was full).
expire_in: 3 days
rules:
- changes: *backend-changes
# Е2Е тесты через Testcontainers (Postgres+Kafka). Профиль `e2e` в
# ordinis-app/pom.xml включает их (default surefire excludes e2e директорию).
# Требует docker:dind service для testcontainers — обычный maven-test runner
# не имеет Docker.
maven-e2e:
stage: test
image: repo.nstart.cloud/library/maven:3.9.9-eclipse-temurin-21
services:
- name: repo.nstart.cloud/library/docker:29.1.2-dind
alias: docker
command: ["--tls=false"]
variables:
DOCKER_HOST: "tcp://docker:2375"
DOCKER_TLS_CERTDIR: ""
TESTCONTAINERS_HOST_OVERRIDE: "docker"
TESTCONTAINERS_RYUK_DISABLED: "true"
script:
- for i in $(seq 1 30); do docker -H tcp://docker:2375 info >/dev/null 2>&1 && break; sleep 1; done
- mvn $MAVEN_CLI_OPTS -pl ordinis-app -am -P e2e test
artifacts:
when: always
reports:
junit: '**/target/surefire-reports/TEST-*.xml'
expire_in: 3 days
rules:
- changes: *backend-changes
# Можно сделать allow_failure: true если e2e будут флакать — пока нет.
# ─── BENCH (manual JMH) ────────────────────────
# Manual trigger only — slow (~5-10 min) + noisy в CI history. Trigger через
# pipeline run UI с variable RUN_BENCH=true. Catches алгоритмическую регрессию
# на hot paths (parseRef, OnCloseAction, schema traversal).
maven-bench:
stage: test
image: repo.nstart.cloud/library/maven:3.9.9-eclipse-temurin-21
script:
- mvn $MAVEN_CLI_OPTS -P bench -pl ordinis-bench -am package -DskipTests
# Quick smoke: 1 fork, 2 warmups, 3 measurements × 2s. Total ~5 min.
# Если нужно надёжнее — bump до -f 2 -wi 5 -i 5 -r 5 (15 min).
- java -jar ordinis-bench/target/benchmarks.jar -f 1 -wi 2 -i 3 -w 1 -r 2
-rf json -rff bench-results.json | tee bench-results.txt
artifacts:
paths:
- bench-results.json
- bench-results.txt
expire_in: 5 days
rules:
- if: '$RUN_BENCH == "true"'
when: on_success
- if: '$CI_PIPELINE_SOURCE == "web"'
when: manual
allow_failure: true
# ─── BUILD (Maven jars) ────────────────────────
# maven-package всегда запускается: docker backend jobs нуждаются в jar даже
# при frontend-only PR (иначе backend образы не пересобрать → ImagePullBackOff
# в кластере при rolling update). BuildKit layer cache делает повторную сборку
# идентичных jar'ов ~30 sec.
maven-package:
stage: build
image: repo.nstart.cloud/library/maven:3.9.9-eclipse-temurin-21
script:
- mvn $MAVEN_CLI_OPTS package -DskipTests
artifacts:
paths:
- "**/target/*.jar"
- ordinis-migrations/src/main/resources/db/changelog/**
expire_in: 1 day
# ─── BUILD: Integrator Guide (sanity check) ────
# Sanity-check build для PRs и non-main branches. Полный image build
# идёт в publish stage (docker-docs). Здесь только verify что yfm
# не выдаёт errors на изменения docs/**.
build-docs:
stage: build
image: repo.nstart.cloud/library/node:20-alpine
before_script:
# npm install pnpm — corepack может hang на CI runner integrity checks.
- npm install -g --silent pnpm@9.15.4
script:
- cd docs
- pnpm install --frozen-lockfile
- pnpm build
- echo "Built $(find _dist -name '*.html' | wc -l) HTML pages, total size:"
- du -sh _dist
artifacts:
name: "ordinis-docs-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA"
paths:
- docs/_dist
# 2 days — docs/_dist используется только для PR sanity check, deployed
# docs идут через docker-docs image (Nexus registry, not artifact).
expire_in: 2 days
rules:
- if: '$CI_COMMIT_BRANCH != "main"'
changes: *docs-changes
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *docs-changes
# ─── PUBLISH (Docker images) ───────────────────
# resource_group: serializes docker-* jobs across all concurrent pipelines на
# одном runner'е. 5 parallel docker builds (app/read-api/projection-writer/
# admin-ui/migrations) каждый поднимает свой DinD container с buildx — суммарно
# выжирали runner disk (~25Gi+) → "no space left on device" в /var/lib/docker
# на тяжёлых stages (см. pipeline 6066). Serialize'я добавляет ~10 min к
# pipeline'у, но вместо retry-roulette мы получаем стабильно зелёный CI.
# При scaling до multi-runner setup можно увеличить parallelism per-runner и
# переименовать group в per-runner identifier.
.docker_base:
stage: publish
image: repo.nstart.cloud/nstart/docker-cli-buildx:1.0.0
services:
- name: repo.nstart.cloud/library/docker:29.1.2-dind
alias: docker
command: ["--tls=false"]
# docker-builds группа залипла в GitLab Sidekiq после force-cancel зомби
# docker-app job 29772 на pipeline 6553. Все последующие docker-* job'ы
# (pipelines 6565/6570/6571) висели в waiting_for_resource — стейл lock
# никто не release'ил, sidekiq promote'р не срабатывал. Переименование
# создаёт fresh resource_group со чистым state. Старая docker-builds
# пусть умирает естественно.
resource_group: docker-builds-v2
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
# Docker сборки только на main + tag. На feature-branch / MR pipeline'ах
# тратить ~2 мин × 5 services было бессмысленно — deploy происходит только
# после merge в main, MR валидируется тестами (maven-test/e2e). Экономия
# ~10 мин на каждый MR push.
# docker-docs переопределяет свой rules: блок (path-filter), этот rules:
# затрагивает app/read-api/projection-writer/migrations/admin-ui.
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_COMMIT_TAG'
.publish_script: &publish_script |
for i in $(seq 1 30); do docker info >/dev/null 2>&1 && break; sleep 1; done
# Lighter cleanup: dangling layers only, build cache сохраняем чтобы BuildKit
# переиспользовал между serialized jobs. Раньше был `prune -af --volumes`
# после Nexus blob loss recovery (см. ниже) — сжирал inline cache, каждая
# сборка с нуля. Сейчас Nexus стабилен → cache сохраняем.
docker image prune -f >/dev/null 2>&1 || true
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
IMG="$REGISTRY/$IMAGE_REPO/$SVC"
# Cache strategy:
# - --cache-from $IMG:latest — BuildKit читает inline cache из previously
# pushed manifest'а. На повторных сборках cached слои переиспользуются,
# полная сборка падает с ~70-90s до 15-25s.
# - --build-arg BUILDKIT_INLINE_CACHE=1 — записывает cache metadata в
# pushed image manifest для следующего pipeline'а.
# - --pull — Nexus docker-proxy лениво re-fetch'ит base image с Docker Hub.
#
# Раньше был --no-cache как temporary recovery после Nexus blob loss
# incident (pipeline 6066). Nexus blob storage стабилизирован, --no-cache
# убран → -2 мин/service × 5 services = -10 мин на pipeline.
# Если Nexus снова потеряет blob refs — симптом: "blob unknown to registry"
# на manifest push. Recovery: вернуть --no-cache temporarily, сделать
# `nexus-cli script run cleanup-orphan-blobs` (или ручной touch через UI),
# потом убрать --no-cache обратно.
#
# Git info passed как build args чтобы Spring Boot build-info goal получил
# реальные значения (внутри Docker context .git/ исключён через .dockerignore,
# git-commit-id-maven-plugin не может прочесть). Backend Dockerfile'ы
# используют эти ARG'и в `mvn -Dgit.commit.id.abbrev=... -Dgit.branch=...`.
# Frontend / docs / migrations Dockerfile'ы их игнорируют (no-op в context'е).
docker pull "$IMG:latest" >/dev/null 2>&1 || true
docker build --pull -f "$DOCKERFILE" \
-t "$IMG:$IMAGE_TAG" -t "$IMG:latest" -t "$IMG:$CI_COMMIT_REF_SLUG" \
--cache-from "$IMG:latest" \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--build-arg GIT_COMMIT="${CI_COMMIT_SHORT_SHA:-unknown}" \
--build-arg GIT_BRANCH="${CI_COMMIT_REF_NAME:-unknown}" \
--build-arg GIT_TAG="${CI_COMMIT_TAG:-none}" \
--build-arg GIT_TIME="${CI_COMMIT_TIMESTAMP:-unknown}" \
"$BUILD_CONTEXT"
docker push "$IMG:$IMAGE_TAG"
docker push "$IMG:latest"
docker push "$IMG:$CI_COMMIT_REF_SLUG"
docker-app:
extends: .docker_base
needs: [maven-package]
variables:
SVC: ordinis-app
DOCKERFILE: ordinis-app/Dockerfile
BUILD_CONTEXT: .
script:
- *publish_script
docker-read-api:
extends: .docker_base
needs: [maven-package]
variables:
SVC: ordinis-read-api
DOCKERFILE: ordinis-read-api/Dockerfile
BUILD_CONTEXT: .
script:
- *publish_script
docker-projection-writer:
extends: .docker_base
needs: [maven-package]
variables:
SVC: ordinis-projection-writer
DOCKERFILE: ordinis-projection-writer/Dockerfile
BUILD_CONTEXT: .
script:
- *publish_script
docker-migrations:
extends: .docker_base
needs: [maven-package]
variables:
SVC: ordinis-migrations
DOCKERFILE: ordinis-migrations/Dockerfile
BUILD_CONTEXT: ordinis-migrations/
script:
- *publish_script
docker-admin-ui:
extends: .docker_base
needs: []
variables:
SVC: ordinis-admin-ui
DOCKERFILE: ordinis-admin-ui/Dockerfile
BUILD_CONTEXT: ordinis-admin-ui/
# docs/changelog.md — single source of truth для public changelog (read'ится
# mkdocs/Diplodoc для /docs/changelog.html AND admin-ui WhatsNewButton).
# admin-ui Docker build context = ordinis-admin-ui/ (sibling, не parent) —
# copy file в src/ перед docker build.
#
# Suffix `-public` — case-insensitive FS macOS dev: `changelog.md` ≡
# `CHANGELOG.md` (existing internal file) → overwrite. Linux CI обычно
# case-sensitive, но единообразное имя минимизирует local/CI drift.
before_script:
- cp docs/changelog.md ordinis-admin-ui/src/changelog-public.md
script:
- *publish_script
# Раньше был path-filter, но это создавало дыру: если backend менялся, а
# frontend нет — резолвили общий тег и helm пытался катить admin-ui:<commit>
# которого не существовало. Layer-cache делает повторную сборку ~30 sec.
# Integrator Guide (Diplodoc) — multi-stage Dockerfile (yfm build → nginx serving).
# Path-filtered: rebuild только при изменении docs/. Если docs не менялся,
# helm берёт previous tag через --reset-then-reuse-values.
docker-docs:
extends: .docker_base
needs: []
variables:
SVC: ordinis-docs
DOCKERFILE: docs/Dockerfile
BUILD_CONTEXT: docs/
script:
- *publish_script
rules:
# На main + tag собираем БЕЗ path-filter. Иначе resolve-tag emit'ит
# docs=main-<sha>, helm пытается pull этого тега, image отсутствует
# (docker-docs не запускался) → atomic rollback с context deadline
# exceeded. Layer cache при отсутствии docs/* changes делает rebuild
# ~30 sec — приемлемая цена за гарантию что image для tag существует.
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/'
- if: '$CI_PIPELINE_SOURCE == "web"'
when: manual
# ─── TRIGGER DEPLOY ───────────────────────────
# Single resolve-tag (always runs) → один UPSTREAM_IMAGE_TAG. Path-filter
# работает на уровне docker-* jobs (backend skip если бэк не менялся,
# layers cache hit делает rebuild ~30 sec). Полное per-service splitting
# через dual dotenv не работает с GitLab trigger var interpolation.
resolve-tag:
stage: trigger-deploy
image: repo.nstart.cloud/library/alpine:3.20
script:
- echo "RESOLVED_IMAGE_TAG=$IMAGE_TAG" > resolved.env
# DOCS_IMAGE_TAG может быть пустым если docker-docs не пересобирался
# (path-filter no-match). Helm в infra оставит старый tag через
# --reset-then-reuse-values если переменная пустая.
- echo "RESOLVED_DOCS_TAG=$IMAGE_TAG" >> resolved.env
- cat resolved.env
artifacts:
reports:
dotenv: resolved.env
# 6 hours — dotenv нужен только для trigger-deploy-* в этом же pipeline;
# дольше держать смысла нет, downstream consumers свой artifact не качают.
# Без явного expire_in defaults to GitLab project policy (30 дней) →
# каждый main commit накапливает stale artifact.
expire_in: 6 hours
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
# На tag-пайплайне $CI_COMMIT_BRANCH пустой — но trigger-deploy-prod
# (тоже tag-only) needs: resolve-tag. Без этого правила prod deploy
# не запускается с ошибкой "resolve-tag does not exist in the pipeline".
- if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/'
trigger-deploy-staging:
stage: trigger-deploy
needs: [resolve-tag]
trigger:
project: 2-6/2-6-4/terravault/ordinis-infra
branch: main
strategy: depend
variables:
UPSTREAM_IMAGE_TAG: $RESOLVED_IMAGE_TAG
DOCS_IMAGE_TAG: $RESOLVED_DOCS_TAG
DEPLOY_ENV: staging
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
trigger-deploy-prod:
stage: trigger-deploy
needs: [resolve-tag]
trigger:
project: 2-6/2-6-4/terravault/ordinis-infra
branch: main
strategy: depend
variables:
UPSTREAM_IMAGE_TAG: $RESOLVED_IMAGE_TAG
DOCS_IMAGE_TAG: $RESOLVED_DOCS_TAG
DEPLOY_ENV: prod
rules:
- if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/'
when: manual
# =============================================================
# Release automation — semantic-release.
# =============================================================
# Анализирует conventional commits с последнего tag'а, решает bump (patch/
# minor/major), генерирует CHANGELOG.md, bump'ит package.json, создаёт git tag
# vX.Y.Z и GitLab Release. Tag pipeline далее автоматически собирает images
# и ждёт manual gate на trigger-deploy-prod.
#
# Запускается ТОЛЬКО на main, ТОЛЬКО после успешного staging deploy.
# Если нет релизных commits (только chore/test/ci/docs/style) — exit 0 без
# tag'а. Loop prevention: коммит `chore(release): vX.Y.Z [skip ci]` —
# `[skip ci]` стандартный GitLab marker, новый pipeline не триггерится.
#
# Setup ONE-TIME (см. RELEASE.md):
# 1. Settings → Access Tokens → create Project Access Token
# - role: Maintainer
# - scopes: write_repository, api
# - expiry: 1 год (renew раз в год)
# 2. Settings → CI/CD → Variables → add:
# GITLAB_TOKEN = <token> (Masked, Protected)
#
# Tuning: чтобы НЕ релизить feat: автоматом — добавь в commit message
# `[skip release]` и semantic-release пропустит этот commit (см. parserOpts).
release:
stage: release
image: repo.nstart.cloud/library/node:22-alpine
needs:
- job: trigger-deploy-staging
artifacts: false
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: on_success
variables:
# semantic-release требует full git history для commit analysis.
GIT_DEPTH: "0"
# GL_TOKEN — @semantic-release/gitlab plugin requires this name specifically.
GL_TOKEN: $GITLAB_TOKEN
before_script:
# Fail fast если token не настроен. Job всё равно allow_failure: true —
# pipeline не покраснеет, но в logs понятная причина (вместо cryptic
# "Authentication failed").
- |
if [ -z "$GITLAB_TOKEN" ]; then
echo "═══════════════════════════════════════════════════════════════"
echo "GITLAB_TOKEN is empty — release job will skip."
echo ""
echo "Setup (one-time):"
echo " 1. Settings → Access Tokens → create Project Access Token"
echo " scopes: write_repository + api, role: Maintainer"
echo " 2. Settings → CI/CD → Variables → add GITLAB_TOKEN"
echo " flags: Masked + Protected"
echo ""
echo "См. RELEASE.md в репозитории для подробностей."
echo "═══════════════════════════════════════════════════════════════"
exit 0
fi
- apk add --no-cache git ca-certificates
# Configure git remote с токеном для push. CI default remote — HTTPS с
# CI_JOB_TOKEN scope, который НЕ может push'ить tags/commits. Переопределяем
# на Project Access Token (GITLAB_TOKEN).
- git remote set-url origin "https://gitlab-ci-token:${GITLAB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
- git config user.email "${GITLAB_USER_EMAIL:-ci@ordinis.local}"
- git config user.name "semantic-release"
- npm install -g --silent pnpm@9.15.4
- cd ordinis-admin-ui
- pnpm install --frozen-lockfile --silent
script:
- npx semantic-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-<sha>
# → 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-<sha>). 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 = <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 = <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.
# printf '%b' разворачивает \n в реальные newlines внутри одной shell
# строки — избегаем multi-line YAML quoting issues (literal block scalar
# ломается на unindented continuation lines).
git add charts/ordinis/values-prod.yaml
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.
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 \
--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