В этой статье я расскажу, как мы организовали последовательное автоматическое увеличение номера версии приложения при выполнении коммита в ветку main с помощью Azure DevOps Pipeline.
Мы делаем этого для того, чтобы все пользователи и разработчики могли видеть, какая именно версия продукта развернута в той или иной среде.
Задача
В зависимости от архитектуры приложения номер версии может храниться в разных местах. Здесь я приведу пример приложения .NET Core, где номер версии находится в теге AssemblyVersion XML-файла проекта с расширением .csproj. По умолчанию этого тега в проекте нет, инициировать его добавление можно, установив в свойствах проекта его значение, например, 1.0.0.1.
В «классических» приложениях .NET Framework номер версии хранится в файле Properties\AssemblyInfo.cs. Этот уже не XML-файл. Способ поиска и редактирования нужного тега будет несколько отличаться.
При работе с git-репозиторием мы используем trunk-подход, описанный здесь: trunkbaseddevelopment.com. В рамках этого подхода, в отличие от GitFlow, разработчики создают ветки с коротким жизненным циклом от основной ветки main. Эту методологию, в частности, продвигает и Microsoft, мы используем слегка упрощенный подход по сравнению с их Release Flow, описанным в этой статье.
В нашем случае в ветку main код попадает в результате пул-реквестов, но без контрольной автоматической сборки. Поэтому мы запускаем CI-сборку сразу после коммита в мастер. Нам требуется, чтобы, в случае успешного завершения этой сборки, увеличивалась бы последняя цифра в номере версии приложения в AssemblyVersion. Например, было 1.3.4.15, стало 1.3.4.16.
Решение
Сценарий пайплайна в общих чертах выглядит так:
Получить содержимое ветки main.
Обновить номер версии с помощью PowerShell-скрипта. Для этого на агенте необходимо создать новую ветку. Мы будем делать все изменения в этой ветке, использовать ее для сборки и потом будет объединять ее с веткой main. Эта ветка останется локальной, она не будет отправляться в серверный репозиторий. В конце процесса мы ее удалим.
Выполнить сборку.
Если сборка прошла успешно, обновить номер версии в репозитории также с помощью PowerShell-скрипта.
Стоит упомянуть несколько важных моментов.
С момента получения содержимого ветки main и до окончания сборки в ветку main могут попасть новые коммиты. По этой причине Azure-агент получает не содержимое ветки, а конкретный коммит. После получения кода репозиторий становится со "сдвинутой головой" (HEAD detached). Именно поэтому и надо обновлять номер версии в отдельной ветке.
Azure-агент, который будет обновлять репозиторий, должен в нем дополнительно авторизоваться. Для этого понадобится на уровне пайплайна сохранить персональный ключ доступа (PAT) одного из авторизованных пользователей. Это значение является секретным и должно соответствующим образом сохраняться. Оно будет использоваться при отправке коммита через HTTPS.
Чтобы не "возбуждать" CI-сборку при попадании нового номера версии в main, при выполнении коммита необходимо в комментарии указать [skip ci].
Реализация
Мы создаем CI-сборку, которая должна запускаться автоматически после того, как код попадает в ветку main. Поэтому скрипт пайплайна начинается с фразы:
trigger:
- main
Далее следует стандартный блок выбора агента, установки базовых переменных и обновления nuget-пакетов:
Pool:
vmImage: 'windows-latest'
variables:
solution: '**/*.sln'
buildPlatform: 'Any CPU'
buildConfiguration: 'Release'
steps:
- task: NuGetToolInstaller@1
- task: NuGetCommand@2
inputs:
restoreSolution: '$(solution)'
Теперь, когда агент получил из репозитория свежее содержимое ветки main, выполняем PoweShell-скрипт.
- task: PowerShell@2
displayName: 'Assembly Version Generation'
# первая часть – увеличение номера версии
# если сборка падает, новый номер версии не сохраняется в репозитории
inputs:
targetType: 'inline'
script: |
Детали этого скрипта могут отличаться, например, если обновить номер версии надо сразу для нескольких проектов или используется другой принцип хранения номера версии.
Опция --quiet при вызове git указывается, чтобы пайплайн не воспринимал служебные сообщения от git как сообщения об ошибках и не останавливал процесс.
# имя XML-файла проекта, в который будут вноситься изменения
$ProjectFile = '.\azure-devops-versioning\azure-devops-versioning.csproj'
# создаем новую ветку
git branch DevOps/test_$(Build.BuildNumber) –quiet
# переключаемся в новую ветку
git checkout DevOps/test_$(Build.BuildNumber) –quiet
# ищем строку, содержащую тег <AssemblyVersion>
foreach($line in Get-Content -Path $ProjectFile)
{$AssemblyVersionLine = $line | Select-String -Pattern '<AssemblyVersion>' -CaseSensitive
if ($AssemblyVersionLine) {break}
}
# ищем номер версии в строке через операцию -match
$AssemblyVersionLine -match ('(<AssemblyVersion>)(.+)(</AssemblyVersion>)$')
# получаем строку версии
$Version = $Matches[2]
# разбиваем строку на массив
$VerParts = $Version.split('.')
# получаем последнюю часть массива и увеличиваем на 1
$VerParts[3] = ([int]$VerParts[3] + 1)
# собираем обратно в строку через точку
$NewVersion = $VerParts -join '.'
# получаем новую строку с тегом <AssemblyVersion> и новым номером версии
$NewAssemblyVersionLine = $AssemblyVersionLine -replace $Version, $NewVersion
# заменяем старую строку с тегом <AssemblyVersion> на новую
(Get-Content $ProjectFile) | Foreach-Object { $_ -replace $AssemblyVersionLine, $NewAssemblyVersionLine } | Set-Content $ProjectFile
# вводим информацию о пользователе-агенте
git config user.email "azure.agent@profinfotech.ru"
git config user.name "Azure Agent"
# индексируем измененный файл
git add $ProjectFile
# фиксируем изменения в локальном репозитории с комментарием
git commit -m "[skip ci] Pipeline Modification: AssemblyVersion = $NewVersion"
В этом скрипте мы создали новую локальную ветку с использованием номера билда, который в Azure DevOps является уникальным. То есть, при каждой CI-сборке на агенте будет создаваться новая локальная ветка. Мы переключились на эту новую ветку, нашли нужный файл и заменили в нем AssemblyVersion, увеличив последний номер на 1. Мы сделали коммит в локальный репозиторий и запускаем сборку с помощью стандартного блока:
- task: VSBuild@1
inputs:
solution: '$(solution)'
msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"'
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'
Если сборка завершилась с ошибкой, на этом процесс закончится. На агенте останется локальная ветка с номером билда, которую можно удалить, но для нас это не критично, поэтому мы не будем здесь на этом останавливаться.
Если же сборка завершилась успешно, мы запускаем PowerShell-скрипт.
- task: PowerShell@2
displayName: 'Send new version to main'
# вторая часть – после успешной сборки новая версия отправляется в серверный репозиторий
inputs:
targetType: 'inline'
script: |
Этот скрипт переключит нас в main, получит обновление main, объединит нашу ветку с веткой main, удалит нашу ветку и сделает push в центральный репозиторий, авторизовавшись с помощью PAT-токена:
# переключаемся на ветку main
git checkout main –quiet
# обновляем локальную ветку main
git pull –quiet
# объединяем нашу ветку с веткой main, в комментарии указываем номер версии
git merge DevOps/test_$(Build.BuildNumber) -m "[skip ci] Pipeline Modification: AssemblyVersion = $NewVersion" –quiet
# удаляем локальную ветку
git branch -d DevOps/test_$(Build.BuildNumber)
# отправляем локальную ветку main в серверный репозиторий
# PAToken – это переменная пайплайна, содержащая значение персонального ключа
git push https://kanailov:$(PAToken)@github.com/Kanailov/azure-devops-versioning.git main –quiet
В приведенном примере git-репозиторий находится в GitHub. В том случае, если используется git-репозиторий, встроенный в Azure DevOps, формат HTTPS-запроса немного отличается:
git push https://Personal%20Access%20Token:$(PAToken)@<URL_git-репозитория> main --quiet
Ссылка
Пример проекта выложен на Github. В репозитории находится yml-скрипт azure-pipelines.yml.
Комментарии (6)
amarao
06.09.2021 13:43Главная глупость азуры - использование hyper-v в качестве гипервизора. Там даже вложенной виртуализации нет. Не то, чтобы я был сильным пользователем азуры, но т.к. их использует github actions, приходится знакомиться. Впечатления - так себе.
saboteur_kiev
Хм, я не сталкивался с миром C#, но неужели у стандартного сборщика нет никаких semver плагинов??
В java инкрементация версии делается через versions maven plugin, например.
Я даже как-то не верю, что у MS нет для этого штатного средства и нужно делать костыль на повершелле
AntonKanailov Автор
Плагины есть, но из-за корпоративных ограничений не всегда есть возможность их установить.
saboteur_kiev
Это очень очень печально. Я работаю уже около 10 лет в проектах, где качать что-либо из инета без секьюрити аудита нельзя.
Но такие плагины - это базовые вещи, и они обязаны быть доступны. Поэтому я бы рекомендовал пробить через менеджера и секьюрити команду, чтобы плагины и библиотеки, которые используются как best practice решение, стали доступны.