ci: stabilize Jenkins jobs and mobile pipeline

This commit is contained in:
2026-04-22 11:27:23 +03:00
parent caf057d712
commit 737bddd631
23 changed files with 762 additions and 66 deletions
+8 -1
View File
@@ -7,6 +7,11 @@
compose_dir: "{{ hw8_root }}/compose"
compose_file: "{{ compose_dir }}/docker-compose.yml"
compose_env_file: "{{ compose_dir }}/.env"
jobs_profile_effective: "{{ jobs_profile | default('full') }}"
jjb_paths:
full: /workspace/hw8/config/jobs
devops: /workspace/hw8/config/jobs-devops
jjb_upload_path: "{{ jjb_paths.get(jobs_profile_effective, jjb_paths.full) }}"
tasks:
- name: Check Docker CLI availability
@@ -89,7 +94,7 @@
- name: Upload Jenkins jobs via JJB container
ansible.builtin.command:
cmd: timeout 900 docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} run --rm jobs_uploader
cmd: timeout 900 docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} run --rm -e JJB_PATH={{ jjb_upload_path }} jobs_uploader
args:
chdir: "{{ compose_dir }}"
@@ -98,3 +103,5 @@
msg:
- "Jenkins UI: http://localhost:8088 (via nginx) or http://localhost:8081"
- "Registry: http://localhost:5005/v2/"
- "JJB profile: {{ jobs_profile_effective }}"
- "JJB path: {{ jjb_upload_path }}"
+7 -5
View File
@@ -33,7 +33,7 @@ services:
- jenkins_home:/var/jenkins_home
- ./jenkins/casc:/var/jenkins_home/casc_configs:ro
- /var/run/docker.sock:/var/run/docker.sock
- ../..:/workspace/otus-autotests:ro
- ..:/workspace/hw8:ro
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/login >/dev/null"]
interval: 10s
@@ -67,9 +67,9 @@ services:
JENKINS_HOSTNAME: http://jenkins:8080
JENKINS_USERNAME: ${JENKINS_ADMIN_ID}
JENKINS_PASSWORD: ${JENKINS_ADMIN_PASSWORD}
JJB_PATH: /workspace/otus-autotests/hw8/config/jobs
JJB_PATH: /workspace/hw8/config/jobs
volumes:
- ../..:/workspace/otus-autotests:ro
- ..:/workspace/hw8:ro
networks:
- jenkins_net
@@ -87,9 +87,10 @@ services:
JENKINS_AGENT_WORKDIR: /home/jenkins/agent
JENKINS_WEB_SOCKET: "true"
JENKINS_LABELS: maven docker
HW8_ROOT: /workspace/hw8
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ../..:/workspace/otus-autotests:ro
- ..:/workspace/hw8:ro
networks:
- jenkins_net
@@ -107,9 +108,10 @@ services:
JENKINS_AGENT_WORKDIR: /home/jenkins/agent
JENKINS_WEB_SOCKET: "true"
JENKINS_LABELS: jjb docker
HW8_ROOT: /workspace/hw8
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ../..:/workspace/otus-autotests:ro
- ..:/workspace/hw8:ro
networks:
- jenkins_net
+1
View File
@@ -8,6 +8,7 @@ RUN apt-get update \
ca-certificates \
curl \
docker.io \
git \
unzip \
&& rm -rf /var/lib/apt/lists/*
+51 -5
View File
@@ -1,9 +1,55 @@
FROM maven:3.9.11-eclipse-temurin-21
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
chromium \
chromium-driver \
&& rm -rf /var/lib/apt/lists/*
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
ca-certificates \
curl \
unzip \
fontconfig \
fonts-liberation \
libasound2t64 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libc6 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgbm1 \
libgcc1 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libstdc++6 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxss1 \
libxtst6 \
xdg-utils; \
CHROME_VERSION="$(curl -fsSL https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE)"; \
curl -fsSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chrome-linux64.zip" -o /tmp/chrome.zip; \
curl -fsSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" -o /tmp/chromedriver.zip; \
unzip -q /tmp/chrome.zip -d /opt; \
unzip -q /tmp/chromedriver.zip -d /opt; \
ln -sf /opt/chrome-linux64/chrome /usr/bin/google-chrome; \
ln -sf /opt/chrome-linux64/chrome /usr/bin/chromium; \
ln -sf /opt/chromedriver-linux64/chromedriver /usr/bin/chromedriver; \
chmod +x /opt/chrome-linux64/chrome /opt/chromedriver-linux64/chromedriver; \
rm -f /tmp/chrome.zip /tmp/chromedriver.zip; \
rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
+1 -1
View File
@@ -30,7 +30,7 @@ jenkins:
- key: JENKINS_URL_INTERNAL
value: "http://jenkins:8080"
- key: OTUS_WORKSPACE_ROOT
value: "/workspace/otus-autotests"
value: "/workspace/hw8"
- key: MOBILE_DB_PASSWORD
value: "${MOBILE_DB_PASSWORD}"
+2 -2
View File
@@ -24,7 +24,7 @@ services:
- EMULATOR_PARAMS=-no-window -no-audio -gpu swiftshader_indirect -no-snapshot -no-boot-anim
shm_size: 2gb
healthcheck:
test: ["CMD-SHELL", "[ \"$(cat /home/androidusr/device_status 2>/dev/null)\" = \"READY\" ]"]
test: ["CMD-SHELL", "[ \"$(cat /home/androidusr/device_status 2>/dev/null)\" = \"READY\" ] && wget -qO- http://127.0.0.1:4723/status >/dev/null 2>&1"]
interval: 15s
timeout: 5s
retries: 40
@@ -45,7 +45,7 @@ services:
- EMULATOR_PARAMS=-no-window -no-audio -gpu swiftshader_indirect -no-snapshot -no-boot-anim
shm_size: 2gb
healthcheck:
test: ["CMD-SHELL", "[ \"$(cat /home/androidusr/device_status 2>/dev/null)\" = \"READY\" ]"]
test: ["CMD-SHELL", "[ \"$(cat /home/androidusr/device_status 2>/dev/null)\" = \"READY\" ] && wget -qO- http://127.0.0.1:4723/status >/dev/null 2>&1"]
interval: 15s
timeout: 5s
retries: 40
+6
View File
@@ -0,0 +1,6 @@
---
- defaults:
name: global
project_folder: /workspace/hw8
test_image_tag: "1.0.0"
build_keep: 40
@@ -0,0 +1,11 @@
---
- job:
name: infra-health-check
description: "Проверка инфраструктуры Jenkins/Registry/Agent образов."
project-type: pipeline
concurrent: false
sandbox: true
properties:
- build-discarder:
num-to-keep: 30
dsl: !include-raw-verbatim: ../../jobs/scripts/infra-health-check.groovy
@@ -0,0 +1,16 @@
---
- job:
name: jobs-uploader
description: "Обновляет Jenkins jobs из JJB YAML (config/jobs)."
project-type: pipeline
concurrent: false
sandbox: true
properties:
- build-discarder:
num-to-keep: 30
parameters:
- string:
name: JJB_PATH
default: /workspace/hw8/config/jobs
description: "Путь до JJB-конфигов"
dsl: !include-raw-verbatim: ../../jobs/scripts/jobs-uploader.groovy
+18
View File
@@ -0,0 +1,18 @@
---
- view:
name: DevOps
view-type: list
description: "Инфраструктурные job"
filter-executors: true
filter-queue: true
job-name:
- jobs-uploader
- infra-health-check
columns:
- status
- weather
- job
- last-success
- last-failure
- last-duration
- build-button
+1 -1
View File
@@ -1,6 +1,6 @@
---
- defaults:
name: global
project_folder: /workspace/otus-autotests
project_folder: /workspace/hw8
test_image_tag: "1.0.0"
build_keep: 40
+15 -4
View File
@@ -5,6 +5,18 @@ pipeline {
ansiColor('xterm')
}
stages {
stage('Prepare Metadata') {
steps {
script {
wrap([$class: 'BuildUser']) {
env.RUN_TRIGGER_USER = env.BUILD_USER_ID ?: env.BUILD_USER ?: 'system'
env.RUN_TRIGGER_NAME = env.BUILD_USER ?: env.BUILD_USER_ID ?: 'system'
}
currentBuild.displayName = "#${env.BUILD_NUMBER} infra"
currentBuild.description = "by=${env.RUN_TRIGGER_NAME}"
}
}
}
stage('Check Docker & Registry') {
steps {
sh '''
@@ -17,13 +29,12 @@ pipeline {
'''
}
}
stage('Check Sources') {
stage('Check Job Configs') {
steps {
sh '''
set -eux
test -f /workspace/otus-autotests/homework_4/pom.xml
test -f /workspace/otus-autotests/hw7/pom.xml
test -f /workspace/otus-autotests/hw8/config/jobs/global.yaml
HW8_ROOT_PATH="${HW8_ROOT:-/workspace/hw8}"
test -f "${HW8_ROOT_PATH}/config/jobs/global.yaml"
'''
}
}
+12
View File
@@ -5,6 +5,18 @@ pipeline {
ansiColor('xterm')
}
stages {
stage('Prepare Metadata') {
steps {
script {
wrap([$class: 'BuildUser']) {
env.RUN_TRIGGER_USER = env.BUILD_USER_ID ?: env.BUILD_USER ?: 'system'
env.RUN_TRIGGER_NAME = env.BUILD_USER ?: env.BUILD_USER_ID ?: 'system'
}
currentBuild.displayName = "#${env.BUILD_NUMBER} jjb"
currentBuild.description = "by=${env.RUN_TRIGGER_NAME}; jjbPath=${params.JJB_PATH}"
}
}
}
stage('Validate Input') {
steps {
sh '''
+30 -2
View File
@@ -5,19 +5,47 @@ pipeline {
ansiColor('xterm')
}
stages {
stage('Prepare Metadata') {
steps {
script {
wrap([$class: 'BuildUser']) {
env.RUN_TRIGGER_USER = env.BUILD_USER_ID ?: env.BUILD_USER ?: 'system'
env.RUN_TRIGGER_NAME = env.BUILD_USER ?: env.BUILD_USER_ID ?: 'system'
}
currentBuild.displayName = "#${env.BUILD_NUMBER} api"
currentBuild.description =
"by=${env.RUN_TRIGGER_NAME}; repo=${params.QA_REPO_URL}; ref=${params.QA_REPO_REF}"
}
}
}
stage('Run API Tests In Docker') {
steps {
sh '''
set -eux
rm -rf ./sources
git clone "${QA_REPO_URL}" ./sources
git -C ./sources checkout "${QA_REPO_REF}"
GIT_SHA="$(git -C ./sources rev-parse --short HEAD)"
rm -rf ./artifacts
mkdir -p ./artifacts
{
echo "job=${JOB_NAME}"
echo "build=${BUILD_NUMBER}"
echo "trigger_user=${RUN_TRIGGER_USER}"
echo "trigger_name=${RUN_TRIGGER_NAME}"
echo "qa_repo_url=${QA_REPO_URL}"
echo "qa_repo_ref=${QA_REPO_REF}"
echo "qa_repo_sha=${GIT_SHA}"
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
} > ./artifacts/run-info.txt
CID="$(docker create localhost:5005/otus/test-api:1.0.0 bash -lc "set -e; cd /workspace; mvn -f citrus-tests/pom.xml test")"
cleanup_container() {
docker rm -f "${CID}" >/dev/null 2>&1 || true
}
trap cleanup_container EXIT INT TERM
tar -C /workspace/otus-autotests/homework_4 -cf - . | docker cp - "${CID}:/workspace"
tar -C ./sources -cf - . | docker cp - "${CID}:/workspace"
set +e
docker start -a "${CID}"
TEST_RC=$?
@@ -32,7 +60,7 @@ pipeline {
post {
always {
junit allowEmptyResults: true, testResults: 'artifacts/citrus-target/surefire-reports/*.xml'
archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/citrus-target/**'
archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/run-info.txt,artifacts/citrus-target/**'
}
}
}
+308 -18
View File
@@ -5,13 +5,61 @@ pipeline {
ansiColor('xterm')
}
stages {
stage('Prepare Metadata') {
steps {
script {
wrap([$class: 'BuildUser']) {
env.RUN_TRIGGER_USER = env.BUILD_USER_ID ?: env.BUILD_USER ?: 'system'
env.RUN_TRIGGER_NAME = env.BUILD_USER ?: env.BUILD_USER_ID ?: 'system'
}
currentBuild.displayName = "#${env.BUILD_NUMBER} mobile"
currentBuild.description =
"by=${env.RUN_TRIGGER_NAME}; repo=${params.MOBILE_REPO_URL}; ref=${params.MOBILE_REPO_REF}; emulators=${params.MOBILE_MAX_EMULATORS}; junit=${params.JUNIT_PARALLELISM}; order=${params.TEST_CLASSES_ORDER}; rerun=${params.SUREFIRE_RERUN_FAILING}"
}
}
}
stage('Prepare Workspace') {
steps {
sh '''
set -eux
rm -rf ./project
mkdir -p ./project
cp -a /workspace/otus-autotests/hw7/. ./project/
git clone "${MOBILE_REPO_URL}" ./project
git -C ./project checkout "${MOBILE_REPO_REF}"
GIT_SHA="$(git -C ./project rev-parse --short HEAD)"
if ! grep -q "<artifactId>allure-junit5</artifactId>" ./project/pom.xml; then
awk -v ver="${ALLURE_ADAPTER_VERSION}" '
index($0, "</dependencies>") && !done {
print " <dependency>"
print " <groupId>io.qameta.allure</groupId>"
print " <artifactId>allure-junit5</artifactId>"
print " <version>" ver "</version>"
print " <scope>test</scope>"
print " </dependency>"
done=1
}
{ print }
' ./project/pom.xml > ./project/pom.xml.tmp
mv ./project/pom.xml.tmp ./project/pom.xml
fi
mkdir -p ./project/target/allure-results
{
echo "job=${JOB_NAME}"
echo "build=${BUILD_NUMBER}"
echo "trigger_user=${RUN_TRIGGER_USER}"
echo "trigger_name=${RUN_TRIGGER_NAME}"
echo "mobile_repo_url=${MOBILE_REPO_URL}"
echo "mobile_repo_ref=${MOBILE_REPO_REF}"
echo "mobile_repo_sha=${GIT_SHA}"
echo "app_url=${APP_URL}"
echo "mobile_max_emulators=${MOBILE_MAX_EMULATORS}"
echo "junit_parallelism=${JUNIT_PARALLELISM}"
echo "test_classes_order=${TEST_CLASSES_ORDER}"
echo "surefire_rerun_failing=${SUREFIRE_RERUN_FAILING}"
echo "db_url=${DB_URL}"
echo "db_user=${DB_USER}"
echo "reservation_owner=${RESERVATION_OWNER}"
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
} > ./project/run-info.txt
'''
}
}
@@ -20,7 +68,8 @@ pipeline {
dir('project') {
sh '''
set -eux
COMPOSE_FILE="/workspace/otus-autotests/hw8/config/compose/mobile-ci.compose.yml"
HW8_ROOT_PATH="${HW8_ROOT:-/workspace/hw8}"
COMPOSE_FILE="${HW8_ROOT_PATH}/config/compose/mobile-ci.compose.yml"
if docker compose version >/dev/null 2>&1; then
compose_cmd() { PROJECT_DIR="$PWD" docker compose -f "${COMPOSE_FILE}" "$@"; }
elif docker-compose version >/dev/null 2>&1; then
@@ -30,11 +79,32 @@ pipeline {
exit 1
fi
export COMPOSE_PROJECT_NAME=mobileci
compose_cmd down -v --remove-orphans || true
compose_cmd up -d wiremock android-emulator-1 android-emulator-2
MAX_EMULATORS="${MOBILE_MAX_EMULATORS:-1}"
if ! [ "${MAX_EMULATORS}" -eq "${MAX_EMULATORS}" ] 2>/dev/null; then
echo "Invalid MOBILE_MAX_EMULATORS='${MAX_EMULATORS}', expected integer"
exit 1
fi
if [ "${MAX_EMULATORS}" -lt 1 ] || [ "${MAX_EMULATORS}" -gt 2 ]; then
echo "Invalid MOBILE_MAX_EMULATORS='${MAX_EMULATORS}', expected 1 or 2"
exit 1
fi
SERVICES="wiremock android-emulator-1"
if [ "${MAX_EMULATORS}" -ge 2 ]; then
SERVICES="${SERVICES} android-emulator-2"
fi
compose_cmd down -v --remove-orphans || true
compose_cmd up -d ${SERVICES}
EMULATORS=""
SELECTED=0
for service in android-emulator-1 android-emulator-2; do
if [ "${service}" = "android-emulator-2" ] && [ "${MAX_EMULATORS}" -lt 2 ]; then
continue
fi
if [ "${SELECTED}" -ge "${MAX_EMULATORS}" ]; then
break
fi
cid="$(compose_cmd ps -q "$service")"
if [ -z "${cid}" ]; then
echo "Container for ${service} not found, skipping"
@@ -55,6 +125,7 @@ pipeline {
EMULATORS="${EMULATORS},"
fi
EMULATORS="${EMULATORS}${service}|http://${service}:4723|Android Emulator|${APP_URL}"
SELECTED=$((SELECTED + 1))
else
echo "Service ${service} is not healthy in time, excluding from test run"
docker logs "${cid}" || true
@@ -76,15 +147,74 @@ pipeline {
dir('project') {
sh '''
set -eux
rm -rf ./target
rm -rf ./target ./target-run
MOBILE_EMULATORS_VALUE="$(cat ./.mobile_emulators.txt)"
EMULATOR_ENTRIES_COUNT="$(printf '%s' "${MOBILE_EMULATORS_VALUE}" | awk -F, '{print NF}')"
DOCKER_NETWORK_ARG="--network mobileci_default"
if [ "${EMULATOR_ENTRIES_COUNT}" -eq 1 ]; then
SINGLE_SERVICE="$(printf '%s' "${MOBILE_EMULATORS_VALUE}" | cut -d'|' -f1)"
if [ "${SINGLE_SERVICE}" = "android-emulator-1" ] || [ "${SINGLE_SERVICE}" = "android-emulator-2" ]; then
SINGLE_EMULATOR_CID="$(docker ps -q --filter "name=mobileci-${SINGLE_SERVICE}" | head -n1 || true)"
if [ -n "${SINGLE_EMULATOR_CID}" ]; then
DOCKER_NETWORK_ARG="--network container:${SINGLE_EMULATOR_CID}"
MOBILE_EMULATORS_VALUE="$(printf '%s' "${MOBILE_EMULATORS_VALUE}" | awk -F'|' 'BEGIN { OFS="|" } { $2="http://127.0.0.1:4723"; print $1, $2, $3, $4 }')"
fi
fi
fi
DB_PASSWORD_EFFECTIVE="${DB_PASSWORD:-${MOBILE_DB_PASSWORD:-}}"
if [ -z "${DB_PASSWORD_EFFECTIVE}" ]; then
echo "DB password is not set. Provide DB_PASSWORD job parameter or MOBILE_DB_PASSWORD env variable."
exit 1
fi
# Validate Appium endpoints from the same Docker network as test container.
if ! docker run --rm \
${DOCKER_NETWORK_ARG} \
-e MOBILE_EMULATORS="${MOBILE_EMULATORS_VALUE}" \
localhost:5005/otus/test-mobile:1.0.0 \
bash -lc '
set -euo pipefail
IFS="," read -ra TARGETS <<< "${MOBILE_EMULATORS}"
for target in "${TARGETS[@]}"; do
endpoint="$(echo "${target}" | cut -d"|" -f2)"
if [ -z "${endpoint}" ]; then
echo "Invalid MOBILE_EMULATORS entry: ${target}"
exit 1
fi
ready="false"
for i in $(seq 1 60); do
if curl -fsS "${endpoint}/status" >/dev/null 2>&1; then
ready="true"
break
fi
sleep 2
done
if [ "${ready}" != "true" ]; then
echo "Appium endpoint is not reachable from test container: ${endpoint}"
exit 1
fi
done
'; then
echo "Appium preflight failed. Dumping emulator logs."
for service in android-emulator-1 android-emulator-2; do
cid="$(docker ps -q --filter "name=mobileci-${service}" | head -n1 || true)"
if [ -n "${cid}" ]; then
echo "--- logs for ${service} (${cid}) ---"
docker logs --tail 200 "${cid}" || true
fi
done
exit 1
fi
run_single_class() {
class_name="$1"
attempt="$2"
class_key="$(echo "${class_name}" | sed 's/[^a-zA-Z0-9._-]/_/g')"
attempt_dir="./target-run/${class_key}/attempt-${attempt}"
mkdir -p "${attempt_dir}"
CID="$(docker create \
--network mobileci_default \
${DOCKER_NETWORK_ARG} \
-e DB_URL="${DB_URL:-}" \
-e DB_USER="${DB_USER:-}" \
-e DB_PASSWORD="${DB_PASSWORD_EFFECTIVE}" \
@@ -98,18 +228,177 @@ pipeline {
-e RESERVATION_PASSWORD="${RESERVATION_PASSWORD:-}" \
-e RESERVATION_OWNER="${RESERVATION_OWNER:-}" \
localhost:5005/otus/test-mobile:1.0.0 \
bash -lc "set -e; cd /workspace; timeout 1800s mvn -Dallure.results.directory=target/allure-results test")"
cleanup_container() {
docker rm -f "${CID}" >/dev/null 2>&1 || true
}
trap cleanup_container EXIT INT TERM
bash -lc "set -e; cd /workspace; timeout 1800s mvn -Dallure.version=${ALLURE_ADAPTER_VERSION} -Djunit.jupiter.execution.parallel.enabled=false -Djunit.jupiter.execution.parallel.config.fixed.parallelism=${JUNIT_PARALLELISM:-1} -Dtest=${class_name} -Dsurefire.rerunFailingTestsCount=${SUREFIRE_RERUN_FAILING:-0} -Dallure.results.directory=target/allure-results test")"
tar -C "$PWD" -cf - . | docker cp - "${CID}:/workspace"
set +e
docker start -a "${CID}"
TEST_RC=$?
docker cp "${CID}:/workspace/target" "./target" || true
trap - EXIT INT TERM
docker rm -f "${CID}" || true
class_rc=$?
set -e
docker cp "${CID}:/workspace/target" "${attempt_dir}/target" || true
docker rm -f "${CID}" >/dev/null 2>&1 || true
return "${class_rc}"
}
collect_attempt_artifacts() {
src_dir="$1"
mkdir -p ./target/allure-results ./target/surefire-reports
if [ -d "${src_dir}/allure-results" ]; then
cp -a "${src_dir}/allure-results/." ./target/allure-results/ || true
fi
if [ -d "${src_dir}/surefire-reports" ]; then
cp -a "${src_dir}/surefire-reports/." ./target/surefire-reports/ || true
fi
}
if [ -n "${TEST_CLASSES_ORDER:-}" ]; then
printf '%s\n' "${TEST_CLASSES_ORDER}" | tr ',' '\n' > .test-classes.txt
else
cat > .test-classes.txt <<'EOF'
ru.otus.mobile.tests.WishlistsTest
ru.otus.mobile.tests.GiftsTest
ru.otus.mobile.tests.ReservationTest
EOF
fi
TEST_RC=0
MAX_CLASS_ATTEMPTS=2
while IFS= read -r raw_class; do
class_name="$(echo "${raw_class}" | xargs)"
if [ -z "${class_name}" ]; then
continue
fi
CLASS_OK=0
LAST_ATTEMPT_DIR=""
for attempt in $(seq 1 "${MAX_CLASS_ATTEMPTS}"); do
echo "Running ${class_name} (attempt ${attempt}/${MAX_CLASS_ATTEMPTS})"
if run_single_class "${class_name}" "${attempt}"; then
CLASS_OK=1
LAST_ATTEMPT_DIR="./target-run/$(echo "${class_name}" | sed 's/[^a-zA-Z0-9._-]/_/g')/attempt-${attempt}/target"
collect_attempt_artifacts "${LAST_ATTEMPT_DIR}"
break
fi
LAST_ATTEMPT_DIR="./target-run/$(echo "${class_name}" | sed 's/[^a-zA-Z0-9._-]/_/g')/attempt-${attempt}/target"
sleep 3
done
if [ "${CLASS_OK}" -ne 1 ]; then
TEST_RC=1
if [ -n "${LAST_ATTEMPT_DIR}" ] && [ -d "${LAST_ATTEMPT_DIR}" ]; then
collect_attempt_artifacts "${LAST_ATTEMPT_DIR}"
fi
fi
done < .test-classes.txt
mkdir -p ./target/mobile-debug
docker network inspect mobileci_default > ./target/mobile-debug/network.inspect.json 2>&1 || true
for service in android-emulator-1 android-emulator-2; do
emu_cid="$(docker ps -q --filter "name=mobileci-${service}" | head -n1 || true)"
if [ -n "${emu_cid}" ]; then
docker inspect "${emu_cid}" > "./target/mobile-debug/${service}.inspect.json" 2>&1 || true
docker logs "${emu_cid}" > "./target/mobile-debug/${service}.log" 2>&1 || true
fi
done
GIT_SHA="$(git -C "$PWD" rev-parse --short HEAD)"
mkdir -p ./target/allure-results
SCREENSHOT_FILES="$(find ./target ./target-run -type f \\( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \\) 2>/dev/null || true)"
ATTACHMENT_COUNT=0
ATTACHMENTS_JSON=""
if [ -n "${SCREENSHOT_FILES}" ]; then
while IFS= read -r screenshot_file; do
if [ -z "${screenshot_file}" ] || [ ! -f "${screenshot_file}" ]; then
continue
fi
ATTACHMENT_COUNT=$((ATTACHMENT_COUNT + 1))
ext="$(echo "${screenshot_file}" | awk -F. '{print tolower($NF)}')"
if [ "${ext}" = "jpg" ] || [ "${ext}" = "jpeg" ]; then
mime="image/jpeg"
else
ext="png"
mime="image/png"
fi
source_name="external-screenshot-${ATTACHMENT_COUNT}.${ext}"
cp "${screenshot_file}" "./target/allure-results/${source_name}" || continue
safe_name="$(basename "${screenshot_file}" | sed 's/"/\\"/g')"
if [ -n "${ATTACHMENTS_JSON}" ]; then
ATTACHMENTS_JSON="${ATTACHMENTS_JSON},"
fi
ATTACHMENTS_JSON="${ATTACHMENTS_JSON}{\"name\":\"${safe_name}\",\"type\":\"${mime}\",\"source\":\"${source_name}\"}"
done <<EOF
${SCREENSHOT_FILES}
EOF
fi
if [ "${ATTACHMENT_COUNT}" -gt 0 ]; then
EXTRA_UUID="$(cat /proc/sys/kernel/random/uuid)"
TS_MS="$(( $(date +%s) * 1000 ))"
cat > "./target/allure-results/${EXTRA_UUID}-result.json" <<EOF
{
"uuid": "${EXTRA_UUID}",
"historyId": "external-screenshots-${BUILD_NUMBER}",
"name": "Collected screenshots",
"fullName": "pipeline.CollectedScreenshots",
"status": "passed",
"stage": "finished",
"start": ${TS_MS},
"stop": ${TS_MS},
"labels": [
{"name": "suite", "value": "Pipeline Artifacts"},
{"name": "package", "value": "pipeline"},
{"name": "testClass", "value": "CollectedScreenshots"},
{"name": "testMethod", "value": "attach"},
{"name": "framework", "value": "jenkins-pipeline"}
],
"attachments": [${ATTACHMENTS_JSON}],
"steps": [],
"parameters": []
}
EOF
fi
{
echo "job=${JOB_NAME}"
echo "build=${BUILD_NUMBER}"
echo "trigger.user=${RUN_TRIGGER_USER}"
echo "trigger.name=${RUN_TRIGGER_NAME}"
echo "repo.url=${MOBILE_REPO_URL}"
echo "repo.ref=${MOBILE_REPO_REF}"
echo "repo.sha=${GIT_SHA}"
echo "app.url=${APP_URL}"
echo "db.url=${DB_URL}"
echo "db.user=${DB_USER}"
echo "reservation.owner=${RESERVATION_OWNER}"
echo "mobile.emulators=${MOBILE_EMULATORS_VALUE}"
echo "mobile.max.emulators=${MOBILE_MAX_EMULATORS:-1}"
echo "junit.parallelism=${JUNIT_PARALLELISM:-1}"
echo "test.classes.order=${TEST_CLASSES_ORDER}"
echo "surefire.rerunFailing=${SUREFIRE_RERUN_FAILING:-1}"
} > ./target/allure-results/environment.properties
cat > ./target/allure-results/executor.json <<EOF
{
"name": "Jenkins",
"type": "jenkins",
"url": "${JENKINS_URL}",
"buildName": "${JOB_NAME} #${BUILD_NUMBER}",
"buildUrl": "${BUILD_URL}",
"reportUrl": "${BUILD_URL}allure",
"buildOrder": ${BUILD_NUMBER}
}
EOF
cat > ./target/allure-results/categories.json <<'EOF'
[
{
"name": "Infrastructure issues",
"matchedStatuses": ["broken"],
"messageRegex": ".*(Connection refused|No route to host|timed out|timeout|No healthy emulators available).*"
},
{
"name": "Mobile UI locator/assertion issues",
"matchedStatuses": ["failed", "broken"],
"messageRegex": ".*(Element not found|NoSuchElement|AssertionError).*"
}
]
EOF
exit "${TEST_RC}"
'''
}
@@ -121,7 +410,8 @@ pipeline {
dir('project') {
sh '''
set +e
COMPOSE_FILE="/workspace/otus-autotests/hw8/config/compose/mobile-ci.compose.yml"
HW8_ROOT_PATH="${HW8_ROOT:-/workspace/hw8}"
COMPOSE_FILE="${HW8_ROOT_PATH}/config/compose/mobile-ci.compose.yml"
if docker compose version >/dev/null 2>&1; then
compose_cmd() { PROJECT_DIR="$PWD" docker compose -f "${COMPOSE_FILE}" "$@"; }
elif docker-compose version >/dev/null 2>&1; then
@@ -142,7 +432,7 @@ pipeline {
}
}
junit allowEmptyResults: true, testResults: 'project/target/surefire-reports/*.xml'
archiveArtifacts allowEmptyArchive: true, artifacts: 'project/target/**'
archiveArtifacts allowEmptyArchive: true, artifacts: 'project/run-info.txt,project/target/**'
}
}
}
+75 -5
View File
@@ -5,29 +5,48 @@ pipeline {
ansiColor('xterm')
}
stages {
stage('Prepare Metadata') {
steps {
script {
wrap([$class: 'BuildUser']) {
env.RUN_TRIGGER_USER = env.BUILD_USER_ID ?: env.BUILD_USER ?: 'system'
env.RUN_TRIGGER_NAME = env.BUILD_USER ?: env.BUILD_USER_ID ?: 'system'
}
currentBuild.displayName = "#${env.BUILD_NUMBER} runner"
currentBuild.description = "by=${env.RUN_TRIGGER_NAME}; qaRef=${params.QA_REPO_REF}; mobileRef=${params.MOBILE_REPO_REF}"
}
}
}
stage('Run Jobs In Parallel') {
steps {
script {
def fanout = [:]
fanout['selenium'] = {
build job: 'qa-selenium-tests',
def run = build job: 'qa-selenium-tests',
wait: true,
propagate: true,
propagate: false,
parameters: [
string(name: 'QA_REPO_URL', value: params.QA_REPO_URL),
string(name: 'QA_REPO_REF', value: params.QA_REPO_REF),
string(name: 'BROWSER', value: params.BROWSER),
string(name: 'BASE_URL', value: params.BASE_URL),
string(name: 'EXECUTION_MODE', value: params.EXECUTION_MODE),
string(name: 'SELENOID_URL', value: params.SELENOID_URL),
string(name: 'HEADLESS', value: params.HEADLESS)
]
writeFile file: 'downstream-selenium.txt', text: "${run.number}|${run.result}\n"
}
fanout['mobile'] = {
build job: 'qa-mobile-appium-tests',
def run = build job: 'qa-mobile-appium-tests',
wait: true,
propagate: true,
propagate: false,
parameters: [
string(name: 'MOBILE_REPO_URL', value: params.MOBILE_REPO_URL),
string(name: 'MOBILE_REPO_REF', value: params.MOBILE_REPO_REF),
string(name: 'MOBILE_MAX_EMULATORS', value: params.MOBILE_MAX_EMULATORS),
string(name: 'JUNIT_PARALLELISM', value: params.JUNIT_PARALLELISM),
string(name: 'APP_URL', value: params.APP_URL),
string(name: 'DB_URL', value: params.DB_URL),
string(name: 'DB_USER', value: params.DB_USER),
@@ -40,13 +59,64 @@ pipeline {
string(name: 'RESERVATION_PASSWORD', value: params.RESERVATION_PASSWORD),
string(name: 'RESERVATION_OWNER', value: params.RESERVATION_OWNER)
]
writeFile file: 'downstream-mobile.txt', text: "${run.number}|${run.result}\n"
}
fanout['api'] = {
build job: 'qa-api-citrus-tests', wait: true, propagate: true
def run = build job: 'qa-api-citrus-tests',
wait: true,
propagate: false,
parameters: [
string(name: 'QA_REPO_URL', value: params.QA_REPO_URL),
string(name: 'QA_REPO_REF', value: params.QA_REPO_REF)
]
writeFile file: 'downstream-api.txt', text: "${run.number}|${run.result}\n"
}
parallel fanout
def parseRun = { String fileName ->
if (!fileExists(fileName)) {
return [number: 'n/a', result: 'NOT_BUILT']
}
def parts = readFile(fileName).trim().tokenize('|')
return [
number: parts ? parts[0] : 'n/a',
result: parts.size() > 1 ? parts[1] : 'UNKNOWN'
]
}
def seleniumRun = parseRun('downstream-selenium.txt')
def mobileRun = parseRun('downstream-mobile.txt')
def apiRun = parseRun('downstream-api.txt')
def lines = []
lines << "job=${env.JOB_NAME}"
lines << "build=${env.BUILD_NUMBER}"
lines << "trigger_user=${env.RUN_TRIGGER_USER}"
lines << "trigger_name=${env.RUN_TRIGGER_NAME}"
lines << "qa_repo_url=${params.QA_REPO_URL}"
lines << "qa_repo_ref=${params.QA_REPO_REF}"
lines << "mobile_repo_url=${params.MOBILE_REPO_URL}"
lines << "mobile_repo_ref=${params.MOBILE_REPO_REF}"
lines << "selenium_build=${seleniumRun.number}"
lines << "selenium_result=${seleniumRun.result}"
lines << "mobile_build=${mobileRun.number}"
lines << "mobile_result=${mobileRun.result}"
lines << "api_build=${apiRun.number}"
lines << "api_result=${apiRun.result}"
lines << "selenium_link=/job/qa-selenium-tests/${seleniumRun.number}/"
lines << "mobile_link=/job/qa-mobile-appium-tests/${mobileRun.number}/"
lines << "api_link=/job/qa-api-citrus-tests/${apiRun.number}/"
lines << "selenium_link_abs=${env.JENKINS_URL}job/qa-selenium-tests/${seleniumRun.number}/"
lines << "mobile_link_abs=${env.JENKINS_URL}job/qa-mobile-appium-tests/${mobileRun.number}/"
lines << "api_link_abs=${env.JENKINS_URL}job/qa-api-citrus-tests/${apiRun.number}/"
writeFile file: 'runner-summary.txt', text: lines.join('\n') + '\n'
archiveArtifacts allowEmptyArchive: false, artifacts: 'runner-summary.txt'
def failed = [seleniumRun, mobileRun, apiRun].any { it.result != 'SUCCESS' }
if (failed) {
error("One or more downstream QA jobs failed. See runner-summary.txt for build links and statuses.")
}
}
}
}
+91 -4
View File
@@ -5,12 +5,60 @@ pipeline {
ansiColor('xterm')
}
stages {
stage('Prepare Metadata') {
steps {
script {
wrap([$class: 'BuildUser']) {
env.RUN_TRIGGER_USER = env.BUILD_USER_ID ?: env.BUILD_USER ?: 'system'
env.RUN_TRIGGER_NAME = env.BUILD_USER ?: env.BUILD_USER_ID ?: 'system'
}
currentBuild.displayName = "#${env.BUILD_NUMBER} ${params.BROWSER}/${params.HEADLESS}"
currentBuild.description =
"by=${env.RUN_TRIGGER_NAME}; repo=${params.QA_REPO_URL}; ref=${params.QA_REPO_REF}; mode=${params.EXECUTION_MODE}"
}
}
}
stage('Run Selenium Tests In Docker') {
steps {
sh '''
set -eux
rm -rf ./sources
git clone "${QA_REPO_URL}" ./sources
git -C ./sources checkout "${QA_REPO_REF}"
GIT_SHA="$(git -C ./sources rev-parse --short HEAD)"
if ! grep -q "<artifactId>allure-junit5</artifactId>" ./sources/pom.xml; then
awk -v ver="${ALLURE_ADAPTER_VERSION}" '
index($0, "</dependencies>") && !done {
print " <dependency>"
print " <groupId>io.qameta.allure</groupId>"
print " <artifactId>allure-junit5</artifactId>"
print " <version>" ver "</version>"
print " <scope>test</scope>"
print " </dependency>"
done=1
}
{ print }
' ./sources/pom.xml > ./sources/pom.xml.tmp
mv ./sources/pom.xml.tmp ./sources/pom.xml
fi
rm -rf ./artifacts
mkdir -p ./artifacts
{
echo "job=${JOB_NAME}"
echo "build=${BUILD_NUMBER}"
echo "trigger_user=${RUN_TRIGGER_USER}"
echo "trigger_name=${RUN_TRIGGER_NAME}"
echo "qa_repo_url=${QA_REPO_URL}"
echo "qa_repo_ref=${QA_REPO_REF}"
echo "qa_repo_sha=${GIT_SHA}"
echo "browser=${BROWSER}"
echo "headless=${HEADLESS}"
echo "execution_mode=${EXECUTION_MODE}"
echo "selenoid_url=${SELENOID_URL}"
echo "base_url=${BASE_URL}"
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
} > ./artifacts/run-info.txt
EXTRA_ARGS=""
if [ "${BROWSER}" = "chrome" ]; then
@@ -20,16 +68,55 @@ pipeline {
CID="$(docker create \
--add-host=host.docker.internal:host-gateway \
localhost:5005/otus/test-selenium:1.0.0 \
bash -lc "set -e; cd /workspace; mvn -Dexecution.mode=${EXECUTION_MODE} -Dbrowser=${BROWSER} -Dbrowser.version= -Dselenoid.url=${SELENOID_URL} -Dbase.url=${BASE_URL} -Dselenide.headless=${HEADLESS} -Dallure.results.directory=target/allure-results ${EXTRA_ARGS} test")"
bash -lc "set -e; cd /workspace; mvn -Dallure.version=${ALLURE_ADAPTER_VERSION} -Dexecution.mode=${EXECUTION_MODE} -Dbrowser=${BROWSER} -Dbrowser.version= -Dselenoid.url=${SELENOID_URL} -Dbase.url=${BASE_URL} -Dselenide.headless=${HEADLESS} -Dallure.results.directory=target/allure-results ${EXTRA_ARGS} test")"
cleanup_container() {
docker rm -f "${CID}" >/dev/null 2>&1 || true
}
trap cleanup_container EXIT INT TERM
tar -C /workspace/otus-autotests/homework_4 -cf - . | docker cp - "${CID}:/workspace"
tar -C ./sources -cf - . | docker cp - "${CID}:/workspace"
set +e
docker start -a "${CID}"
TEST_RC=$?
docker cp "${CID}:/workspace/target" "./artifacts/target" || true
docker cp "${CID}:/workspace/target" "./artifacts" || true
mkdir -p ./artifacts/target/allure-results
{
echo "job=${JOB_NAME}"
echo "build=${BUILD_NUMBER}"
echo "trigger.user=${RUN_TRIGGER_USER}"
echo "trigger.name=${RUN_TRIGGER_NAME}"
echo "repo.url=${QA_REPO_URL}"
echo "repo.ref=${QA_REPO_REF}"
echo "repo.sha=${GIT_SHA}"
echo "browser=${BROWSER}"
echo "headless=${HEADLESS}"
echo "execution.mode=${EXECUTION_MODE}"
echo "base.url=${BASE_URL}"
} > ./artifacts/target/allure-results/environment.properties
cat > ./artifacts/target/allure-results/executor.json <<EOF
{
"name": "Jenkins",
"type": "jenkins",
"url": "${JENKINS_URL}",
"buildName": "${JOB_NAME} #${BUILD_NUMBER}",
"buildUrl": "${BUILD_URL}",
"reportUrl": "${BUILD_URL}allure",
"buildOrder": ${BUILD_NUMBER}
}
EOF
cat > ./artifacts/target/allure-results/categories.json <<'EOF'
[
{
"name": "Infrastructure issues",
"matchedStatuses": ["broken"],
"messageRegex": ".*(Connection refused|No route to host|timed out|timeout).*"
},
{
"name": "UI locator/assertion issues",
"matchedStatuses": ["failed", "broken"],
"messageRegex": ".*(Element not found|NoSuchElement|AssertionError).*"
}
]
EOF
trap - EXIT INT TERM
docker rm -f "${CID}" || true
exit "${TEST_RC}"
@@ -47,7 +134,7 @@ pipeline {
}
}
junit allowEmptyResults: true, testResults: 'artifacts/target/surefire-reports/*.xml'
archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/target/**'
archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/run-info.txt,artifacts/target/**'
}
}
}
+1 -1
View File
@@ -11,6 +11,6 @@
parameters:
- string:
name: JJB_PATH
default: /workspace/otus-autotests/hw8/config/jobs
default: /workspace/hw8/config/jobs
description: "Путь до JJB-конфигов"
dsl: !include-raw-verbatim: ../scripts/jobs-uploader.groovy
@@ -8,4 +8,13 @@
properties:
- build-discarder:
num-to-keep: 50
parameters:
- string:
name: QA_REPO_URL
default: https://git.kovbasa.ru/otus-autotests/homework_4.git
description: "Git URL репозитория с Selenium/API тестами"
- string:
name: QA_REPO_REF
default: master
description: "Git branch/tag/commit для checkout"
dsl: !include-raw-verbatim: ../scripts/qa-api-citrus-tests.groovy
@@ -9,6 +9,38 @@
- build-discarder:
num-to-keep: 30
parameters:
- string:
name: MOBILE_REPO_URL
default: https://git.kovbasa.ru/otus-autotests/homework_7.git
description: "Git URL репозитория с Appium тестами"
- string:
name: MOBILE_REPO_REF
default: master
description: "Git branch/tag/commit для checkout"
- string:
name: ALLURE_ADAPTER_VERSION
default: 2.29.1
description: "Версия allure-junit5 адаптера"
- choice:
name: MOBILE_MAX_EMULATORS
choices:
- "1"
- "2"
description: "Сколько эмуляторов использовать в прогоне (1=стабильнее, 2=быстрее)"
- choice:
name: JUNIT_PARALLELISM
choices:
- "1"
- "2"
description: "Параллельность JUnit классов в мобильных тестах"
- string:
name: TEST_CLASSES_ORDER
default: "ru.otus.mobile.tests.WishlistsTest,ru.otus.mobile.tests.GiftsTest,ru.otus.mobile.tests.ReservationTest"
description: "Порядок классов для поочередного запуска (-Dtest)"
- string:
name: SUREFIRE_RERUN_FAILING
default: "0"
description: "Количество surefire rerun внутри одного запуска класса (обычно 0, т.к. ретрай делается на уровне pipeline)"
- string:
name: APP_URL
default: http://wiremock:8080/wishlist.apk
+23 -1
View File
@@ -9,6 +9,28 @@
- build-discarder:
num-to-keep: 50
parameters:
- string:
name: QA_REPO_URL
default: https://git.kovbasa.ru/otus-autotests/homework_4.git
- string:
name: QA_REPO_REF
default: master
- string:
name: MOBILE_REPO_URL
default: https://git.kovbasa.ru/otus-autotests/homework_7.git
- string:
name: MOBILE_REPO_REF
default: master
- choice:
name: MOBILE_MAX_EMULATORS
choices:
- "1"
- "2"
- choice:
name: JUNIT_PARALLELISM
choices:
- "1"
- "2"
- choice:
name: BROWSER
choices:
@@ -19,7 +41,7 @@
default: https://otus.ru
- string:
name: EXECUTION_MODE
default: selenoid
default: local
- string:
name: SELENOID_URL
default: http://host.docker.internal:4444/wd/hub
+13 -1
View File
@@ -9,6 +9,18 @@
- build-discarder:
num-to-keep: 50
parameters:
- string:
name: QA_REPO_URL
default: https://git.kovbasa.ru/otus-autotests/homework_4.git
description: "Git URL репозитория с Selenium/API тестами"
- string:
name: QA_REPO_REF
default: master
description: "Git branch/tag/commit для checkout"
- string:
name: ALLURE_ADAPTER_VERSION
default: 2.29.1
description: "Версия allure-junit5 адаптера"
- choice:
name: BROWSER
choices:
@@ -21,7 +33,7 @@
description: "Базовый URL тестируемого сайта"
- string:
name: EXECUTION_MODE
default: selenoid
default: local
description: "Режим запуска (local|selenoid)"
- string:
name: SELENOID_URL
+18 -2
View File
@@ -18,13 +18,29 @@
- build-button
- view:
name: QA
name: QA-Runner
view-type: list
description: "Тестовые job и раннер"
description: "Точка входа для запуска всех тестов"
filter-executors: true
filter-queue: true
job-name:
- qa-runner
columns:
- status
- weather
- job
- last-success
- last-failure
- last-duration
- build-button
- view:
name: QA-Tests
view-type: list
description: "Тестовые job"
filter-executors: true
filter-queue: true
job-name:
- qa-selenium-tests
- qa-api-citrus-tests
- qa-mobile-appium-tests