Всем привет! Меня зовут Павел, я технический лидер тестирования в Альфа-Банке в направлении мобильной разработки.

Через плагин withAllureUpload для Jenkins нельзя из нескольких джоб залить отчёт автотестов в один запуск TestOps. Готовых решений в интернете не нашёл, и даже поддержка TestOps не смогла нормально подсказать, как из нескольких Jenkins Jobs отправлять результат в один запуск TestOps. Методом проб и ошибок это сделать удалось — в статье расписал решения, как это работает через терминальную программу.

Относительно недавно, в начале года Банк переехал на новую ТМС Allure Testops. Автотесты (сейчас их >200) запускаются через CI Jenkins, и запускаются параллельно на нескольких девайсах одновременно.

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

Изначально решили скопировать все результаты нисходящих джоб в восходящую и уже из одной Jenkins джобы отправить результаты в один запуск TestOps. Но столкнулись с такой проблемой, что дефолтный плагин TestOps withAllureUpload так не работает. Вместо этого мы получали пустой запуск без результатов.

Далее попробовали передавать параметр ALLURE_JOB_RUN_ID. (этот параметр передаётся от TestOps в Jenkins джобу при создании джобы из TestOps) из восходящей джобы в нисходящую, а уже из нисходящих джоб отправлять через плагин withAllureUpload. Но нисходящая джоба даже не запускалась, а падала с ошибкой. Потому что плагин создаёт новый объект, а если объект уже есть, то плагин падает с ошибкой.

После поисков везде, где только возможно, и множества попыток, решение было найдено в документации самого TestOps. Делается это через allurectl — приложение командной строки. Но предыдущие неудачные попытки также принесли свои плоды. Смогли проработать два решения отправки результатов автотестов в один агрегированный запуск TestOps из разных Jenkins джоб, которые описал ниже.

Способ №1. Отправка результатов из нисходящих Jenkins джоб в один агрегированный запуск TestOps

Этот способ подходит, когда вы создаёте запуск из TestOps и в восходящей Jenkins джобе есть параметр ALLURE_JOB_RUN_ID., который передаётся нисходящим джобам. Таким образом, каждая нисходящая джоба может передавать свои результаты автотестов сразу после своего завершения. Однако при таком флоу в нисходящих Jenkins джобах не будет генерироваться ссылка на запуск TestOps, как показано на скринах ниже.

Настройки восходящей джобы

В pipeline восходящей джобы нужно при запуске нисходящей джобе передать параметр ALLURE_JOB_RUN_ID..

stage('Android Tests SmzUserGoToSmzScreenFromMainPageTest') {
            steps {
                script {
                    smzBuild1 = build job: params.DOWNSTREAM_JOB,
                        parameters: [
                            string(name: 'branchName', value: 'master'),
                            string(name: 'reportPortalTags', value: 'scheduledRegressLaunch'),
                            string(name: 'platform', value: 'ANDROID'),
                            string(name: 'selenoidDeviceName', value: 'Samsung Galaxy A55 5G,Xiaomi Redmi Note 13 Pro,Google Pixel 4a,Samsung Galaxy S21+ 5G'),
                            string(name: 'selenoidEmulatorVersion', value: '13.0'),
                            string(name: 'isRetryEnable', value: 'false'),
                            string(name: 'useLatestRCBuild', value: 'true'),
                            string(name: 'rpLaunchName', value: 'AM.Smz_entry_point_cycle'),
                            string(name: 'runTest', value: 'SmzUserGoToSmzScreenFromMainPageTest'),
                            string(name: 'runInSelenoid', value: 'true'),
                            string(name: 'ALLURE_JOB_RUN_ID', value: "${params.ALLURE_JOB_RUN_ID}") //Передаём., как параметр ALLURE_JOB_RUN_ID
                        ],
                        propagate: false
 
                    echo "SMZ build number - ${smzBuild1.number}" // Исправлена переменная
                }
            }
        }

Настройки нисходящей джобы

Состоит из двух шагов:

№1. Сначала скачиваем приложение командной строки allurectl.

stage('Install allurectl') {
            steps {
                script {
                    sh '''
                        if ! command -v allurectl &> /dev/null; then
                            echo "Installing allurectl..."
                            curl -sL https://github.com/allure-framework/allurectl/releases/latest/download/allurectl_linux_amd64 -o allurectl
                            chmod +x allurectl
                            ./allurectl --version
                        else
                            echo "allurectl already installed"
                        fi
                    '''
                }
            }
        }

№2. Затем настраиваем отправку результатов автотестов в агрегированный запуск TestOps.

stage('Running tests') {
            steps {
                script {
                    // Дефолтное значение для запуска всех тестов в проекте
                    def testToRun = "ru.testing.*"
                    println("Chosen test parameter as ${params.runTest}")
                    if ("${params.runTest}" != "") {
                        // Для запуска конкретного теста
                        testToRun = "${params.runTest}"
                    } else if ("${params.runCycle}" == FULL_REGRESS) {
                        // Для запуска всех регрессных тестов
                        testToRun = "ru.testing.regress.*"
                        gradleArgs.remove("-Dorg.gradle.project.tags=FULL_REGRESS")
                    }
 
                    withCredentials([string(credentialsId: 'testops-token', variable: 'ALLURE_TOKEN')]) {
                        sh """
                            # Запуск тестов с мониторингом через allurectl
                            ./allurectl watch \
                                --endpoint https://testops \
                                --project-id 4 \
                                --launch-name "AM.Regress-#${env.BUILD_NUMBER}" \
                                --launch-tags "Jenkins_Android,regression" \
                                --results build/allure-results -- ./gradlew --info clean test ${gradleArgs.join(' ')} --tests "${testToRun}"
                        """
                    }
 
                }
            }
            post {
                always {
                    allure includeProperties: false,
                            jdk: '',
                            results: [[path: 'build/allure-results']]
                }
            }
        }

Так как мы передали параметр ALLURE_JOB_RUN_ID. нисходящей джобе, allurectl не будет создавать новый объект, а запушит отчёты автотестов в уже существующий запуск TestOps.

Полезная ссылка: официальная документация TestOps.

Способ №2. Отправка результатов из восходящей Jenkins джобы в один агрегированный запуск TestOps

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

Поэтому флоу для такой передачи будет несколько иным. Также для этого способа нужен установленный в Jenkins плагин copyArtifacts, чтобы можно было копировать результаты нисходящих джоб в восходящую.

Настройка восходящей джобы

В восходящей джобе, как и в прошлом решении, не будет сгенерирована ссылка на запуск TestOps.

№1. Добавляем в pipeline запуск нисходящих джоб.

  stage("Android Tests ${cycle}") {
     downstreamJob = build job: params.DOWNSTREAM_JOB,
         parameters: [
             string(name: 'branchName', value: 'master'),
             string(name: 'platform', value: 'ANDROID'),
             string(name: 'selenoidDeviceName', value: selenoidDeviceNameList),
             string(name: 'selenoidEmulatorVersion', value: '13.0'),
             booleanParam(name: 'isRetryEnable', value: 'false'),
             string(name: 'useLatestRCBuild', value: 'true'),
             string(name: 'rpLaunchName', value: cycle),
             string(name: 'runCycle', value: cycle),
             string(name: 'runTest', value: ''),
             string(name: 'runInSelenoid', value: 'true')
         ],
         propagate: false
  
  
     // Проверка на null перед использованием
     if (downstreamJob?.number) {
         echo "Build number for ${cycle}: ${downstreamJob.number}"
     } else {
         echo "Build for ${cycle} failed to start"
     }
  }

№2. Вторым пунктом добавляем шаг сбора логов из нисходящих джоб.

stage("Copy Results From ${cycle}") {
    script {
      if (downstreamJob?.number) {
        copyArtifacts(
          projectName: params.DOWNSTREAM_JOB,
          selector: specific("${downstreamJob.number}"),
          filter: 'build/allure-results/**/*',
          target: ".",
          flatten: false
        )
          sh "ls -la build/allure-results/"
        } else {
        echo "No artifacts to copy for ${cycle}"
        }
    }
  }

№3. Устанавливаем приложение командной строки allurectl.

stages {
        stage('Install allurectl') {
            steps {
                script {
                    sh """
                        if ! command -v ./allurectl &> /dev/null; then
                            echo "Installing allurectl..."
                            curl -sL https://github.com/allure-framework/allurectl/releases/latest/download/allurectl_linux_amd64 -o allurectl
                            chmod +x allurectl
                            ./allurectl --version
                        else
                            echo "allurectl already installed"
                        fi
                    """
                }
            }
        }

Этот шаг может быть любым по счету, главное, чтобы он выполнялся до отправки результатов в TestOps. Здесь уже кому как больше нравится.

№4. И, наконец, отправляем результаты в TestOps.

  stage("Push report ${cycle}") {
    script {
      if (downstreamJob?.number) {
        withCredentials([string(credentialsId: 'testops-token', variable: 'ALLURE_TOKEN')]) {
          def uploadOutput = sh(
            script: """
              ./allurectl upload \
              --endpoint https://testops \
              --project-id 4 \
              --launch-name "AM ANDROID Regress - #${env.BUILD_NUMBER}" \
              --launch-tags "Android,AM,regress" \
              "build/allure-results"
           """,
            returnStdout: true
          ).trim()
            if (uploadOutput =~ /Report link: /) {
              reportUrl = (uploadOutput =~ /Report link: (https?:\/\/\S+)/)[0][1]
              }
          currentBuild.description = "Ссылка на запуск Allure TestOps - ${reportUrl}"
          }
        sh "rm -f build/allure-results/*" //очищаем директорию с результатами
        }
    }
  }

[!NOTE]
Обратите внимание, что при отправке отчетов без запуска автотестов нужно использовать команду upload, а не watch, как в способе №1

[ВАЖНО!]
После отправки результатов в TestOps обязательно очищайте директорию build/allure‑results, иначе в TestOps будут дублироваться результаты автотестов!

Настройка нисходящей джобы

В нисходящей джобе нужно добавить в pipeline только архивирование сырых результатов allure после автотестов.

post {
   always {
       // Архивируем сырые результаты для копирования
       archiveArtifacts(
              artifacts: 'build/allure-results/**/*',
              fingerprint: true,
              allowEmptyArchive: true
       )
       allure includeProperties: false,
               jdk: '',
               results: [[path: 'build/allure-results']]
   }
}

Также у нисходящих джоб будет работать плагин withAllureUpload, и результаты каждой нисходящей джобы также можно отправлять в TestOps отдельным запуском! Здесь уже как вам нужно и/или как нравится.

Полезные ссылки:

Подведение итогов

Есть как минимум два способа отправить результаты автотестов из нескольких Jenkins джоб в один агрегированный запуск TestOps. Надеюсь статья будет полезной. Спасибо за внимание и до новых встреч!!!

Комментарии (0)