Skip to content

GitLab CI/CD + Semantic Release

Historical context. Этот гайд писался, когда partizap-frontend использовал npm. С 2026-04-09 проект мигрирован на pnpm (DEV-328/329/330), см. plans/2026-04-09-pnpm-migration-design.md. Примеры команд в этом гайде (npm ci, npm run ...) остаются справочными для общей схемы semantic-release, но в актуальном CI partizap-frontend используются pnpm install --frozen-lockfile и pnpm exec semantic-release внутри docker run node:22-alpine с bind-mount /home/gitlab-runner/pnpm-store. Актуальный .gitlab-ci.yml — единственный источник правды.

Гайд по настройке CI/CD пайплайнов с автоматическим версионированием через semantic-release. Два варианта: с Docker (все команды в контейнерах) и без Docker (node на хосте).

Содержание

  1. Conventional Commits
  2. Semantic Release: установка и конфигурация
  3. GitLab CI: вариант с Docker
  4. GitLab CI: вариант без Docker
  5. Trivy: security audit gate
  6. Dockerfile (multi-stage)
  7. .npmrc для кросс-платформенных сборок
  8. GitLab: токены и переменные
  9. Подводные камни и решения

1. Conventional Commits

Все коммиты должны следовать формату type(scope): description.

Типы и их влияние на версию

ТипРелизПример
feat: / feature:minor (1.0.0 -> 1.1.0)feat: каталог с фильтрами
fix:patch (1.0.0 -> 1.0.1)fix(auth): валидация email
perf:patchperf: кэширование категорий
revert:patchrevert: откат миграции
ci:patchci: лимит памяти Docker
refactor:patchrefactor: extract composable
style:patchstyle: fix spacing
build:patchbuild: update Dockerfile
BREAKING CHANGE: в bodymajor (1.0.0 -> 2.0.0)любой тип + breaking change
chore: / docs: / test:нет релизаchore: обновление зависимостей

По умолчанию semantic-release не создаёт релиз для ci:, refactor:, style:, build:. Добавляется через releaseRules.


2. Semantic Release

Установка

bash
npm install -D semantic-release \
  @semantic-release/commit-analyzer \
  @semantic-release/release-notes-generator \
  @semantic-release/exec \
  @semantic-release/gitlab \
  conventional-changelog-conventionalcommits

Не использовать @semantic-release/changelog + @semantic-release/git — они создают chore(release) коммиты в main, засоряя историю и рассинхронизируя lockfile. Release notes хранятся в GitLab Releases.

package.json version

Установить "version": "0.0.0-development" — реальная версия определяется git-тегами и передаётся через --build-arg NUXT_PUBLIC_APP_VERSION.

.releaserc.json

json
{
  "branches": ["main"],
  "tagFormat": "v${version}",
  "plugins": [
    ["@semantic-release/commit-analyzer", {
      "preset": "conventionalcommits",
      "releaseRules": [
        { "type": "feat", "release": "minor" },
        { "type": "feature", "release": "minor" },
        { "type": "fix", "release": "patch" },
        { "type": "perf", "release": "patch" },
        { "type": "revert", "release": "patch" },
        { "type": "ci", "release": "patch" },
        { "type": "refactor", "release": "patch" },
        { "type": "style", "release": "patch" },
        { "type": "build", "release": "patch" }
      ]
    }],
    ["@semantic-release/release-notes-generator", {
      "preset": "conventionalcommits",
      "presetConfig": {
        "types": [
          { "type": "feat", "section": "Features" },
          { "type": "feature", "section": "Features" },
          { "type": "fix", "section": "Bug Fixes" },
          { "type": "perf", "section": "Performance" },
          { "type": "revert", "section": "Reverts" },
          { "type": "ci", "section": "CI/CD" },
          { "type": "refactor", "section": "Refactoring" },
          { "type": "style", "section": "Styles" },
          { "type": "build", "section": "Build" }
        ]
      }
    }],
    ["@semantic-release/exec", {
      "prepareCmd": "echo APP_VERSION=${nextRelease.version} > release.env"
    }],
    "@semantic-release/gitlab"
  ]
}

@semantic-release/exec и dotenv-артефакт

@semantic-release/exec записывает APP_VERSION=x.y.z в release.env. В CI release-джоба экспортирует этот файл как dotenv artifact — последующие джобы (build:prod, deploy:prod) получают $APP_VERSION автоматически.


### Self-hosted GitLab

Если GitLab не на gitlab.com, нужно указать URL для API:

```json
["@semantic-release/gitlab", {
  "gitlabUrl": "http://127.0.0.1:8081"
}]

gitlabUrl имеет наивысший приоритет (выше env vars). Это необходимо когда:

  • GitLab на localhost с нестандартным портом
  • Внешний URL (https://gitlab.example.com) недоступен из Docker-контейнера (SSL, DNS)
  • CI_SERVER_URL указывает на внешний адрес, а не на localhost

3. GitLab CI: Docker

Все npm-команды бегут внутри docker run node:22-alpine. На хосте нужен только Docker.

Архитектура пайплайнов

feature/fix branches + merge requests:
  validate (lint+typecheck+test) + security:audit (Trivy) — early feedback

develop push:
  validate + security:audit → docker build dev → deploy staging (auto) → e2e

main push (единый пайплайн):
  release (semantic-release → тег + release.env с APP_VERSION)
    → build:prod (v1.2.3 + latest) → deploy:prod (manual) → cleanup:prod

tag v* (только для rollback):
  deploy:rollback (manual, без пересборки — retag + restart)

DEV-324: Ранее release и build/deploy были в разных пайплайнах (main → tag). Теперь всё в одном через dotenv-артефакт. Tag-пайплайн оставлен только для rollback.

.gitlab-ci.yml (полный)

yaml
workflow:
  rules:
    - if: $CI_COMMIT_TAG =~ /^v/        # rollback only
    - if: $CI_COMMIT_BRANCH == "develop"
    - if: $CI_COMMIT_BRANCH == "main"

stages:
  - validate
  - release
  - build
  - deploy

variables:
  DEPLOY_DIR: /opt/myapp-frontend
  DOCKER_BUILDKIT: "1"
  NODE_IMAGE: node:22-alpine

# ===================================
# Validate (develop only)
# ===================================

lint:
  stage: validate
  tags: [myapp-shell]
  script:
    - >-
      docker run --rm
      --user "$(id -u):$(id -g)"
      -e HOME=/tmp
      -v "$CI_PROJECT_DIR:/app" -w /app
      $NODE_IMAGE
      sh -c "npm ci && npm run lint"
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"

typecheck:
  stage: validate
  tags: [myapp-shell]
  script:
    - >-
      docker run --rm
      --user "$(id -u):$(id -g)"
      -e HOME=/tmp
      -v "$CI_PROJECT_DIR:/app" -w /app
      $NODE_IMAGE
      sh -c "npm ci && npm run typecheck"
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"

test:
  stage: validate
  tags: [myapp-shell]
  script:
    - >-
      docker run --rm
      --user "$(id -u):$(id -g)"
      -e HOME=/tmp
      -v "$CI_PROJECT_DIR:/app" -w /app
      $NODE_IMAGE
      sh -c "npm ci && npm run test:run"
  allow_failure: true  # убрать когда тесты стабилизируются
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"

# ===================================
# Release (main only)
# semantic-release → тег v* + release.env (dotenv artifact)
# ===================================

release:
  stage: release
  tags: [myapp-shell]
  script:
    - >-
      docker run --rm
      --network=host
      -e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)"
      -v "$CI_PROJECT_DIR:/app" -w /app
      -e GITLAB_TOKEN=$GITLAB_TOKEN
      -e GL_TOKEN=$GITLAB_TOKEN
      -e CI=true
      -e GITLAB_CI=true
      -e CI_SERVER_URL=$CI_SERVER_URL
      -e CI_SERVER_HOST=$CI_SERVER_HOST
      -e GITLAB_URL=http://127.0.0.1:8081
      -e CI_PROJECT_ID=$CI_PROJECT_ID
      -e CI_PROJECT_PATH=$CI_PROJECT_PATH
      -e CI_PROJECT_URL=$CI_PROJECT_URL
      -e CI_COMMIT_BRANCH=$CI_COMMIT_BRANCH
      -e CI_COMMIT_REF_NAME=$CI_COMMIT_REF_NAME
      -e CI_COMMIT_SHA=$CI_COMMIT_SHA
      -e CI_JOB_TOKEN=$CI_JOB_TOKEN
      -e CI_REPOSITORY_URL=$CI_REPOSITORY_URL
      $NODE_IMAGE
      sh -c "apk add --no-cache git &&
        git config --global safe.directory /app &&
        git config --global credential.helper '!f() { echo username=oauth2; echo \"password=\${GL_TOKEN}\"; }; f' &&
        git checkout -B \${CI_COMMIT_BRANCH} HEAD &&
        npm ci --ignore-scripts &&
        npx semantic-release;
        status=\$?; rm -rf node_modules; chown -R \${HOST_UID}:\${HOST_GID} .git; exit \$status"
    # Fallback: если semantic-release не создал новый релиз
    - |
      if [ ! -f release.env ]; then
        FALLBACK_VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//')
        if [ -n "$FALLBACK_VERSION" ]; then
          echo "APP_VERSION=${FALLBACK_VERSION}" > release.env
        else
          echo "APP_VERSION=0.0.0" > release.env
        fi
      fi
  artifacts:
    reports:
      dotenv: release.env
    expire_in: 1 day
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

# ===================================
# Develop: build + deploy
# ===================================

build:dev:
  stage: build
  tags: [myapp-shell]
  needs: [lint, typecheck, test]
  resource_group: frontend-build
  script:
    - APP_VERSION=$(git describe --tags --always 2>/dev/null || echo "dev")
    - docker build --memory=1g --memory-swap=1536m
      --build-arg NUXT_PUBLIC_APP_VERSION=${APP_VERSION}
      -t myapp-dev:latest .
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"

deploy:dev:
  stage: deploy
  tags: [myapp-shell]
  needs: [build:dev]
  script:
    - cp docker-compose.dev.yml $DEPLOY_DIR/docker-compose.dev.yml
    - cd $DEPLOY_DIR
    - docker compose -p myapp-dev -f docker-compose.dev.yml up -d --force-recreate
    - docker image prune -f
  environment:
    name: development
    url: https://dev.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"

# ===================================
# Production: build + deploy (main, after release)
# APP_VERSION приходит из dotenv-артефакта release-джобы
# ===================================

build:prod:
  stage: build
  tags: [myapp-shell]
  needs:
    - job: release
      artifacts: true
  resource_group: frontend-build
  script:
    - >-
      docker build --memory=1g --memory-swap=1536m
      --build-arg NUXT_PUBLIC_APP_VERSION=${APP_VERSION}
      -t myapp-prod:v${APP_VERSION}
      -t myapp-prod:latest .
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy:prod:
  stage: deploy
  tags: [myapp-shell]
  needs:
    - job: build:prod
    - job: release
      artifacts: true
  script:
    - docker rm -f myapp-prod || true
    - cp docker-compose.prod.yml $DEPLOY_DIR/docker-compose.prod.yml
    - cd $DEPLOY_DIR
    - docker compose -p myapp-prod -f docker-compose.prod.yml up -d --force-recreate
    - docker image prune -f
  environment:
    name: production
    url: https://example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual

# ===================================
# Rollback: tag-triggered deploy (без пересборки)
# Использует существующий образ — retag + restart
# ===================================

deploy:rollback:
  stage: deploy
  tags: [myapp-shell]
  script:
    - docker tag myapp-prod:${CI_COMMIT_TAG} myapp-prod:latest
    - docker rm -f myapp-prod || true
    - cd $DEPLOY_DIR
    - docker compose -p myapp-prod -f docker-compose.prod.yml up -d --force-recreate
  environment:
    name: production
    url: https://example.com
  rules:
    - if: $CI_COMMIT_TAG =~ /^v/
      when: manual

Ключевые моменты Docker-варианта

ПараметрЗачем
--user "$(id -u):$(id -g)"Файлы в volume создаются от пользователя runner, а не root
-e HOME=/tmpnpm cache writable для non-root пользователя
--network=hostRelease-джоба достучится до GitLab на localhost
--ignore-scriptsВ release-джобе nuxt prepare не нужен
rm -rf node_modulesОчистка root-owned файлов в конце release-джобы
chown -R $UID:$GID .gitВозврат прав на .git для runner'а после git ops в контейнере
git checkout -B $BRANCH HEADGitLab делает detached HEAD, semantic-release требует ветку
git credential.helperАвторизация для git push тега обратно в репозиторий
GITLAB_URL=http://127.0.0.1:8081Плагин @semantic-release/gitlab ходит на API напрямую
artifacts.reports.dotenvrelease.env экспортирует $APP_VERSION в последующие джобы
needs: [release] + artifacts: truebuild:prod/deploy:prod получают $APP_VERSION из dotenv
deploy:rollbackManual-джоба по тегу v* — retag + restart без пересборки

4. GitLab CI: без Docker

Node.js установлен на хосте runner'а. Используется pnpm (или npm).

Архитектура пайплайнов

main push:
  install → lint / type-check / test (параллельно) → release (тег v*)

tag v*:
  install → build staging (auto) → build production (manual) → deploy

.gitlab-ci.yml (полный, pnpm)

yaml
workflow:
  rules:
    - if: $CI_COMMIT_TAG =~ /^v/
    - if: $CI_COMMIT_BRANCH == "main"

stages:
  - install
  - validate
  - release
  - build
  - deploy

variables:
  npm_config_cache: "$CI_PROJECT_DIR/.npm"

cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - node_modules/
    - .pnpm-store/

# ===================================
# Install
# ===================================

install:
  stage: install
  script:
    - pnpm install --frozen-lockfile
  artifacts:
    paths:
      - node_modules/
    expire_in: 1 hour

# ===================================
# Validate (main only)
# ===================================

lint:
  stage: validate
  needs: [install]
  script:
    - pnpm run lint
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

type-check:
  stage: validate
  needs: [install]
  script:
    - pnpm run type-check
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

test:
  stage: validate
  needs: [install]
  script:
    - pnpm run test
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

# ===================================
# Release (main, after validation)
# ===================================

release:
  stage: release
  needs: [lint, type-check, test]
  script:
    - npx semantic-release
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

# ===================================
# Build + Deploy (tag v*)
# ===================================

build_staging:
  stage: build
  needs: [install]
  script:
    - pnpm vite build --mode staging
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour
  rules:
    - if: $CI_COMMIT_TAG =~ /^v/

deploy_staging:
  stage: deploy
  needs: [build_staging]
  script:
    - scp -r dist/* user@staging-server:/var/www/app/
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: $CI_COMMIT_TAG =~ /^v/

build_production:
  stage: build
  needs: [install]
  script:
    - pnpm vite build --mode production
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour
  rules:
    - if: $CI_COMMIT_TAG =~ /^v/

deploy_production:
  stage: deploy
  needs: [build_production]
  script:
    - scp -r dist/* user@prod-server:/var/www/app/
  environment:
    name: production
    url: https://example.com
  rules:
    - if: $CI_COMMIT_TAG =~ /^v/
      when: manual

Ключевые отличия от Docker-варианта

АспектDockerБез Docker
Node.js на хостеНе нуженОбязателен
ИзоляцияПолная (контейнер)Нет (shared state)
Install стадияВнутри каждой джобыОтдельная джоба + artifacts
CacheНе нуженpnpm-lock / package-lock
Release-джобаПрокидывание env varsВсё доступно из коробки
nuxt prepareМожет потребовать musl bindingsРаботает нативно
Права на файлы--user + HOME=/tmpНет проблем

5. Trivy: security audit gate

Trivy сканирует npm-зависимости на известные уязвимости (CVE). Блокирует пайплайн при HIGH/CRITICAL.

CI-джоба

yaml
variables:
  TRIVY_IMAGE: aquasec/trivy:0.69.6  # пин версии, обновлять вручную

security:audit:
  stage: validate
  tags:
    - partizap-shell
  before_script:
    # Используем локальный кеш образа, пулим только если нет (обход Docker Hub rate limit)
    - docker image inspect $TRIVY_IMAGE >/dev/null 2>&1 || docker pull $TRIVY_IMAGE
  script:
    # JSON report — артефакт для уведомлений (DEV-182)
    - >-
      docker run --rm
      -v "$CI_PROJECT_DIR:/app" -w /app
      -v trivy-cache:/root/.cache/trivy
      $TRIVY_IMAGE
      fs --scanners vuln --severity HIGH,CRITICAL
      --format json --output /app/trivy-report.json
      /app
    # Table output — exit 1 если есть уязвимости
    - >-
      docker run --rm
      -v "$CI_PROJECT_DIR:/app" -w /app
      -v trivy-cache:/root/.cache/trivy
      $TRIVY_IMAGE
      fs --scanners vuln --severity HIGH,CRITICAL
      --exit-code 1
      /app
  artifacts:
    when: on_failure
    paths:
      - trivy-report.json
    expire_in: 30 days
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"
    - if: $CI_MERGE_REQUEST_IID
    - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != "develop" && $CI_COMMIT_BRANCH != "main"

Ключевые параметры

ПараметрЗачем
TRIVY_IMAGE variableПин версии образа — обход Docker Hub rate limit, воспроизводимость
docker image inspect || docker pullИспользуем локальный кеш, пулим только если образа нет на runner'е
fs --scanners vulnСканирование файловой системы, только уязвимости (без secrets/misconfig)
--severity HIGH,CRITICALИгнорировать LOW/MEDIUM — слишком шумно
--exit-code 1Падать при обнаружении уязвимостей (gate)
-v trivy-cache:/root/.cache/trivyКэш БД уязвимостей (~88MB) между запусками
--format json --outputJSON-отчёт для будущих уведомлений

Когда запускается

  • feature/fix ветки — при каждом пуше (ранний фидбек)
  • merge request — блокирует MR
  • develop push — блокирует build если есть уязвимости

.trivyignore

Файл для подавления ложных срабатываний или unfixable CVE. Формат — один CVE на строку:

# Review by 2026-04-15: no fix in transitive dep
CVE-2026-XXXXX

Каждая запись должна иметь дату ревью. Не оставлять stale исключения.

Локальная проверка

bash
docker run --rm \
  -v "$(pwd):/app" -w /app \
  -v trivy-cache:/root/.cache/trivy \
  aquasec/trivy:0.69.6 \
  fs --scanners vuln --severity HIGH,CRITICAL \
  --exit-code 1 \
  /app

Версию образа держать в синхроне с TRIVY_IMAGE в .gitlab-ci.yml. БД уязвимостей обновляется автоматически при каждом запуске — образ обновлять редко (раз в пару месяцев).

Исправление уязвимостей

  1. Запустить скан, посмотреть таблицу
  2. Все уязвимые пакеты транзитивные? → npm audit fix
  3. Прямая зависимость? → обновить в package.json
  4. Нет фикса? → добавить в .trivyignore с датой ревью
  5. Перезапустить скан — убедиться что 0 HIGH/CRITICAL

6. Dockerfile

Multi-stage build для Nuxt/Node.js приложений:

dockerfile
# syntax=docker/dockerfile:1

# Stage 1: Install dependencies
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

# Stage 2: Build
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ARG NUXT_PUBLIC_APP_VERSION=""
ENV NUXT_PUBLIC_APP_VERSION=$NUXT_PUBLIC_APP_VERSION
ENV NODE_ENV=production
ENV NODE_OPTIONS="--max-old-space-size=1536"
RUN --mount=type=cache,target=/root/.npm \
    npm run build

# Stage 3: Production (только .output, минимальный образ)
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/.output ./.output
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=3000
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

Принципы

  • deps ставит ВСЕ зависимости (без --omit=dev) — postinstall (nuxt prepare) нужен
  • build собирает приложение; версия передаётся через --build-arg NUXT_PUBLIC_APP_VERSION (из dotenv-артефакта release-джобы) и вшивается в runtimeConfig.public.appVersion — отображается в футере. package.json содержит "version": "0.0.0-development" — не является источником версии
  • production копирует только .output — финальный образ ~50MB
  • --mount=type=cache ускоряет повторные сборки

7. .npmrc

Необходим при использовании node:22-alpine в Docker. Lockfile с macOS не содержит native bindings для Alpine (musl libc).

ini
supportedArchitectures[os][]=current
supportedArchitectures[os][]=linux
supportedArchitectures[cpu][]=current
supportedArchitectures[cpu][]=x64
supportedArchitectures[libc][]=current
supportedArchitectures[libc][]=musl

Что это делает

  • npm install на macOS включает в lockfile bindings для обеих платформ (darwin + linux-musl)
  • npm ci в Alpine-контейнере находит свой @oxc-parser/binding-linux-x64-musl в lockfile
  • Лишние бинарники (пара МБ) скачиваются локально, но не используются
  • current = текущая платформа разработчика (macOS arm64/x64)

Когда нужен

  • Dockerfile использует Alpine (node:*-alpine)
  • Lockfile генерируется на macOS/Windows
  • Есть native bindings (oxc-parser, esbuild, lightningcss, rollup и др.)

Когда НЕ нужен

  • Dockerfile использует Debian (node:22) — нет проблем с musl
  • Node.js установлен на хосте (без Docker) — та же платформа
  • Lockfile генерируется на той же платформе где собирается

Никогда не пересоздавайте lockfile внутри Docker. Это ломает воспроизводимость. .npmrc с supportedArchitectures — правильное решение.


8. GitLab: токены и переменные

Project Access Token

Settings → Access Tokens → Add new token:

ПараметрЗначение
Namesemantic-release
Scopesapi, read_api, read_repository, write_repository
RoleMaintainer
Expiration12 месяцев (обновлять!)

CI/CD Variables

Settings → CI/CD → Variables:

VariableЗначениеFlags
GITLAB_TOKENзначение Project Access TokenMasked, Protected

Protected = доступна только на protected branches (main). Для develop-пайплайна не нужна.

Проверка

Если semantic-release выдаёт EINVALIDGLTOKEN:

  1. Токен действующий? (не истёк)
  2. Scope api есть?
  3. Role = Maintainer?
  4. GITLAB_URL указывает на доступный адрес? (для self-hosted)
  5. Из Docker-контейнера доступен GitLab API? (--network=host или DNS)

9. Подводные камни

Docker: root-owned файлы

Проблема: docker run -v + npm ci создаёт node_modules/ от root. Runner не может удалить при следующем checkout.

Решение: --user "$(id -u):$(id -g)" для validate-джоб. Для release-джобы (нужен root для apk add) — rm -rf node_modules в конце скрипта.

Docker: npm cache permission denied

Проблема: --user + npm пытается писать в /.npm (root-owned).

Решение: -e HOME=/tmp — npm cache уходит в /tmp/.npm.

Docker: detached HEAD

Проблема: GitLab checkout делает detached HEAD. semantic-release не видит ветку (branch: undefined).

Решение: git checkout -B ${CI_COMMIT_BRANCH} HEAD перед npx semantic-release. Также передавать CI_COMMIT_REF_NAME как fallback.

Docker: GitLab API недоступен

Проблема: @semantic-release/gitlab ходит на https://gitlab.example.com (внешний URL из CI_SERVER_URL). Из контейнера может быть недоступен (SSL, DNS).

Решение: gitlabUrl в .releaserc.jsonhttp://127.0.0.1:PORT. Контейнер должен использовать --network=host.

Docker: git authentication

Проблема: semantic-release делает git ls-remote и git push — нужны credentials.

Решение: git credential helper внутри контейнера:

sh
git config --global credential.helper \
  '!f() { echo username=oauth2; echo "password=${GL_TOKEN}"; }; f'

Docker Hub: rate limit при pull

Проблема: error from registry: You have reached your unauthenticated pull rate limit. Анонимный лимит Docker Hub — 100 pull/6h на IP.

Решение:

  1. Пинить версию образа (переменная TRIVY_IMAGE), не использовать :latest
  2. before_script: docker image inspect || docker pull — пулить только если образа нет
  3. Первичная загрузка образа на сервер вручную:
    bash
    # На Mac (arm64) пулим amd64 версию для сервера
    docker pull --platform linux/amd64 aquasec/trivy:0.69.6
    docker save aquasec/trivy:0.69.6 -o /tmp/trivy-amd64.tar
    scp /tmp/trivy-amd64.tar user@server:/tmp/
    ssh user@server "docker load < /tmp/trivy-amd64.tar && rm /tmp/trivy-amd64.tar"

Важно: если разработка на Mac (arm64), а сервер amd64 — нужен --platform linux/amd64 при docker pull. Иначе exec format error.

Alpine: missing native bindings

Проблема: Cannot find module '@oxc-parser/binding-linux-x64-musl'. Lockfile с macOS не включает musl-bindings.

Решение: .npmrc с supportedArchitectures (см. раздел 6). Затем rm -rf node_modules && npm install локально.

semantic-release: нет релиза

Проблема: Пайплайн зелёный, но тег не создан.

Причины:

  • Все коммиты chore: / docs: / test: — не триггерят релиз (при стандартных releaseRules)
  • ci:, refactor:, style:, build: по умолчанию не триггерят — нужен releaseRules
  • Ветка не в списке branches в .releaserc.json

semantic-release: первый релиз

При первом запуске semantic-release анализирует ВСЕ коммиты с начала репозитория и создаёт v1.0.0 (если есть хотя бы один feat: или fix:).


Чеклист инициализации

Новый проект с Docker

[ ] npm install -D semantic-release и плагины (exec, gitlab, commit-analyzer, release-notes-generator)
[ ] Создать .releaserc.json (с @semantic-release/exec для release.env)
[ ] Установить "version": "0.0.0-development" в package.json
[ ] Добавить release.env в .gitignore
[ ] Создать .npmrc (если Alpine)
[ ] rm -rf node_modules && npm install (пересоздать lockfile)
[ ] Создать Dockerfile (multi-stage)
[ ] Создать docker-compose.dev.yml / docker-compose.prod.yml
[ ] Создать .gitlab-ci.yml (release с dotenv-артефактом, build:prod по main, rollback по тегу)
[ ] GitLab: создать Project Access Token (api + write_repository, Maintainer)
[ ] GitLab: добавить GITLAB_TOKEN в CI/CD Variables (Masked + Protected)
[ ] Первый push в main — проверить что release → build:prod → deploy:prod работает в одном пайплайне

Новый проект без Docker

[ ] npm install -D semantic-release и плагины (или pnpm add -D)
[ ] Создать .releaserc.json
[ ] Установить "version": "0.0.0-development" в package.json
[ ] Создать .gitlab-ci.yml
[ ] GitLab: создать Project Access Token
[ ] GitLab: добавить GITLAB_TOKEN в CI/CD Variables
[ ] Убедиться что Node.js установлен на runner
[ ] Первый push в main — проверить release-джобу