diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 3b1b9825..40197ead 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -14,13 +14,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up JDK 17 uses: actions/setup-java@v5 with: java-version: '17' distribution: 'temurin' - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 - name: Execute Gradle build run: ./gradlew check diff --git a/.github/workflows/rebase.yaml b/.github/workflows/rebase.yaml index bee4a270..2902fe38 100644 --- a/.github/workflows/rebase.yaml +++ b/.github/workflows/rebase.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the latest code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 # otherwise, you will fail to push refs to dest repo diff --git a/README.md b/README.md index 9cf5c50b..d711f5d6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Цель -Создание библиотеки (или плагина) для Jenkins, позволяющей: +Создание библиотеки для Jenkins, позволяющей: * максимально упростить написание Jenkinsfile для процесса CI в условиях платформы 1С:Предприятие 8 * иметь схожий и контролируемый пайплайн для всех проектов @@ -17,12 +17,19 @@ * использовать на свой страх и риск; * любая помощь приветствуется. +## Требования + +* Jenkins 2.524 и выше +* набор плагинов Jenkins, которые можно установить, выполнив [этот скрипт](plugins.groovy) через `Manage Jenkins` -> `Script console` (`$JENKINS_URL/manage/script`) + +> Если используются все возможности библиотеки, то требуется установка всех перечисленных в скрипте плагинов. + ## Ограничения 1. Хранение исходников в корне репозитория или в каталоге первого уровня (например, в `src`) не рекомендуется. 1. Для шага подготовки требуется любой агент с меткой `agent`. 1. Для запуска шага анализа SonarQube требуется агент с меткой `sonar`. -1. Для запуска шагов, работающих с EDT (валидация, трансформация формата исходников) требуется агент с меткой `edt` (если используется несколько версий EDT необходимо к метке добавить версию, например `edt@2021.3.4:x86_64`) и агент с меткой `oscript`, на котором глобально установлена библиотека [stebi](https://github.com/Stepa86/stebi) версии 1.11.1 и выше или [edt-ripper](https://github.com/bia-technologies/edt_ripper). +1. Для запуска шагов, работающих с EDT (валидация, трансформация формата исходников) требуется агент с меткой `edt` (если используется несколько версий EDT необходимо к метке добавить версию, например `edt@2021.3.4`) и агент с меткой `oscript`, на котором глобально установлена библиотека [stebi](https://github.com/Stepa86/stebi) версии 1.11.1 и выше или [edt-ripper](https://github.com/bia-technologies/edt_ripper). 1. При использовании EDT версии 2024.1.0 и выше вместо ring используется 1cedtcli, который должен быть прописан в PATH на агенте. 1. Для запуска шагов, работающих с 1С (подготовка, синтаксический контроль и т.д.) требуется агент с меткой, совпадающей со значением в поле `v8version` файла конфигурации. 1. В качестве ИБ используется файловая база, создаваемая в каталоге `./build/ib`. При необходимости вы можете создать пользователей на фазе инициализации ИБ. @@ -30,11 +37,11 @@ 1. Для параллельного выполнения шагов `bdd` и `smoke` с включенными замерами покрытия на одной ноде необходимо, чтобы в `jobConfiguration.json` были указаны **разные** порты сервера отладки для каждого шага. Параллельные билды с замерами покрытия на одной ноде не поддерживаются. 1. Для сборов замеров покрытия в ОС Windows на мастер-ноде Jenkins, который запущен как служба под учетной записью LOCAL SYSTEM, необходимо использовать версию Coverage41C >= 2.7.3. Другие варианты обхода проблемы: 1. запустить службу Jenkins под обычной учетной записью - 1. запретить выполнение сборок на мастер-ноде и настроить их выполнение на агенте, который запущен интерактивно под обычной учетной записью (рекомендуется). + 1. запретить выполнение сборок на мастер-ноде и настроить их выполнение на агенте, который запущен интерактивно под обычной учетной записью (рекомендуется). ## Возможности -1. Все шаги можно запустить на базе docker-образов из https://github.com/firstBitMarksistskaya/onec-docker. См. [памятку по слоям и последовательности сборки](https://github.com/firstBitMarksistskaya/onec-docker/blob/feature/first-bit/Layers.md). +1. Все шаги можно запустить на базе docker-образов из [onec-docker](https://github.com/firstBitMarksistskaya/onec-docker). См. [памятку по слоям и последовательности сборки](https://github.com/firstBitMarksistskaya/onec-docker/blob/feature/first-bit/Layers.md). 1. Поддержка как формата выгрузки из Конфигуратора, так и формата EDT. 1. Подготовка информационной базы по версии из хранилища конфигурации, из исходных файлов конфигурации, комбинированный режим (основная ветка - из хранилища, остальные - из исходников). 1. Подготовка и загрузка расширений конфигурации из исходных файлов расширения, из cfe-файлов. @@ -52,10 +59,11 @@ 1. Конфигурирование логгера запускаемых oscript-приложений. 1. Замер покрытия при выполнении тестов. 1. Возможность сохранить информационную базу в виде артефакта сборки после выполнения шагов инициализации и\или после выполнения сценарных тестов. +1. Debug Overrides: временная подмена `jobConfiguration.json`, `sonar-project.properties` и файлов из `tools/` через Jenkins Managed Files без изменений в прикладном репозитории. Подробная настройка описана в [docs/feat_debug_replace/how_to_add_debug_settings.md](docs/feat_debug_replace/how_to_add_debug_settings.md). ## Подключение -Инструкция по подключению библиотеки: https://jenkins.io/doc/book/pipeline/shared-libraries/#using-libraries +Инструкция по подключению библиотеки находится [тут](https://jenkins.io/doc/book/pipeline/shared-libraries/#using-libraries). ## Примеры Jenkinsfile @@ -79,7 +87,6 @@ pipeline1C() ![image](https://github.com/ovcharenko-di/jenkins-lib/assets/24920942/19eabbc3-e33e-44f5-8f23-142f44817628) - ## Конфигурирование По умолчанию применяется [файл конфигурации из ресурсов библиотеки](resources/globalConfiguration.json) @@ -88,10 +95,10 @@ pipeline1C() Пример переопределения: -* указывается точная версия платформы (и соответственно метка агента, см. ограничения) -* указывается точная версия модуля EDT (и соответственно метка агента, см. ограничения) +* указывается точная версия платформы (и соответственно метка агента, см. [ограничения](#ограничения)) +* указывается точная версия модуля EDT (и соответственно метка агента, см. [ограничения](#ограничения)) * идентификаторы credentials для пути к хранилищу и к паре логин/пароль для авторизации в хранилище (необходимы, если применяются шаги, работающие с информационной базой) -* включаются шаги запуска статического анализа SonarQube, валидации средствами EDT и синтаксического контроля +* включаются шаги запуска статического анализа SonarQube, валидации средствами EDT и синтаксического контроля ```json { @@ -118,7 +125,7 @@ pipeline1C() * Общее: * В качестве маски версии платформы используется строка "8.3" (`v8version`). - * По умолчанию версия модуля EDT не заполнена, т.к. в случае единственной версии для утилиты ring дополнительного указания не требуется (`edtVersion`). + * По умолчанию версия модуля EDT не заполнена, т.к. в случае единственной версии для утилиты ring дополнительного указания не требуется (`edtVersion`). * Исходники конфигурации ожидаются в каталоге `src/cf` (`srcDir`). * Формат исходников - выгрузка из Конфигуратора (`sourceFormat`). * Ветка по умолчанию (для комбинированного режима загрузки конфигурации) - "main" (`defaultBranch`). @@ -141,17 +148,19 @@ pipeline1C() * После загрузки конфигурации в ИБ будет выполняться запуск ИБ с целью запуска обработчиков обновления из БСП (`initInfobase` -> `runMigration`). * Если в настройках шага инициализации не заполнен массив дополнительных шагов миграции (`initInfobase` -> `additionalInitializationSteps`), но в каталоге `tools` присутствуют файлы с именами, удовлетворяющими шаблону `vrunner.init*.json`, то автоматически выполняется запуск `vrunner vanessa` с передачей найденных файлов в качестве значения настроек (параметр `--settings`) в порядке лексикографической сортировки имен файлов. * BDD: - * Если в конфигурационном файле проекта не заполнена настройка `bdd` -> `vrunnerSteps`, то автоматически выполняется запуск `vrunner vanessa --settings tools/vrunner.json`. + * Если в конфигурационном файле проекта не заполнена настройка `bdd` -> `vrunnerSteps`, то автоматически выполняется запуск `vrunner vanessa --settings `, где `` — значение настройки `bdd` -> `vrunnerSettings` (по умолчанию `./tools/vrunner.json`). * Инструмент, который вызывается на шаге bdd с помощью команды `vrunner vanessa`, должен сохранять код возврата в файл `build/out/bdd-exit-code.log`. Например, в конфигурационном файле для Vanessa Automation нужно установить значение параметров: -``` +```json ... "ВыгружатьСтатусВыполненияСценариевВФайл": true, "ПутьКФайлуДляВыгрузкиСтатусаВыполненияСценариев": "build/out/bdd-exit-code.log", ... ``` + Если файл не будет найден, то пайплайн будет считать, что шаг выполнился успешно. + * YAXUnit: * Если в репозитории существует файл `tools/yaxunit.json`, то он будет передан в качестве параметра для YAXUnit при запуске тестов. Если файла с таким именем нет, то в YAXUnit будет передан файл из текущей библиотеки `resources/yaxunit.json`. Он содержит минимально необходимые параметры и настроен на поиск сценариев в расширении с именем `YAXUnit`. * Дымовые тесты: @@ -181,7 +190,7 @@ pipeline1C() * Информационная база по умолчанию не сохраняется в виде артефакта сборки. * Рассылка уведомлений: * Электронная почта: - * Для отправки используется плагин [`email-ext`](https://plugins.jenkins.io/email-ext). Шаблоны сообщений конфигурируются в настройках плагина. + * Для отправки используется плагин [`email-ext`](https://plugins.jenkins.io/email-ext). Шаблоны сообщений конфигурируются в настройках плагина. * Уведомления о результатах сборки по умолчанию рассылаются только при полном падении сборочной линии (`notifications` -> `email` -> `onAlways`, `onFailure`, `onUnstable`, `onSuccess`). * Лог сборки прикладывается к письму при полном падении сборочной линии и при отправке в режиме "всегда отправлять" (`notifications` -> `email` -> `*options` -> `attachLog`). * В качестве получателей писем (`notifications` -> `email` -> `*options` -> `recipientProviders`) в различных режимах отправки используются: @@ -193,7 +202,11 @@ pipeline1C() * Telegram: * Уведомления о результатах сборки по умолчанию рассылаются всегда (`notifications` -> `telegram` -> `onAlways`, `onFailure`, `onUnstable`, `onSuccess`). -## Настройка загрузки расширений +## Инициализация базы + +Если в настройках шага инициализации заполнен массив дополнительных шагов миграции `initInfobase` -> `additionalInitializationSteps`, то шаги `bdd`, `smoke` и `yaxunit` будут запущены только если дополнительные шаги миграции завершились успешно. + +### Настройка загрузки расширений Если у вас есть расширения, которые необходимо загрузить в базу для проведения тестов и проверок, это можно сделать на этапе подготовки базы. @@ -204,11 +217,15 @@ pipeline1C() 1. Укажите имя расширения(`extensions` -> `name`). 1. Определите метод загрузки для каждого расширения(`extensions` -> `initMethod`). Поддерживаются два метода загрузки: + * `fromSource` - загрузка из исходников; * `fromFile` - загрузка cfe-файла. + 1. Укажите путь до расширения или URL для скачивания cfe-файла(`extensions` -> `path`). + * В случае загрузки из исходников - необходимо указать путь к исходникам расширения -* В случае загрузки cfe - Укажите путь по которому будет скачан cfe. На данный момент можно указывать как локальный путь, так и url для скачивания cfe(Прим.: https://github.com/bia-technologies/yaxunit/releases/download/23.05/YAXUNIT-23.05.cfe) +* В случае загрузки cfe - Укажите путь, по которому будет скачан cfe. На данный момент можно указывать как локальный путь, так и url для скачивания cfe, например `https://github.com/bia-technologies/yaxunit/releases/download/23.05/YAXUNIT-23.05.cfe`) + 1. Укажите этапы сборки, на которых должно быть загружено расширение (`initInfobase` -> `extensions` -> `stages`). Если оставить это поле пустым, то расширение будет загружено на этапе `initInfobase` и будет активно на всех последующих этапах. В противном случае расширение будет использоваться только на перечисленных этапах. Пример конфигурации для загрузки расширений: @@ -230,7 +247,8 @@ pipeline1C() ] } ``` -## Загрузка эталонной базы + +### Загрузка эталонной базы Реализована возможность загрузки эталонной базы на этапе инициализации информационной базы. Для этого необходимо указать в конфигурационном файле параметр `initInfobase` -> `templateDBPath`: @@ -253,9 +271,9 @@ pipeline1C() ## Настройка шага YAXUnit - * Добавить расширение `YAXUnit` и дополнительные расширения с тестами можно в `jobConfiguration.json` -> `initInfobase` -> `extensions`. Они будут загружены при инициализации ИБ. - * Если ваши тесты размещены в отдельных расширениях, скопируйте файл `./resources/yaxunit.json` из текущей библиотеки в свой репозиторий (`./tools/yaxunit.json`) и перечислите в нем имена ваших расширений. - * Если используется собственный файл `tools/yaxunit.json`, то значение параметра `reportPath` в нем должно быть равно `./build/out/yaxunit/junit.xml` +* Добавить расширение `YAXUnit` и дополнительные расширения с тестами можно в `jobConfiguration.json` -> `initInfobase` -> `extensions`. Они будут загружены при инициализации ИБ. +* Если ваши тесты размещены в отдельных расширениях, скопируйте файл `./resources/yaxunit.json` из текущей библиотеки в свой репозиторий (`./tools/yaxunit.json`) и перечислите в нем имена ваших расширений. +* Если используется собственный файл `tools/yaxunit.json`, то значение параметра `reportPath` в нем должно быть равно `./build/out/yaxunit/junit.xml` ## Настройка трансформации результата валидации EDT @@ -278,10 +296,24 @@ pipeline1C() Пример настройки подключения для `bdd`: jobConfiguration.json + +Если нужно использовать нестандартный путь к файлу настроек, достаточно указать только `vrunnerSettings`: + +```json +"bdd": { + "vrunnerSettings": "./tools/my-vrunner.json", + "coverage": true, + "dbgsPort": 1550 + }, +``` + +Если нужно выполнить несколько команд, используйте `vrunnerSteps`: + ```json "bdd": { "vrunnerSteps": [ - "vanessa --settings ./tools/vrunner.json" + "vanessa --settings ./tools/vrunner.json", + "vanessa --settings ./tools/vrunner2.json" ], "coverage": true, "dbgsPort": 1550 @@ -289,6 +321,7 @@ jobConfiguration.json ``` ./tools/vrunner.json + ```json "vanessa": { "--vanessasettings": "tools/VAParams.json", @@ -296,6 +329,7 @@ jobConfiguration.json ``` ./tools/VAParams.json + ```json "КлиентТестирования": { "ЗапускатьТестКлиентВРежимеОтладки": true, @@ -306,7 +340,8 @@ jobConfiguration.json Для `yaxunit`: ./tools/vrunner.json -``` + +```json "default": { "--additional": "/debug -http -attach /debuggerURL http://localhost:1550" } @@ -315,7 +350,8 @@ jobConfiguration.json Для `smoke` (после исправления ошибки в vanessa-add): ./tools/vrunner.json -``` + +```json "xunit": { "--testclient-additional": "/debug -http -attach /debuggerURL http://localhost:1550" } @@ -330,5 +366,6 @@ jobConfiguration.json Можно управлять тем, при каких статусах сборки ИБ будет сохранена, см. `onAlways`, `onFailure`, `onUnstable`, `onSuccess`. Имя файла формируется следующим образом: -* для шага `initInfoBase`: '1Cv8.1CD.zip' + +* для шага `initInfoBase`: '1Cv8.1CD.zip' * для шага `bdd`: '1Cv8.1CD.bdd.zip' diff --git a/build.gradle.kts b/build.gradle.kts index 9d8d96ef..ac96d6e9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,6 +11,9 @@ plugins { repositories { mavenCentral() + maven { + url = uri("https://repo.jenkins-ci.org/public/") + } } tasks { @@ -120,7 +123,6 @@ sharedLibrary { dependency("org.jenkins-ci.modules", "sshd", "3.374.v19b_d59ce6610") dependency("org.6wind.jenkins", "lockable-resources", "1412.v3f305a_fb_a_117") - dependency("ru.yandex.qatools.allure", "allure-jenkins-plugin", "2.32.0") dependency("io.jenkins.blueocean", "blueocean-pipeline-api-impl", "1.27.21") dependency("sp.sd", "file-operations", "353.vf3b_9b_a_f1f7f7") @@ -129,6 +131,5 @@ sharedLibrary { dependency("org.jenkinsci.plugins", "pipeline-model-api", declarativePluginsVersion) dependency("org.jenkinsci.plugins", "pipeline-model-definition", declarativePluginsVersion) dependency("org.jenkinsci.plugins", "pipeline-model-extensions", declarativePluginsVersion) - dependency("org.jenkinsci.plugins", "pipeline-model-declarative-agent", "1.1.1") } } diff --git a/docs/feat_debug_replace/how_to_add_debug_settings.md b/docs/feat_debug_replace/how_to_add_debug_settings.md new file mode 100644 index 00000000..2d9b97b7 --- /dev/null +++ b/docs/feat_debug_replace/how_to_add_debug_settings.md @@ -0,0 +1,259 @@ +# How To Add Debug Settings + +## Что это дает +Этот механизм позволяет подложить свои файлы настроек в Jenkins workspace для MR-пайплайна, не меняя прикладной репозиторий. + +Поддерживаемые файлы: +- `jobConfiguration.json` +- `sonar-project.properties` +- `tools/vrunner.json` +- `tools/VAParams.json` + +## Что нужно подготовить в Jenkins + +### 1. Проверить plugin +Убедиться, что в Jenkins установлен plugin `Config File Provider`. + +Если plugin не установлен: +- механизм подмен не сработает; +- пайплайн продолжит работу по старому сценарию. + +### 2. Создать control JSON +В Jenkins нужно создать managed file с `fileId`: + +```text +jenkins-debug-overrides-control +``` + +Рекомендуемый тип файла при создании: + +```text +Json file +``` + +В этом файле хранится карта профилей и список подмен. + +Допустимо также использовать `Custom file`, но для JSON удобнее `Json file`, потому что в Jenkins он лучше читается визуально. + +Пример содержимого: + +```json +{ + "profiles": { + "ci_uh_MR": { + "enabled": true, + "description": "Отладочные подмены для MR пайплайнов УХ", + "replacements": [ + { + "fileId": "debug-ci-uh-mr-jobConfiguration", + "target": "jobConfiguration.json" + }, + { + "fileId": "debug-ci-uh-mr-sonar-properties", + "target": "sonar-project.properties" + }, + { + "fileId": "debug-ci-uh-mr-vrunner", + "target": "tools/vrunner.json" + }, + { + "fileId": "debug-ci-uh-mr-vaparams", + "target": "tools/VAParams.json" + } + ] + } + } +} +``` + +## Как добавить свою настройку + +### Вариант 1. Изменить существующий профиль +Если вы работаете в уже существующем Jenkins folder, например `ci_uh_MR`, то: + +1. Откройте managed file `jenkins-debug-overrides-control`. +2. Найдите профиль: + +```json +"ci_uh_MR": { + ... +} +``` + +3. Убедитесь, что: + +```json +"enabled": true +``` + +4. Обновите список `replacements`, если нужно добавить или убрать файлы. + +### Вариант 2. Создать новый профиль под другой folder +Если у вас другой Jenkins folder, добавьте новый ключ в `profiles`. + +Пример: + +```json +{ + "profiles": { + "ci_uh_MR": { + "enabled": true, + "replacements": [ + { + "fileId": "debug-ci-uh-mr-jobConfiguration", + "target": "jobConfiguration.json" + } + ] + }, + "ci_erp_MR": { + "enabled": true, + "replacements": [ + { + "fileId": "debug-ci-erp-mr-jobConfiguration", + "target": "jobConfiguration.json" + } + ] + } + } +} +``` + +Ключ профиля должен совпадать с предпоследним сегментом `JOB_NAME`. + +Пример: +- `CPC/ci_uh_MR/MR-1101` -> профиль `ci_uh_MR` +- `CPC/ci_erp_MR/MR-42` -> профиль `ci_erp_MR` + +## Как создать свои файлы подмены +Для каждого файла из `replacements` нужно создать отдельный managed file в Jenkins. + +Пример набора для `ci_uh_MR`: +- `debug-ci-uh-mr-jobConfiguration` +- `debug-ci-uh-mr-sonar-properties` +- `debug-ci-uh-mr-vrunner` +- `debug-ci-uh-mr-vaparams` + +Содержимое: +- `debug-ci-uh-mr-jobConfiguration` -> ваш `jobConfiguration.json` +- `debug-ci-uh-mr-sonar-properties` -> ваш `sonar-project.properties` +- `debug-ci-uh-mr-vrunner` -> ваш `tools/vrunner.json` +- `debug-ci-uh-mr-vaparams` -> ваш `tools/VAParams.json` + +При создании каждого managed file: +- `ID` должен точно совпадать со значением `fileId` из control JSON; +- `Name` можно оставить таким же, как `ID`; +- `Content` должно содержать полный текст соответствующего файла; +- тип файла лучше выбирать по содержимому: + - для `debug-ci-uh-mr-jobConfiguration` -> `Json file` + - для `debug-ci-uh-mr-sonar-properties` -> `Properties file` + - для `debug-ci-uh-mr-vrunner` -> `Json file` + - для `debug-ci-uh-mr-vaparams` -> `Json file` +- при желании все эти файлы можно хранить и как `Custom file`, механизм библиотеки от этого не меняется. + +## Как временно отключить свою настройку +Самый простой способ: + +```json +"enabled": false +``` + +Тогда профиль останется в control JSON, но подмены применяться не будут. + +## Что происходит во время сборки +1. В `pre-stage` библиотека определяет профиль по `JOB_NAME`. +2. Загружает `jenkins-debug-overrides-control`. +3. Если профиль найден и включен, раскладывает файлы в workspace. +4. `jobConfiguration.json` используется сразу. +5. Остальные файлы сохраняются в `stash`. +6. На нужных downstream agents файлы восстанавливаются через `unstash`. + +## Как проверить, что подмена сработала +В логах сборки должны появиться сообщения вида: + +```text +Debug overrides: resolved profile key = ci_uh_MR +Debug overrides: applying 4 replacement(s) +Debug overrides: wrote jobConfiguration.json from managed file debug-ci-uh-mr-jobConfiguration +Debug overrides: stashed files for downstream agents +Debug overrides: restored files from stash +``` + +Если профиль не применился, в логах будет одно из сообщений: + +```text +Debug overrides: profile not found, skip +Debug overrides: profile is disabled, skip +Debug overrides: control file is unavailable, skip +Debug overrides: Config File Provider plugin is unavailable, skip +``` + +### Что искать в логах Jenkins +Если профиль найден и подмена реально произошла, ищите такие строки: + +```text +Debug overrides: resolved profile key = ci_uh_MR +Debug overrides: applying 4 replacement(s) +Debug overrides: wrote jobConfiguration.json from managed file debug-ci-uh-mr-jobConfiguration +Debug overrides: wrote sonar-project.properties from managed file debug-ci-uh-mr-sonar-properties +Debug overrides: wrote tools/vrunner.json from managed file debug-ci-uh-mr-vrunner +Debug overrides: wrote tools/VAParams.json from managed file debug-ci-uh-mr-vaparams +Debug overrides: stashed files for downstream agents +``` + +Если потом на другом Jenkins agent файлы были успешно восстановлены, будет строка: + +```text +Debug overrides: restored files from stash +``` + +Если подмена не была применена, в логах будет один из вариантов: + +```text +Debug overrides: profile ci_uh_MR not found, skip +Debug overrides: profile ci_uh_MR is disabled, skip +Debug overrides: control file is unavailable, skip +Debug overrides: Config File Provider plugin is unavailable, skip +Debug overrides: downstream stash is absent, skip restore +``` + +Содержимое самих файлов в лог не выводится. В логе виден только: +- найденный профиль; +- факт подмены; +- `fileId`, из которого был взят файл; +- факт восстановления файлов на downstream stages. + +## На что обратить внимание +- `fileId` в control JSON должен точно совпадать с `fileId` managed file в Jenkins. +- `target` должен быть относительным путем внутри workspace. +- Абсолютные пути и `..` в `target` не поддерживаются. +- Если в профиле нет downstream-файлов, stash не создается. +- Повторный запуск build просто перезапишет подложенные файлы, это штатно. + +## Минимальный пример для быстрого старта +Если хотите проверить только подмену `jobConfiguration.json`, достаточно: + +1. Создать managed file: + +```text +debug-ci-uh-mr-jobConfiguration +``` + +2. Указать его в control JSON: + +```json +{ + "profiles": { + "ci_uh_MR": { + "enabled": true, + "replacements": [ + { + "fileId": "debug-ci-uh-mr-jobConfiguration", + "target": "jobConfiguration.json" + } + ] + } + } +} +``` + +После этого пайплайн начнет читать ваш `jobConfiguration.json` из Jenkins, а не из репозитория. diff --git a/plugins.groovy b/plugins.groovy new file mode 100644 index 00000000..c4b498ba --- /dev/null +++ b/plugins.groovy @@ -0,0 +1,72 @@ +import jenkins.model.* + +def plugins = [ + "allure-jenkins-plugin", + "blueocean", + "blueocean-pipeline-api-impl", + "bouncycastle-api", + "cloudbees-folder", + "command-launcher", + "copyartifact", + "credentials", + "docker-commons", + "docker-java-api", + "docker-workflow", + "durable-task", + "email-ext", + "file-operations", + "git", + "git-client", + "http_request", + "jackson2-api", + "jdk-tool", + "junit", + "kubernetes", + "lockable-resources", + "matrix-project", + "nodelabelparameter", + "pipeline-build-step", + "pipeline-model-api", + "pipeline-model-definition", + "pipeline-model-extensions", + "pipeline-stage-view", + "pipeline-utility-steps", + "scm-api", + "script-security", + "sonar", + "structs", + "swarm-agents-cloud", + "timestamper", + "token-macro", + "workflow-aggregator", + "workflow-api", + "workflow-durable-task-step", + "workflow-cps", + "workflow-job", + "workflow-multibranch", + "workflow-step-api", + "workflow-support" + ] + +def instance = Jenkins.getInstance() +def pm = instance.getPluginManager() +def uc = instance.getUpdateCenter() + +uc.updateAllSites() // Обновить список плагинов + +plugins.each { pluginName -> + if (!pm.getPlugin(pluginName)) { + def plugin = uc.getPlugin(pluginName) + if (plugin) { + def installFuture = plugin.deploy() + while(!installFuture.isDone()) { + sleep(3000) + } + println "Установлен: ${pluginName}" + } else { + println "Плагин не найден: ${pluginName}" + } + } else { + println "Уже установлен: ${pluginName}" + } +} \ No newline at end of file diff --git a/resources/dbgs.os b/resources/dbgs.os index 77f0318a..609ca6b7 100644 --- a/resources/dbgs.os +++ b/resources/dbgs.os @@ -1,3 +1,7 @@ #Использовать v8find -Сообщить(Платформа1С.ПутьКDBGS(АргументыКоманднойСтроки[0])); +ПутьКDBGS = Платформа1С.ПутьКDBGS(АргументыКоманднойСтроки[0]); + +ТекстовыйДокумент = Новый ТекстовыйДокумент(); +ТекстовыйДокумент.УстановитьТекст(ПутьКDBGS); +ТекстовыйДокумент.Записать(АргументыКоманднойСтроки[1], КодировкаТекста.UTF8NoBom); diff --git a/resources/globalConfiguration.json b/resources/globalConfiguration.json index 5c5fdc7d..6bde3f8f 100644 --- a/resources/globalConfiguration.json +++ b/resources/globalConfiguration.json @@ -54,9 +54,7 @@ "extensions": [] }, "bdd": { - "vrunnerSteps": [ - "vanessa --settings ./tools/vrunner.json" - ], + "vrunnerSettings": "./tools/vrunner.json", "archiveInfobase": { "onAlways": false, "onFailure": false, diff --git a/resources/schema.json b/resources/schema.json index ab935556..8de73d75 100644 --- a/resources/schema.json +++ b/resources/schema.json @@ -72,6 +72,10 @@ "items" : { "type" : "string" } + }, + "vrunnerSettings" : { + "type" : "string", + "description" : "Путь к конфигурационному файлу vanessa-runner.\n По умолчанию содержит значение \"./tools/vrunner.json\".\n " } }, "description" : "Настройки шага запуска BDD сценариев" diff --git a/src/ru/pulsar/jenkins/library/IStepExecutor.groovy b/src/ru/pulsar/jenkins/library/IStepExecutor.groovy index 9fdd8a76..feec423d 100644 --- a/src/ru/pulsar/jenkins/library/IStepExecutor.groovy +++ b/src/ru/pulsar/jenkins/library/IStepExecutor.groovy @@ -67,6 +67,8 @@ interface IStepExecutor { void createDir(String path) + void createDir(String path, boolean deleteDir) + void deleteDir() void deleteDir(String path) diff --git a/src/ru/pulsar/jenkins/library/StepExecutor.groovy b/src/ru/pulsar/jenkins/library/StepExecutor.groovy index 50372f85..63273e98 100644 --- a/src/ru/pulsar/jenkins/library/StepExecutor.groovy +++ b/src/ru/pulsar/jenkins/library/StepExecutor.groovy @@ -10,7 +10,6 @@ import org.jenkinsci.plugins.workflow.support.steps.build.RunWrapper import ru.pulsar.jenkins.library.configuration.JobConfiguration import ru.pulsar.jenkins.library.configuration.StepCoverageOptions import ru.pulsar.jenkins.library.steps.Coverable -import ru.yandex.qatools.allure.jenkins.config.ResultsConfig import sp.sd.fileoperations.FileOperation class StepExecutor implements IStepExecutor { @@ -137,6 +136,11 @@ class StepExecutor implements IStepExecutor { steps.createDir(path) } + @Override + void createDir(String path, boolean deleteDir) { + steps.createDir(path, deleteDir) + } + @Override def dir(String path, Closure body) { steps.dir(path) { @@ -252,7 +256,7 @@ class StepExecutor implements IStepExecutor { jdk: '', properties: [], reportBuildPolicy: 'ALWAYS', - results: ResultsConfig.convertPaths(results) + results: results.collect { [path: it] } ]) } diff --git a/src/ru/pulsar/jenkins/library/configuration/BddOptions.groovy b/src/ru/pulsar/jenkins/library/configuration/BddOptions.groovy index 0f3687f2..8fdbac2f 100644 --- a/src/ru/pulsar/jenkins/library/configuration/BddOptions.groovy +++ b/src/ru/pulsar/jenkins/library/configuration/BddOptions.groovy @@ -7,14 +7,35 @@ import com.fasterxml.jackson.annotation.JsonPropertyDescription @JsonIgnoreProperties(ignoreUnknown = true) class BddOptions extends StepCoverageOptions implements Serializable { + @JsonPropertyDescription("""Путь к конфигурационному файлу vanessa-runner. + По умолчанию содержит значение "./tools/vrunner.json". + """) + String vrunnerSettings = "./tools/vrunner.json" + @JsonPropertyDescription("""Шаги, запускаемые через vrunner. В каждой строке передается отдельная команда vrunner и ее аргументы (например, "vanessa --settings ./tools/vrunner.json"). - По умолчанию содержит одну команду "vanessa --settings ./tools/vrunner.json". + По умолчанию содержит одну команду "vanessa --settings ". """) - String[] vrunnerSteps = [ - 'vanessa --settings ./tools/vrunner.json' - ] + String[] vrunnerSteps + + /** + * Возвращает эффективное значение vrunnerSteps. + * Если vrunnerSteps не задан явно, возвращает значение по умолчанию + * на основе vrunnerSettings. + */ + @NonCPS + String[] getEffectiveVrunnerSteps() { + if (vrunnerSteps == null) { + String step = "vanessa --settings " + vrunnerSettings + return [step] as String[] + } + return vrunnerSteps + } + + boolean hasCustomVrunnerSteps() { + return vrunnerSteps != null + } @JsonPropertyDescription("""Настройки сохранения базы после выполнения всех шагов """) @@ -24,10 +45,11 @@ class BddOptions extends StepCoverageOptions implements Serializable { @NonCPS String toString() { return "BddOptions{" + - "vrunnerSteps=" + vrunnerSteps + - "archiveInfobase=" + archiveInfobase + - "coverage=" + coverage + - "dbgsPort=" + dbgsPort + + "vrunnerSettings='" + vrunnerSettings + '\'' + + ", effectiveVrunnerSteps=" + effectiveVrunnerSteps + + ", archiveInfobase=" + archiveInfobase + + ", coverage=" + coverage + + ", dbgsPort=" + dbgsPort + '}' } } diff --git a/src/ru/pulsar/jenkins/library/configuration/YaxunitOptions.groovy b/src/ru/pulsar/jenkins/library/configuration/YaxunitOptions.groovy index eae18a79..369c0fb6 100644 --- a/src/ru/pulsar/jenkins/library/configuration/YaxunitOptions.groovy +++ b/src/ru/pulsar/jenkins/library/configuration/YaxunitOptions.groovy @@ -25,7 +25,7 @@ class YaxunitOptions extends StepCoverageOptions implements Serializable { @JsonPropertyDescription("""Выполнять публикацию результатов в отчет JUnit. По умолчанию включено. """) - boolean publishToJUnitReport + boolean publishToJUnitReport = true @Override @NonCPS diff --git a/src/ru/pulsar/jenkins/library/ioc/ContextRegistry.groovy b/src/ru/pulsar/jenkins/library/ioc/ContextRegistry.groovy index c1dcfd6d..d05319b1 100644 --- a/src/ru/pulsar/jenkins/library/ioc/ContextRegistry.groovy +++ b/src/ru/pulsar/jenkins/library/ioc/ContextRegistry.groovy @@ -1,5 +1,7 @@ package ru.pulsar.jenkins.library.ioc +import ru.pulsar.jenkins.library.configuration.JobConfiguration + class ContextRegistry implements Serializable { private static IContext context @@ -11,7 +13,16 @@ class ContextRegistry implements Serializable { context = new DefaultContext(steps) } + static JobConfiguration registerJobConfiguration(JobConfiguration config) { + context.registerJobConfiguration(config) + return config + } + static IContext getContext() { return context } + + static JobConfiguration getJobConfiguration() { + return context.getJobConfiguration() + } } diff --git a/src/ru/pulsar/jenkins/library/ioc/DefaultContext.groovy b/src/ru/pulsar/jenkins/library/ioc/DefaultContext.groovy index deeca481..eba02433 100644 --- a/src/ru/pulsar/jenkins/library/ioc/DefaultContext.groovy +++ b/src/ru/pulsar/jenkins/library/ioc/DefaultContext.groovy @@ -2,9 +2,11 @@ package ru.pulsar.jenkins.library.ioc import ru.pulsar.jenkins.library.IStepExecutor import ru.pulsar.jenkins.library.StepExecutor +import ru.pulsar.jenkins.library.configuration.JobConfiguration class DefaultContext implements IContext, Serializable { private steps + private JobConfiguration jobConfiguration DefaultContext(steps) { this.steps = steps @@ -14,4 +16,14 @@ class DefaultContext implements IContext, Serializable { IStepExecutor getStepExecutor() { return new StepExecutor(this.steps) } + + @Override + JobConfiguration getJobConfiguration() { + return jobConfiguration + } + + @Override + void registerJobConfiguration(JobConfiguration config) { + jobConfiguration = config + } } diff --git a/src/ru/pulsar/jenkins/library/ioc/IContext.groovy b/src/ru/pulsar/jenkins/library/ioc/IContext.groovy index 4510f63f..360810b7 100644 --- a/src/ru/pulsar/jenkins/library/ioc/IContext.groovy +++ b/src/ru/pulsar/jenkins/library/ioc/IContext.groovy @@ -1,7 +1,10 @@ package ru.pulsar.jenkins.library.ioc import ru.pulsar.jenkins.library.IStepExecutor +import ru.pulsar.jenkins.library.configuration.JobConfiguration interface IContext { IStepExecutor getStepExecutor() -} \ No newline at end of file + JobConfiguration getJobConfiguration() + void registerJobConfiguration(JobConfiguration config) +} diff --git a/src/ru/pulsar/jenkins/library/steps/Bdd.groovy b/src/ru/pulsar/jenkins/library/steps/Bdd.groovy index 335859f9..de3dab93 100644 --- a/src/ru/pulsar/jenkins/library/steps/Bdd.groovy +++ b/src/ru/pulsar/jenkins/library/steps/Bdd.groovy @@ -39,7 +39,7 @@ class Bdd implements Serializable, Coverable { steps.withCoverage(config, this, options) { - config.bddOptions.vrunnerSteps.each { + config.bddOptions.effectiveVrunnerSteps.each { Logger.println("Шаг запуска сценариев командой ${it}") String vrunnerPath = VRunner.getVRunnerPath() diff --git a/src/ru/pulsar/jenkins/library/steps/EdtValidate.groovy b/src/ru/pulsar/jenkins/library/steps/EdtValidate.groovy index bb4f7886..1f338768 100644 --- a/src/ru/pulsar/jenkins/library/steps/EdtValidate.groovy +++ b/src/ru/pulsar/jenkins/library/steps/EdtValidate.groovy @@ -11,7 +11,7 @@ import ru.pulsar.jenkins.library.utils.Logger class EdtValidate implements Serializable { public static final String RESULT_STASH = 'edt-validate' - public static final String RESULT_FILE = 'build/out/edt-validate.out' + public static final String RESULT_FILE = 'build/out/edt-validate/edt-validate.out' private final JobConfiguration config @@ -55,8 +55,17 @@ class EdtValidate implements Serializable { engine.edtValidate(steps, config, projectList) - steps.archiveArtifacts("$DesignerToEdtFormatTransformation.WORKSPACE/.metadata/.log") - steps.archiveArtifacts(RESULT_FILE) steps.stash(RESULT_STASH, RESULT_FILE) + + // Архивируем все результаты в отдельном архиве и отправляем в артефакты. + def resultDir = FileUtils.getFilePath("$RESULT_FILE").getParent() + + String resultLogFrom = FileUtils.getFilePath("$env.WORKSPACE/$DesignerToEdtFormatTransformation.WORKSPACE/.metadata/.log") + String resultLogTo = FileUtils.getFilePath("$env.WORKSPACE/$resultDir/.log") + FileUtils.loadFile(resultLogFrom, env, resultLogTo) // копируем лог в папку, которая будет архивироваться + + String archivePath = "edt-validate.zip" + Boolean archiveArtifacts = true + steps.zip("$resultDir", archivePath, '', archiveArtifacts) } } diff --git a/src/ru/pulsar/jenkins/library/steps/InitInfoBase.groovy b/src/ru/pulsar/jenkins/library/steps/InitInfoBase.groovy index d3cedc6b..c2a4b29c 100644 --- a/src/ru/pulsar/jenkins/library/steps/InitInfoBase.groovy +++ b/src/ru/pulsar/jenkins/library/steps/InitInfoBase.groovy @@ -6,6 +6,7 @@ import ru.pulsar.jenkins.library.configuration.JobConfiguration import ru.pulsar.jenkins.library.ioc.ContextRegistry import ru.pulsar.jenkins.library.utils.Logger import ru.pulsar.jenkins.library.utils.VRunner +import ru.pulsar.jenkins.library.utils.FileUtils class InitInfoBase implements Serializable { @@ -27,6 +28,17 @@ class InitInfoBase implements Serializable { return } + def env = steps.env(); + + String workspaceAllure = FileUtils.getFilePath("$env.WORKSPACE/build/out/allure").getRemote() + Logger.println("Очистка каталога Allure: $workspaceAllure") + steps.deleteDir(workspaceAllure) + String workspaceCucumber = FileUtils.getFilePath("$env.WORKSPACE/build/out/cucumber").getRemote() + Logger.println("Очистка каталога Cucumber: $workspaceCucumber") + steps.deleteDir(workspaceCucumber) + + def isInfobaseInitialized = true + List logosConfig = ["LOGOS_CONFIG=$config.logosConfig"] steps.withEnv(logosConfig) { @@ -40,6 +52,8 @@ class InitInfoBase implements Serializable { settingsIncrement = " --settings $vrunnerSettings" } + Map exitStatuses = new LinkedHashMap<>() + if (options.runMigration) { Logger.println("Запуск миграции ИБ") @@ -52,32 +66,58 @@ class InitInfoBase implements Serializable { command += ' --ibconnection "/F./build/ib"' command += settingsIncrement + def migrationStatusFile = "build/migration-exit-status.log" + command += " --exitCodePath \"${migrationStatusFile}\"" // Запуск миграции steps.catchError { - VRunner.exec(command) + Integer exitStatus = VRunner.exec(command, true) + exitStatuses.put(command, VRunner.readExitStatusFromFile(migrationStatusFile, exitStatus)) } } else { Logger.println("Шаг миграции ИБ выключен") } - steps.catchError { - if (options.additionalInitializationSteps.length == 0) { - FileWrapper[] files = steps.findFiles("tools/vrunner.init*.json") - files = files.sort new OrderBy( { it.name }) - files.each { - Logger.println("Первичная инициализация файлом ${it.path}") - VRunner.exec("$vrunnerPath vanessa --settings ${it.path} --ibconnection \"/F./build/ib\"") - } - } else { - options.additionalInitializationSteps.each { - Logger.println("Первичная инициализация командой ${it}") - VRunner.exec("$vrunnerPath ${it} --ibconnection \"/F./build/ib\"${settingsIncrement}") - } + if (options.additionalInitializationSteps.length == 0) { + FileWrapper[] files = steps.findFiles("tools/vrunner.init*.json") + files = files.sort new OrderBy({ it.name }) + files.each { + Logger.println("Первичная инициализация файлом ${it.path}") + def command = "$vrunnerPath vanessa --settings ${it.path} --ibconnection \"/F./build/ib\"" + Integer exitStatus = VRunner.exec(command, true) + exitStatuses.put(command, exitStatus) + } + } else { + options.additionalInitializationSteps.each { + Logger.println("Первичная инициализация командой ${it}") + def command = "$vrunnerPath ${it} --ibconnection \"/F./build/ib\"${settingsIncrement}" + Integer exitStatus = VRunner.exec(command, true) + exitStatuses.put(command, exitStatus) } } + + if (Collections.max(exitStatuses.values()) >= 2) { + Logger.println("Получен неожиданный/неверный результат работы шагов инициализации ИБ. Возможно, имеется ошибка в параметрах запуска vanessa-runner") + isInfobaseInitialized = false + } else if (exitStatuses.values().contains(1)) { + Logger.println("Инициализация ИБ завершилась, но некоторые ее шаги выполнились некорректно") + isInfobaseInitialized = false + } else { + Logger.println("Инициализация ИБ завершилась успешно") + } + + def exitStatusesMessage = "Статусы команд инициализации ИБ:" + exitStatuses.each { key, value -> + exitStatusesMessage += "\n${key}: status ${value}" + } + Logger.println(exitStatusesMessage) } steps.stash('init-allure', 'build/out/allure/**', true) steps.stash('init-cucumber', 'build/out/cucumber/**', true) + + if (!isInfobaseInitialized) { + // Throws exception + steps.error("Инициализация ИБ завершилась с ошибками") + } } } diff --git a/src/ru/pulsar/jenkins/library/steps/ResultsTransformer.groovy b/src/ru/pulsar/jenkins/library/steps/ResultsTransformer.groovy index 0eabf616..383d3fd8 100644 --- a/src/ru/pulsar/jenkins/library/steps/ResultsTransformer.groovy +++ b/src/ru/pulsar/jenkins/library/steps/ResultsTransformer.groovy @@ -13,7 +13,7 @@ import java.nio.file.Paths class ResultsTransformer implements Serializable { public static final String RESULT_STASH = 'edt-issues' - public static final String RESULT_FILE = 'build/out/edt-issues.json' + public static final String RESULT_FILE = 'build/out/edt-validate/edt-issues.json' private final JobConfiguration config @@ -73,8 +73,13 @@ class ResultsTransformer implements Serializable { } - steps.archiveArtifacts(RESULT_FILE) steps.stash(RESULT_STASH, RESULT_FILE) + // Архивируем результат в отдельный архив и отправляем в артефакты. + def resultDir = FileUtils.getFilePath("$edtValidateFile").getParent() + String archivePath = "edt-validate-ResultsTransformer.zip" + Boolean archiveArtifacts = true + steps.zip("$resultDir", archivePath, '', archiveArtifacts) + } } diff --git a/src/ru/pulsar/jenkins/library/steps/SmokeTest.groovy b/src/ru/pulsar/jenkins/library/steps/SmokeTest.groovy index d60d4b38..77f25513 100644 --- a/src/ru/pulsar/jenkins/library/steps/SmokeTest.groovy +++ b/src/ru/pulsar/jenkins/library/steps/SmokeTest.groovy @@ -68,7 +68,8 @@ class SmokeTest implements Serializable, Coverable { StringJoiner reportsConfigConstructor = new StringJoiner(";") if (options.publishToJUnitReport) { - steps.createDir(junitReportDir) + boolean deleteDir = true + steps.createDir(junitReportDir, deleteDir) String junitReportCommand = "ГенераторОтчетаJUnitXML{$junitReport}" @@ -76,7 +77,8 @@ class SmokeTest implements Serializable, Coverable { } if (options.publishToAllureReport) { - steps.createDir(allureReportDir) + boolean deleteDir = true + steps.createDir(allureReportDir, deleteDir) String allureReportCommand = "ГенераторОтчетаAllureXMLВерсия2{$allureReport}" diff --git a/src/ru/pulsar/jenkins/library/steps/SyntaxCheck.groovy b/src/ru/pulsar/jenkins/library/steps/SyntaxCheck.groovy index 8a0aa5d7..d7e125fc 100644 --- a/src/ru/pulsar/jenkins/library/steps/SyntaxCheck.groovy +++ b/src/ru/pulsar/jenkins/library/steps/SyntaxCheck.groovy @@ -56,12 +56,14 @@ class SyntaxCheck { } if (options.publishToJUnitReport) { - steps.createDir(junitReportDir) + boolean deleteDir = true + steps.createDir(junitReportDir, deleteDir) command += " --junitpath $pathToJUnitReport" } if (options.publishToAllureReport) { - steps.createDir(allureReportDir) + boolean deleteDir = true + steps.createDir(allureReportDir, deleteDir) command += " --allure-results2 $allureReportDir" } diff --git a/src/ru/pulsar/jenkins/library/steps/WithCoverage.groovy b/src/ru/pulsar/jenkins/library/steps/WithCoverage.groovy index 5180c9fb..53075aa6 100644 --- a/src/ru/pulsar/jenkins/library/steps/WithCoverage.groovy +++ b/src/ru/pulsar/jenkins/library/steps/WithCoverage.groovy @@ -172,7 +172,8 @@ class WithCoverage implements Serializable { def dbgsFindScript = steps.libraryResource("dbgs.os") steps.writeFile(dbgsFindScriptPath, dbgsFindScript, 'UTF-8') - steps.cmd("oscript ${dbgsFindScriptPath} ${config.v8version} > ${dbgsPathResult}") + steps.cmd("oscript ${dbgsFindScriptPath} ${config.v8version} ${dbgsPathResult}") + dbgsPath = steps.readFile(dbgsPathResult).strip() if (dbgsPath.isEmpty()) { diff --git a/src/ru/pulsar/jenkins/library/steps/Yaxunit.groovy b/src/ru/pulsar/jenkins/library/steps/Yaxunit.groovy index 43bb739f..be44e083 100644 --- a/src/ru/pulsar/jenkins/library/steps/Yaxunit.groovy +++ b/src/ru/pulsar/jenkins/library/steps/Yaxunit.groovy @@ -66,6 +66,10 @@ class Yaxunit implements Serializable, Coverable { } steps.withEnv(logosConfig) { + + String workspaceYaxunit = FileUtils.getFilePath("$env.WORKSPACE/build/out/allure").getRemote() + Logger.println("Очистка каталога результатов allure: $workspaceYaxunit") + steps.deleteDir(workspaceYaxunit) steps.withCoverage(config, this, options) { VRunner.exec(runTestsCommand, true) diff --git a/src/ru/pulsar/jenkins/library/utils/DebugOverrides.groovy b/src/ru/pulsar/jenkins/library/utils/DebugOverrides.groovy new file mode 100644 index 00000000..eb7abc35 --- /dev/null +++ b/src/ru/pulsar/jenkins/library/utils/DebugOverrides.groovy @@ -0,0 +1,198 @@ +package ru.pulsar.jenkins.library.utils + +class DebugOverrides { + + static final String CONTROL_FILE_ID = 'jenkins-debug-overrides-control' + static final String CONTROL_FILE_VARIABLE = 'DEBUG_OVERRIDES_CONTROL_FILE' + static final String STASH_NAME = 'debug-overrides-files' + + static final List DOWNSTREAM_TARGETS = [ + 'sonar-project.properties', + 'tools/vrunner.json', + 'tools/VAParams.json' + ].asImmutable() + + // buildStashIncludes joins entries with commas, so downstream targets must + // remain plain relative paths without commas or Ant wildcard syntax. + static final String STASH_SAFE_TARGET_PATTERN = /^[^,*?{}]+$/ + + static String resolveProfileKey(String jobName) { + if (jobName == null) { + return null + } + + List segments = jobName + .split('/') + .collect { it?.trim() } + .findAll { it } + + if (segments.size() < 2) { + return null + } + + return segments[-2] + } + + static String normalizeTarget(String target) { + if (target == null) { + return null + } + + String normalized = target.trim().replace('\\', '/') + + while (normalized.startsWith('./')) { + normalized = normalized.substring(2) + } + + return normalized + } + + static void validateDebugProfile(Map profile) { + if (profile == null) { + throw new IllegalArgumentException('Debug overrides profile is null') + } + + def replacements = profile.replacements + if (!(replacements instanceof List)) { + throw new IllegalArgumentException('Debug overrides profile must contain a replacements array') + } + + replacements.eachWithIndex { replacement, index -> + if (!(replacement instanceof Map)) { + throw new IllegalArgumentException("Replacement at index ${index} must be an object") + } + + if (!replacement.fileId?.toString()?.trim()) { + throw new IllegalArgumentException("Replacement at index ${index} must contain non-empty fileId") + } + + validateTargetPath(replacement.target?.toString()) + } + } + + static void validateTargetPath(String target) { + String normalized = normalizeTarget(target) + + if (!normalized) { + throw new IllegalArgumentException('Replacement target must be non-empty') + } + + if (normalized.startsWith('//')) { + throw new IllegalArgumentException("UNC target paths are not allowed: ${target}") + } + + if (normalized.startsWith('/')) { + throw new IllegalArgumentException("Absolute target paths are not allowed: ${target}") + } + + if (normalized ==~ /^[A-Za-z]:\/.*/) { + throw new IllegalArgumentException("Windows absolute target paths are not allowed: ${target}") + } + + List segments = normalized.split('/') as List + if (segments.any { it == '..' }) { + throw new IllegalArgumentException("Target path traversal is not allowed: ${target}") + } + } + + static List> buildConfigFileProviderEntries(List replacements) { + List> entries = [] + + replacements.eachWithIndex { replacement, index -> + entries.add([ + fileId : replacement.fileId.toString(), + variable: "DEBUG_OVERRIDE_FILE_${index}" + ]) + } + + return entries + } + + static List collectDownstreamTargets(List replacements) { + replacements + .collect { normalizeTarget(it.target?.toString()) } + .findAll { it in DOWNSTREAM_TARGETS } + .unique() + } + + static String buildStashIncludes(List targets) { + List unsafeTargets = targets.findAll { !(it ==~ STASH_SAFE_TARGET_PATTERN) } + if (!unsafeTargets.isEmpty()) { + throw new IllegalArgumentException( + "Downstream targets are not stash-safe: ${unsafeTargets.join(', ')}" + ) + } + + targets.join(',') + } + + static String parentPath(String target) { + String normalized = normalizeTarget(target) + int lastSlash = normalized.lastIndexOf('/') + + if (lastSlash <= 0) { + return '' + } + + return normalized.substring(0, lastSlash) + } + + static boolean shouldTreatConfigFileProviderErrorAsMissingPlugin(Exception exception) { + String message = exception?.message ?: '' + + return exception instanceof MissingMethodException || + exception instanceof NoSuchMethodError || + message.contains("No such DSL method 'configFileProvider'") || + message.contains("No such DSL method 'configFile'") || + ( + message.contains('configFileProvider') && + message.toLowerCase().contains('no such dsl method') + ) + } + + static boolean shouldTreatConfigFileProviderErrorAsMissingControlFile(Exception exception) { + String message = exception?.message ?: '' + String lowerCaseMessage = message.toLowerCase() + + return message.contains(CONTROL_FILE_ID) && ( + lowerCaseMessage.contains('not found') || + lowerCaseMessage.contains('no such file') || + lowerCaseMessage.contains('can\'t be resolved') || + lowerCaseMessage.contains('cannot be resolved') || + lowerCaseMessage.contains('unable to find') || + lowerCaseMessage.contains('managed file') + ) + } + + static boolean shouldTreatConfigFileProviderErrorAsInvalidControlFile(Exception exception) { + String className = exception?.class?.name ?: '' + String message = exception?.message ?: '' + String lowerCaseMessage = message.toLowerCase() + boolean isJsonParseError = + className.endsWith('JsonException') || + className.endsWith('JSONException') || + className.endsWith('JsonParseException') || + lowerCaseMessage.contains('net.sf.json') || + lowerCaseMessage.contains('jsonparseexception') || + lowerCaseMessage.contains('jsonexception') || + lowerCaseMessage.contains('unexpected character') || + lowerCaseMessage.contains('unable to parse') + boolean referencesControlFile = + message.contains(CONTROL_FILE_ID) || + lowerCaseMessage.contains(CONTROL_FILE_VARIABLE.toLowerCase()) || + lowerCaseMessage.contains('readjson') || + (exception?.stackTrace ?: []).any { it.methodName == 'loadControlConfig' } + + return isJsonParseError && referencesControlFile + } + + static boolean shouldTreatUnstashErrorAsMissingStash(Exception exception) { + String message = exception?.message ?: '' + String normalizedMessage = message + .replace('\u2018', "'") + .replace('\u2019', "'") + + return normalizedMessage.contains("No such saved stash '${STASH_NAME}'") || + (normalizedMessage.contains('No such saved stash') && normalizedMessage.contains(STASH_NAME)) + } +} diff --git a/src/ru/pulsar/jenkins/library/utils/VRunner.groovy b/src/ru/pulsar/jenkins/library/utils/VRunner.groovy index b7b843d7..9ac70d18 100644 --- a/src/ru/pulsar/jenkins/library/utils/VRunner.groovy +++ b/src/ru/pulsar/jenkins/library/utils/VRunner.groovy @@ -24,12 +24,23 @@ class VRunner { static int exec(String command, boolean returnStatus = false) { IStepExecutor steps = ContextRegistry.getContext().getStepExecutor() + String commandWithVersion = appendV8Version(command, ContextRegistry.getJobConfiguration()?.v8version) steps.withEnv([DEFAULT_VRUNNER_OPTS]) { - return steps.cmd(command, returnStatus) + return steps.cmd(commandWithVersion, returnStatus) } as int } + static String appendV8Version(String command, String v8version) { + if (v8version == null || v8version.trim().isEmpty()) { + return command + } + if (command.contains("--v8version")) { + return command + } + return "${command} --v8version ${v8version}" + } + static boolean configContainsSetting(String configPath, String settingName) { IStepExecutor steps = ContextRegistry.getContext().getStepExecutor() @@ -58,7 +69,7 @@ class VRunner { return content.toInteger() } } catch (NoSuchFileException e) { - Logger.println("Файл со статусом возврата ${path} не найден: ${e.message}") + Logger.println("Файл со статусом возврата ${path} не найден: ${e.message}. Будет использован переданный статус ${valueIfNoSuchFile}") return valueIfNoSuchFile } catch (NumberFormatException e) { Logger.println("В файле со статусом возврата ${path} записано не числовое значение: ${e.message}") diff --git a/test/integration/README.md b/test/integration/README.md new file mode 100644 index 00000000..d838023f --- /dev/null +++ b/test/integration/README.md @@ -0,0 +1,136 @@ +# Описание интеграционных тестов + +## Общая информация + +Все интеграционные тесты используют: +- **Jenkins Test Harness** для запуска тестового экземпляра Jenkins +- **RuleBootstrapper** для настройки локальной библиотеки как shared library +- **JUnit 4** для структуры тестов +- **Declarative Pipeline** для проверки работы в реальном Jenkins окружении + +--- + +## 1. `jobConfigurationTest.groovy` + +**Назначение:** Проверка функции `jobConfiguration()` для чтения и слияния конфигураций. + +### Тесты: + +1. **`"jobConfiguration should not fail without file"`** + - Проверяет, что `jobConfiguration()` не падает при отсутствии файла конфигурации + - Использует значения по умолчанию + - Ожидается успешное выполнение пайплайна + +2. **`"jobConfiguration should merge configurations"`** + - Проверяет слияние конфигураций из файла `jobConfiguration.json` + - Создает файл конфигурации в workspace и проверяет значения: + - `v8version='8.3.12.1500'` + - `sonarScannerToolName='sonar-scanner'` + - `initMethod=FROM_SOURCE` + - `dbgsPath=C:\Program files\1cv8\8.3.12.1500\bin\dbgs.exe` + - `coverage41CPath=C:\coverage\Coverage41C.exe` + +--- + +## 2. `pipeline1cTest.groovy` + +**Назначение:** Проверка основного пайплайна `pipeline1C()`. + +### Тесты: + +1. **`"pipeline1C should do something"`** + - Проверяет базовое выполнение `pipeline1C()` + - Создает агента с меткой `"agent"` + - Проверяет наличие `'(pre-stage)'` в логах + - Минимальная проверка, что пайплайн запускается + +--- + +## 3. `cmdTest.groovy` + +**Назначение:** Проверка функции `cmd()` для выполнения команд. + +### Тесты: + +1. **`"cmd should echo something"`** + - Проверяет выполнение команды `echo helloWorld` + - Проверяет наличие `'helloWorld'` в логах + - Проверяет базовое выполнение команды + +2. **`"cmd should return status"`** + - Проверяет возврат кода возврата команды + - Выполняет `cmd("false", true)` (второй параметр - возврат статуса) + - Проверяет, что статус равен `1` (команда `false` возвращает 1) + - Проверяет корректную обработку кода возврата + +--- + +## 4. `printLocationTest.groovy` + +**Назначение:** Проверка функции `printLocation()` для логирования информации о ноде. + +### Тесты: + +1. **`"Logger should echo current node name"`** + - Проверяет вывод имени текущего нода + - Проверяет наличие `'Running on node built-in'` в логах + - Проверяет корректное определение нода выполнения + +--- + +## 5. `RuleBootstrapper.groovy` (вспомогательный класс) + +**Назначение:** Утилита для настройки тестового окружения. + +### Функции: +- Настраивает `JenkinsRule` для использования локального исходного кода как shared library +- Создает `LibraryConfiguration` с именем `'testLibrary'` +- Устанавливает библиотеку как неявно загружаемую (`implicit = true`) +- Устанавливает таймаут тестов в 30 секунд +- Используется во всех тестах через аннотацию `@Before` + +--- + +## Ресурсы + +### `test/integration/resources/jobConfiguration.json` + +Тестовый файл конфигурации, содержит: +- Версию платформы 1С: `8.3.12.1500` +- Настройки инициализации ИБ (`fromSource`) +- Пример расширения конфигурации +- Настройки покрытия кода (пути к `dbgs.exe` и `Coverage41C.exe`) + +--- + +## Итоговая статистика + +- **Всего тестовых классов:** 4 +- **Всего тестов:** 6 +- **Вспомогательных классов:** 1 (`RuleBootstrapper`) +- **Тестовых ресурсов:** 1 (`jobConfiguration.json`) + +## Покрытие функциональности + +- ✅ Чтение и слияние конфигураций (`jobConfiguration`) +- ✅ Основной пайплайн (`pipeline1C`) +- ✅ Выполнение команд (`cmd`) +- ✅ Логирование информации о ноде (`printLocation`) + +--- + +## Запуск тестов + +Для запуска всех интеграционных тестов используйте: + +```bash +.\gradlew.bat integrationTest +``` + +Для запуска конкретного теста: + +```bash +.\gradlew.bat integrationTest --tests "jobConfigurationTest" +``` + +Тесты проверяют базовую функциональность библиотеки в реальном окружении Jenkins через Jenkins Test Harness. diff --git a/test/integration/groovy/createDirTest.groovy b/test/integration/groovy/createDirTest.groovy new file mode 100644 index 00000000..0581bb07 --- /dev/null +++ b/test/integration/groovy/createDirTest.groovy @@ -0,0 +1,76 @@ +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition +import org.jenkinsci.plugins.workflow.job.WorkflowJob +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.jvnet.hudson.test.JenkinsRule + +class createDirTest { + + @Rule + public JenkinsRule rule = new JenkinsRule() + + @Before + void configureGlobalGitLibraries() { + RuleBootstrapper.setup(rule) + } + + @Test + void "createDir should create directory without cleanup"() { + def pipeline = ''' + pipeline { + agent any + stages { + stage('test') { + steps { + script { + createDir('build/out/custom') + writeFile file: 'build/out/custom/created.txt', text: 'created' + echo "createdFile=${fileExists('build/out/custom/created.txt')}" + } + } + } + } + } + '''.stripIndent() + + final CpsFlowDefinition flow = new CpsFlowDefinition(pipeline, true) + final WorkflowJob workflowJob = rule.createProject(WorkflowJob, 'project-create-dir') + workflowJob.definition = flow + + rule.assertLogContains('createdFile=true', rule.buildAndAssertSuccess(workflowJob)) + } + + @Test + void "createDir should recreate directory after cleanup"() { + def pipeline = ''' + pipeline { + agent any + stages { + stage('test') { + steps { + script { + createDir('build/out/custom') + writeFile file: 'build/out/custom/stale.txt', text: 'old data' + + createDir('build/out/custom', true) + + writeFile file: 'build/out/custom/fresh.txt', text: 'fresh data' + echo "staleExists=${fileExists('build/out/custom/stale.txt')}" + echo "freshExists=${fileExists('build/out/custom/fresh.txt')}" + } + } + } + } + } + '''.stripIndent() + + final CpsFlowDefinition flow = new CpsFlowDefinition(pipeline, true) + final WorkflowJob workflowJob = rule.createProject(WorkflowJob, 'project-create-dir-clean') + workflowJob.definition = flow + + def build = rule.buildAndAssertSuccess(workflowJob) + rule.assertLogContains('staleExists=false', build) + rule.assertLogContains('freshExists=true', build) + } +} diff --git a/test/integration/groovy/pipeline1cTest.groovy b/test/integration/groovy/pipeline1cTest.groovy index 5930571d..c8a773e5 100644 --- a/test/integration/groovy/pipeline1cTest.groovy +++ b/test/integration/groovy/pipeline1cTest.groovy @@ -30,4 +30,20 @@ class pipeline1cTest { rule.assertLogContains('(pre-stage)', rule.buildAndAssertSuccess(workflowJob)) } + @Test + void "pipeline1C should skip debug overrides when profile key is unavailable"() { + def pipeline = ''' + pipeline1C() + '''.stripIndent() + + rule.createSlave(Label.get("agent")) + final CpsFlowDefinition flow = new CpsFlowDefinition(pipeline, true) + final WorkflowJob workflowJob = rule.createProject(WorkflowJob, 'project-debug-overrides') + workflowJob.definition = flow + + def build = rule.buildAndAssertSuccess(workflowJob) + + rule.assertLogContains('Debug overrides: profile key is unavailable, skip', build) + } + } diff --git a/test/unit/groovy/ru/pulsar/jenkins/library/configuration/BddOptionsTest.java b/test/unit/groovy/ru/pulsar/jenkins/library/configuration/BddOptionsTest.java new file mode 100644 index 00000000..62e698f5 --- /dev/null +++ b/test/unit/groovy/ru/pulsar/jenkins/library/configuration/BddOptionsTest.java @@ -0,0 +1,148 @@ +package ru.pulsar.jenkins.library.configuration; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import ru.pulsar.jenkins.library.utils.TestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +class BddOptionsTest { + + @BeforeEach + void setUp() { + TestUtils.setupMockedContext(); + } + + @Test + @DisplayName("getVrunnerSteps должен возвращать null если не задан явно") + void testGetVrunnerStepsReturnsNullByDefault() { + // given + BddOptions options = new BddOptions(); + + // when + String[] steps = options.getVrunnerSteps(); + + // then + assertThat(steps).isNull(); + } + + @Test + @DisplayName("getEffectiveVrunnerSteps должен использовать значение по умолчанию из vrunnerSettings") + void testEffectiveVrunnerStepsUsesDefaultFromVrunnerSettings() { + // given + BddOptions options = new BddOptions(); + + // when + String[] steps = options.getEffectiveVrunnerSteps(); + + // then + assertThat(steps).hasSize(1); + assertThat(steps[0]).isEqualTo("vanessa --settings ./tools/vrunner.json"); + } + + @Test + @DisplayName("getEffectiveVrunnerSteps должен использовать кастомный путь из vrunnerSettings") + void testEffectiveVrunnerStepsUsesCustomVrunnerSettings() { + // given + BddOptions options = new BddOptions(); + options.setVrunnerSettings("./custom/path/vrunner.json"); + + // when + String[] steps = options.getEffectiveVrunnerSteps(); + + // then + assertThat(steps).hasSize(1); + assertThat(steps[0]).isEqualTo("vanessa --settings ./custom/path/vrunner.json"); + } + + @Test + @DisplayName("getEffectiveVrunnerSteps должен использовать явно заданное значение vrunnerSteps") + void testEffectiveVrunnerStepsUsesExplicitValue() { + // given + BddOptions options = new BddOptions(); + options.setVrunnerSettings("./custom/path/vrunner.json"); + options.setVrunnerSteps(new String[]{"vanessa --settings ./explicit/path.json"}); + + // when + String[] steps = options.getEffectiveVrunnerSteps(); + + // then + assertThat(steps).hasSize(1); + assertThat(steps[0]).isEqualTo("vanessa --settings ./explicit/path.json"); + } + + @Test + @DisplayName("getVrunnerSteps должен возвращать явно заданные шаги") + void testGetVrunnerStepsReturnsExplicitSteps() { + // given + BddOptions options = new BddOptions(); + options.setVrunnerSteps(new String[]{ + "vanessa --settings ./tools/vrunner.json", + "vanessa --settings ./tools/vrunner2.json" + }); + + // when + String[] steps = options.getVrunnerSteps(); + + // then + assertThat(steps).hasSize(2); + assertThat(steps[0]).isEqualTo("vanessa --settings ./tools/vrunner.json"); + assertThat(steps[1]).isEqualTo("vanessa --settings ./tools/vrunner2.json"); + } + + @Test + @DisplayName("hasCustomVrunnerSteps должен возвращать false по умолчанию") + void testHasCustomVrunnerStepsReturnsFalseByDefault() { + // given + BddOptions options = new BddOptions(); + + // then + assertThat(options.hasCustomVrunnerSteps()).isFalse(); + } + + @Test + @DisplayName("hasCustomVrunnerSteps должен возвращать true после явной установки значения") + void testHasCustomVrunnerStepsReturnsTrueAfterSet() { + // given + BddOptions options = new BddOptions(); + options.setVrunnerSteps(new String[]{"vanessa --settings ./custom.json"}); + + // then + assertThat(options.hasCustomVrunnerSteps()).isTrue(); + } + + @Test + @DisplayName("изменение vrunnerSettings должно влиять на getEffectiveVrunnerSteps") + void testChangingVrunnerSettingsAffectsEffectiveVrunnerSteps() { + // given + BddOptions options = new BddOptions(); + + // when - проверяем начальное значение + String[] initialSteps = options.getEffectiveVrunnerSteps(); + assertThat(initialSteps[0]).isEqualTo("vanessa --settings ./tools/vrunner.json"); + + // when - меняем vrunnerSettings + options.setVrunnerSettings("./another/path.json"); + String[] updatedSteps = options.getEffectiveVrunnerSteps(); + + // then - значение по умолчанию должно измениться + assertThat(updatedSteps[0]).isEqualTo("vanessa --settings ./another/path.json"); + } + + @Test + @DisplayName("явно заданное vrunnerSteps не должно изменяться при смене vrunnerSettings") + void testExplicitVrunnerStepsNotAffectedByVrunnerSettingsChange() { + // given + BddOptions options = new BddOptions(); + options.setVrunnerSteps(new String[]{"vanessa --settings ./explicit.json"}); + + // when - меняем vrunnerSettings + options.setVrunnerSettings("./another/path.json"); + String[] steps = options.getEffectiveVrunnerSteps(); + + // then - явно заданное значение не должно измениться + assertThat(steps[0]).isEqualTo("vanessa --settings ./explicit.json"); + } +} + diff --git a/test/unit/groovy/ru/pulsar/jenkins/library/configuration/ConfigurationReaderTest.java b/test/unit/groovy/ru/pulsar/jenkins/library/configuration/ConfigurationReaderTest.java index 27a56c4c..e5e0c366 100644 --- a/test/unit/groovy/ru/pulsar/jenkins/library/configuration/ConfigurationReaderTest.java +++ b/test/unit/groovy/ru/pulsar/jenkins/library/configuration/ConfigurationReaderTest.java @@ -68,7 +68,7 @@ void testCreateJobConfigurationObject() throws IOException { assertThat(jobConfiguration.getInitInfoBaseOptions().getArchiveInfobase().getOnAlways()).isTrue(); assertThat(jobConfiguration.getInitInfoBaseOptions().getAdditionalInitializationSteps()).contains("vanessa --settings ./tools/vrunner.first.json"); - assertThat(jobConfiguration.getBddOptions().getVrunnerSteps()).contains("vanessa --settings ./tools/vrunner.json"); + assertThat(jobConfiguration.getBddOptions().getEffectiveVrunnerSteps()).contains("vanessa --settings ./tools/vrunner.json"); assertThat(jobConfiguration.getBddOptions().getCoverage()).isFalse(); assertThat(jobConfiguration.getLogosConfig()).isEqualTo("logger.rootLogger=DEBUG"); @@ -137,4 +137,48 @@ void testInfoBaseFromFiles() throws IOException { assertThat(jobConfiguration.infoBaseFromFiles()).isFalse(); } -} \ No newline at end of file + @Test + void testBddVrunnerStepsUsesVrunnerSettingsByDefault() { + // given - конфигурация без явного указания vrunnerSteps, но с кастомным vrunnerSettings + String config = + """ + { + "bdd": { + "vrunnerSettings": "./custom/vrunner.json" + } + } + """; + + // when + JobConfiguration jobConfiguration = ConfigurationReader.create(config); + + // then - vrunnerSteps должен быть null (не задан явно) + assertThat(jobConfiguration.getBddOptions().getVrunnerSteps()).isNull(); + + // effectiveVrunnerSteps должен использовать значение из vrunnerSettings + String[] steps = jobConfiguration.getBddOptions().getEffectiveVrunnerSteps(); + String vrunnerSettings = jobConfiguration.getBddOptions().getVrunnerSettings(); + + assertThat(vrunnerSettings).isEqualTo("./custom/vrunner.json"); + assertThat(steps).hasSize(1); + assertThat(steps[0]).isEqualTo("vanessa --settings ./custom/vrunner.json"); + } + + @Test + void testBddVrunnerStepsExplicitValueOverridesDefault() throws IOException { + // given - конфигурация с явным указанием vrunnerSteps + String config = "{ \"bdd\": { \"vrunnerSettings\": \"./custom/vrunner.json\", \"vrunnerSteps\": [\"vanessa --settings ./explicit.json\"] } }"; + + // when + JobConfiguration jobConfiguration = ConfigurationReader.create(config); + + // then - явно заданный vrunnerSteps не должен использовать vrunnerSettings + assertThat(jobConfiguration.getBddOptions().getVrunnerSteps()).hasSize(1); + assertThat(jobConfiguration.getBddOptions().getVrunnerSteps()[0]) + .isEqualTo("vanessa --settings ./explicit.json"); + } + +} + + + diff --git a/test/unit/groovy/ru/pulsar/jenkins/library/steps/DebugOverridesStepsTest.groovy b/test/unit/groovy/ru/pulsar/jenkins/library/steps/DebugOverridesStepsTest.groovy new file mode 100644 index 00000000..591dff44 --- /dev/null +++ b/test/unit/groovy/ru/pulsar/jenkins/library/steps/DebugOverridesStepsTest.groovy @@ -0,0 +1,201 @@ +package ru.pulsar.jenkins.library.steps + +import groovy.json.JsonSlurper +import groovy.lang.Binding +import groovy.lang.GroovyShell +import groovy.lang.Script +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +import java.nio.file.Files +import java.nio.file.Path + +import static org.assertj.core.api.Assertions.assertThat + +class DebugOverridesStepsTest { + + @TempDir + Path tempDir + + @Test + void applyDebugOverridesIfNeeded_appliesAndRestoreDebugOverridesIfNeeded_restoresManagedFiles() { + Map env = [JOB_NAME: 'CPC/ci_uh_MR/MR-1101'] + List logs = [] + Map> stashes = [:] + Map managedFiles = [ + 'jenkins-debug-overrides-control': ''' + { + "profiles": { + "ci_uh_MR": { + "enabled": true, + "replacements": [ + { + "fileId": "debug-job-configuration", + "target": "jobConfiguration.json" + }, + { + "fileId": "debug-vrunner-settings", + "target": "tools/vrunner.json" + } + ] + } + } + } + '''.stripIndent().trim(), + 'debug-job-configuration': '{"stages":{"bdd":false,"smoke":false,"yaxunit":false,"syntaxCheck":false,"sonarqube":false,"edtValidate":false}}', + 'debug-vrunner-settings': '{"settings":"debug"}' + ] + + Script applyScript = loadScript('vars/applyDebugOverridesIfNeeded.groovy', env) + configureStepScript(applyScript, managedFiles, stashes, logs) + + boolean applied = (boolean) applyScript.invokeMethod('call', null) + + assertThat(applied).isTrue() + assertThat(logs).contains('Debug overrides: applying 2 replacement(s)') + assertThat(logs).contains('Debug overrides: stashed files for downstream agents') + assertThat(readFile('jobConfiguration.json')).contains('"stages"') + assertThat(readFile('tools/vrunner.json')).isEqualTo('{"settings":"debug"}') + + writeFile('tools/vrunner.json', '{"settings":"stale"}') + + Script restoreScript = loadScript('vars/restoreDebugOverridesIfNeeded.groovy', env) + configureStepScript(restoreScript, managedFiles, stashes, logs) + + boolean restored = (boolean) restoreScript.invokeMethod('call', null) + + assertThat(restored).isTrue() + assertThat(logs).contains('Debug overrides: restored files from stash') + assertThat(readFile('tools/vrunner.json')).isEqualTo('{"settings":"debug"}') + } + + @Test + void applyDebugOverridesIfNeeded_skipsInvalidProfileConfigWithoutFailing() { + Map env = [JOB_NAME: 'CPC/ci_uh_MR/MR-1101'] + List logs = [] + Map> stashes = [:] + Map managedFiles = [ + 'jenkins-debug-overrides-control': ''' + { + "profiles": { + "ci_uh_MR": { + "enabled": true, + "replacements": { + "fileId": "debug-job-configuration", + "target": "jobConfiguration.json" + } + } + } + } + '''.stripIndent().trim() + ] + + Script applyScript = loadScript('vars/applyDebugOverridesIfNeeded.groovy', env) + configureStepScript(applyScript, managedFiles, stashes, logs) + + boolean applied = (boolean) applyScript.invokeMethod('call', null) + + assertThat(applied).isFalse() + assertThat(logs.find { it.startsWith('Debug overrides: invalid profile ci_uh_MR, skip') }).isNotNull() + } + + @Test + void applyDebugOverridesIfNeeded_skipsInvalidControlFileWithoutFailing() { + Map env = [JOB_NAME: 'CPC/ci_uh_MR/MR-1101'] + List logs = [] + Map> stashes = [:] + Map managedFiles = [ + 'jenkins-debug-overrides-control': '{ invalid json' + ] + + Script applyScript = loadScript('vars/applyDebugOverridesIfNeeded.groovy', env) + configureStepScript(applyScript, managedFiles, stashes, logs) + + boolean applied = (boolean) applyScript.invokeMethod('call', null) + + assertThat(applied).isFalse() + assertThat(logs.find { it.startsWith('Debug overrides: control file is invalid, skip') }).isNotNull() + } + + private Script loadScript(String path, Map env) { + Binding binding = new Binding() + binding.setVariable('env', env) + GroovyShell shell = new GroovyShell(this.class.classLoader, binding) + return shell.parse(new File(path)) + } + + private void configureStepScript( + Script script, + Map managedFiles, + Map> stashes, + List logs + ) { + script.metaClass.echo = { String message -> + logs.add(message) + } + script.metaClass.createDir = { String path -> + Files.createDirectories(tempDir.resolve(path)) + } + script.metaClass.readFile = { Map args -> + String encoding = (args.encoding ?: 'UTF-8') as String + tempDir.resolve(args.file as String).toFile().getText(encoding) + } + script.metaClass.writeFile = { Map args -> + Path target = tempDir.resolve(args.file as String) + Files.createDirectories(target.parent ?: tempDir) + target.toFile().setText(args.text as String, (args.encoding ?: 'UTF-8') as String) + } + script.metaClass.readJSON = { Map args -> + new JsonSlurper().parse(tempDir.resolve(args.file as String).toFile()) + } + script.metaClass.configFile = { Map args -> + args + } + script.metaClass.configFileProvider = { List entries, Closure body -> + Files.createDirectories(tempDir.resolve('.managed')) + + entries.each { Map entry -> + String fileId = entry.fileId as String + String variable = entry.variable as String + String content = managedFiles[fileId] + + if (content == null) { + throw new RuntimeException("Managed file with id ${fileId} not found") + } + + Path managedFile = tempDir.resolve(".managed/${variable}.txt") + managedFile.toFile().setText(content, 'UTF-8') + script.binding.getVariable('env')[variable] = tempDir.relativize(managedFile).toString().replace('\\', '/') + } + + body.call() + } + script.metaClass.stash = { Map args -> + Map files = [:] + (args.includes as String).split(',').each { String include -> + files[include] = readFile(include) + } + stashes[args.name as String] = files + } + script.metaClass.unstash = { String name -> + Map files = stashes[name] + if (files == null) { + throw new RuntimeException("No such saved stash '${name}'") + } + + files.each { String path, String content -> + writeFile(path, content) + } + } + } + + private String readFile(String relativePath) { + tempDir.resolve(relativePath).toFile().getText('UTF-8') + } + + private void writeFile(String relativePath, String content) { + Path target = tempDir.resolve(relativePath) + Files.createDirectories(target.parent ?: tempDir) + target.toFile().setText(content, 'UTF-8') + } +} diff --git a/test/unit/groovy/ru/pulsar/jenkins/library/steps/InitInfoBaseTest.java b/test/unit/groovy/ru/pulsar/jenkins/library/steps/InitInfoBaseTest.java new file mode 100644 index 00000000..a2b2c4cd --- /dev/null +++ b/test/unit/groovy/ru/pulsar/jenkins/library/steps/InitInfoBaseTest.java @@ -0,0 +1,73 @@ +package ru.pulsar.jenkins.library.steps; + +import groovy.lang.Closure; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import ru.pulsar.jenkins.library.IStepExecutor; +import ru.pulsar.jenkins.library.configuration.ConfigurationReader; +import ru.pulsar.jenkins.library.configuration.JobConfiguration; +import ru.pulsar.jenkins.library.utils.TestUtils; +import ru.pulsar.jenkins.library.utils.VRunner; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class InitInfoBaseTest { + + private IStepExecutor steps; + + @BeforeEach + void setUp() { + steps = TestUtils.getMockedStepExecutor(); + + when(steps.withEnv(anyList(), any(Closure.class))) + .thenAnswer(invocation -> { + Closure body = invocation.getArgument(1); + return body.call(); + }); + + when(steps.catchError(any(Closure.class))) + .thenAnswer(invocation -> { + Closure body = invocation.getArgument(0); + return body.call(); + }); + + TestUtils.setupMockedContext(steps); + } + + @Test + void runUsesCommandExitStatusWhenMigrationStatusFileIsMissing() throws IOException { + + // given + String config = IOUtils.resourceToString( + "initInfoBaseRunMigration.json", + StandardCharsets.UTF_8, + this.getClass().getClassLoader() + ); + JobConfiguration jobConfiguration = ConfigurationReader.create(config); + + try (MockedStatic vrunner = Mockito.mockStatic(VRunner.class)) { + vrunner.when(VRunner::getVRunnerPath).thenReturn("vrunner"); + vrunner.when(() -> VRunner.exec(anyString(), eq(true))).thenReturn(0); + vrunner.when(() -> VRunner.readExitStatusFromFile("build/migration-exit-status.log", 0)).thenReturn(0); + + // when + new InitInfoBase(jobConfiguration).run(); + + // then + vrunner.verify(() -> VRunner.readExitStatusFromFile("build/migration-exit-status.log", 0)); + verify(steps, never()).error(anyString()); + } + } +} diff --git a/test/unit/groovy/ru/pulsar/jenkins/library/utils/DebugOverridesTest.java b/test/unit/groovy/ru/pulsar/jenkins/library/utils/DebugOverridesTest.java new file mode 100644 index 00000000..53f86b6f --- /dev/null +++ b/test/unit/groovy/ru/pulsar/jenkins/library/utils/DebugOverridesTest.java @@ -0,0 +1,192 @@ +package ru.pulsar.jenkins.library.utils; + +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DebugOverridesTest { + + @Test + void resolveProfileKey_returnsPenultimateSegment() { + assertThat(DebugOverrides.resolveProfileKey("CPC/ci_uh_MR/MR-1101")) + .isEqualTo("ci_uh_MR"); + } + + @Test + void resolveProfileKey_returnsNullForShortPath() { + assertThat(DebugOverrides.resolveProfileKey("ci_uh_MR")) + .isNull(); + } + + @Test + void resolveProfileKey_returnsNullForNull() { + assertThat(DebugOverrides.resolveProfileKey(null)) + .isNull(); + } + + @Test + void resolveProfileKey_returnsNullForEmptyString() { + assertThat(DebugOverrides.resolveProfileKey("")) + .isNull(); + } + + @Test + void normalizeTarget_normalizesRelativePath() { + assertThat(DebugOverrides.normalizeTarget(".\\tools\\vrunner.json")) + .isEqualTo("tools/vrunner.json"); + } + + @Test + void validateTargetPath_rejectsAbsolutePath() { + assertThatThrownBy(() -> DebugOverrides.validateTargetPath("C:/temp/file.json")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void validateTargetPath_rejectsTraversal() { + assertThatThrownBy(() -> DebugOverrides.validateTargetPath("../tools/vrunner.json")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void validateTargetPath_rejectsUncPath() { + assertThatThrownBy(() -> DebugOverrides.validateTargetPath("//server/share/file.json")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("UNC target paths are not allowed"); + } + + @Test + void validateDebugProfile_requiresReplacementFields() { + Map replacement = new LinkedHashMap<>(); + replacement.put("target", "tools/vrunner.json"); + + Map profile = new LinkedHashMap<>(); + profile.put("enabled", true); + profile.put("replacements", List.of(replacement)); + + assertThatThrownBy(() -> DebugOverrides.validateDebugProfile(profile)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void validateDebugProfile_acceptsValidProfile() { + Map profile = new LinkedHashMap<>(); + profile.put("enabled", true); + profile.put("replacements", List.of(replacement("debug-1", "tools/vrunner.json"))); + + DebugOverrides.validateDebugProfile(profile); + } + + @Test + void buildConfigFileProviderEntries_buildsVariables() { + Map first = replacement("debug-1", "jobConfiguration.json"); + Map second = replacement("debug-2", "tools/vrunner.json"); + + List entries = DebugOverrides.buildConfigFileProviderEntries(List.of(first, second)); + Map firstEntry = (Map) entries.get(0); + Map secondEntry = (Map) entries.get(1); + + assertThat(entries).hasSize(2); + assertThat(firstEntry.get("fileId").toString()).isEqualTo("debug-1"); + assertThat(firstEntry.get("variable").toString()).isEqualTo("DEBUG_OVERRIDE_FILE_0"); + assertThat(secondEntry.get("fileId").toString()).isEqualTo("debug-2"); + assertThat(secondEntry.get("variable").toString()).isEqualTo("DEBUG_OVERRIDE_FILE_1"); + } + + @Test + void collectDownstreamTargets_returnsOnlyConfiguredDownstreamFiles() { + List targets = DebugOverrides.collectDownstreamTargets(List.of( + replacement("debug-1", "jobConfiguration.json"), + replacement("debug-2", "./tools/vrunner.json"), + replacement("debug-3", "sonar-project.properties") + )); + + assertThat(targets).containsExactly("tools/vrunner.json", "sonar-project.properties"); + } + + @Test + void collectDownstreamTargets_returnsEmptyListForEmptyReplacements() { + assertThat(DebugOverrides.collectDownstreamTargets(List.of())) + .isEmpty(); + } + + @Test + void buildStashIncludes_joinsTargets() { + assertThat(DebugOverrides.buildStashIncludes(List.of("tools/vrunner.json", "sonar-project.properties"))) + .isEqualTo("tools/vrunner.json,sonar-project.properties"); + } + + @Test + void buildStashIncludes_rejectsUnsafeTargets() { + assertThatThrownBy(() -> DebugOverrides.buildStashIncludes(List.of("tools/*.json"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Downstream targets are not stash-safe"); + } + + @Test + void shouldTreatConfigFileProviderErrorAsMissingPlugin_detectsMissingDslMethod() { + Exception exception = new RuntimeException("No such DSL method 'configFileProvider' found among steps"); + + assertThat(DebugOverrides.shouldTreatConfigFileProviderErrorAsMissingPlugin(exception)) + .isTrue(); + } + + @Test + void shouldTreatConfigFileProviderErrorAsMissingControlFile_detectsMissingManagedFile() { + Exception exception = new RuntimeException( + "Managed file with id jenkins-debug-overrides-control not found" + ); + + assertThat(DebugOverrides.shouldTreatConfigFileProviderErrorAsMissingControlFile(exception)) + .isTrue(); + } + + @Test + void shouldTreatConfigFileProviderErrorAsInvalidControlFile_detectsInvalidJsonException() { + Exception exception = new RuntimeException( + "Failed to parse jenkins-debug-overrides-control: Unexpected character at line 1" + ); + exception.setStackTrace(new StackTraceElement[] { + new StackTraceElement("applyDebugOverridesIfNeeded", "loadControlConfig", "applyDebugOverridesIfNeeded.groovy", 77) + }); + + assertThat(DebugOverrides.shouldTreatConfigFileProviderErrorAsInvalidControlFile(exception)) + .isTrue(); + } + + @Test + void shouldTreatConfigFileProviderErrorAsInvalidControlFile_ignoresUnrelatedJsonErrors() { + Exception exception = new RuntimeException("Unexpected character at line 1"); + + assertThat(DebugOverrides.shouldTreatConfigFileProviderErrorAsInvalidControlFile(exception)) + .isFalse(); + } + + @Test + void shouldTreatUnstashErrorAsMissingStash_detectsMissingStash() { + Exception exception = new RuntimeException("No such saved stash 'debug-overrides-files'"); + + assertThat(DebugOverrides.shouldTreatUnstashErrorAsMissingStash(exception)) + .isTrue(); + } + + @Test + void shouldTreatUnstashErrorAsMissingStash_normalizesCurlyQuotes() { + Exception exception = new RuntimeException("No such saved stash \u2018debug-overrides-files\u2019"); + + assertThat(DebugOverrides.shouldTreatUnstashErrorAsMissingStash(exception)) + .isTrue(); + } + + private static Map replacement(String fileId, String target) { + Map replacement = new LinkedHashMap<>(); + replacement.put("fileId", fileId); + replacement.put("target", target); + return replacement; + } +} diff --git a/test/unit/groovy/ru/pulsar/jenkins/library/utils/VRunnerTest.java b/test/unit/groovy/ru/pulsar/jenkins/library/utils/VRunnerTest.java new file mode 100644 index 00000000..b29fa0e6 --- /dev/null +++ b/test/unit/groovy/ru/pulsar/jenkins/library/utils/VRunnerTest.java @@ -0,0 +1,127 @@ +package ru.pulsar.jenkins.library.utils; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +class VRunnerTest { + + @BeforeEach + void setUp() { + TestUtils.setupMockedContext(); + } + + @Test + void readExitStatusFromFile_success() { + + // given + String resource = Objects.requireNonNull(getClass() + .getClassLoader() + .getResource("exitStatus0")) + .getPath(); + + // when + Integer exitStatus = VRunner.readExitStatusFromFile(resource); + // then + assertThat(exitStatus).isEqualTo(0); + + } + + @Test + void readExitStatusFromFile_failure() { + + // given + String resource = Objects.requireNonNull(getClass() + .getClassLoader() + .getResource("exitStatus1")) + .getPath(); + + // when + Integer exitStatus = VRunner.readExitStatusFromFile(resource); + // then + assertThat(exitStatus).isEqualTo(1); + + } + + @Test + void readExitStatusFromFile_does_not_exist() { + + // given + String resource = "exitStatusDoesNotExist"; + + // when + Integer exitStatus = VRunner.readExitStatusFromFile(resource); + // then + assertThat(exitStatus).isEqualTo(1); + + } + + @Test + void appendV8Version_appends_parameter() { + + // given + String command = "vrunner xunit --ibconnection \"/F./build/ib\""; + + // when + String result = VRunner.appendV8Version(command, "8.3.21.1644"); + + // then + assertThat(result).isEqualTo("vrunner xunit --ibconnection \"/F./build/ib\" --v8version 8.3.21.1644"); + } + + @Test + void appendV8Version_returns_command_when_v8version_is_null() { + + // given + String command = "vrunner xunit"; + + // when + String result = VRunner.appendV8Version(command, null); + + // then + assertThat(result).isEqualTo(command); + } + + @Test + void appendV8Version_returns_command_when_v8version_is_blank() { + + // given + String command = "vrunner xunit"; + + // when + String result = VRunner.appendV8Version(command, " "); + + // then + assertThat(result).isEqualTo(command); + } + + @Test + void appendV8Version_does_not_duplicate_parameter() { + + // given + String command = "vrunner xunit --v8version 8.3.21.1644"; + + // when + String result = VRunner.appendV8Version(command, "8.3.25.1299"); + + // then + assertThat(result).isEqualTo(command); + } + + @Test + void readExitStatusFromFile_does_not_exist_uses_provided_fallback() { + + // given + String resource = "exitStatusDoesNotExist"; + + // when + Integer exitStatus = VRunner.readExitStatusFromFile(resource, 0); + // then + assertThat(exitStatus).isEqualTo(0); + + } +} + diff --git a/test/unit/resources/exitStatus0 b/test/unit/resources/exitStatus0 new file mode 100644 index 00000000..218a95f5 --- /dev/null +++ b/test/unit/resources/exitStatus0 @@ -0,0 +1 @@ +0 diff --git a/test/unit/resources/exitStatus1 b/test/unit/resources/exitStatus1 new file mode 100644 index 00000000..7bcb5fb6 --- /dev/null +++ b/test/unit/resources/exitStatus1 @@ -0,0 +1 @@ +1 diff --git a/test/unit/resources/initInfoBaseRunMigration.json b/test/unit/resources/initInfoBaseRunMigration.json new file mode 100644 index 00000000..1d0ae24d --- /dev/null +++ b/test/unit/resources/initInfoBaseRunMigration.json @@ -0,0 +1,11 @@ +{ + "stages": { + "initSteps": true + }, + "initInfobase": { + "runMigration": true, + "additionalInitializationSteps": [ + "vanessa --settings ./tools/vrunner.first.json" + ] + } +} diff --git a/vars/applyDebugOverridesIfNeeded.groovy b/vars/applyDebugOverridesIfNeeded.groovy new file mode 100644 index 00000000..d493774b --- /dev/null +++ b/vars/applyDebugOverridesIfNeeded.groovy @@ -0,0 +1,99 @@ +import ru.pulsar.jenkins.library.ioc.ContextRegistry +import ru.pulsar.jenkins.library.utils.DebugOverrides + +boolean call() { + ContextRegistry.registerDefaultContext(this) + + String profileKey = DebugOverrides.resolveProfileKey(env.JOB_NAME as String) + if (!profileKey) { + echo 'Debug overrides: profile key is unavailable, skip' + return false + } + + echo "Debug overrides: resolved profile key = ${profileKey}" + + Map controlConfig = loadControlConfig() + if (controlConfig == null) { + return false + } + + Map profile = controlConfig.profiles?."${profileKey}" as Map + if (profile == null) { + echo "Debug overrides: profile ${profileKey} not found, skip" + return false + } + + if (!(profile.enabled as boolean)) { + echo "Debug overrides: profile ${profileKey} is disabled, skip" + return false + } + + try { + DebugOverrides.validateDebugProfile(profile) + } catch (IllegalArgumentException exception) { + echo "Debug overrides: invalid profile ${profileKey}, skip (${exception.message})" + return false + } + + List replacements = profile.replacements as List + echo "Debug overrides: applying ${replacements.size()} replacement(s)" + + List> entries = DebugOverrides.buildConfigFileProviderEntries(replacements) + List downstreamTargets = [] + + configFileProvider(entries.collect { configFile(fileId: it.fileId, variable: it.variable) }) { + replacements.eachWithIndex { replacement, index -> + String target = DebugOverrides.normalizeTarget(replacement.target.toString()) + String sourcePath = env[entries[index].variable] + String parentPath = DebugOverrides.parentPath(target) + + if (parentPath) { + createDir(parentPath) + } + + writeFile file: target, text: readFile(file: sourcePath, encoding: 'UTF-8'), encoding: 'UTF-8' + echo "Debug overrides: wrote ${target} from managed file ${replacement.fileId}" + } + + downstreamTargets = DebugOverrides.collectDownstreamTargets(replacements) + } + + if (!downstreamTargets.isEmpty()) { + stash name: DebugOverrides.STASH_NAME, includes: DebugOverrides.buildStashIncludes(downstreamTargets) + echo 'Debug overrides: stashed files for downstream agents' + } + + return true +} + +private Map loadControlConfig() { + try { + Map controlConfig + + configFileProvider([ + configFile(fileId: DebugOverrides.CONTROL_FILE_ID, variable: DebugOverrides.CONTROL_FILE_VARIABLE) + ]) { + String controlPath = env[DebugOverrides.CONTROL_FILE_VARIABLE] + controlConfig = readJSON(file: controlPath) as Map + } + + return controlConfig + } catch (Exception exception) { + if (DebugOverrides.shouldTreatConfigFileProviderErrorAsMissingPlugin(exception)) { + echo 'Debug overrides: Config File Provider plugin is unavailable, skip' + return null + } + + if (DebugOverrides.shouldTreatConfigFileProviderErrorAsMissingControlFile(exception)) { + echo 'Debug overrides: control file is unavailable, skip' + return null + } + + if (DebugOverrides.shouldTreatConfigFileProviderErrorAsInvalidControlFile(exception)) { + echo "Debug overrides: control file is invalid, skip (${exception.message})" + return null + } + + throw exception + } +} diff --git a/vars/createDir.groovy b/vars/createDir.groovy index 56e83da5..739bd677 100644 --- a/vars/createDir.groovy +++ b/vars/createDir.groovy @@ -1,3 +1,12 @@ -def call(String path) { - dir(path) { echo '' } +def call(String path, boolean cleanDir = false) { + if (cleanDir && fileExists(path)) { + dir(path) { + deleteDir() + } + } + if (isUnix()) { + sh "mkdir -p '${path}'" + } else { + bat "@if not exist \"${path}\" mkdir \"${path}\"" + } } diff --git a/vars/initInfobase.groovy b/vars/initInfobase.groovy index bb6bdd8e..932b3276 100644 --- a/vars/initInfobase.groovy +++ b/vars/initInfobase.groovy @@ -6,5 +6,11 @@ def call(JobConfiguration config) { ContextRegistry.registerDefaultContext(this) def initInfobase = new InitInfoBase(config) - initInfobase.run() + try { + initInfobase.run() + return true + } catch (Exception e) { + unstable("Инициализация ИБ не выполнена: ${e.getMessage()}") + return false + } } \ No newline at end of file diff --git a/vars/jobConfiguration.groovy b/vars/jobConfiguration.groovy index 96215f72..eddd0e1c 100644 --- a/vars/jobConfiguration.groovy +++ b/vars/jobConfiguration.groovy @@ -7,9 +7,9 @@ JobConfiguration call(String path = "jobConfiguration.json") { if (fileExists(path)) { def config = readFile(path) - return ConfigurationReader.create(config) + return ContextRegistry.registerJobConfiguration(ConfigurationReader.create(config)) } else { - return ConfigurationReader.create() + return ContextRegistry.registerJobConfiguration(ConfigurationReader.create()) } -} \ No newline at end of file +} diff --git a/vars/pipeline1C.groovy b/vars/pipeline1C.groovy index 70d40138..f27c5ab7 100644 --- a/vars/pipeline1C.groovy +++ b/vars/pipeline1C.groovy @@ -15,6 +15,10 @@ String agent1C @Field String agentEdt +@Field +// Флаг, указывающий на успешность инициализации информационной базы +Boolean isInfobaseInitialized = true + void call() { //noinspection GroovyAssignabilityCheck @@ -38,6 +42,7 @@ void call() { steps { script { + applyDebugOverridesIfNeeded() config = jobConfiguration() as JobConfiguration agent1C = config.v8AgentLabel() agentEdt = config.edtAgentLabel() @@ -48,7 +53,7 @@ void call() { stage('Подготовка') { parallel { - stage('Подготовка 1C базы') { + stage('Подготовка 1С базы') { when { beforeAgent true expression { config.stageFlags.needInfoBase() } @@ -70,7 +75,7 @@ void call() { } } - stage('Подготовка 1С базы') { + stage('Подготовка ИБ') { agent { label agent1C } @@ -81,6 +86,7 @@ void call() { expression { config.needLoadExtensions() } } steps { + restoreDebugOverridesIfNeeded() timeout(time: config.timeoutOptions.getBinaries, unit: TimeUnit.MINUTES) { createDir('build/out/cfe') // Соберем или загрузим cfe из исходников и положим их в папку build/out/cfe @@ -113,7 +119,7 @@ void call() { } } - stage('Загрузка расширений в конфигурацию'){ + stage('Загрузка расширений в конфигурацию') { when { beforeAgent true expression { config.needLoadExtensions() } @@ -133,7 +139,9 @@ void call() { steps { timeout(time: config.timeoutOptions.initInfoBase, unit: TimeUnit.MINUTES) { // Инициализация и первичная миграция - initInfobase config + script { + isInfobaseInitialized = initInfobase config + } } } } @@ -209,11 +217,12 @@ void call() { } when { beforeAgent true - expression { config.stageFlags.bdd } + expression { config.stageFlags.bdd && isInfobaseInitialized } } stages { stage('Распаковка ИБ') { steps { + restoreDebugOverridesIfNeeded() unzipInfobase() } } @@ -261,6 +270,7 @@ void call() { stages { stage('Распаковка ИБ') { steps { + restoreDebugOverridesIfNeeded() unzipInfobase() } } @@ -281,11 +291,12 @@ void call() { } when { beforeAgent true - expression { config.stageFlags.smoke } + expression { config.stageFlags.smoke && isInfobaseInitialized } } stages { stage('Распаковка ИБ') { steps { + restoreDebugOverridesIfNeeded() unzipInfobase() } } @@ -318,11 +329,12 @@ void call() { } when { beforeAgent true - expression { config.stageFlags.yaxunit } + expression { config.stageFlags.yaxunit && isInfobaseInitialized } } stages { stage('Распаковка ИБ') { steps { + restoreDebugOverridesIfNeeded() unzipInfobase() } } @@ -360,6 +372,7 @@ void call() { expression { config.stageFlags.sonarqube } } steps { + restoreDebugOverridesIfNeeded() timeout(time: config.timeoutOptions.sonarqube, unit: TimeUnit.MINUTES) { sonarScanner config } diff --git a/vars/restoreDebugOverridesIfNeeded.groovy b/vars/restoreDebugOverridesIfNeeded.groovy new file mode 100644 index 00000000..db1d1475 --- /dev/null +++ b/vars/restoreDebugOverridesIfNeeded.groovy @@ -0,0 +1,19 @@ +import ru.pulsar.jenkins.library.ioc.ContextRegistry +import ru.pulsar.jenkins.library.utils.DebugOverrides + +boolean call() { + ContextRegistry.registerDefaultContext(this) + + try { + unstash DebugOverrides.STASH_NAME + echo 'Debug overrides: restored files from stash' + return true + } catch (Exception exception) { + if (DebugOverrides.shouldTreatUnstashErrorAsMissingStash(exception)) { + echo 'Debug overrides: downstream stash is absent, skip restore' + return false + } + + throw exception + } +}