Этот урок про то, куда складывать собранные артефакты и как настроить публикацию из Jenkins в нормальное хранилище. Разберём два главных репозитория - Sonatype Nexus и JFrog Artifactory, версионирование, политики хранения сборок и частный случай - публикацию Docker-образов в registry.
Зачем нужен репозиторий артефактов
Артефакт - это собранный, готовый к доставке результат: бинарник, архив, образ, пакет. Хранить его прямо в Git - плохая идея: репозиторий распухнет, а Git не про бинарные блобы. Для этого есть отдельный класс систем - репозитории артефактов. Они дают версионирование, дедупликацию, контроль доступа, проксирование внешних реестров (Maven Central, npm, PyPI) и метаданные о происхождении сборки.
Два лидера рынка:
- Sonatype Nexus Repository - есть бесплатная Community-редакция, классика для Maven/Gradle-мира, умеет npm, PyPI, NuGet, Docker и raw.
- JFrog Artifactory - мощнее по фичам, сильная связка с собственным CLI (jf), хранит "build info" - подробные метаданные о сборке, что критично для трассируемости. Есть OSS-редакция, но многое в платных.

Публикация артефактов в Nexus из конвейера
Для Nexus стандартный путь - плагин Nexus Artifact Uploader, он даёт declarative-совместимый шаг nexusArtifactUploader. Доступ к Nexus храним не в открытом виде, а в Jenkins Credentials (тип Username with password), а в pipeline дёргаем через withCredentials либо через nexusCredentialsId.
Вот рабочий пример публикации jar в Nexus после сборки:
Код: Выделить всё
pipeline {
agent any
options {
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumKeepStr: '5'))
timestamps()
}
environment {
NEXUS_URL = 'nexus.internal:8081'
APP_GROUP = 'ru.cyberlake'
APP_NAME = 'billing-service'
}
stages {
stage('Build') {
steps {
sh './mvnw -B -DskipTests clean package'
}
}
stage('Publish to Nexus') {
steps {
nexusArtifactUploader(
nexusVersion: 'nexus3',
protocol: 'http',
nexusUrl: "${NEXUS_URL}",
groupId: "${APP_GROUP}",
version: "1.4.${BUILD_NUMBER}",
repository: 'releases',
credentialsId: 'nexus-deploy',
artifacts: [[
artifactId: "${APP_NAME}",
classifier: '',
file: "target/${APP_NAME}-1.4.${BUILD_NUMBER}.jar",
type: 'jar'
]]
)
}
}
}
}
Код: Выделить всё
stage('Maven deploy') {
steps {
configFileProvider([configFile(fileId: 'maven-settings', variable: 'MVN_SETTINGS')]) {
sh './mvnw -B -s $MVN_SETTINGS deploy -DskipTests'
}
}
}
Для Artifactory в 2026 году актуальный плагин - JFrog Plugin (jenkins-jfrog-plugin). Старый шаг rtUpload из плагина Artifactory всё ещё работает и встречается в легаси-конвейерах, но новый подход - запускать команды JFrog CLI (jf) прямо из pipeline через шаг jf. Это даёт больше контроля и, главное, автоматически собирает build-info - метаданные о том, какие зависимости пошли в сборку и из какого окружения.
Сервер Artifactory настраивается глобально (Manage Jenkins -> JFrog) либо через Configuration as Code, а в конвейере используется так:
Код: Выделить всё
pipeline {
agent any
tools {
jfrog 'jfrog-cli'
}
options {
buildDiscarder(logRotator(numToKeepStr: '30', artifactNumKeepStr: '10'))
}
stages {
stage('Build') {
steps {
sh './gradlew clean assemble'
}
}
stage('Resolve deps from Artifactory') {
steps {
// получение зависимостей из приватного репозитория
jf 'rt dl libs-release/internal/ deps/'
}
}
stage('Upload artifact') {
steps {
jf 'rt upload "build/libs/*.jar" libs-release-local/ru/cyberlake/billing/1.4.${BUILD_NUMBER}/'
}
}
stage('Publish build info') {
steps {
jf 'rt build-collect-env'
jf 'rt build-publish'
}
}
}
}
Для сравнения - в GitLab CI и GitHub Actions публикация артефактов в Nexus/Artifactory делается обычно голым curl, mvn deploy или тем же jf CLI внутри шага; отдельной богатой интеграции с build-info, как у Jenkins-плагина JFrog, там из коробки нет, поэтому связка Jenkins + Artifactory до сих пор остаётся сильным аргументом в пользу Jenkins для зрелого артефакт-менеджмента.
Версионирование и политики хранения сборок
Версия артефакта - это его удостоверение личности. Плохая практика - публиковать всё как latest или перетирать одну и ту же версию: ты теряешь воспроизводимость. Хорошая практика - неизменяемые (immutable) версии. Для снапшотов годится схема вида 1.4.0-SNAPSHOT в snapshot-репозиторий, для релизов - чистый SemVer 1.4.7 в release-репозиторий, который Nexus/Artifactory защищают от перезаписи.
Удобно встраивать в версию номер сборки и короткий хеш коммита:
Код: Выделить всё
stage('Set version') {
steps {
script {
env.GIT_SHA = sh(returnStdout: true,
script: 'git rev-parse --short HEAD').trim()
env.APP_VERSION = "1.4.${BUILD_NUMBER}-${env.GIT_SHA}"
}
echo "Будем публиковать версию ${env.APP_VERSION}"
}
}
Отдельная важная тема - сколько билдов хранить в самом Jenkins. Без присмотра история сборок съест диск контроллера. Для этого есть директива buildDiscarder в блоке options:
Код: Выделить всё
options {
buildDiscarder(logRotator(
numToKeepStr: '50', // хранить последние 50 сборок
daysToKeepStr: '30', // и/или не старше 30 дней
artifactNumKeepStr: '5', // тяжёлые артефакты - только у 5 последних
artifactDaysToKeepStr: '7'
))
}
Docker-образ как частный случай артефакта
Образ контейнера - такой же артефакт, просто хранится в Docker registry. Им может быть тот же Nexus или Artifactory (у обоих есть Docker-репозитории), GitLab Registry, GitHub Container Registry или Harbor. Принцип тот же: собрал, протегировал осмысленной версией, запушил, а потом этот тег уезжает в деплой.
Код: Выделить всё
stage('Build & push image') {
steps {
script {
def tag = "registry.internal/cyberlake/billing:1.4.${BUILD_NUMBER}"
docker.withRegistry('https://registry.internal', 'registry-creds') {
def img = docker.build(tag, '--pull .')
img.push()
img.push('latest')
}
}
}
}
Код: Выделить всё
sh 'docker build -t registry.internal/cyberlake/billing:1.4.${BUILD_NUMBER} .'
jf 'docker push registry.internal/cyberlake/billing:1.4.${BUILD_NUMBER}'
jf 'rt build-publish'
Типичные грабли
- Latest вместо версии. Перезаписываешь один тег - и теряешь возможность откатиться и понять, что в продакшене. Только неизменяемые версии.
- Кредлы Nexus/Artifactory в открытом виде. Логин-пароль в Jenkinsfile или в переменной окружения, видимой в логах - это утечка. Только Jenkins Credentials и withCredentials, маскирование секретов.
- Публикация до прохождения тестов. Стейдж публикации должен идти после тестов и качества, иначе в репозиторий попадёт битьё.
- Забытый buildDiscarder. Диск контроллера забивается архивами, Jenkins начинает тормозить. Ставь ротацию сразу в шаблоне пайплайна.
- Снапшоты в release-репозиторий. Mutable-снапшоты в защищённый релизный репозиторий ломают неизменяемость. Разводи snapshot и release по разным репозиториям.
- Подними локально Nexus (docker run sonatype/nexus3) или Artifactory OSS, создай hosted-репозиторий для своих артефактов.
- Заведи в Jenkins Credentials деплой-пользователя (Username with password) и пропиши credentialsId в pipeline.
- Собери простой Maven- или Gradle-проект и опубликуй jar шагом nexusArtifactUploader или jf rt upload с версией вида 1.0.${BUILD_NUMBER}.
- Добавь в options блок buildDiscarder и проверь, что старые сборки начинают вычищаться.
- Собери Docker-образ, протегируй номером сборки, запушь в registry и убедись, что тег latest указывает на свежий образ, а версионный тег неизменен.
- Чем hosted-репозиторий отличается от proxy и group, и куда из них публикует Jenkins?
- Почему публиковать артефакт как latest - плохо, и какую схему версионирования стоит выбрать для релизов?
- За что отвечает numToKeepStr, а за что artifactNumKeepStr в buildDiscarder?
- Что такое build-info в Artifactory и зачем нужен шаг jf rt build-publish?
Артефакт без хранилища - это потерянная сборка. Nexus и Artifactory дают версионирование, контроль доступа и трассируемость, а Jenkins публикует в них через nexusArtifactUploader или современный jf CLI с build-info. Версии держи неизменяемыми, секреты - в Credentials, историю сборок ограничивай через buildDiscarder, а Docker-образы трактуй как обычные артефакты с осмысленными тегами. Тогда цепочка сборка -> артефакт -> деплой становится прозрачной и воспроизводимой - именно то, ради чего и затевался весь конвейер.