В этой статье я расскажу, как мы организовали последовательное автоматическое увеличение номера версии приложения при выполнении коммита в ветку 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.

Решение

Сценарий пайплайна в общих чертах выглядит так:

  1. Получить содержимое ветки main.

  2. Обновить номер версии с помощью PowerShell-скрипта. Для этого на агенте необходимо создать новую ветку. Мы будем делать все изменения в этой ветке, использовать ее для сборки и потом будет объединять ее с веткой main. Эта ветка останется локальной, она не будет отправляться в серверный репозиторий. В конце процесса мы ее удалим.

  3. Выполнить сборку.

  4. Если сборка прошла успешно, обновить номер версии в репозитории также с помощью 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)


  1. saboteur_kiev
    06.09.2021 12:54
    +1

    Хм, я не сталкивался с миром C#, но неужели у стандартного сборщика нет никаких semver плагинов??

    В java инкрементация версии делается через versions maven plugin, например.

    Я даже как-то не верю, что у MS нет для этого штатного средства и нужно делать костыль на повершелле


    1. AntonKanailov Автор
      06.09.2021 13:51

      Плагины есть, но из-за корпоративных ограничений не всегда есть возможность их установить.


      1. saboteur_kiev
        06.09.2021 14:12

        Это очень очень печально. Я работаю уже около 10 лет в проектах, где качать что-либо из инета без секьюрити аудита нельзя.
        Но такие плагины - это базовые вещи, и они обязаны быть доступны. Поэтому я бы рекомендовал пробить через менеджера и секьюрити команду, чтобы плагины и библиотеки, которые используются как best practice решение, стали доступны.


  1. amarao
    06.09.2021 13:43

    Главная глупость азуры - использование hyper-v в качестве гипервизора. Там даже вложенной виртуализации нет. Не то, чтобы я был сильным пользователем азуры, но т.к. их использует github actions, приходится знакомиться. Впечатления - так себе.


    1. Sm1le291
      06.09.2021 15:09

      Azure Devops это переименованный TFS, ещё можно сказать аналог TeamCity, причём здесь вообще гипервизор? Вы кажется темой ошиблись


      1. amarao
        06.09.2021 15:31
        +1

        Да, ошибся. Удачно переименовали.