Русский
Русский
English
Статистика
Реклама

Azure devops

PVS-Studio Анализ pull request-ов в Azure DevOps при помощи self-hosted агентов

27.07.2020 18:22:43 | Автор: admin


Статический анализ кода показывает наибольшую эффективность во время внесения изменений в проект, поскольку ошибки всегда сложнее исправлять в будущем, чем не допустить их появления на ранних этапах. Мы продолжаем расширять варианты использования PVS-Studio в системах непрерывной разработки и покажем, как настроить анализ pull request-ов при помощи self-hosted агентов в Microsoft Azure DevOps, на примере игры Minetest.

Вкратце о том, с чем мы имеем дело


Minetest это открытый кроссплатформенный игровой движок, содержащий около 200 тысяч строк кода на C, C++ и Lua. Он позволяет создавать разные игровые режимы в воксельном пространстве. Поддерживает мультиплеер, и множество модов от сообщества. Репозиторий проекта размещен здесь: https://github.com/minetest/minetest.

Для настройки регулярного поиска ошибок использованы следующие инструменты:

PVS-Studio статический анализатор кода на языках C, C++, C# и Java для поиска ошибок и дефектов безопасности.

Azure DevOps облачная платформа, предоставляющая возможность разработки, выполнения приложений и хранения данных на удаленных серверах.

Для выполнения задач разработки в Azure возможно использование виртуальных машин-агентов Windows и Linux. Однако запуск агентов на локальном оборудовании имеет несколько весомых преимуществ:

  • У локального хоста может быть больше ресурсов, чем у ВМ Azure;
  • Агент не "исчезает" после выполнения своей задачи;
  • Возможность прямой настройки окружения, и более гибкое управление процессами сборки;
  • Локальное хранение промежуточных файлов положительно влияет на скорость сборки;
  • Можно бесплатно выполнять более 30 задач в месяц.

Подготовка к использованию self-hosted агента


Процесс начала работы в Azure подробно описан в статье "PVS-Studio идёт в облака: Azure DevOps", поэтому перейду сразу к созданию self-hosted агента.

Для того, чтобы агенты имели право подключиться к пулам проекта, им нужен специальный Access Token. Получить его можно на странице "Personal Access Tokens", в меню "User settings".

image2.png

После нажатия на "New token" необходимо указать имя и выбрать Read & manage Agent Pools (может понадобиться развернуть полный список через "Show all scopes").

image3.png

Нужно скопировать токен, так как Azure больше его не покажет, и придется делать новый.

image4.png

В качестве агента будет использован Docker контейнер на основе Windows Server Core. Хостом является мой рабочий компьютер на Windows 10 x64 с Hyper-V.

Сначала понадобится расширить объём дискового пространства, доступный Docker контейнерам.

В Windows для этого нужно модифицировать файл 'C:\ProgramData\Docker\config\daemon.json' следующим образом:

{  "registry-mirrors": [],  "insecure-registries": [],  "debug": true,  "experimental": false,  "data-root": "d:\\docker",  "storage-opts": [ "size=40G" ]}

Для создания Docker образа для агентов со сборочной системой и всем необходимым в директории 'D:\docker-agent' добавим Docker файл с таким содержимым:

# escape=`FROM mcr.microsoft.com/dotnet/framework/runtimeSHELL ["cmd", "/S", "/C"]ADD https://aka.ms/vs/16/release/vs_buildtools.exe C:\vs_buildtools.exeRUN C:\vs_buildtools.exe --quiet --wait --norestart --nocache `  --installPath C:\BuildTools `  --add Microsoft.VisualStudio.Workload.VCTools `  --includeRecommendedRUN powershell.exe -Command `  Set-ExecutionPolicy Bypass -Scope Process -Force; `  [System.Net.ServicePointManager]::SecurityProtocol =    [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; `  iex ((New-Object System.Net.WebClient)    .DownloadString('https://chocolatey.org/install.ps1')); `  choco feature enable -n=useRememberedArgumentsForUpgrades;  RUN powershell.exe -Command `  choco install -y cmake --installargs '"ADD_CMAKE_TO_PATH=System"'; `  choco install -y git --params '"/GitOnlyOnPath /NoShellIntegration"'RUN powershell.exe -Command `  git clone https://github.com/microsoft/vcpkg.git; `  .\vcpkg\bootstrap-vcpkg -disableMetrics; `  $env:Path += '";C:\vcpkg"'; `  [Environment]::SetEnvironmentVariable(    '"Path"', $env:Path, [System.EnvironmentVariableTarget]::Machine); `  [Environment]::SetEnvironmentVariable(    '"VCPKG_DEFAULT_TRIPLET"', '"x64-windows"',  [System.EnvironmentVariableTarget]::Machine)RUN powershell.exe -Command `  choco install -y pvs-studio; `  $env:Path += '";C:\Program Files (x86)\PVS-Studio"'; `  [Environment]::SetEnvironmentVariable(    '"Path"', $env:Path, [System.EnvironmentVariableTarget]::Machine)RUN powershell.exe -Command `  $latest_agent =    Invoke-RestMethod -Uri "https://api.github.com/repos/Microsoft/                          azure-pipelines-agent/releases/latest"; `  $latest_agent_version =    $latest_agent.name.Substring(1, $latest_agent.tag_name.Length-1); `  $latest_agent_url =    '"https://vstsagentpackage.azureedge.net/agent/"' + $latest_agent_version +  '"/vsts-agent-win-x64-"' + $latest_agent_version + '".zip"'; `  Invoke-WebRequest -Uri $latest_agent_url -Method Get -OutFile ./agent.zip; `  Expand-Archive -Path ./agent.zip -DestinationPath ./agentUSER ContainerAdministratorRUN reg add hklm\system\currentcontrolset\services\cexecsvc        /v ProcessShutdownTimeoutSeconds /t REG_DWORD /d 60  RUN reg add hklm\system\currentcontrolset\control        /v WaitToKillServiceTimeout /t REG_SZ /d 60000 /fADD .\entrypoint.ps1 C:\entrypoint.ps1SHELL ["powershell", "-Command",       "$ErrorActionPreference = 'Stop';     $ProgressPreference = 'SilentlyContinue';"]ENTRYPOINT .\entrypoint.ps1

В результате получится сборочная система на основе MSBuild для C++, с Chocolatey для установки PVS-Studio, CMake и Git. Для удобного управления библиотеками, от которых зависит проект, собирается Vcpkg. А также скачивается свежая версия, собственно, Azure Pipelines Agent.

Для инициализации агента из ENTRYPOINT-а Docker файла вызывается PowerShell скрипт 'entrypoint.ps1', в который нужно добавить URL "организации" проекта, токен пула агентов, и параметры лицензии PVS-Studio:

$organization_url = "https://dev.azure.com/<аккаунт Microsoft Azure>"$agents_token = "<token агента>"$pvs_studio_user = "<имя пользователя PVS-Studio>"$pvs_studio_key = "<ключ PVS-Studio>"try{  C:\BuildTools\VC\Auxiliary\Build\vcvars64.bat  PVS-Studio_Cmd credentials -u $pvs_studio_user -n $pvs_studio_key    .\agent\config.cmd --unattended `    --url $organization_url `    --auth PAT `    --token $agents_token `    --replace;  .\agent\run.cmd} finally{  # Agent graceful shutdown  # https://github.com/moby/moby/issues/25982    .\agent\config.cmd remove --unattended `    --auth PAT `    --token $agents_token}

Команды для сборки образа и запуска агента:

docker build -t azure-agent -m 4GB .docker run -id --name my-agent -m 4GB --cpu-count 4 azure-agent

image5.png

Агент запущен и готов выполнять задачи.

image6.png

Запуск анализа на self-hosted агенте


Для анализа PR создается новый pipeline со следующим скриптом:

image7.png

trigger: nonepr:  branches:    include:    - '*'pool: Defaultsteps:- script: git diff --name-only    origin/%SYSTEM_PULLREQUEST_TARGETBRANCH% >    diff-files.txt  displayName: 'Get committed files'- script: |    cd C:\vcpkg    git pull --rebase origin    CMD /C ".\bootstrap-vcpkg -disableMetrics"    vcpkg install ^    irrlicht zlib curl[winssl] openal-soft libvorbis ^    libogg sqlite3 freetype luajit    vcpkg upgrade --no-dry-run  displayName: 'Manage dependencies (Vcpkg)'- task: CMake@1  inputs:    cmakeArgs: -A x64      -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake      -DCMAKE_BUILD_TYPE=Release -DENABLE_GETTEXT=0 -DENABLE_CURSES=0 ..  displayName: 'Run CMake'- task: MSBuild@1  inputs:    solution: '**/*.sln'    msbuildArchitecture: 'x64'    platform: 'x64'    configuration: 'Release'    maximumCpuCount: true  displayName: 'Build'- script: |    IF EXIST .\PVSTestResults RMDIR /Q/S .\PVSTestResults    md .\PVSTestResults    PVS-Studio_Cmd ^    -t .\build\minetest.sln ^    -S minetest ^    -o .\PVSTestResults\minetest.plog ^    -c Release ^    -p x64 ^    -f diff-files.txt ^    -D C:\caches    PlogConverter ^    -t FullHtml ^    -o .\PVSTestResults\ ^    -a GA:1,2,3;64:1,2,3;OP:1,2,3 ^    .\PVSTestResults\minetest.plog    IF NOT EXIST "$(Build.ArtifactStagingDirectory)" ^    MKDIR "$(Build.ArtifactStagingDirectory)"    powershell -Command ^    "Compress-Archive -Force ^    '.\PVSTestResults\fullhtml' ^    '$(Build.ArtifactStagingDirectory)\fullhtml.zip'"  displayName: 'PVS-Studio analyze'  continueOnError: true- task: PublishBuildArtifacts@1  inputs:    PathtoPublish: '$(Build.ArtifactStagingDirectory)'    ArtifactName: 'psv-studio-analisys'    publishLocation: 'Container'  displayName: 'Publish analysis report'

Этот скрипт сработает при получении PR и будет выполнен на агентах, назначенных в пул по умолчанию. Необходимо только дать ему разрешение на работу с этим пулом.

image8.png


image9.png

В скрипте происходит сохранение списка измененных файлов, полученного при помощи git diff. Затем обновляются зависимости, генерируется solution проекта через CMake, и производится его сборка.

Если сборка прошла успешно, запускается анализ изменившихся файлов (флаг '-f diff-files.txt'), игнорируя созданные CMake вспомогательные проекты (выбираем только нужный проект флагом '-S minetest'). Для ускорения поиска связей между заголовочными и исходными C++ файлами создается специальный кэш, который будет храниться в отдельной директории (флаг '-D C:\caches').

Таким образом, мы теперь можем получать отчеты об анализе изменений в проекте.

image10.png


image11.png

Как было сказано в начале статьи, приятным бонусом использования self-hosted агентов является ощутимое ускорение выполнения задач, за счет локального хранения промежуточных файлов.

image13.png

Некоторые ошибки, найденные в Minetest


Затирание результата

V519 The 'color_name' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 621, 627. string.cpp 627

static bool parseNamedColorString(const std::string &value,                                  video::SColor &color){  std::string color_name;  std::string alpha_string;  size_t alpha_pos = value.find('#');  if (alpha_pos != std::string::npos) {    color_name = value.substr(0, alpha_pos);    alpha_string = value.substr(alpha_pos + 1);  } else {    color_name = value;  }  color_name = lowercase(value); // <=  std::map<const std::string, unsigned>::const_iterator it;  it = named_colors.colors.find(color_name);  if (it == named_colors.colors.end())    return false;  ....}

Эта функция должна производить разбор названия цвета с параметром прозрачности (например, Green#77) и вернуть его код. В зависимости от результата проверки условия в переменную color_name передается результат разбиения строки либо копия аргумента функции. Однако затем в нижний регистр переводится не сама полученная строка, а исходный аргумент. В результате её не удастся найти в словаре цветов при наличии параметра прозрачности. Можем исправить эту строку так:

color_name = lowercase(color_name);

Лишние проверки условий

V547 Expression 'nearest_emergefull_d == 1' is always true. clientiface.cpp 363

void RemoteClient::GetNextBlocks (....){  ....  s32 nearest_emergefull_d = -1;  ....  s16 d;  for (d = d_start; d <= d_max; d++) {    ....      if (block == NULL || surely_not_found_on_disk || block_is_invalid) {        if (emerge->enqueueBlockEmerge(peer_id, p, generate)) {          if (nearest_emerged_d == -1)            nearest_emerged_d = d;        } else {          if (nearest_emergefull_d == -1) // <=            nearest_emergefull_d = d;          goto queue_full_break;        }  ....  }  ....queue_full_break:  if (nearest_emerged_d != -1) { // <=    new_nearest_unsent_d = nearest_emerged_d;  } else ....}

Переменная nearest_emergefull_d в процессе работы цикла не меняется, и её проверка не влияет на ход выполнения алгоритма. Либо это результат неаккуратного copy-paste, либо с ней забыли провести какие-то вычисления.

V560 A part of conditional expression is always false: y > max_spawn_y. mapgen_v7.cpp 262

int MapgenV7::getSpawnLevelAtPoint(v2s16 p){  ....  while (iters > 0 && y <= max_spawn_y) {               // <=    if (!getMountainTerrainAtPoint(p.X, y + 1, p.Y)) {      if (y <= water_level || y > max_spawn_y)          // <=        return MAX_MAP_GENERATION_LIMIT; // Unsuitable spawn point      // y + 1 due to biome 'dust'      return y + 1;    }  ....}

Значение переменной 'y' проверяется перед очередной итерацией цикла. Последующее, противоположное ей, сравнение всегда вернет false и, в целом, не влияет на результат проверки условия.

Потеря проверки указателя

V595 The 'm_client' pointer was utilized before it was verified against nullptr. Check lines: 183, 187. game.cpp 183

void gotText(const StringMap &fields){  ....  if (m_formname == "MT_DEATH_SCREEN") {    assert(m_client != 0);    m_client->sendRespawn();    return;  }  if (m_client && m_client->modsLoaded())    m_client->getScript()->on_formspec_input(m_formname, fields);}

Перед обращением по указателю m_client проверяется не нулевой ли он при помощи макроса assert. Но это относится только к отладочной сборке. Такая мера предосторожности, при сборке в релиз, заменяется на пустышку, и появляется риск разыменования нулевого указателя.

Бит или не бит?

V616 The '(FT_RENDER_MODE_NORMAL)' named constant with the value of 0 is used in the bitwise operation. CGUITTFont.h 360

typedef enum  FT_Render_Mode_{  FT_RENDER_MODE_NORMAL = 0,  FT_RENDER_MODE_LIGHT,  FT_RENDER_MODE_MONO,  FT_RENDER_MODE_LCD,  FT_RENDER_MODE_LCD_V,  FT_RENDER_MODE_MAX} FT_Render_Mode;#define FT_LOAD_TARGET_( x )   ( (FT_Int32)( (x) & 15 ) << 16 )#define FT_LOAD_TARGET_NORMAL  FT_LOAD_TARGET_( FT_RENDER_MODE_NORMAL )void update_load_flags(){  // Set up our loading flags.  load_flags = FT_LOAD_DEFAULT | FT_LOAD_RENDER;  if (!useHinting()) load_flags |= FT_LOAD_NO_HINTING;  if (!useAutoHinting()) load_flags |= FT_LOAD_NO_AUTOHINT;  if (useMonochrome()) load_flags |=     FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO | FT_RENDER_MODE_MONO;  else load_flags |= FT_LOAD_TARGET_NORMAL; // <=}

Макрос FT_LOAD_TARGET_NORMAL разворачивается в ноль, и побитовое "или" не будет устанавливать никакие флаги в load_flags, ветка else может быть убрана.

Округление целочисленного деления

V636 The 'rect.getHeight() / 16' expression was implicitly cast from 'int' type to 'float' type. Consider utilizing an explicit type cast to avoid the loss of a fractional part. An example: double A = (double)(X) / Y;. hud.cpp 771

void drawItemStack(....){  float barheight = rect.getHeight() / 16;  float barpad_x = rect.getWidth() / 16;  float barpad_y = rect.getHeight() / 16;  core::rect<s32> progressrect(    rect.UpperLeftCorner.X + barpad_x,    rect.LowerRightCorner.Y - barpad_y - barheight,    rect.LowerRightCorner.X - barpad_x,    rect.LowerRightCorner.Y - barpad_y);}

Геттеры rect возвращают целочисленные значения. В переменную с плавающей точкой записывается результат деления целых чисел, и теряется дробная часть. Похоже, что в этих расчетах рассогласованные типы данных.

Подозрительная последовательность операторов ветвления

V646 Consider inspecting the application's logic. It's possible that 'else' keyword is missing. treegen.cpp 413

treegen::error make_ltree(...., TreeDef tree_definition){  ....  std::stack <core::matrix4> stack_orientation;  ....    if ((stack_orientation.empty() &&      tree_definition.trunk_type == "double") ||      (!stack_orientation.empty() &&      tree_definition.trunk_type == "double" &&      !tree_definition.thin_branches)) {      ....    } else if ((stack_orientation.empty() &&      tree_definition.trunk_type == "crossed") ||      (!stack_orientation.empty() &&      tree_definition.trunk_type == "crossed" &&      !tree_definition.thin_branches)) {      ....    } if (!stack_orientation.empty()) {                  // <=  ....  }  ....}

Здесь последовательности else-if в алгоритме генерации деревьев. Посередине очередной блок if находится на одной строке с закрывающей предыдущий else скобкой. Возможно, код работает правильно: до этого if-а создаются блоки ствола, а следом листьев; а может быть пропустили else. Наверняка это сказать может только разработчик.

Неправильная проверка выделения памяти

V668 There is no sense in testing the 'clouds' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. game.cpp 1367

bool Game::createClient(....){  if (m_cache_enable_clouds) {    clouds = new Clouds(smgr, -1, time(0));    if (!clouds) {      *error_message = "Memory allocation error (clouds)";      errorstream << *error_message << std::endl;      return false;    }  }}

В случае, если new не сможет создать объект, будет брошено исключение std::bad_alloc, и оно должно быть обработано try-catch блоком. А проверка в таком виде бесполезна.

Чтение за границей массива

V781 The value of the 'i' index is checked after it was used. Perhaps there is a mistake in program logic. irrString.h 572

bool equalsn(const string<T,TAlloc>& other, u32 n) const{  u32 i;  for(i=0; array[i] && other[i] && i < n; ++i) // <=    if (array[i] != other[i])      return false;  // if one (or both) of the strings was smaller then they  // are only equal if they have the same length  return (i == n) || (used == other.used);}

Происходит обращение к элементам массивов перед проверкой индекса, что может привести к ошибке. Возможно, стоит переписать цикл так:

for (i=0; i < n; ++i) // <=  if (!array[i] || !other[i] || array[i] != other[i])    return false;

Другие ошибки

Данная статья посвящена анализу pull request-ов в Azure DevOps и не ставит целью провести подробный обзор ошибок проекта Minetest. Здесь выписаны только некоторые фрагменты кода, которые мне показались интересными. Предлагаем авторам проекта не руководствоваться этой статьёй для исправления ошибок и выполнить более тщательный анализ предупреждений, которые выдаст PVS-Studio.

Заключение


Благодаря гибкой конфигурации в режиме командной строки, анализ PVS-Studio может быть встроен в самые разнообразные сценарии CI/CD. А правильное использование доступных ресурсов окупается увеличением производительности.

Нужно отметить, что режим проверки pull request-ов доступен только в Enterprise редакции анализатора. Чтобы получить демонстрационную Enterprise лицензию, укажите это в комментарии при запросе лицензии на странице скачивания. Более подробно о разнице между лицензиями можно узнать на странице заказа PVS-Studio.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Alexey Govorov. PVS-Studio: analyzing pull requests in Azure DevOps using self-hosted agents.
Подробнее..

CICD для Dynamics CRM на базе Azure DevOps

11.02.2021 18:04:31 | Автор: admin

image


В прошлом году на митапе "Dynamics 365 & Power Platform meetup Moscow 25 февраля 2020" я рассказывал про то как мы выстроили пайплайн непрерывной поставки CI/CD на базе GitLab CI для Microsoft Dynamics CRM.


В этой статье я расскажу и покажу как построить CI-часть пайплайна непрерывной поставки расширения функциональности Microsoft Dynamics CRM на базе Azure DevOps.


Я давно уже не заглядывал в Azure и на новогодних праздниках наконец-то появилось время в нем "поковыряться". :-) Соответственно, ничего сложного в данной статье я описывать не буду и если у вас уже есть построенный пайплайн в Azure DevOps то вы, скорее всего, ничего нового здесь не найдете. Однако, если вы чувствуете потребность в автоматизации выноса но не знаете с чего начать то эта статья как раз для вас.


Я буду использовать облачный сервис Azure DevOps Service т.к. развернуть локально и настроить не успел но, локально должно работать точно также. Начинается все, естественно, с репозитория. Для Azure Devops Pipelines можно использовать разные репозитории, Azure Repos Git, Bitbucket Cloud, GitHub, просто Git и Subversion но я, для удобства, остановлюсь на Azure Repos Git чтобы все было в одном проекте который я оставлю в публичном доступе здесь https://dev.azure.com/ZhukoffPublic/CRMCICD/.


Microsoft Dynamics CRM это CRM система на базе технологий ASP.NET, которое теперь входит в семейство Dynamics 365, которую можно расширять путем написания плагинов на C# для бэковой части и JScript для фронтальной.

Для демонстрационных целей я создал 4 проекта (вы их можете клонировать из моего репозитория):


  • Plugins плагины CRM. В нашем случае плагином который переопределяет формирование полного имени.
  • Plugins.Tests юнит-тесты для плагинов.
  • Solution кастомизации CRM. В нашем случае это сущность Контакт и основная форма для него.
  • WebResources вебресурсы CRM. В нашем случае это скрипт на форме Контакта который выводит нотификацию если не заполнен телефон.

Таким образом мы покроем основные элементы расширения MS Dynamics CRM.


Для автоматизации я буду использовать SparkleXrm написанный небезызвестным Scott Durow. Описание возможностей можно почитать здесь Simple, No fuss, Dynamics 365 Deployment Task Runner как пользовать можно посмотреть на его канале в YouTube. Также у него есть видеокурс на PaktPub Designing and Building Custom Apps using Dynamics 365 где он подробнейшим образом рассказывает что и как использовать и даже описывает как создать пайплайн на тот момент это был еще VSTS. Я выбрал spkl потому что он уже написан и сам проект в открытом доступе на GitHub, что позволяет сделать доработки самому если что-то не устраивает. Это вариант, конечно, не без недостатков, о них я расскажу позже.


Будем считать, что проект у нас есть и что он, конечно, собирается и деплоится из командной строки с помощью spkl. У меня это виртуальная машина установленной MS CRM 365 (9.0), Visual Studio, SDK, XRMToolBox и пр., короче, все в одном.


Сам пайплайн делится на две основные части, CI непрерывная интеграция (Continuous Integration) т.е. когда код непрерывно интегрируется в ветке репозитория и CD которая может быть реализована как непрерывная поставка (Continuous Delivery) или непрерывное развертывание (Continuous Deployment).


Мы начнем, конечно, с CI и думать сейчас как делать CD вовсе необязательно.


CI/CD Pipeline


Подготовительные работы


Создаем проект CRMCICD в Azure DevOps.



Клонируем репозиторий и заливаем наш проект.



Теперь создадим сам пайплайн, для этого нам нужно перейти в секцию Pipelines, жмем Create Pipeline, выбираем репозиторий Azure Repos Git далее выбираем наш репозиторий CRMCICD далее выбираем Starter Pipeline и получаем следующую картину.



Это и есть пайплайн, YAML код в файле azure-pipelines.yml который исполняется агентом в каком-то окружении, windows или linux например. Он должен состоять минимум из трех секций:


  • trigger: триггер для запуска пайплайна, в данном случае это -main, т.е. по коммиту в ветку main.
  • pool: окружение где будет выполняться пайплайн, в данном случае образ ubuntu в облаке Azure.
  • steps: сами действия которые должны быть выполнены.

Если сохранить пайплайн в таком виде то он будет запускаться на каждый коммит в ветку main. Поэтому ставим trigger: none и сохраняем так пока его не настроим и не отладим.


Наш CI будет состоять из 2 этапов:


  • Сборка проекта и решения
  • Развертывание на локальную среду

2-й шаг не обязателен но я его добавил чтобы был пример развертывания на локальную среду с помощью spkl, полезно сразу получить то, что только что залили в main ветку.


Сборка проекта и решения


Первым делом меняем агента на windows т.к. linux нас никак не устроит.
vmImage: 'windows-latest'


Добавляем переменные для удобства.


variables:  solution: '**/*.sln'  buildPlatform: 'Any CPU'  buildConfiguration: 'Debug'

Добавляем следующие шаги:


steps:# Установка NuGet- task: NuGetToolInstaller@1  displayName: 'Install NuGet tool'# Восстановление пакетов NuGet для проекта- task: NuGetCommand@2  displayName: 'Restore NuGet packages'  inputs:    restoreSolution: '$(solution)'# Сборка- task: VSBuild@1  displayName: Buld  inputs:    solution: '$(solution)'    platform: '$(buildPlatform)'    configuration: '$(buildConfiguration)'# Прогон юнит-тестов- task: VSTest@2  displayName: 'Run Unit Tests'  inputs:    platform: '$(buildPlatform)'    configuration: '$(buildConfiguration)'# Публикация собранной dll-ки в хранилище артефактов- task: CopyFiles@2  displayName: 'Copy plugins dll'  inputs:    SourceFolder: '$(Build.SourcesDirectory)\CRMCICD\Plugins\bin\Debug\'    Contents: 'CRMCI.*.dll'    targetFolder: '$(Build.StagingDirectory)/plugins'- publish: '$(Build.StagingDirectory)/plugins'  displayName: 'Publish plugins dll as an artifact'  artifact: plugins

Где $(xxx) это обращение к переменным которые я объявил сам или встроенным, подробнее смотреть тут Use predefined variables а описание что и в каких папках лежит можно посмотреть тут Understanding the directory structure created by Azure DevOps tasks.


И будьте внимательны, это YAML и один лишний или недостающий пробел может все поломать.

Сохраняем и запускаем пайп руками, кнопка Run pipeline справа вверху.



В следующем окне ничего не меняя просто жмем Run.



В результате чего попадаем на страницу конкретного экземпляра пайплайна который мы только что запустили и который стоит в очереди на исполнение где-то в недрах Azure DevOps. Он состоит из одной джобы (Job).



Если кликнуть на джобу то попадем на страницу с логами выполнения каждой задачи где увидим их статус и время выполнения.



Если у кого-то не получилось можете взять версию пайпа из репозитория, коммит eb2a1465


Первое, что мне не нравится это версия пайпа, #20210129.1. Давайте сделаем свою, я обычно использую 4 цифры, где первые две это версия CRM а вторые 2 версия моих доработок, первая цифра это номер релиза а вторая номер сборки, например, 9.0.1.0. Данный подход позволяет иметь уникальный номер даже если одно и тоже решение делается для разных версий CRM. Получилось почти семантическое версионирование (подробнее см. https://semver.org/lang/ru/).


Итак, чтобы получить такой номер версии добавляем в пайп в секцию с переменными еще 3.


crmmajor: 9crmminor: 0rel: 1

И, между variables и steps добавляем следующую строчку


name: $(crmmajor).$(crmminor).$(rel).$(BuildID) 

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


Сохраняем и запускаем пайп. Теперь видим нашу версию в заголовке Jobs in run #9.0.1.7. Однако, наша новая версия не сохранилась ни в dll-ке с плагинами ни в решении.


Чтобы это исправить необходим PowerShell скрипт который лежит в папке CRMCI/Solution/BuildScripts/update-build-versions.ps1 это доработанный скрипт с сайта документации Microsoft по Azure DevOps Example: Version your assemblies, который меняет версию на текущую версию пайпа во всех dll и XML-файле решения CRM в проекте Solution.


Добавим его запуск перед сборкой проекта.


- task: PowerShell@2  displayName: 'Update version'  inputs:    filePath: '$(Build.SourcesDirectory)\CRMCI\Solution\BuildScripts\update-build-versions.ps1'    arguments: 'BUILD_BUILDNUMBER $(Build.BuildNumber) BUILD_SOURCESDIRECTORY $(Build.SourcesDirectory)'

Сохраняем и запускаем пайп. Проверяем, что в dll с плагинами, которая сохранилась в артефакты стоит правильная версия.
Теперь добавим сборку солюшена CRM, которая будет состоять из 2 задач, сама сборка и публикация солюшена в хранилище артефактов.


# Сборка решения CRM- task: CmdLine@2  displayName: 'Pack CRM Solution'  inputs:      script: |        dir        @echo off        set package_root=..\..\        REM Find the spkl in the package folder (irrespective of version)        For /R %package_root% %%G IN (spkl.exe) do (            IF EXIST "%%G" (set spkl_path=%%G            goto :continue)            )        :continue        REM spkl instrument [path] [connection-string] [/p:release]        "%spkl_path%" pack "$(Build.SourcesDirectory)\CRMCI\Solution\spkl.json" ""        exit /b %ERRORLEVEL%# Публикация решения CRM- task: CopyFiles@2  displayName: 'Copy CRM Solution'  inputs:      SourceFolder: '$(Build.SourcesDirectory)\CRMCI\Solution\'      Contents: 'CICDDemo_*.zip'      TargetFolder: '$(Build.StagingDirectory)/solutions'- publish: '$(Build.StagingDirectory)/solutions'  displayName: 'Publish solution as an artifact'  artifact: solutions

Сохраняем и запускаем пайп, проверяем, что помимо dll в артефактах еще появилось решение. Версия пайпа на текущий момент в коммите 647dae37.


В моем решении только кастомизации. Если захотите добавить плагины то придется добавить еще одну задачу на копирование dll в папку решения т.к. чтобы плагины добавились в решение необходимо чтобы они были в определенном месте и сам файл был без точек в имени, поэтому пришлось использовать PShell а не CopyFiles, пример ниже.


- task: PowerShell@2  displayName: 'Copy CRMCI.Plugins.dll в папку решения'  inputs:    targetType: 'inline'    script: 'Copy-Item  -Path $(System.ArtifactsDirectory)\plugins\CRMCI.Plugins.dll  -Destination $(Build.SourcesDirectory)\CRMCI\Solution\package\PluginAssemblies\CRMCIPlugins-766F0C7B-4B44-EB11-A812-0022489AC434\CRMCIPlugins.dll -verbose'    errorActionPreference: 'silentlyContinue'

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


Если вы используете TypeScript или webpack или что-то подобное то необходимо добавить еще и сборку скриптов. В случае со spkl их тоже придется подкладывать в рабочую папку. :-(

В итоге мы получили пайп результат работы которого лежит в артефактах в виде dll с плагинами и солюшена CRM которые можно будет использовать для развертывания на среды в рамках CD для чего в AzureDevOps сделаны отдельные пайплайны Release pipelines. О них я расскажу в следующий раз.


Развертывание на локальную среду


Т.к. развертывание на локальную среду подразумевает доступ к локальной CRM то облачный агент нам не подойдет. Нужно настраивать свой. Как это сделать можно почитать тут Self-hosted Windows agents и тут How to create and configure Azure DevOps Pipelines Agent. Перед тем как устанавливать сам агент установите необходимое ПО, минимально это следующее:



В итоге, после установки и настройки агента, мы должны увидеть в настройках что он виден и запущен. У меня он в пуле Default.



Т.к. это отдельный этап и он должен выполняться на другом агенте то разобьем наш пайп на этапы, все что мы уже сделали это будет первый этап а деплой это второй. Для этого мы разобьем наш пайп на этапы (stages), которые в свою очередь состоят из джобов (jobs) которые в свою очередь уже состоят из уже знакомых нам тасков (tasks).


Здесь надо отметить, что для каждой джобы указывается агент (pool), что позволяет выполнять разные джобы на разных агентах специально заточенных под конкретную работу и экономить ресурсы дорогих агентов.

Если в этапе есть несколько джобов то они запустятся одновременно, это где-то полезно а где-то нет. Если нужно последовательное выполнение джобов то либо разнесите их по разным этапам либо проставьте условие dependsOn, подробнее тут Specify conditions.

Также, в пайпе можно перезапускать "упавшие" джобы а не конкретные такси. Учитывайте это при построении пайпа.

Структура следующая:


stages:- stage: Build  displayName: 'Build solution'  jobs:    - job: Build      displayName: 'Build job'      pool: 'Custom'      steps:      - task: NuGetToolInstaller@1        displayName: 'Install NuGet tool'- stage: Deploy  displayName: 'Deploy local'  jobs:    - job: Deploy      displayName: 'Deploy job'      pool: 'Custom'      steps:      - task: NuGetToolInstaller@1        displayName: 'Install NuGet tool'

Однако, есть и минусы, которые надо учитывать, т.к.джоба запускается на новом агенте то рабочее пространство очищается, заново выкачиваются исходники и всего того, что было сделано на предыдущем шаге не будет, т.е. у каждой джобы, если нужен ее результат работы, обязательно необходимо сохранять его в артефакты. Для того чтобы использовать файлы из предыдущих шагов есть шаг загрузки артефактов, который мы как раз используем для получения плагинов.


Наш файл будет выглядеть так, коммит 97f91fec


trigger: nonepool:  vmImage: 'windows-latest'variables:  solution: '**/*.sln'  buildPlatform: 'Any CPU'  buildConfiguration: 'Debug'  crmmajor: 9  crmminor: 0  rel: 1name: $(crmmajor).$(crmminor).$(rel).$(BuildID)stages:- stage: Build  displayName: 'Build solution'  jobs:    - job: Build      displayName: 'Build job'      pool: 'Default'      steps:      # Установка NuGet      - task: NuGetToolInstaller@1        displayName: 'Install NuGet tool'      # Восстановление пакетов NuGet для проекта      - task: NuGetCommand@2        displayName: 'Restore NuGet packages'        inputs:          restoreSolution: '$(solution)'      # Обновление версии      - task: PowerShell@2        displayName: 'Update version'        inputs:          filePath: '$(Build.SourcesDirectory)\CRMCI\Solution\BuildScripts\update-build-versions.ps1'          arguments: 'BUILD_BUILDNUMBER $(Build.BuildNumber) BUILD_SOURCESDIRECTORY $(Build.SourcesDirectory)'      # Сборка      - task: VSBuild@1        displayName: Buld        inputs:          solution: '$(solution)'          platform: '$(buildPlatform)'          configuration: '$(buildConfiguration)'      # Прогон юнит-тестов      - task: VSTest@2        displayName: 'Run Unit Tests'        inputs:          platform: '$(buildPlatform)'          configuration: '$(buildConfiguration)'      # Публикация собранной dll-ки в хранилище артефактов      - task: CopyFiles@2        displayName: 'Publish plugins dll to artifacts'        inputs:          SourceFolder: '$(Build.SourcesDirectory)\CRMCI\Plugins\bin\Debug\'          Contents: 'CRMCI*.dll'          targetFolder: '$(Build.StagingDirectory)/plugins'      - publish: '$(Build.StagingDirectory)/plugins'        displayName: 'Publish plugins dll as an artifact'        artifact: plugins      # Сборка решения CRM      - task: CmdLine@2        displayName: 'Pack CRM Solution'        inputs:            script: |              dir              @echo off              set package_root=..\..\              REM Find the spkl in the package folder (irrespective of version)              For /R %package_root% %%G IN (spkl.exe) do (                  IF EXIST "%%G" (set spkl_path=%%G                  goto :continue)                  )              :continue              REM spkl instrument [path] [connection-string] [/p:release]              "%spkl_path%" pack "$(Build.SourcesDirectory)\CRMCI\Solution\spkl.json" ""              exit /b %ERRORLEVEL%      # Публикация решения CRM      - task: CopyFiles@2        displayName: 'Copy CRM Solution'        inputs:            SourceFolder: '$(Build.SourcesDirectory)\CRMCI\Solution\'            Contents: 'CICDDemo_*.zip'            TargetFolder: '$(Build.StagingDirectory)/solutions'      - publish: '$(Build.StagingDirectory)/solutions'        displayName: 'Publish solution as an artifact'        artifact: solutions- stage: Deploy  displayName: 'Deploy local'  jobs:    - job: Deploy      displayName: 'Deploy job'      pool: 'Default'      steps:      - task: NuGetToolInstaller@1        displayName: 'Install NuGet tool'

На странице пайплайна мы теперь видим два этапа.



Далее разберем таски второго этапа.
Первые две это восстановление NuGet пакетов чтобы у нас был доступен spkl.


Здесь, конечно, хорошо бы оптимизировать и закачивать только spkl т.к. остальное нам не нужно.

  # Установка NuGet  - task: NuGetToolInstaller@1    displayName: 'Install NuGet tool'  # Восстановление пакетов NuGet для проекта  - task: NuGetCommand@2    displayName: 'Restore NuGet packages'    inputs:      restoreSolution: '$(solution)'

Дальше начинаются костыли. :-) Т.к. я не нашел в spkl возможность импорта решения отдельно, там есть только совмещенный шаг сборки решения и его импорта import, который и придется использовать. Поэтому придется еще раз сделать обновление версии.


  # Обновление версии  - task: PowerShell@2    displayName: 'Update version'    inputs:      filePath: '$(Build.SourcesDirectory)\CRMCI\Solution\BuildScripts\update-build-versions.ps1'      arguments: 'BUILD_BUILDNUMBER $(Build.BuildNumber) BUILD_SOURCESDIRECTORY $(Build.SourcesDirectory)'

Теперь собираем и импортируем решение. Для этого нам понадобится строка подключения к CRM, которую мы сохраним в переменные пайпа под именем connstr-local. Кнопку добавления переменных можно найти на странице редактирования пайпа.



  # Импорт решения CRM  - task: CmdLine@2    displayName: 'Deploy solution'    inputs:      script: |        @echo off        set package_root=..\..\        For /R %package_root% %%G IN (spkl.exe) do (          IF EXIST "%%G" (set spkl_path=%%G          goto :continue)          )        :continue        @echo Using '%spkl_path%'         "%spkl_path%" import "$(Build.SourcesDirectory)\CRMCI\Solution\spkl.json" "$(connstr-local)"        if errorlevel 1 (        echo Error Code=%errorlevel%        exit /b %errorlevel%        )

По хорошему, надо скачивать решение собранное на предыдущем этапе и импортировать его, это можно сделать с помощью PowerShell или написать консольку.

Дальше мы скачиваем собранную dll-ку с плагинами и подкладываем туда где она должна оказаться если бы мы собирали решение т.к. я не нашел способа явно параметром указать откуда брать dll при импорте плагинов. Опять костыль. :-)


  # Скачивание плагинов  - task: DownloadPipelineArtifact@2    displayName: 'Download plugins artifacts'    inputs:      buildType: 'specific'      project: 'b44552ad-1d85-4c3b-834b-5109b4c9c2b4'      definition: '1'      buildVersionToDownload: 'latest'      artifactName: 'plugins'      targetPath: '$(System.ArtifactsDirectory)/plugins'  # Копируем dll  - task: CopyFiles@2    displayName: 'Copy plugins dll'    inputs:      SourceFolder: '$(System.ArtifactsDirectory)\plugins'      Contents: '*.dll'      TargetFolder: '$(Build.SourcesDirectory)\CRMCI\Plugins\bin\Debug\'      OverWrite: true

Следующий шаг это импорт плагинов, в том числе и шагов.


  # Импорт плагинов  - task: CmdLine@2    displayName: 'Deploy plugins'    inputs:      script: |        @echo off        set package_root=..\..\        For /R %package_root% %%G IN (spkl.exe) do (          IF EXIST "%%G" (set spkl_path=%%G          goto :continue)          )        :continue        @echo Using '%spkl_path%'         "%spkl_path%" plugins "$(Build.SourcesDirectory)\CRMCI\Plugins\spkl.json" "$(connstr-local)"        if errorlevel 1 (        echo Error Code=%errorlevel%        exit /b %errorlevel%        )

Ну и последним шагом будет импорт веб-ресурсов. Тут spkl просто берет файлы из проекта и публикует их согласно маппингу в spkl.json.


  # Импорт веб-ресурсов  - task: CmdLine@2    displayName: 'Deploy web-resources'    inputs:      script: |        @echo off        set package_root=..\..\        For /R %package_root% %%G IN (spkl.exe) do (          IF EXIST "%%G" (set spkl_path=%%G          goto :continue)          )        :continue        @echo Using '%spkl_path%'         "%spkl_path%" webresources "$(Build.SourcesDirectory)\CRMCI\\WebResources\spkl.json" "$(connstr-local)"        if errorlevel 1 (        echo Error Code=%errorlevel%        exit /b %errorlevel%        )

Вот, собственно, и все. Теперь можно вернуть триггер на срабатывание по коммиту в ветке main.


trigger:- main

Но это не совсем то, что нам нужно т.к. пайп будет вызываться на изменение любого файла, в том числе, самого файла azure-pipelines.yml что не всегда удобно. Поэтому, в секцию trigger можно добавить исключения по ветке, тегу или по маске. Я добавил следующее.


trigger:  branches:    include:    - main  paths:    include:    - '*'    exclude:    - 'azure-pipelines.yml'    - '*.md'

Также если в комментарии к коммиту присутствует одно из следующих словосочетаний то он не запустится:


  • [skip ci] or [ci skip]
  • skip-checks: true or skip-checks:true
  • [skip azurepipelines] or [azurepipelines skip]
  • [skip azpipelines] or [azpipelines skip]
  • [skip azp] or [azp skip]

Подробнее смотрите здесь CI triggers


Первая часть закончена, CI построен и даже немного больше. Тут, есть конечно, куда развиваться, например, добавить статический анализ кода (кому интересно см. лабу Подключение SonarCloud к AzureDevops) или автотесты.


В следующей статье расскажу про релизные пайплайны и CD.

a2-ia.png)
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru