CI для NuGet-библиотек: версионирование по имени ветки и публикация в два реестра
Published: 2026-05-29
Общим .NET-библиотекам нужен иной пайплайн, чем приложениям: никакого Docker-образа, никакого деплоя в Kubernetes — только dotnet pack и dotnet nuget push. Самое интересное здесь — управление версиями и публикация в два реестра: GitLab Package Registry для быстрых внутренних итераций, Artifactory для стабильных релизов, которые используют внешние команды.
Имя ветки как источник версии
У нас принято соглашение: имя ветки задаёт версию пакета:
rel/1.4.2 → версия пакета 1.4.2
CI job извлекает её через подстановку параметров shell:
bashVERSION=${CI_COMMIT_BRANCH##*/}
# rel/1.4.2 → 1.4.2
##*/ отрезает всё до последнего / включительно. Никаких внешних инструментов, никакого git describe, никаких правок .csproj в CI.
Это значит: инженер создаёт release-ветку с правильным именем версии, ветка запускает пайплайн, и dotnet pack встраивает эту версию в метаданные .nupkg:
bashdotnet build -p:Version=$VERSION -c Release src/
dotnet pack -p:Version=$VERSION -c Release --no-build src/
Передача -p:Version= во время сборки переопределяет <Version> в .csproj без изменения файла. --no-build в pack использует уже готовый результат сборки.
Полный build job
yamlbuild:
image: registry.example.com/ci-images/dotnet-build:net8
stage: build
script:
- VERSION=${CI_COMMIT_BRANCH##*/}
- dotnet restore -p:Configuration=Release src/
- dotnet build -p:Version=$VERSION -c Release src/
- dotnet pack -p:Version=$VERSION -c Release --no-build src/
artifacts:
expire_in: 1 day
paths:
- "**/*.nupkg"
tags:
- office-dind
Файлы .nupkg передаются на стадию push через artifacts. expire_in: 1 day — после публикации в оба реестра файл больше не нужен.
Push в GitLab Package Registry
У GitLab есть встроенный NuGet feed для каждого проекта. Внешние учётные данные не нужны: CI_JOB_TOKEN — краткосрочный токен, выдаваемый на один job:
yamlpush:
image: registry.example.com/ci-images/dotnet-build:latest
stage: push
script:
# Удалить устаревший source 'push', если он остался от предыдущего запуска
- |
if dotnet nuget list source | grep -q "push \["; then
dotnet nuget remove source push
fi
- dotnet nuget add source \
"${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/nuget/index.json" \
--name push \
--username gitlab-ci-token \
--password $CI_JOB_TOKEN \
--store-password-in-clear-text
- dotnet nuget push "**/*.nupkg" --source push
tags:
- office-dind
CI_JOB_TOKEN истекает при завершении job, поэтому --store-password-in-clear-text здесь допустим — учётные данные нигде не хранятся постоянно.
Защита grep -q "push \[" нужна, потому что nuget.config внутри образа может уже содержать source с именем push от предыдущей сборки самого образа. Без неё dotnet nuget add source завершается ошибкой «source already exists».
Push в Artifactory (стабильные релизы)
Artifactory — реестр для внешних потребителей. Туда идут только стабильные релизы, только из веток rel/*, и только при ручном запуске конкретными инженерами:
yamlpush_artifactory:
image: registry.example.com/ci-images/dotnet-build:latest
stage: push
variables:
# Список пакетов из этого репо для внешней публикации (через пробел)
PACKETS: >-
MyCompany.Api.Schema
MyCompany.Api.Schema.Serialization
MyCompany.Api.Schema.XmlUtils
script:
- |
if dotnet nuget list source | grep -q "push \["; then
dotnet nuget remove source push
fi
- VERSION=${CI_COMMIT_BRANCH##*/}
- dotnet nuget add source \
"https://artifacts.example.com/repository/nuget-releases/index.json" \
--name push \
--username $REGISTRY_NUGET_USERNAME \
--password $REGISTRY_NUGET_PASSWORD \
--store-password-in-clear-text
- |
for pkg in $PACKETS; do
dotnet nuget push "**/${pkg}.${VERSION}.nupkg" --source push
done
rules:
- if: $CI_COMMIT_BRANCH =~ /^rel\/.*/ && $GITLAB_USER_LOGIN =~ /^(alice|bob)$/
when: manual
- if: "$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS"
when: never
tags:
- office-dind
Ключевые решения:
Цикл push по пакетам — dotnet nuget push "**/*.nupkg" опубликовал бы все пакеты репозитория. Если в нём 3 публичных пакета и 5 внутренних вспомогательных проектов, внутренние пакеты в Artifactory не нужны. Переменная PACKETS перечисляет именно то, что публикуется наружу.
Manual + ограничение по пользователю — when: manual защищает от случайной публикации. Проверка $GITLAB_USER_LOGIN — второй барьер: даже если кто-то создаст ветку rel/, нажать кнопку публикации в Artifactory могут только конкретные люди. Ограничение срабатывает на уровне rules GitLab — ещё до обращения к Vault и каким-либо учётным данным.
Учётные данные — $REGISTRY_NUGET_USERNAME и $REGISTRY_NUGET_PASSWORD — групповые CI-переменные GitLab с маскировкой и защитой (видны только на protected-ветках).
Управление NuGet source в build-образе
В build-образе есть nuget.config, указывающий на внутренний NuGet-прокси (Artifactory или групповой feed GitLab) для dotnet restore:
xml<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="internal" value="https://artifacts.example.com/repository/nuget-group/index.json" />
</packageSources>
<packageSourceCredentials>
<internal>
<add key="Username" value="%NUGET_USERNAME%" />
<add key="ClearTextPassword" value="%NUGET_PASSWORD%" />
</internal>
</packageSourceCredentials>
</configuration>
%NUGET_USERNAME% / %NUGET_PASSWORD% — переменные окружения, запечённые в образ через build args. В итоге dotnet restore в CI просто работает — никакой настройки source в YAML пайплайна, образ уже знает, где живут пакеты.
Workflow rules для библиотечных пайплайнов
Библиотечные репо не деплоятся в Kubernetes, поэтому workflow проще:
yamlworkflow:
rules:
- if: $CI_MERGE_REQUEST_ID
- if: $CI_COMMIT_BRANCH == "dev"
- if: $CI_COMMIT_BRANCH =~ /^rel\/.*/
- if: "$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS"
when: never
Граничный случай: переиздание версии
Если в 1.4.2 нашли баг после публикации в Artifactory, заново запушить её нельзя (Artifactory отклоняет по умолчанию). Варианты:
- Опубликовать
1.4.3с фиксом (предпочтительно). - Включить «Allow re-deployment» для Artifactory-репозитория (не рекомендуется для стабильных релизов — потребители могут получить разный пакет под одной версией).
- Вручную удалить сломанную версию из Artifactory через UI, затем перепушить.
Используем вариант 1: ветка rel/1.4.3 создаётся из того же коммита, что и rel/1.4.2, плюс коммит с фиксом.
Итог
| Задача | Решение |
|---|---|
| Версионирование | Имя ветки rel/X.Y.Z |
| Внутренняя публикация | GitLab Package Registry + CI_JOB_TOKEN |
| Внешняя публикация | Artifactory, только rel/*, вручную и только определёнными людьми |
| Избирательная публикация | Переменная PACKETS с перечнем пакетов |
| Учётные данные для restore | nuget.config в build-образе |