feat: finalize projectwork CI jobs, docs and test integration

This commit is contained in:
2026-04-23 11:39:08 +03:00
parent 737bddd631
commit 20bdacf5c5
39 changed files with 1819 additions and 70 deletions
+4
View File
@@ -6,3 +6,7 @@ build/
jenkins_home/ jenkins_home/
compose/.env compose/.env
lib/ lib/
compose/selenoid/video/*
!compose/selenoid/video/.gitkeep
compose/selenoid/logs/*
!compose/selenoid/logs/.gitkeep
+56 -31
View File
@@ -1,18 +1,23 @@
# OTUS HW8: Jenkins + Ansible + JJB + Docker # OTUS ProjectWork: Jenkins + Ansible + JJB + Docker
Полностью автоматизированная раскатка ДЗ8: Полностью автоматизированная раскатка проектной работы:
- Jenkins controller в Docker; - Jenkins controller в Docker;
- reverse proxy Nginx; - reverse proxy Nginx;
- локальный Docker Registry; - локальный Docker Registry;
- Jenkins agents (docker slaves); - Jenkins agents (docker slaves);
- Jenkins Job Builder (JJB), без ручного создания job; - Jenkins Job Builder (JJB), без ручного создания job;
- Ansible для полного подъема/сноса стенда; - Ansible для полного подъема/сноса стенда;
- 6 job (runner + uploader + 3 тестовые + infra check); - Selenoid + Selenoid UI;
- Allure отчеты для UI и mobile. - расширенный набор job (web/api/mobile + runner + uploader + infra check);
- Allure отчеты для всех контуров.
Тестовые проекты всегда берутся из Git в runtime: Тестовые проекты берутся из Git в runtime:
- `homework_4` (Selenium + API Citrus); - `homework_1` (`main`, `homework_2`) - WEB BDD/Cucumber;
- `homework_7` (Appium, APK скачивается автоматически через `APP_URL`). - `hw3` - API REST;
- `homework_4` - Selenium + API Citrus;
- `hw3` (`homework_5`) - HW5 как ветка в репозитории `hw3` (job `qa-maven-extra-tests`);
- `homework_6` - Playwright;
- `homework_7` - Appium, APK скачивается автоматически через `APP_URL`;
## Что поднимается ## Что поднимается
@@ -20,20 +25,27 @@
- Jenkins: `http://localhost:8081` - Jenkins: `http://localhost:8081`
- Jenkins через Nginx: `http://localhost:8088` - Jenkins через Nginx: `http://localhost:8088`
- Registry: `http://localhost:5005/v2/` - Registry: `http://localhost:5005/v2/`
- Selenoid: `http://localhost:4444/wd/hub`
- Selenoid UI: `http://localhost:8089`
- Agent `maven` (docker label: `maven docker`) - Agent `maven` (docker label: `maven docker`)
- Agent `jjb` (docker label: `jjb docker`) - Agent `jjb` (docker label: `jjb docker`)
- one-shot `jobs_uploader` container - one-shot `jobs_uploader` container
Контроллер не исполняет сборки (`numExecutors: 0`, mode `EXCLUSIVE`), все job идут только на агентах. Контроллер не исполняет сборки (`numExecutors: 0`, mode `EXCLUSIVE`), все job идут только на агентах.
## 6 job ## Набор job
1. `jobs-uploader` - накатывает/обновляет job через JJB. 1. `jobs-uploader` - накатывает/обновляет job через JJB.
2. `infra-health-check` - проверка Jenkins/Registry/образов/агентов. 2. `infra-health-check` - проверка Jenkins/Registry/образов/агентов.
3. `qa-selenium-tests` - Selenium/Selenide с выбором браузера. 3. `qa-runner` - оркестратор параллельного запуска всех тестовых контуров (cron: `15 1 * * *`).
4. `qa-api-citrus-tests` - API тесты. 4. `qa-selenium-tests`
5. `qa-mobile-appium-tests` - Appium тесты, APK через `APP_URL`. 5. `qa-web-bdd-tests`
6. `qa-runner` - параллельный запуск `selenium + api + mobile`. 6. `qa-api-citrus-tests`
7. `qa-api-contract-tests`
8. `qa-api-rest-tests`
9. `qa-playwright-tests`
10. `qa-maven-extra-tests`
11. `qa-mobile-appium-tests`
Вьюхи в Jenkins: Вьюхи в Jenkins:
- `DevOps` - `DevOps`
@@ -59,7 +71,7 @@
Подготовка: Подготовка:
```bash ```bash
cd /path/to/hw8 cd /path/to/projectwork
cp compose/.env.example compose/.env cp compose/.env.example compose/.env
``` ```
@@ -77,10 +89,10 @@ ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/site.yml -e jo
`wsl`: `wsl`:
```powershell ```powershell
wsl bash -lc "cd /mnt/c/Users/spawn/IdeaProjects/otus-autotests/hw8 && ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/site.yml -e jobs_profile=devops" wsl bash -lc "cd /mnt/c/Users/spawn/IdeaProjects/otus-autotests/projectwork && ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/site.yml -e jobs_profile=devops"
``` ```
После этого запускаете `jobs-uploader` (дефолт `JJB_PATH=/workspace/hw8/config/jobs`) и получаете QA job. После этого запускаете `jobs-uploader` (дефолт `JJB_PATH=/workspace/projectwork/config/jobs`) и получаете QA job.
### Вариант 2: сразу все job ### Вариант 2: сразу все job
@@ -91,7 +103,7 @@ ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/site.yml
`wsl`: `wsl`:
```powershell ```powershell
wsl bash -lc "cd /mnt/c/Users/spawn/IdeaProjects/otus-autotests/hw8 && ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/site.yml" wsl bash -lc "cd /mnt/c/Users/spawn/IdeaProjects/otus-autotests/projectwork && ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/site.yml"
``` ```
Примечание: можно запускать и без `-e jobs_profile=full` — по умолчанию в `site.yml` используется профиль `full` (раскатываются все job). Примечание: можно запускать и без `-e jobs_profile=full` — по умолчанию в `site.yml` используется профиль `full` (раскатываются все job).
@@ -100,56 +112,67 @@ wsl bash -lc "cd /mnt/c/Users/spawn/IdeaProjects/otus-autotests/hw8 && ansible-p
Рекомендуемый вход: `QA-Runner` -> `qa-runner` -> `Build with Parameters`. Рекомендуемый вход: `QA-Runner` -> `qa-runner` -> `Build with Parameters`.
`qa-runner` запускает в параллели: `qa-runner` запускает в параллели все контуры и всегда публикует `runner-summary.txt` с:
- `qa-selenium-tests`
- `qa-api-citrus-tests`
- `qa-mobile-appium-tests`
И всегда публикует `runner-summary.txt` с:
- номерами дочерних сборок; - номерами дочерних сборок;
- статусами; - статусами;
- ссылками на каждую дочернюю job. - ссылками на каждую дочернюю job.
Для BDD по умолчанию запускаются обе ветки из `homework_1`: `main` и `homework_2` (`BDD_REFS=main,homework_2`).
## Отчетность ## Отчетность
`qa-selenium-tests` и `qa-mobile-appium-tests` публикуют: Все тестовые job публикуют:
- Allure report (`/allure`); - Allure report (`/allure`);
- JUnit результаты; - JUnit результаты;
- `run-info.txt`; - `run-info.txt`;
- `target/**` артефакты (включая скриншоты/логи тестового проекта, если они туда пишутся). - `target/**` артефакты (или аналогичные артефакты проекта).
- для mobile дополнительно сохраняются `project/target/mobile-debug/*` (`docker logs` и `docker inspect` эмуляторов/сети).
- для mobile параметр `SUREFIRE_RERUN_FAILING` (по умолчанию `1`) снижает flaky-падения Appium-сессий без правок кода HW7.
Дополнительно в Allure: Дополнительно в Allure:
- `environment.properties` - `environment.properties`
- `executor.json` - `executor.json`
- `categories.json` - `categories.json` (там, где применимо)
Также в `currentBuild.description` пишется, кто запустил job и ключевые параметры. Для `qa-selenium-tests` и `qa-web-bdd-tests`:
- включен сбор всех новых mp4 из Selenoid за прогон;
- видео прикладываются в Allure через external attachments.
Для `qa-playwright-tests`:
- по умолчанию используется `mcr.microsoft.com/playwright/java:v1.58.0-jammy` (`PLAYWRIGHT_DOCKER_IMAGE`).
Для `qa-mobile-appium-tests`:
- перед запуском синхронизируются `wiremock/mappings` и `wiremock/__files/wishlist.apk` в контейнер;
- проверяется доступность `APP_URL` изнутри эмулятора;
- сохраняются `project/target/mobile-debug/*`.
`qa-runner` завершает сборку как `UNSTABLE`, если падают только UI-контуры (`qa-selenium-tests`, `qa-web-bdd-tests`, `qa-playwright-tests`). Для остальных контуров падение блокирующее (`FAILURE`).
## Команды для повторного запуска с нуля ## Команды для повторного запуска с нуля
Снести все: Снести все:
`bash`:
```bash ```bash
ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/down.yml ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/down.yml
``` ```
`wsl`:
```powershell ```powershell
wsl bash -lc "cd /mnt/c/Users/spawn/IdeaProjects/otus-autotests/hw8 && ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/down.yml" wsl bash -lc "cd /mnt/c/Users/spawn/IdeaProjects/otus-autotests/projectwork && ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/down.yml"
``` ```
Полный reset + deploy: Полный reset + deploy:
По умолчанию эта команда раскатывает профиль `full` (все job: DevOps + QA), так как в `ansible/playbooks/site.yml` задано `jobs_profile | default('full')`. По умолчанию эта команда раскатывает профиль `full` (все job: DevOps + QA), так как в `ansible/playbooks/site.yml` задано `jobs_profile | default('full')`.
`bash`:
```bash ```bash
ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/down.yml ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/down.yml
ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/site.yml ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/site.yml
``` ```
`wsl`:
```powershell ```powershell
wsl bash -lc "cd /mnt/c/Users/spawn/IdeaProjects/otus-autotests/hw8 && ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/down.yml && ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/site.yml" wsl bash -lc "cd /mnt/c/Users/spawn/IdeaProjects/otus-autotests/projectwork && ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/down.yml && ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/site.yml"
``` ```
Если нужен только профиль `devops`, добавьте `-e jobs_profile=devops` во вторую команду (`site.yml`). Если нужен только профиль `devops`, добавьте `-e jobs_profile=devops` во вторую команду (`site.yml`).
@@ -157,7 +180,9 @@ wsl bash -lc "cd /mnt/c/Users/spawn/IdeaProjects/otus-autotests/hw8 && ansible-p
## Где что лежит ## Где что лежит
- `ansible/` - playbook раскатки/удаления - `ansible/` - playbook раскатки/удаления
- `compose/` - инфраструктура Jenkins/agents/registry/nginx - `compose/` - инфраструктура Jenkins/agents/registry/nginx/selenoid
- `compose/images/` - Dockerfile для агентов и test-runner образов - `compose/images/` - Dockerfile для агентов и test-runner образов
- `config/jobs/` - full JJB (DevOps + QA) - `config/jobs/` - full JJB (DevOps + QA)
- `config/jobs-devops/` - JJB профиль только для DevOps job - `config/jobs-devops/` - JJB профиль только для DevOps job
- `config/wiremock/` - маппинги для контрактных API тестов
- `contracts-tests/` - API контрактные тесты (JsonSchemaValidation)
+3 -3
View File
@@ -1,10 +1,10 @@
--- ---
- name: Destroy OTUS HW8 Jenkins infrastructure - name: Destroy OTUS ProjectWork Jenkins infrastructure
hosts: local hosts: local
gather_facts: false gather_facts: false
vars: vars:
hw8_root: "{{ playbook_dir }}/../.." project_root: "{{ playbook_dir }}/../.."
compose_dir: "{{ hw8_root }}/compose" compose_dir: "{{ project_root }}/compose"
compose_file: "{{ compose_dir }}/docker-compose.yml" compose_file: "{{ compose_dir }}/docker-compose.yml"
compose_env_file: "{{ compose_dir }}/.env" compose_env_file: "{{ compose_dir }}/.env"
+33 -7
View File
@@ -1,16 +1,17 @@
--- ---
- name: Deploy OTUS HW8 Jenkins infrastructure - name: Deploy OTUS ProjectWork Jenkins infrastructure
hosts: local hosts: local
gather_facts: true gather_facts: true
vars: vars:
hw8_root: "{{ playbook_dir }}/../.." project_root: "{{ playbook_dir }}/../.."
compose_dir: "{{ hw8_root }}/compose" compose_dir: "{{ project_root }}/compose"
compose_file: "{{ compose_dir }}/docker-compose.yml" compose_file: "{{ compose_dir }}/docker-compose.yml"
compose_env_file: "{{ compose_dir }}/.env" compose_env_file: "{{ compose_dir }}/.env"
selenoid_video_output_dir: "{{ compose_dir }}/selenoid/video"
jobs_profile_effective: "{{ jobs_profile | default('full') }}" jobs_profile_effective: "{{ jobs_profile | default('full') }}"
jjb_paths: jjb_paths:
full: /workspace/hw8/config/jobs full: /workspace/projectwork/config/jobs
devops: /workspace/hw8/config/jobs-devops devops: /workspace/projectwork/config/jobs-devops
jjb_upload_path: "{{ jjb_paths.get(jobs_profile_effective, jjb_paths.full) }}" jjb_upload_path: "{{ jjb_paths.get(jobs_profile_effective, jjb_paths.full) }}"
tasks: tasks:
@@ -42,11 +43,13 @@
delay: 20 delay: 20
until: pull_jenkins_base.rc == 0 until: pull_jenkins_base.rc == 0
- name: Build and start registry, jenkins and nginx - name: Build and start core services (registry, jenkins, nginx, selenoid)
ansible.builtin.command: ansible.builtin.command:
cmd: docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} up -d --build registry jenkins nginx cmd: docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} up -d --build registry jenkins nginx selenoid selenoid-ui
args: args:
chdir: "{{ compose_dir }}" chdir: "{{ compose_dir }}"
environment:
SELENOID_VIDEO_OUTPUT_DIR: "{{ selenoid_video_output_dir }}"
register: compose_bootstrap register: compose_bootstrap
retries: 4 retries: 4
delay: 20 delay: 20
@@ -61,6 +64,23 @@
delay: 5 delay: 5
until: jenkins_ready.status == 200 until: jenkins_ready.status == 200
- name: Wait for Selenoid to become available
ansible.builtin.uri:
url: "http://127.0.0.1:4444/status"
status_code: 200
register: selenoid_ready
retries: 60
delay: 5
until: selenoid_ready.status == 200
- name: Pre-pull Selenoid browser and recorder images
ansible.builtin.command:
cmd: docker pull {{ item }}
loop:
- selenoid/vnc:chrome_128.0
- selenoid/vnc:firefox_125.0
- selenoid/video-recorder:latest-release
- name: Build and push slave/test images to local registry - name: Build and push slave/test images to local registry
ansible.builtin.command: ansible.builtin.command:
cmd: ./scripts/build_and_push_images.sh localhost:5005 1.0.0 cmd: ./scripts/build_and_push_images.sh localhost:5005 1.0.0
@@ -76,12 +96,16 @@
cmd: docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} up -d agent-maven agent-jjb cmd: docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} up -d agent-maven agent-jjb
args: args:
chdir: "{{ compose_dir }}" chdir: "{{ compose_dir }}"
environment:
SELENOID_VIDEO_OUTPUT_DIR: "{{ selenoid_video_output_dir }}"
- name: Build jobs_uploader image with latest changes - name: Build jobs_uploader image with latest changes
ansible.builtin.command: ansible.builtin.command:
cmd: docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} build jobs_uploader cmd: docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} build jobs_uploader
args: args:
chdir: "{{ compose_dir }}" chdir: "{{ compose_dir }}"
environment:
SELENOID_VIDEO_OUTPUT_DIR: "{{ selenoid_video_output_dir }}"
- name: Remove stale jobs_uploader run containers - name: Remove stale jobs_uploader run containers
ansible.builtin.shell: | ansible.builtin.shell: |
@@ -97,6 +121,8 @@
cmd: timeout 900 docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} run --rm -e JJB_PATH={{ jjb_upload_path }} 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: args:
chdir: "{{ compose_dir }}" chdir: "{{ compose_dir }}"
environment:
SELENOID_VIDEO_OUTPUT_DIR: "{{ selenoid_video_output_dir }}"
- name: Show endpoint details - name: Show endpoint details
ansible.builtin.debug: ansible.builtin.debug:
+70 -7
View File
@@ -33,7 +33,8 @@ services:
- jenkins_home:/var/jenkins_home - jenkins_home:/var/jenkins_home
- ./jenkins/casc:/var/jenkins_home/casc_configs:ro - ./jenkins/casc:/var/jenkins_home/casc_configs:ro
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ..:/workspace/hw8:ro - ..:/workspace/projectwork:ro
- ../..:/workspace/otus:ro
healthcheck: healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/login >/dev/null"] test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/login >/dev/null"]
interval: 10s interval: 10s
@@ -55,6 +56,60 @@ services:
networks: networks:
- jenkins_net - jenkins_net
selenoid:
image: aerokube/selenoid:1.11.3
restart: unless-stopped
environment:
DOCKER_API_VERSION: "1.44"
OVERRIDE_VIDEO_OUTPUT_DIR: ${SELENOID_VIDEO_OUTPUT_DIR:-/opt/selenoid/video}
ports:
- "4444:4444"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./selenoid/browsers.json:/etc/selenoid/browsers.json:ro
- ./selenoid/video:/opt/selenoid/video
- ./selenoid/logs:/opt/selenoid/logs
command:
- "-conf"
- "/etc/selenoid/browsers.json"
- "-video-recorder-image"
- "selenoid/video-recorder:latest-release"
- "-container-network"
- "otus_jenkins_net"
- "-limit"
- "4"
- "-timeout"
- "5m"
- "-service-startup-timeout"
- "120s"
- "-session-attempt-timeout"
- "120s"
- "-video-output-dir"
- "/opt/selenoid/video"
- "-log-output-dir"
- "/opt/selenoid/logs"
healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:4444/status >/dev/null 2>&1"]
interval: 10s
timeout: 5s
retries: 30
networks:
- jenkins_net
selenoid-ui:
image: aerokube/selenoid-ui:1.10.11
restart: unless-stopped
depends_on:
selenoid:
condition: service_healthy
ports:
- "8089:8080"
command:
- "--selenoid-uri"
- "http://selenoid:4444"
networks:
- jenkins_net
jobs_uploader: jobs_uploader:
build: build:
context: ./jobs_uploader context: ./jobs_uploader
@@ -67,9 +122,10 @@ services:
JENKINS_HOSTNAME: http://jenkins:8080 JENKINS_HOSTNAME: http://jenkins:8080
JENKINS_USERNAME: ${JENKINS_ADMIN_ID} JENKINS_USERNAME: ${JENKINS_ADMIN_ID}
JENKINS_PASSWORD: ${JENKINS_ADMIN_PASSWORD} JENKINS_PASSWORD: ${JENKINS_ADMIN_PASSWORD}
JJB_PATH: /workspace/hw8/config/jobs JJB_PATH: /workspace/projectwork/config/jobs
volumes: volumes:
- ..:/workspace/hw8:ro - ..:/workspace/projectwork:ro
- ../..:/workspace/otus:ro
networks: networks:
- jenkins_net - jenkins_net
@@ -87,10 +143,13 @@ services:
JENKINS_AGENT_WORKDIR: /home/jenkins/agent JENKINS_AGENT_WORKDIR: /home/jenkins/agent
JENKINS_WEB_SOCKET: "true" JENKINS_WEB_SOCKET: "true"
JENKINS_LABELS: maven docker JENKINS_LABELS: maven docker
HW8_ROOT: /workspace/hw8 OTUS_WORKSPACE_ROOT: /workspace/projectwork
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ..:/workspace/hw8:ro - ..:/workspace/projectwork:ro
- ../..:/workspace/otus:ro
extra_hosts:
- "host.docker.internal:host-gateway"
networks: networks:
- jenkins_net - jenkins_net
@@ -108,15 +167,19 @@ services:
JENKINS_AGENT_WORKDIR: /home/jenkins/agent JENKINS_AGENT_WORKDIR: /home/jenkins/agent
JENKINS_WEB_SOCKET: "true" JENKINS_WEB_SOCKET: "true"
JENKINS_LABELS: jjb docker JENKINS_LABELS: jjb docker
HW8_ROOT: /workspace/hw8 OTUS_WORKSPACE_ROOT: /workspace/projectwork
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ..:/workspace/hw8:ro - ..:/workspace/projectwork:ro
- ../..:/workspace/otus:ro
extra_hosts:
- "host.docker.internal:host-gateway"
networks: networks:
- jenkins_net - jenkins_net
networks: networks:
jenkins_net: jenkins_net:
name: otus_jenkins_net
driver: bridge driver: bridge
volumes: volumes:
+1 -1
View File
@@ -30,7 +30,7 @@ jenkins:
- key: JENKINS_URL_INTERNAL - key: JENKINS_URL_INTERNAL
value: "http://jenkins:8080" value: "http://jenkins:8080"
- key: OTUS_WORKSPACE_ROOT - key: OTUS_WORKSPACE_ROOT
value: "/workspace/hw8" value: "/workspace/projectwork"
- key: MOBILE_DB_PASSWORD - key: MOBILE_DB_PASSWORD
value: "${MOBILE_DB_PASSWORD}" value: "${MOBILE_DB_PASSWORD}"
+22
View File
@@ -0,0 +1,22 @@
{
"chrome": {
"default": "128.0",
"versions": {
"128.0": {
"image": "selenoid/vnc:chrome_128.0",
"port": "4444",
"path": "/"
}
}
},
"firefox": {
"default": "125.0",
"versions": {
"125.0": {
"image": "selenoid/vnc:firefox_125.0",
"port": "4444",
"path": "/wd/hub"
}
}
}
}
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
- defaults: - defaults:
name: global name: global
project_folder: /workspace/hw8 project_folder: /workspace/projectwork
test_image_tag: "1.0.0" test_image_tag: "1.0.0"
build_keep: 40 build_keep: 40
@@ -11,6 +11,6 @@
parameters: parameters:
- string: - string:
name: JJB_PATH name: JJB_PATH
default: /workspace/hw8/config/jobs default: /workspace/projectwork/config/jobs
description: "Путь до JJB-конфигов" description: "Путь до JJB-конфигов"
dsl: !include-raw-verbatim: ../../jobs/scripts/jobs-uploader.groovy dsl: !include-raw-verbatim: ../../jobs/scripts/jobs-uploader.groovy
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
- defaults: - defaults:
name: global name: global
project_folder: /workspace/hw8 project_folder: /workspace/projectwork
test_image_tag: "1.0.0" test_image_tag: "1.0.0"
build_keep: 40 build_keep: 40
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
- property: - property:
name: hw8-build-policy name: projectwork-build-policy
properties: properties:
- build-discarder: - build-discarder:
num-to-keep: 40 num-to-keep: 40
@@ -23,6 +23,9 @@ pipeline {
set -eux set -eux
docker version docker version
curl -fsS http://registry:5000/v2/_catalog curl -fsS http://registry:5000/v2/_catalog
curl -fsS http://host.docker.internal:4444/status
docker pull selenoid/vnc:chrome_128.0
docker pull selenoid/video-recorder:latest-release || docker pull selenoid/video-recorder:latest
docker pull localhost:5005/otus/test-selenium:1.0.0 docker pull localhost:5005/otus/test-selenium:1.0.0
docker pull localhost:5005/otus/test-api:1.0.0 docker pull localhost:5005/otus/test-api:1.0.0
docker pull localhost:5005/otus/test-mobile:1.0.0 docker pull localhost:5005/otus/test-mobile:1.0.0
@@ -33,8 +36,8 @@ pipeline {
steps { steps {
sh ''' sh '''
set -eux set -eux
HW8_ROOT_PATH="${HW8_ROOT:-/workspace/hw8}" PROJECT_ROOT_PATH="${OTUS_WORKSPACE_ROOT:-/workspace/projectwork}"
test -f "${HW8_ROOT_PATH}/config/jobs/global.yaml" test -f "${PROJECT_ROOT_PATH}/config/jobs/global.yaml"
''' '''
} }
} }
+56 -1
View File
@@ -22,6 +22,7 @@ pipeline {
steps { steps {
sh ''' sh '''
set -eux set -eux
git config --global --add safe.directory '*' || true
rm -rf ./sources rm -rf ./sources
git clone "${QA_REPO_URL}" ./sources git clone "${QA_REPO_URL}" ./sources
git -C ./sources checkout "${QA_REPO_REF}" git -C ./sources checkout "${QA_REPO_REF}"
@@ -40,7 +41,7 @@ pipeline {
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
} > ./artifacts/run-info.txt } > ./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")" CID="$(docker create localhost:5005/otus/test-api:1.0.0 bash -lc "set -e; cd /workspace; mvn -f citrus-tests/pom.xml -Dallure.results.directory=target/allure-results test")"
cleanup_container() { cleanup_container() {
docker rm -f "${CID}" >/dev/null 2>&1 || true docker rm -f "${CID}" >/dev/null 2>&1 || true
} }
@@ -50,6 +51,53 @@ pipeline {
docker start -a "${CID}" docker start -a "${CID}"
TEST_RC=$? TEST_RC=$?
docker cp "${CID}:/workspace/citrus-tests/target" "./artifacts/citrus-target" || true docker cp "${CID}:/workspace/citrus-tests/target" "./artifacts/citrus-target" || true
mkdir -p ./artifacts/citrus-target/allure-results
if ! ls ./artifacts/citrus-target/allure-results/* >/dev/null 2>&1; then
EXTRA_UUID="$(cat /proc/sys/kernel/random/uuid)"
TS_MS="$(( $(date +%s) * 1000 ))"
STATUS="passed"
if [ "${TEST_RC}" -ne 0 ]; then
STATUS="failed"
fi
cat > "./artifacts/citrus-target/allure-results/${EXTRA_UUID}-result.json" <<EOF
{
"uuid": "${EXTRA_UUID}",
"historyId": "api-citrus-summary-${BUILD_NUMBER}",
"name": "API Citrus pipeline summary",
"fullName": "pipeline.ApiCitrusSummary",
"status": "${STATUS}",
"stage": "finished",
"start": ${TS_MS},
"stop": ${TS_MS},
"labels": [
{"name": "suite", "value": "Pipeline"},
{"name": "package", "value": "pipeline"},
{"name": "testClass", "value": "ApiCitrusSummary"},
{"name": "testMethod", "value": "run"}
],
"steps": [],
"parameters": []
}
EOF
fi
{
echo "job=${JOB_NAME}"
echo "build=${BUILD_NUMBER}"
echo "repo.url=${QA_REPO_URL}"
echo "repo.ref=${QA_REPO_REF}"
echo "repo.sha=${GIT_SHA}"
} > ./artifacts/citrus-target/allure-results/environment.properties
cat > ./artifacts/citrus-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
trap - EXIT INT TERM trap - EXIT INT TERM
docker rm -f "${CID}" || true docker rm -f "${CID}" || true
exit "${TEST_RC}" exit "${TEST_RC}"
@@ -59,6 +107,13 @@ pipeline {
} }
post { post {
always { always {
script {
try {
allure commandline: 'allure', includeProperties: false, jdk: '', results: [[path: 'artifacts/citrus-target/allure-results']]
} catch (Exception ex) {
echo "Allure publisher unavailable: ${ex.message}"
}
}
junit allowEmptyResults: true, testResults: 'artifacts/citrus-target/surefire-reports/*.xml' junit allowEmptyResults: true, testResults: 'artifacts/citrus-target/surefire-reports/*.xml'
archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/run-info.txt,artifacts/citrus-target/**' archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/run-info.txt,artifacts/citrus-target/**'
} }
@@ -0,0 +1,129 @@
pipeline {
agent { label 'maven' }
options {
timestamps()
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} contract-api"
currentBuild.description = "by=${env.RUN_TRIGGER_NAME}; scope=users-contract"
}
}
}
stage('Run Contract API Tests') {
steps {
sh '''
set -eux
PROJECT_ROOT_PATH="${OTUS_WORKSPACE_ROOT:-/workspace/projectwork}"
WIREMOCK_DIR="${PROJECT_ROOT_PATH}/config/wiremock"
test -d "${WIREMOCK_DIR}"
test -f "${PROJECT_ROOT_PATH}/contracts-tests/pom.xml"
rm -rf ./artifacts
mkdir -p ./artifacts
WM_NAME="wiremock-contract-${BUILD_NUMBER}"
WM_CID="$(docker run -d --rm --name "${WM_NAME}" -p 18080:8080 wiremock/wiremock:3.9.1)"
WM_IP="$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "${WM_CID}")"
test -n "${WM_IP}"
docker exec "${WM_CID}" mkdir -p /home/wiremock/mappings
tar -C "${WIREMOCK_DIR}" -cf - mappings | docker cp - "${WM_CID}:/home/wiremock"
docker restart "${WM_CID}" >/dev/null
cleanup_all() {
docker rm -f "${WM_CID}" >/dev/null 2>&1 || true
docker rm -f "${CID:-}" >/dev/null 2>&1 || true
}
trap cleanup_all EXIT INT TERM
for _ in $(seq 1 30); do
if curl -fsS "http://${WM_IP}:8080/__admin/health" >/dev/null; then
break
fi
sleep 2
done
curl -fsS "http://${WM_IP}:8080/__admin/health" >/dev/null
CID="$(docker create \
--add-host=host.docker.internal:host-gateway \
localhost:5005/otus/test-api:1.0.0 \
bash -lc "set -e; cd /workspace; mvn -f contracts-tests/pom.xml -Dallure.results.directory=target/allure-results -DbaseUrl=http://${WM_IP}:8080 test")"
tar -C "${PROJECT_ROOT_PATH}" -cf - contracts-tests | docker cp - "${CID}:/workspace"
set +e
docker start -a "${CID}"
TEST_RC=$?
docker cp "${CID}:/workspace/contracts-tests/target" "./artifacts" || true
mkdir -p ./artifacts/target/allure-results
if ! ls ./artifacts/target/allure-results/* >/dev/null 2>&1; then
EXTRA_UUID="$(cat /proc/sys/kernel/random/uuid)"
TS_MS="$(( $(date +%s) * 1000 ))"
STATUS="passed"
if [ "${TEST_RC}" -ne 0 ]; then
STATUS="failed"
fi
cat > "./artifacts/target/allure-results/${EXTRA_UUID}-result.json" <<EOF
{
"uuid": "${EXTRA_UUID}",
"historyId": "api-contract-summary-${BUILD_NUMBER}",
"name": "API contract pipeline summary",
"fullName": "pipeline.ApiContractSummary",
"status": "${STATUS}",
"stage": "finished",
"start": ${TS_MS},
"stop": ${TS_MS},
"labels": [
{"name": "suite", "value": "Pipeline"},
{"name": "package", "value": "pipeline"},
{"name": "testClass", "value": "ApiContractSummary"},
{"name": "testMethod", "value": "run"}
],
"steps": [],
"parameters": []
}
EOF
fi
{
echo "job=${JOB_NAME}"
echo "build=${BUILD_NUMBER}"
echo "scope=users-contract"
echo "wiremock.url=http://${WM_IP}:8080"
} > ./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
set -e
exit "${TEST_RC}"
'''
}
}
}
post {
always {
script {
try {
allure commandline: 'allure', includeProperties: false, jdk: '', results: [[path: 'artifacts/target/allure-results']]
} catch (Exception ex) {
echo "Allure publisher unavailable: ${ex.message}"
}
}
junit allowEmptyResults: true, testResults: 'artifacts/target/surefire-reports/*.xml'
archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/target/**'
}
}
}
+125
View File
@@ -0,0 +1,125 @@
pipeline {
agent { label 'maven' }
options {
timestamps()
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-rest"
currentBuild.description = "by=${env.RUN_TRIGGER_NAME}; repo=${params.API_REST_REPO_URL}; ref=${params.API_REST_REPO_REF}"
}
}
}
stage('Run API REST Tests') {
steps {
sh '''
set -eux
git config --global --add safe.directory '*' || true
rm -rf ./sources ./artifacts
mkdir -p ./artifacts
git clone "${API_REST_REPO_URL}" ./sources
TARGET_REF="${API_REST_REPO_REF}"
if git -C ./sources show-ref --verify --quiet "refs/remotes/origin/${TARGET_REF}"; then
git -C ./sources checkout -B "${TARGET_REF}" "origin/${TARGET_REF}"
else
DEFAULT_REF="$(git -C ./sources symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@')"
echo "Requested ref '${TARGET_REF}' not found, fallback to default '${DEFAULT_REF}'"
TARGET_REF="${DEFAULT_REF}"
git -C ./sources checkout -B "${TARGET_REF}" "origin/${TARGET_REF}"
fi
GIT_SHA="$(git -C ./sources rev-parse --short HEAD)"
CID="$(docker create localhost:5005/otus/test-api:1.0.0 bash -lc "set -e; cd /workspace; mvn -Dmaven.compiler.release=${API_REST_JAVA_RELEASE} -Dmaven.compiler.source=${API_REST_JAVA_RELEASE} -Dmaven.compiler.target=${API_REST_JAVA_RELEASE} -Dallure.results.directory=target/allure-results ${API_REST_MAVEN_GOAL}")"
cleanup_container() {
docker rm -f "${CID}" >/dev/null 2>&1 || true
}
trap cleanup_container EXIT INT TERM
tar -C ./sources -cf - . | docker cp - "${CID}:/workspace"
set +e
docker start -a "${CID}"
TEST_RC=$?
docker cp "${CID}:/workspace/target" "./artifacts" || true
mkdir -p ./artifacts/target/allure-results
if ! ls ./artifacts/target/allure-results/* >/dev/null 2>&1; then
EXTRA_UUID="$(cat /proc/sys/kernel/random/uuid)"
TS_MS="$(( $(date +%s) * 1000 ))"
STATUS="passed"
if [ "${TEST_RC}" -ne 0 ]; then
STATUS="failed"
fi
cat > "./artifacts/target/allure-results/${EXTRA_UUID}-result.json" <<EOF
{
"uuid": "${EXTRA_UUID}",
"historyId": "api-rest-summary-${BUILD_NUMBER}",
"name": "API REST pipeline summary",
"fullName": "pipeline.ApiRestSummary",
"status": "${STATUS}",
"stage": "finished",
"start": ${TS_MS},
"stop": ${TS_MS},
"labels": [
{"name": "suite", "value": "Pipeline"},
{"name": "package", "value": "pipeline"},
{"name": "testClass", "value": "ApiRestSummary"},
{"name": "testMethod", "value": "run"}
],
"steps": [],
"parameters": []
}
EOF
fi
{
echo "repo=${API_REST_REPO_URL}"
echo "ref=${TARGET_REF}"
echo "sha=${GIT_SHA}"
echo "maven_goal=${API_REST_MAVEN_GOAL}"
echo "java_release=${API_REST_JAVA_RELEASE}"
} > ./artifacts/run-info.txt
{
echo "job=${JOB_NAME}"
echo "build=${BUILD_NUMBER}"
echo "repo.url=${API_REST_REPO_URL}"
echo "repo.ref=${TARGET_REF}"
echo "repo.sha=${GIT_SHA}"
echo "maven.goal=${API_REST_MAVEN_GOAL}"
} > ./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
trap - EXIT INT TERM
docker rm -f "${CID}" || true
exit "${TEST_RC}"
'''
}
}
}
post {
always {
script {
try {
allure commandline: 'allure', includeProperties: false, jdk: '', results: [[path: 'artifacts/target/allure-results']]
} catch (Exception ex) {
echo "Allure publisher unavailable: ${ex.message}"
}
}
junit allowEmptyResults: true, testResults: 'artifacts/target/surefire-reports/*.xml'
archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/run-info.txt,artifacts/target/**'
}
}
}
+166
View File
@@ -0,0 +1,166 @@
pipeline {
agent { label 'maven' }
options {
timestamps()
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} maven-extra"
currentBuild.description = "by=${env.RUN_TRIGGER_NAME}; repo=${params.EXTRA_REPO_URL}; ref=${params.EXTRA_REPO_REF}"
}
}
}
stage('Run Maven Extra Tests') {
steps {
sh '''
set -eux
git config --global --add safe.directory '*' || true
rm -rf ./sources ./artifacts
mkdir -p ./artifacts
if [ "${EXTRA_REPO_URL#/}" != "${EXTRA_REPO_URL}" ] && [ ! -d "${EXTRA_REPO_URL}" ]; then
mkdir -p ./artifacts/target/allure-results
EXTRA_UUID="$(cat /proc/sys/kernel/random/uuid)"
TS_MS="$(( $(date +%s) * 1000 ))"
cat > "./artifacts/target/allure-results/${EXTRA_UUID}-result.json" <<EOF
{
"uuid": "${EXTRA_UUID}",
"historyId": "maven-extra-skipped-${BUILD_NUMBER}",
"name": "Maven extra pipeline summary",
"fullName": "pipeline.MavenExtraSummary",
"status": "skipped",
"stage": "finished",
"start": ${TS_MS},
"stop": ${TS_MS},
"labels": [
{"name": "suite", "value": "Pipeline"},
{"name": "package", "value": "pipeline"},
{"name": "testClass", "value": "MavenExtraSummary"},
{"name": "testMethod", "value": "run"}
],
"steps": [],
"parameters": []
}
EOF
{
echo "job=${JOB_NAME}"
echo "build=${BUILD_NUMBER}"
echo "repo.url=${EXTRA_REPO_URL}"
echo "repo.ref=${EXTRA_REPO_REF}"
echo "status=SKIPPED_MISSING_REPO"
} > ./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
{
echo "repo=${EXTRA_REPO_URL}"
echo "ref=${EXTRA_REPO_REF}"
echo "status=SKIPPED_MISSING_REPO"
} > ./artifacts/run-info.txt
exit 0
fi
git clone "${EXTRA_REPO_URL}" ./sources
git -C ./sources checkout "${EXTRA_REPO_REF}"
GIT_SHA="$(git -C ./sources rev-parse --short HEAD)"
CID="$(docker create localhost:5005/otus/test-api:1.0.0 bash -lc "set -e; cd /workspace; mvn -Dallure.results.directory=target/allure-results ${EXTRA_MAVEN_GOAL}")"
cleanup_container() {
docker rm -f "${CID}" >/dev/null 2>&1 || true
}
trap cleanup_container EXIT INT TERM
tar -C ./sources -cf - . | docker cp - "${CID}:/workspace"
set +e
docker start -a "${CID}"
TEST_RC=$?
docker cp "${CID}:/workspace/target" "./artifacts" || true
mkdir -p ./artifacts/target/allure-results
if ! ls ./artifacts/target/allure-results/* >/dev/null 2>&1; then
EXTRA_UUID="$(cat /proc/sys/kernel/random/uuid)"
TS_MS="$(( $(date +%s) * 1000 ))"
STATUS="passed"
if [ "${TEST_RC}" -ne 0 ]; then
STATUS="failed"
fi
cat > "./artifacts/target/allure-results/${EXTRA_UUID}-result.json" <<EOF
{
"uuid": "${EXTRA_UUID}",
"historyId": "maven-extra-summary-${BUILD_NUMBER}",
"name": "Maven extra pipeline summary",
"fullName": "pipeline.MavenExtraSummary",
"status": "${STATUS}",
"stage": "finished",
"start": ${TS_MS},
"stop": ${TS_MS},
"labels": [
{"name": "suite", "value": "Pipeline"},
{"name": "package", "value": "pipeline"},
{"name": "testClass", "value": "MavenExtraSummary"},
{"name": "testMethod", "value": "run"}
],
"steps": [],
"parameters": []
}
EOF
fi
{
echo "repo=${EXTRA_REPO_URL}"
echo "ref=${EXTRA_REPO_REF}"
echo "sha=${GIT_SHA}"
echo "maven_goal=${EXTRA_MAVEN_GOAL}"
} > ./artifacts/run-info.txt
{
echo "job=${JOB_NAME}"
echo "build=${BUILD_NUMBER}"
echo "repo.url=${EXTRA_REPO_URL}"
echo "repo.ref=${EXTRA_REPO_REF}"
echo "repo.sha=${GIT_SHA}"
echo "maven.goal=${EXTRA_MAVEN_GOAL}"
} > ./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
trap - EXIT INT TERM
docker rm -f "${CID}" || true
exit "${TEST_RC}"
'''
}
}
}
post {
always {
script {
try {
allure commandline: 'allure', includeProperties: false, jdk: '', results: [[path: 'artifacts/target/allure-results']]
} catch (Exception ex) {
echo "Allure publisher unavailable: ${ex.message}"
}
}
junit allowEmptyResults: true, testResults: 'artifacts/target/surefire-reports/*.xml'
archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/run-info.txt,artifacts/target/**'
}
}
}
@@ -0,0 +1,161 @@
pipeline {
agent { label 'maven' }
options {
timestamps()
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} playwright/${params.PLAYWRIGHT_BROWSER}"
currentBuild.description = "by=${env.RUN_TRIGGER_NAME}; repo=${params.PLAYWRIGHT_REPO_URL}; ref=${params.PLAYWRIGHT_REPO_REF}"
}
}
}
stage('Run Playwright Tests') {
steps {
sh '''
set -eux
git config --global --add safe.directory '*' || true
rm -rf ./sources ./artifacts
mkdir -p ./artifacts
git clone "${PLAYWRIGHT_REPO_URL}" ./sources
TARGET_REF="${PLAYWRIGHT_REPO_REF}"
if git -C ./sources show-ref --verify --quiet "refs/remotes/origin/${TARGET_REF}"; then
git -C ./sources checkout -B "${TARGET_REF}" "origin/${TARGET_REF}"
else
DEFAULT_REF="$(git -C ./sources symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@')"
echo "Requested ref '${TARGET_REF}' not found, fallback to default '${DEFAULT_REF}'"
TARGET_REF="${DEFAULT_REF}"
git -C ./sources checkout -B "${TARGET_REF}" "origin/${TARGET_REF}"
fi
GIT_SHA="$(git -C ./sources rev-parse --short HEAD)"
TEST_IMAGE="${PLAYWRIGHT_DOCKER_IMAGE}"
docker pull "${TEST_IMAGE}" || true
CID="$(docker create \
--add-host=host.docker.internal:host-gateway \
"${TEST_IMAGE}" \
bash -lc "set -e; cd /tmp; mvn -Dheadless=${PLAYWRIGHT_HEADLESS} -Dbrowser=${PLAYWRIGHT_BROWSER} -DbaseUrl=${PLAYWRIGHT_BASE_URL} test")"
cleanup_container() {
docker rm -f "${CID}" >/dev/null 2>&1 || true
}
trap cleanup_container EXIT INT TERM
tar -C ./sources -cf - . | docker cp - "${CID}:/tmp"
set +e
docker start -a "${CID}"
TEST_RC=$?
docker cp "${CID}:/tmp/target" "./artifacts/target" || true
docker cp "${CID}:/tmp/traces" "./artifacts/traces" || true
mkdir -p ./artifacts/target/allure-results
ATTACHMENT_COUNT=0
ATTACHMENTS_MANIFEST="./artifacts/target/allure-results/.external-attachments.txt"
: > "${ATTACHMENTS_MANIFEST}"
ATTACH_FILES="$(find ./artifacts -type f \\( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.mp4' -o -iname '*.webm' -o -iname '*.zip' \\) 2>/dev/null || true)"
if [ -n "${ATTACH_FILES}" ]; then
while IFS= read -r attachment_file; do
if [ -z "${attachment_file}" ] || [ ! -f "${attachment_file}" ]; then
continue
fi
ATTACHMENT_COUNT=$((ATTACHMENT_COUNT + 1))
ext="$(echo "${attachment_file}" | awk -F. '{print tolower($NF)}')"
mime="application/octet-stream"
case "${ext}" in
png) mime="image/png" ;;
jpg|jpeg) mime="image/jpeg" ;;
mp4) mime="video/mp4" ;;
webm) mime="video/webm" ;;
zip) mime="application/zip" ;;
esac
source_name="external-attachment-${ATTACHMENT_COUNT}.${ext}"
cp "${attachment_file}" "./artifacts/target/allure-results/${source_name}" || continue
safe_name="$(basename "${attachment_file}" | sed 's/"/\\"/g')"
printf '%s|%s|%s\n' "${safe_name}" "${mime}" "${source_name}" >> "${ATTACHMENTS_MANIFEST}"
done <<EOF
${ATTACH_FILES}
EOF
fi
if [ "${ATTACHMENT_COUNT}" -gt 0 ]; then
ATTACHMENTS_JSON="$(awk -F'|' 'BEGIN{first=1} {if(!first){printf(",")} first=0; gsub(/"/,"\\\"", $1); printf("{\\\"name\\\":\\\"%s\\\",\\\"type\\\":\\\"%s\\\",\\\"source\\\":\\\"%s\\\"}", $1, $2, $3)}' "${ATTACHMENTS_MANIFEST}")"
EXTRA_UUID="$(cat /proc/sys/kernel/random/uuid)"
TS_MS="$(( $(date +%s) * 1000 ))"
cat > "./artifacts/target/allure-results/${EXTRA_UUID}-result.json" <<EOF
{
"uuid": "${EXTRA_UUID}",
"historyId": "external-artifacts-${BUILD_NUMBER}",
"name": "Collected artifacts",
"fullName": "pipeline.CollectedArtifacts",
"status": "passed",
"stage": "finished",
"start": ${TS_MS},
"stop": ${TS_MS},
"labels": [
{"name": "suite", "value": "Pipeline Artifacts"},
{"name": "package", "value": "pipeline"},
{"name": "testClass", "value": "CollectedArtifacts"},
{"name": "testMethod", "value": "attach"}
],
"attachments": [${ATTACHMENTS_JSON}],
"steps": [],
"parameters": []
}
EOF
fi
{
echo "repo=${PLAYWRIGHT_REPO_URL}"
echo "ref=${TARGET_REF}"
echo "sha=${GIT_SHA}"
echo "browser=${PLAYWRIGHT_BROWSER}"
echo "headless=${PLAYWRIGHT_HEADLESS}"
echo "base_url=${PLAYWRIGHT_BASE_URL}"
echo "docker_image=${TEST_IMAGE}"
} > ./artifacts/run-info.txt
{
echo "job=${JOB_NAME}"
echo "build=${BUILD_NUMBER}"
echo "repo.url=${PLAYWRIGHT_REPO_URL}"
echo "repo.ref=${TARGET_REF}"
echo "repo.sha=${GIT_SHA}"
echo "browser=${PLAYWRIGHT_BROWSER}"
echo "headless=${PLAYWRIGHT_HEADLESS}"
echo "base.url=${PLAYWRIGHT_BASE_URL}"
echo "docker.image=${TEST_IMAGE}"
} > ./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
trap - EXIT INT TERM
docker rm -f "${CID}" || true
exit "${TEST_RC}"
'''
}
}
}
post {
always {
script {
try {
allure commandline: 'allure', includeProperties: false, jdk: '', results: [[path: 'artifacts/target/allure-results']]
} catch (Exception ex) {
echo "Allure publisher unavailable: ${ex.message}"
}
}
junit allowEmptyResults: true, testResults: 'artifacts/target/surefire-reports/*.xml'
archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/run-info.txt,artifacts/target/**,artifacts/traces/**'
}
}
}
@@ -22,6 +22,7 @@ pipeline {
steps { steps {
sh ''' sh '''
set -eux set -eux
git config --global --add safe.directory '*' || true
rm -rf ./project rm -rf ./project
git clone "${MOBILE_REPO_URL}" ./project git clone "${MOBILE_REPO_URL}" ./project
git -C ./project checkout "${MOBILE_REPO_REF}" git -C ./project checkout "${MOBILE_REPO_REF}"
@@ -68,8 +69,8 @@ pipeline {
dir('project') { dir('project') {
sh ''' sh '''
set -eux set -eux
HW8_ROOT_PATH="${HW8_ROOT:-/workspace/hw8}" PROJECT_ROOT_PATH="${OTUS_WORKSPACE_ROOT:-/workspace/projectwork}"
COMPOSE_FILE="${HW8_ROOT_PATH}/config/compose/mobile-ci.compose.yml" COMPOSE_FILE="${PROJECT_ROOT_PATH}/config/compose/mobile-ci.compose.yml"
if docker compose version >/dev/null 2>&1; then if docker compose version >/dev/null 2>&1; then
compose_cmd() { PROJECT_DIR="$PWD" docker compose -f "${COMPOSE_FILE}" "$@"; } compose_cmd() { PROJECT_DIR="$PWD" docker compose -f "${COMPOSE_FILE}" "$@"; }
elif docker-compose version >/dev/null 2>&1; then elif docker-compose version >/dev/null 2>&1; then
@@ -96,6 +97,42 @@ pipeline {
compose_cmd down -v --remove-orphans || true compose_cmd down -v --remove-orphans || true
compose_cmd up -d ${SERVICES} compose_cmd up -d ${SERVICES}
WIREMOCK_CID="$(compose_cmd ps -q wiremock)"
if [ -z "${WIREMOCK_CID}" ]; then
echo "Wiremock container is not found"
exit 1
fi
if [ ! -f "./wiremock/mappings/wishlist-apk.json" ] || [ ! -f "./wiremock/__files/wishlist.apk" ]; then
echo "Missing wiremock APK assets in repository checkout: ./wiremock/mappings/wishlist-apk.json or ./wiremock/__files/wishlist.apk"
exit 1
fi
docker exec "${WIREMOCK_CID}" sh -lc 'rm -rf /home/wiremock/mappings /home/wiremock/__files && mkdir -p /home/wiremock/mappings /home/wiremock/__files'
tar -C ./wiremock -cf - . | docker exec -i "${WIREMOCK_CID}" sh -lc 'tar -C /home/wiremock -xf -'
docker restart "${WIREMOCK_CID}"
for i in $(seq 1 30); do
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}starting{{end}}' "${WIREMOCK_CID}" || true)"
if [ "${status}" = "healthy" ]; then
break
fi
if [ "${i}" -eq 30 ]; then
echo "Wiremock is not healthy after reload"
docker logs "${WIREMOCK_CID}" || true
exit 1
fi
sleep 2
done
if echo "${APP_URL}" | grep -Eq 'https?://wiremock:8080'; then
APK_PATH="$(echo "${APP_URL}" | sed -E 's#https?://[^/]+##')"
if [ -z "${APK_PATH}" ]; then
APK_PATH="/wishlist.apk"
fi
if ! docker exec "${WIREMOCK_CID}" sh -lc "wget -qO- \"http://127.0.0.1:8080${APK_PATH}\" >/dev/null"; then
echo "Wiremock can't serve APK path ${APK_PATH}"
docker exec "${WIREMOCK_CID}" sh -lc 'ls -la /home/wiremock /home/wiremock/mappings /home/wiremock/__files' || true
docker logs "${WIREMOCK_CID}" || true
exit 1
fi
fi
EMULATORS="" EMULATORS=""
SELECTED=0 SELECTED=0
for service in android-emulator-1 android-emulator-2; do for service in android-emulator-1 android-emulator-2; do
@@ -205,6 +242,39 @@ pipeline {
done done
exit 1 exit 1
fi fi
if echo "${APP_URL}" | grep -Eq 'https?://wiremock:8080'; then
APK_PATH="$(echo "${APP_URL}" | sed -E 's#https?://[^/]+##')"
if [ -z "${APK_PATH}" ]; then
APK_PATH="/wishlist.apk"
fi
OLD_IFS="${IFS}"
IFS=','
for target in ${MOBILE_EMULATORS_VALUE}; do
service_name="$(echo "${target}" | cut -d'|' -f1)"
if [ -z "${service_name}" ]; then
continue
fi
emu_cid="$(docker ps -q --filter "name=mobileci-${service_name}" | head -n1 || true)"
if [ -z "${emu_cid}" ]; then
echo "Emulator container not found for service ${service_name}"
exit 1
fi
ready="false"
for i in $(seq 1 30); do
if docker exec "${emu_cid}" sh -lc "wget -qO- \"http://wiremock:8080${APK_PATH}\" >/dev/null"; then
ready="true"
break
fi
sleep 2
done
if [ "${ready}" != "true" ]; then
echo "APK URL is not reachable from ${service_name}: ${APP_URL}"
docker logs --tail 200 "${emu_cid}" || true
exit 1
fi
done
IFS="${OLD_IFS}"
fi
run_single_class() { run_single_class() {
class_name="$1" class_name="$1"
@@ -410,8 +480,8 @@ EOF
dir('project') { dir('project') {
sh ''' sh '''
set +e set +e
HW8_ROOT_PATH="${HW8_ROOT:-/workspace/hw8}" PROJECT_ROOT_PATH="${OTUS_WORKSPACE_ROOT:-/workspace/projectwork}"
COMPOSE_FILE="${HW8_ROOT_PATH}/config/compose/mobile-ci.compose.yml" COMPOSE_FILE="${PROJECT_ROOT_PATH}/config/compose/mobile-ci.compose.yml"
if docker compose version >/dev/null 2>&1; then if docker compose version >/dev/null 2>&1; then
compose_cmd() { PROJECT_DIR="$PWD" docker compose -f "${COMPOSE_FILE}" "$@"; } compose_cmd() { PROJECT_DIR="$PWD" docker compose -f "${COMPOSE_FILE}" "$@"; }
elif docker-compose version >/dev/null 2>&1; then elif docker-compose version >/dev/null 2>&1; then
+148 -4
View File
@@ -8,12 +8,24 @@ pipeline {
stage('Prepare Metadata') { stage('Prepare Metadata') {
steps { steps {
script { script {
def bddRefsRaw = (params.BDD_REFS ?: '').trim()
def bddRefs = bddRefsRaw
? bddRefsRaw.split(',').collect { it.trim() }.findAll { it }.unique()
: []
if (bddRefs.isEmpty() && (params.BDD_REPO_REF ?: '').trim()) {
bddRefs = [params.BDD_REPO_REF.trim()]
}
if (bddRefs.isEmpty()) {
bddRefs = ['main', 'homework_2']
}
env.BDD_REFS_EFFECTIVE = bddRefs.join(',')
wrap([$class: 'BuildUser']) { wrap([$class: 'BuildUser']) {
env.RUN_TRIGGER_USER = env.BUILD_USER_ID ?: env.BUILD_USER ?: 'system' env.RUN_TRIGGER_USER = env.BUILD_USER_ID ?: env.BUILD_USER ?: 'system'
env.RUN_TRIGGER_NAME = env.BUILD_USER ?: env.BUILD_USER_ID ?: 'system' env.RUN_TRIGGER_NAME = env.BUILD_USER ?: env.BUILD_USER_ID ?: 'system'
} }
currentBuild.displayName = "#${env.BUILD_NUMBER} runner" currentBuild.displayName = "#${env.BUILD_NUMBER} runner"
currentBuild.description = "by=${env.RUN_TRIGGER_NAME}; qaRef=${params.QA_REPO_REF}; mobileRef=${params.MOBILE_REPO_REF}" currentBuild.description = "by=${env.RUN_TRIGGER_NAME}; bddRefs=${env.BDD_REFS_EFFECTIVE}; qaRef=${params.QA_REPO_REF}; mobileRef=${params.MOBILE_REPO_REF}"
} }
} }
} }
@@ -21,6 +33,33 @@ pipeline {
steps { steps {
script { script {
def fanout = [:] def fanout = [:]
def bddRefs = env.BDD_REFS_EFFECTIVE
.split(',')
.collect { it.trim() }
.findAll { it }
if (bddRefs.isEmpty()) {
bddRefs = ['main', 'homework_2']
}
bddRefs.each { ref ->
def refValue = ref
def refKey = ref.replaceAll(/[^A-Za-z0-9_.-]/, '_')
def runFile = "downstream-web-bdd-${refKey}.txt"
fanout["web-bdd-${refKey}"] = {
def run = build job: 'qa-web-bdd-tests',
wait: true,
propagate: false,
parameters: [
string(name: 'BDD_REPO_URL', value: params.BDD_REPO_URL),
string(name: 'BDD_REPO_REF', value: refValue),
string(name: 'BROWSER', value: params.BROWSER),
string(name: 'BASE_URL', value: params.BASE_URL),
string(name: 'SELENOID_URL', value: params.SELENOID_URL),
string(name: 'HEADLESS', value: params.HEADLESS)
]
writeFile file: runFile, text: "${run.number}|${run.result}\n"
}
}
fanout['selenium'] = { fanout['selenium'] = {
def run = build job: 'qa-selenium-tests', def run = build job: 'qa-selenium-tests',
@@ -73,6 +112,50 @@ pipeline {
writeFile file: 'downstream-api.txt', text: "${run.number}|${run.result}\n" writeFile file: 'downstream-api.txt', text: "${run.number}|${run.result}\n"
} }
fanout['api-contract'] = {
def run = build job: 'qa-api-contract-tests',
wait: true,
propagate: false
writeFile file: 'downstream-api-contract.txt', text: "${run.number}|${run.result}\n"
}
fanout['api-rest'] = {
def run = build job: 'qa-api-rest-tests',
wait: true,
propagate: false,
parameters: [
string(name: 'API_REST_REPO_URL', value: params.API_REST_REPO_URL),
string(name: 'API_REST_REPO_REF', value: params.API_REST_REPO_REF)
]
writeFile file: 'downstream-api-rest.txt', text: "${run.number}|${run.result}\n"
}
fanout['extra'] = {
def run = build job: 'qa-maven-extra-tests',
wait: true,
propagate: false,
parameters: [
string(name: 'EXTRA_REPO_URL', value: params.EXTRA_REPO_URL),
string(name: 'EXTRA_REPO_REF', value: params.EXTRA_REPO_REF)
]
writeFile file: 'downstream-extra.txt', text: "${run.number}|${run.result}\n"
}
fanout['playwright'] = {
def run = build job: 'qa-playwright-tests',
wait: true,
propagate: false,
parameters: [
string(name: 'PLAYWRIGHT_REPO_URL', value: params.PLAYWRIGHT_REPO_URL),
string(name: 'PLAYWRIGHT_REPO_REF', value: params.PLAYWRIGHT_REPO_REF),
string(name: 'PLAYWRIGHT_BROWSER', value: params.PLAYWRIGHT_BROWSER),
string(name: 'PLAYWRIGHT_HEADLESS', value: params.PLAYWRIGHT_HEADLESS),
string(name: 'PLAYWRIGHT_BASE_URL', value: params.BASE_URL),
string(name: 'PLAYWRIGHT_DOCKER_IMAGE', value: params.PLAYWRIGHT_DOCKER_IMAGE)
]
writeFile file: 'downstream-playwright.txt', text: "${run.number}|${run.result}\n"
}
parallel fanout parallel fanout
def parseRun = { String fileName -> def parseRun = { String fileName ->
@@ -86,36 +169,97 @@ pipeline {
] ]
} }
def bddRuns = [:]
bddRefs.each { ref ->
def refKey = ref.replaceAll(/[^A-Za-z0-9_.-]/, '_')
bddRuns[ref] = parseRun("downstream-web-bdd-${refKey}.txt")
}
def seleniumRun = parseRun('downstream-selenium.txt') def seleniumRun = parseRun('downstream-selenium.txt')
def mobileRun = parseRun('downstream-mobile.txt') def mobileRun = parseRun('downstream-mobile.txt')
def apiRun = parseRun('downstream-api.txt') def apiRun = parseRun('downstream-api.txt')
def apiContractRun = parseRun('downstream-api-contract.txt')
def apiRestRun = parseRun('downstream-api-rest.txt')
def extraRun = parseRun('downstream-extra.txt')
def playwrightRun = parseRun('downstream-playwright.txt')
def lines = [] def lines = []
lines << "job=${env.JOB_NAME}" lines << "job=${env.JOB_NAME}"
lines << "build=${env.BUILD_NUMBER}" lines << "build=${env.BUILD_NUMBER}"
lines << "trigger_user=${env.RUN_TRIGGER_USER}" lines << "trigger_user=${env.RUN_TRIGGER_USER}"
lines << "trigger_name=${env.RUN_TRIGGER_NAME}" lines << "trigger_name=${env.RUN_TRIGGER_NAME}"
lines << "bdd_repo_url=${params.BDD_REPO_URL}"
lines << "bdd_refs=${bddRefs.join(',')}"
lines << "qa_repo_url=${params.QA_REPO_URL}" lines << "qa_repo_url=${params.QA_REPO_URL}"
lines << "qa_repo_ref=${params.QA_REPO_REF}" lines << "qa_repo_ref=${params.QA_REPO_REF}"
lines << "mobile_repo_url=${params.MOBILE_REPO_URL}" lines << "mobile_repo_url=${params.MOBILE_REPO_URL}"
lines << "mobile_repo_ref=${params.MOBILE_REPO_REF}" lines << "mobile_repo_ref=${params.MOBILE_REPO_REF}"
lines << "api_rest_repo_url=${params.API_REST_REPO_URL}"
lines << "api_rest_repo_ref=${params.API_REST_REPO_REF}"
lines << "extra_repo_url=${params.EXTRA_REPO_URL}"
lines << "extra_repo_ref=${params.EXTRA_REPO_REF}"
lines << "playwright_repo_url=${params.PLAYWRIGHT_REPO_URL}"
lines << "playwright_repo_ref=${params.PLAYWRIGHT_REPO_REF}"
lines << "playwright_docker_image=${params.PLAYWRIGHT_DOCKER_IMAGE}"
bddRefs.each { ref ->
def refKey = ref.replaceAll(/[^A-Za-z0-9_.-]/, '_')
def run = bddRuns[ref]
lines << "web_bdd_${refKey}_build=${run.number}"
lines << "web_bdd_${refKey}_result=${run.result}"
lines << "web_bdd_${refKey}_link=/job/qa-web-bdd-tests/${run.number}/"
lines << "web_bdd_${refKey}_link_abs=${env.JENKINS_URL}job/qa-web-bdd-tests/${run.number}/"
}
def primaryBddRef = bddRefs[0]
def primaryBddRun = bddRuns[primaryBddRef]
lines << "web_bdd_build=${primaryBddRun.number}"
lines << "web_bdd_result=${primaryBddRun.result}"
lines << "selenium_build=${seleniumRun.number}" lines << "selenium_build=${seleniumRun.number}"
lines << "selenium_result=${seleniumRun.result}" lines << "selenium_result=${seleniumRun.result}"
lines << "mobile_build=${mobileRun.number}" lines << "mobile_build=${mobileRun.number}"
lines << "mobile_result=${mobileRun.result}" lines << "mobile_result=${mobileRun.result}"
lines << "api_build=${apiRun.number}" lines << "api_build=${apiRun.number}"
lines << "api_result=${apiRun.result}" lines << "api_result=${apiRun.result}"
lines << "api_contract_build=${apiContractRun.number}"
lines << "api_contract_result=${apiContractRun.result}"
lines << "api_rest_build=${apiRestRun.number}"
lines << "api_rest_result=${apiRestRun.result}"
lines << "extra_build=${extraRun.number}"
lines << "extra_result=${extraRun.result}"
lines << "playwright_build=${playwrightRun.number}"
lines << "playwright_result=${playwrightRun.result}"
lines << "web_bdd_link=/job/qa-web-bdd-tests/${primaryBddRun.number}/"
lines << "selenium_link=/job/qa-selenium-tests/${seleniumRun.number}/" lines << "selenium_link=/job/qa-selenium-tests/${seleniumRun.number}/"
lines << "mobile_link=/job/qa-mobile-appium-tests/${mobileRun.number}/" lines << "mobile_link=/job/qa-mobile-appium-tests/${mobileRun.number}/"
lines << "api_link=/job/qa-api-citrus-tests/${apiRun.number}/" lines << "api_link=/job/qa-api-citrus-tests/${apiRun.number}/"
lines << "api_contract_link=/job/qa-api-contract-tests/${apiContractRun.number}/"
lines << "api_rest_link=/job/qa-api-rest-tests/${apiRestRun.number}/"
lines << "extra_link=/job/qa-maven-extra-tests/${extraRun.number}/"
lines << "playwright_link=/job/qa-playwright-tests/${playwrightRun.number}/"
lines << "web_bdd_link_abs=${env.JENKINS_URL}job/qa-web-bdd-tests/${primaryBddRun.number}/"
lines << "selenium_link_abs=${env.JENKINS_URL}job/qa-selenium-tests/${seleniumRun.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 << "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}/" lines << "api_link_abs=${env.JENKINS_URL}job/qa-api-citrus-tests/${apiRun.number}/"
lines << "api_contract_link_abs=${env.JENKINS_URL}job/qa-api-contract-tests/${apiContractRun.number}/"
lines << "api_rest_link_abs=${env.JENKINS_URL}job/qa-api-rest-tests/${apiRestRun.number}/"
lines << "extra_link_abs=${env.JENKINS_URL}job/qa-maven-extra-tests/${extraRun.number}/"
lines << "playwright_link_abs=${env.JENKINS_URL}job/qa-playwright-tests/${playwrightRun.number}/"
writeFile file: 'runner-summary.txt', text: lines.join('\n') + '\n' writeFile file: 'runner-summary.txt', text: lines.join('\n') + '\n'
archiveArtifacts allowEmptyArchive: false, artifacts: 'runner-summary.txt' archiveArtifacts allowEmptyArchive: false, artifacts: 'runner-summary.txt'
def failed = [seleniumRun, mobileRun, apiRun].any { it.result != 'SUCCESS' } def criticalRuns = [mobileRun, apiRun, apiContractRun, apiRestRun, extraRun]
if (failed) { def optionalRuns = []
error("One or more downstream QA jobs failed. See runner-summary.txt for build links and statuses.") optionalRuns << seleniumRun
optionalRuns.addAll(bddRuns.values())
optionalRuns << playwrightRun
def criticalFailed = criticalRuns.any { it.result != 'SUCCESS' }
def optionalFailed = optionalRuns.any { it.result != 'SUCCESS' }
if (criticalFailed) {
error("One or more critical downstream QA jobs failed. See runner-summary.txt for build links and statuses.")
}
if (optionalFailed) {
currentBuild.result = 'UNSTABLE'
echo "Optional downstream jobs have failures (web-bdd refs/playwright). See runner-summary.txt for details."
} }
} }
} }
+107 -1
View File
@@ -22,10 +22,15 @@ pipeline {
steps { steps {
sh ''' sh '''
set -eux set -eux
git config --global --add safe.directory '*' || true
rm -rf ./sources rm -rf ./sources
git clone "${QA_REPO_URL}" ./sources git clone "${QA_REPO_URL}" ./sources
git -C ./sources checkout "${QA_REPO_REF}" git -C ./sources checkout "${QA_REPO_REF}"
GIT_SHA="$(git -C ./sources rev-parse --short HEAD)" GIT_SHA="$(git -C ./sources rev-parse --short HEAD)"
REMOTE_FACTORY="./sources/src/main/java/ru/kovbasa/driver/RemoteDriverFactory.java"
if [ -f "${REMOTE_FACTORY}" ] && ! grep -q "selenoid.video.enabled" "${REMOTE_FACTORY}"; then
sed -i 's/"enableVideo", false/"enableVideo", Boolean.parseBoolean(System.getProperty("selenoid.video.enabled", "false"))/' "${REMOTE_FACTORY}"
fi
if ! grep -q "<artifactId>allure-junit5</artifactId>" ./sources/pom.xml; then if ! grep -q "<artifactId>allure-junit5</artifactId>" ./sources/pom.xml; then
awk -v ver="${ALLURE_ADAPTER_VERSION}" ' awk -v ver="${ALLURE_ADAPTER_VERSION}" '
index($0, "</dependencies>") && !done { index($0, "</dependencies>") && !done {
@@ -64,11 +69,34 @@ pipeline {
if [ "${BROWSER}" = "chrome" ]; then if [ "${BROWSER}" = "chrome" ]; then
EXTRA_ARGS="-Dchrome.binary=/usr/bin/chromium" EXTRA_ARGS="-Dchrome.binary=/usr/bin/chromium"
fi fi
VIDEO_ARGS=""
SELENOID_BASE=""
SELENOID_VIDEO_BEFORE="./artifacts/selenoid-videos-before.txt"
SELENOID_VIDEO_AFTER="./artifacts/selenoid-videos-after.txt"
SELENOID_VIDEO_NEW="./artifacts/selenoid-videos-new.txt"
: > "${SELENOID_VIDEO_BEFORE}"
: > "${SELENOID_VIDEO_AFTER}"
: > "${SELENOID_VIDEO_NEW}"
if [ "${EXECUTION_MODE}" = "selenoid" ]; then
case "${BROWSER}" in
chrome) docker pull selenoid/vnc:chrome_128.0 || true ;;
firefox) docker pull selenoid/vnc:firefox_125.0 || true ;;
esac
docker pull selenoid/video-recorder:latest-release || docker pull selenoid/video-recorder:latest || true
VIDEO_ARGS="-Dselenoid.video.enabled=true"
if [ -n "${SELENOID_URL}" ]; then
SELENOID_BASE="${SELENOID_URL%/wd/hub}"
(curl -fsS "${SELENOID_BASE}/video/" || true) \
| tr '"' '\n' \
| grep -E '\\.mp4$' \
| sort -u > "${SELENOID_VIDEO_BEFORE}" || true
fi
fi
CID="$(docker create \ CID="$(docker create \
--add-host=host.docker.internal:host-gateway \ --add-host=host.docker.internal:host-gateway \
localhost:5005/otus/test-selenium:1.0.0 \ localhost:5005/otus/test-selenium:1.0.0 \
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")" 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 ${VIDEO_ARGS} ${EXTRA_ARGS} test")"
cleanup_container() { cleanup_container() {
docker rm -f "${CID}" >/dev/null 2>&1 || true docker rm -f "${CID}" >/dev/null 2>&1 || true
} }
@@ -79,6 +107,83 @@ pipeline {
TEST_RC=$? TEST_RC=$?
docker cp "${CID}:/workspace/target" "./artifacts" || true docker cp "${CID}:/workspace/target" "./artifacts" || true
mkdir -p ./artifacts/target/allure-results mkdir -p ./artifacts/target/allure-results
if [ "${EXECUTION_MODE}" = "selenoid" ] && [ -n "${SELENOID_URL}" ]; then
(curl -fsS "${SELENOID_BASE}/video/" || true) \
| tr '"' '\n' \
| grep -E '\\.mp4$' \
| sort -u > "${SELENOID_VIDEO_AFTER}" || true
comm -13 "${SELENOID_VIDEO_BEFORE}" "${SELENOID_VIDEO_AFTER}" > "${SELENOID_VIDEO_NEW}" || true
if [ ! -s "${SELENOID_VIDEO_NEW}" ]; then
cp "${SELENOID_VIDEO_AFTER}" "${SELENOID_VIDEO_NEW}" || true
fi
while IFS= read -r video_file; do
[ -n "${video_file}" ] || continue
downloaded=0
for i in $(seq 1 20); do
if curl -fsS "${SELENOID_BASE}/video/${video_file}" -o "./artifacts/target/${video_file}"; then
downloaded=1
break
fi
sleep 2
done
if [ "${downloaded}" -ne 1 ]; then
echo "Failed to fetch Selenoid video: ${video_file}"
fi
done < "${SELENOID_VIDEO_NEW}"
fi
ATTACHMENT_COUNT=0
ATTACHMENTS_MANIFEST="./artifacts/target/allure-results/.external-attachments.txt"
: > "${ATTACHMENTS_MANIFEST}"
ATTACH_FILES="$(find ./artifacts -type f \\( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.mp4' -o -iname '*.webm' \\) 2>/dev/null || true)"
if [ -n "${ATTACH_FILES}" ]; then
while IFS= read -r attachment_file; do
if [ -z "${attachment_file}" ] || [ ! -f "${attachment_file}" ]; then
continue
fi
ATTACHMENT_COUNT=$((ATTACHMENT_COUNT + 1))
ext="$(echo "${attachment_file}" | awk -F. '{print tolower($NF)}')"
mime="application/octet-stream"
case "${ext}" in
png) mime="image/png" ;;
jpg|jpeg) mime="image/jpeg" ;;
mp4) mime="video/mp4" ;;
webm) mime="video/webm" ;;
esac
source_name="external-attachment-${ATTACHMENT_COUNT}.${ext}"
cp "${attachment_file}" "./artifacts/target/allure-results/${source_name}" || continue
safe_name="$(basename "${attachment_file}" | sed 's/"/\\"/g')"
printf '%s|%s|%s\n' "${safe_name}" "${mime}" "${source_name}" >> "${ATTACHMENTS_MANIFEST}"
done <<EOF
${ATTACH_FILES}
EOF
fi
if [ "${ATTACHMENT_COUNT}" -gt 0 ]; then
ATTACHMENTS_JSON="$(awk -F'|' 'BEGIN{first=1} {if(!first){printf(",")} first=0; gsub(/"/,"\\\"", $1); printf("{\\\"name\\\":\\\"%s\\\",\\\"type\\\":\\\"%s\\\",\\\"source\\\":\\\"%s\\\"}", $1, $2, $3)}' "${ATTACHMENTS_MANIFEST}")"
EXTRA_UUID="$(cat /proc/sys/kernel/random/uuid)"
TS_MS="$(( $(date +%s) * 1000 ))"
cat > "./artifacts/target/allure-results/${EXTRA_UUID}-result.json" <<EOF
{
"uuid": "${EXTRA_UUID}",
"historyId": "external-artifacts-${BUILD_NUMBER}",
"name": "Collected artifacts",
"fullName": "pipeline.CollectedArtifacts",
"status": "passed",
"stage": "finished",
"start": ${TS_MS},
"stop": ${TS_MS},
"labels": [
{"name": "suite", "value": "Pipeline Artifacts"},
{"name": "package", "value": "pipeline"},
{"name": "testClass", "value": "CollectedArtifacts"},
{"name": "testMethod", "value": "attach"}
],
"attachments": [${ATTACHMENTS_JSON}],
"steps": [],
"parameters": []
}
EOF
fi
{ {
echo "job=${JOB_NAME}" echo "job=${JOB_NAME}"
echo "build=${BUILD_NUMBER}" echo "build=${BUILD_NUMBER}"
@@ -91,6 +196,7 @@ pipeline {
echo "headless=${HEADLESS}" echo "headless=${HEADLESS}"
echo "execution.mode=${EXECUTION_MODE}" echo "execution.mode=${EXECUTION_MODE}"
echo "base.url=${BASE_URL}" echo "base.url=${BASE_URL}"
echo "video.selection=all-new-from-selenoid"
} > ./artifacts/target/allure-results/environment.properties } > ./artifacts/target/allure-results/environment.properties
cat > ./artifacts/target/allure-results/executor.json <<EOF cat > ./artifacts/target/allure-results/executor.json <<EOF
{ {
+259
View File
@@ -0,0 +1,259 @@
pipeline {
agent { label 'maven' }
options {
timestamps()
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} bdd/${params.BDD_REPO_REF}"
currentBuild.description =
"by=${env.RUN_TRIGGER_NAME}; repo=${params.BDD_REPO_URL}; ref=${params.BDD_REPO_REF}; browser=${params.BROWSER}"
}
}
}
stage('Run WEB BDD Tests In Docker') {
steps {
sh '''
set -eux
git config --global --add safe.directory '*' || true
rm -rf ./sources
git clone "${BDD_REPO_URL}" ./sources
TARGET_REF="${BDD_REPO_REF}"
if git -C ./sources show-ref --verify --quiet "refs/remotes/origin/${TARGET_REF}"; then
git -C ./sources checkout -B "${TARGET_REF}" "origin/${TARGET_REF}"
else
DEFAULT_REF="$(git -C ./sources symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@')"
echo "Requested ref '${TARGET_REF}' not found, fallback to default '${DEFAULT_REF}'"
TARGET_REF="${DEFAULT_REF}"
git -C ./sources checkout -B "${TARGET_REF}" "origin/${TARGET_REF}"
fi
GIT_SHA="$(git -C ./sources rev-parse --short HEAD)"
CHROME_FACTORY="./sources/src/main/java/ru/kovbasa/driver/ChromeDriverFactory.java"
if [ -f "${CHROME_FACTORY}" ]; then
cat > "${CHROME_FACTORY}" <<'EOF'
package ru.kovbasa.driver;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
import java.net.URI;
import java.net.URL;
import java.util.List;
import java.util.Map;
public class ChromeDriverFactory implements DriverFactory {
@Override
public WebDriver createDriver() {
final ChromeOptions options = new ChromeOptions();
options.addArguments("--start-maximized");
options.addArguments("--disable-notifications");
final boolean headless = Boolean.parseBoolean(System.getProperty("selenide.headless", "false"));
if (headless) {
options.addArguments("--headless=new");
}
options.addArguments("--no-sandbox");
options.addArguments("--disable-dev-shm-usage");
options.addArguments("--disable-gpu");
options.addArguments("--window-size=1920,1080");
options.addArguments("--remote-allow-origins=*");
final String remoteUrl = System.getProperty("selenoid.url", "").trim();
if (!remoteUrl.isEmpty()) {
try {
options.setCapability("selenoid:options", Map.of(
"name", "bdd-ui-tests",
"enableVNC", true,
"enableVideo", Boolean.parseBoolean(System.getProperty("selenoid.video.enabled", "false")),
"env", List.of("TZ=UTC")
));
final URL url = URI.create(remoteUrl).toURL();
return new RemoteWebDriver(url, options);
} catch (Exception ex) {
throw new RuntimeException("Failed to create RemoteWebDriver for selenoid.url=" + remoteUrl, ex);
}
}
return new ChromeDriver(options);
}
}
EOF
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 "bdd_repo_url=${BDD_REPO_URL}"
echo "bdd_repo_ref=${TARGET_REF}"
echo "bdd_repo_sha=${GIT_SHA}"
echo "browser=${BROWSER}"
echo "headless=${HEADLESS}"
echo "base_url=${BASE_URL}"
echo "selenoid_url=${SELENOID_URL}"
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
} > ./artifacts/run-info.txt
EXTRA_ARGS=""
if [ "${BROWSER}" = "chrome" ]; then
EXTRA_ARGS="-Dchrome.binary=/usr/bin/chromium"
fi
SELENOID_ARGS=""
SELENOID_BASE=""
SELENOID_VIDEO_BEFORE="./artifacts/selenoid-videos-before.txt"
SELENOID_VIDEO_AFTER="./artifacts/selenoid-videos-after.txt"
SELENOID_VIDEO_NEW="./artifacts/selenoid-videos-new.txt"
: > "${SELENOID_VIDEO_BEFORE}"
: > "${SELENOID_VIDEO_AFTER}"
: > "${SELENOID_VIDEO_NEW}"
if [ -n "${SELENOID_URL}" ]; then
case "${BROWSER}" in
chrome) docker pull selenoid/vnc:chrome_128.0 || true ;;
firefox) docker pull selenoid/vnc:firefox_125.0 || true ;;
esac
docker pull selenoid/video-recorder:latest-release || docker pull selenoid/video-recorder:latest || true
SELENOID_ARGS="-Dselenoid.video.enabled=true"
SELENOID_BASE="${SELENOID_URL%/wd/hub}"
(curl -fsS "${SELENOID_BASE}/video/" || true) \
| tr '"' '\n' \
| grep -E '\\.mp4$' \
| sort -u > "${SELENOID_VIDEO_BEFORE}" || true
fi
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 -Dallure.version=${ALLURE_ADAPTER_VERSION} -Dbrowser=${BROWSER} -Dbase.url=${BASE_URL} -Dselenoid.url=${SELENOID_URL} -Dselenide.headless=${HEADLESS} -Dallure.results.directory=target/allure-results ${SELENOID_ARGS} ${EXTRA_ARGS} test")"
cleanup_container() {
docker rm -f "${CID}" >/dev/null 2>&1 || true
}
trap cleanup_container EXIT INT TERM
tar -C ./sources -cf - . | docker cp - "${CID}:/workspace"
set +e
docker start -a "${CID}"
TEST_RC=$?
docker cp "${CID}:/workspace/target" "./artifacts" || true
mkdir -p ./artifacts/target/allure-results
if [ -n "${SELENOID_URL}" ]; then
(curl -fsS "${SELENOID_BASE}/video/" || true) \
| tr '"' '\n' \
| grep -E '\\.mp4$' \
| sort -u > "${SELENOID_VIDEO_AFTER}" || true
comm -13 "${SELENOID_VIDEO_BEFORE}" "${SELENOID_VIDEO_AFTER}" > "${SELENOID_VIDEO_NEW}" || true
if [ ! -s "${SELENOID_VIDEO_NEW}" ]; then
cp "${SELENOID_VIDEO_AFTER}" "${SELENOID_VIDEO_NEW}" || true
fi
while IFS= read -r video_file; do
[ -n "${video_file}" ] || continue
downloaded=0
for i in $(seq 1 20); do
if curl -fsS "${SELENOID_BASE}/video/${video_file}" -o "./artifacts/target/${video_file}"; then
downloaded=1
break
fi
sleep 2
done
if [ "${downloaded}" -ne 1 ]; then
echo "Failed to fetch Selenoid video: ${video_file}"
fi
done < "${SELENOID_VIDEO_NEW}"
fi
ATTACHMENT_COUNT=0
ATTACHMENTS_MANIFEST="./artifacts/target/allure-results/.external-attachments.txt"
: > "${ATTACHMENTS_MANIFEST}"
ATTACH_FILES="$(find ./artifacts -type f \\( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.mp4' -o -iname '*.webm' \\) 2>/dev/null || true)"
if [ -n "${ATTACH_FILES}" ]; then
while IFS= read -r attachment_file; do
if [ -z "${attachment_file}" ] || [ ! -f "${attachment_file}" ]; then
continue
fi
ATTACHMENT_COUNT=$((ATTACHMENT_COUNT + 1))
ext="$(echo "${attachment_file}" | awk -F. '{print tolower($NF)}')"
mime="application/octet-stream"
case "${ext}" in
png) mime="image/png" ;;
jpg|jpeg) mime="image/jpeg" ;;
mp4) mime="video/mp4" ;;
webm) mime="video/webm" ;;
esac
source_name="external-attachment-${ATTACHMENT_COUNT}.${ext}"
cp "${attachment_file}" "./artifacts/target/allure-results/${source_name}" || continue
safe_name="$(basename "${attachment_file}" | sed 's/"/\\"/g')"
printf '%s|%s|%s\n' "${safe_name}" "${mime}" "${source_name}" >> "${ATTACHMENTS_MANIFEST}"
done <<EOF
${ATTACH_FILES}
EOF
fi
if [ "${ATTACHMENT_COUNT}" -gt 0 ]; then
ATTACHMENTS_JSON="$(awk -F'|' 'BEGIN{first=1} {if(!first){printf(",")} first=0; gsub(/"/,"\\\"", $1); printf("{\\\"name\\\":\\\"%s\\\",\\\"type\\\":\\\"%s\\\",\\\"source\\\":\\\"%s\\\"}", $1, $2, $3)}' "${ATTACHMENTS_MANIFEST}")"
EXTRA_UUID="$(cat /proc/sys/kernel/random/uuid)"
TS_MS="$(( $(date +%s) * 1000 ))"
cat > "./artifacts/target/allure-results/${EXTRA_UUID}-result.json" <<EOF
{
"uuid": "${EXTRA_UUID}",
"historyId": "external-artifacts-${BUILD_NUMBER}",
"name": "Collected artifacts",
"fullName": "pipeline.CollectedArtifacts",
"status": "passed",
"stage": "finished",
"start": ${TS_MS},
"stop": ${TS_MS},
"labels": [
{"name": "suite", "value": "Pipeline Artifacts"},
{"name": "package", "value": "pipeline"},
{"name": "testClass", "value": "CollectedArtifacts"},
{"name": "testMethod", "value": "attach"}
],
"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=${BDD_REPO_URL}"
echo "repo.ref=${TARGET_REF}"
echo "repo.sha=${GIT_SHA}"
echo "browser=${BROWSER}"
echo "headless=${HEADLESS}"
echo "base.url=${BASE_URL}"
echo "selenoid.url=${SELENOID_URL}"
echo "video.selection=all-new-from-selenoid"
} > ./artifacts/target/allure-results/environment.properties
trap - EXIT INT TERM
docker rm -f "${CID}" || true
exit "${TEST_RC}"
'''
}
}
}
post {
always {
script {
try {
allure commandline: 'allure', includeProperties: false, jdk: '', results: [[path: 'artifacts/target/allure-results']]
} catch (Exception ex) {
echo "Allure publisher unavailable: ${ex.message}"
}
}
junit allowEmptyResults: true, testResults: 'artifacts/target/surefire-reports/*.xml'
archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/run-info.txt,artifacts/target/**'
}
}
}
+1 -1
View File
@@ -11,6 +11,6 @@
parameters: parameters:
- string: - string:
name: JJB_PATH name: JJB_PATH
default: /workspace/hw8/config/jobs default: /workspace/projectwork/config/jobs
description: "Путь до JJB-конфигов" description: "Путь до JJB-конфигов"
dsl: !include-raw-verbatim: ../scripts/jobs-uploader.groovy dsl: !include-raw-verbatim: ../scripts/jobs-uploader.groovy
@@ -0,0 +1,11 @@
---
- job:
name: qa-api-contract-tests
description: "Контрактные API тесты (WireMock + JsonSchemaValidation)."
project-type: pipeline
concurrent: true
sandbox: true
properties:
- build-discarder:
num-to-keep: 50
dsl: !include-raw-verbatim: ../scripts/qa-api-contract-tests.groovy
@@ -0,0 +1,24 @@
---
- job:
name: qa-api-rest-tests
description: "API REST тесты (RestAssured + JsonSchema + quality gates)."
project-type: pipeline
concurrent: true
sandbox: true
properties:
- build-discarder:
num-to-keep: 50
parameters:
- string:
name: API_REST_REPO_URL
default: https://git.kovbasa.ru/otus-autotests/hw3.git
- string:
name: API_REST_REPO_REF
default: main
- string:
name: API_REST_MAVEN_GOAL
default: verify
- string:
name: API_REST_JAVA_RELEASE
default: "21"
dsl: !include-raw-verbatim: ../scripts/qa-hw3-api-tests.groovy
+21
View File
@@ -0,0 +1,21 @@
---
- job:
name: qa-maven-extra-tests
description: "Дополнительные Maven тесты (универсальная job)."
project-type: pipeline
concurrent: true
sandbox: true
properties:
- build-discarder:
num-to-keep: 50
parameters:
- string:
name: EXTRA_REPO_URL
default: https://git.kovbasa.ru/otus-autotests/hw3.git
- string:
name: EXTRA_REPO_REF
default: homework_5
- string:
name: EXTRA_MAVEN_GOAL
default: test
dsl: !include-raw-verbatim: ../scripts/qa-hw5-tests.groovy
@@ -0,0 +1,35 @@
---
- job:
name: qa-playwright-tests
description: "Playwright UI тесты."
project-type: pipeline
concurrent: true
sandbox: true
properties:
- build-discarder:
num-to-keep: 50
parameters:
- string:
name: PLAYWRIGHT_REPO_URL
default: https://git.kovbasa.ru/otus-autotests/homework_6.git
- string:
name: PLAYWRIGHT_REPO_REF
default: master
- choice:
name: PLAYWRIGHT_BROWSER
choices:
- chromium
- firefox
- webkit
- choice:
name: PLAYWRIGHT_HEADLESS
choices:
- "true"
- "false"
- string:
name: PLAYWRIGHT_BASE_URL
default: https://otus.ru
- string:
name: PLAYWRIGHT_DOCKER_IMAGE
default: mcr.microsoft.com/playwright/java:v1.58.0-jammy
dsl: !include-raw-verbatim: ../scripts/qa-hw6-playwright-tests.groovy
+46 -2
View File
@@ -5,10 +5,22 @@
project-type: pipeline project-type: pipeline
concurrent: true concurrent: true
sandbox: true sandbox: true
triggers:
- timed: "15 1 * * *"
properties: properties:
- build-discarder: - build-discarder:
num-to-keep: 50 num-to-keep: 50
parameters: parameters:
- string:
name: BDD_REPO_URL
default: https://git.kovbasa.ru/otus-autotests/homework_1.git
- string:
name: BDD_REFS
default: main,homework_2
- string:
name: BDD_REPO_REF
default: homework_2
description: "Legacy fallback if BDD_REFS is empty"
- string: - string:
name: QA_REPO_URL name: QA_REPO_URL
default: https://git.kovbasa.ru/otus-autotests/homework_4.git default: https://git.kovbasa.ru/otus-autotests/homework_4.git
@@ -41,15 +53,15 @@
default: https://otus.ru default: https://otus.ru
- string: - string:
name: EXECUTION_MODE name: EXECUTION_MODE
default: local default: selenoid
- string: - string:
name: SELENOID_URL name: SELENOID_URL
default: http://host.docker.internal:4444/wd/hub default: http://host.docker.internal:4444/wd/hub
- choice: - choice:
name: HEADLESS name: HEADLESS
choices: choices:
- "true"
- "false" - "false"
- "true"
- string: - string:
name: APP_URL name: APP_URL
default: http://wiremock:8080/wishlist.apk default: http://wiremock:8080/wishlist.apk
@@ -83,4 +95,36 @@
- string: - string:
name: RESERVATION_OWNER name: RESERVATION_OWNER
default: user4us default: user4us
- string:
name: API_REST_REPO_URL
default: https://git.kovbasa.ru/otus-autotests/hw3.git
- string:
name: API_REST_REPO_REF
default: main
- string:
name: EXTRA_REPO_URL
default: https://git.kovbasa.ru/otus-autotests/hw3.git
- string:
name: EXTRA_REPO_REF
default: homework_5
- string:
name: PLAYWRIGHT_REPO_URL
default: https://git.kovbasa.ru/otus-autotests/homework_6.git
- string:
name: PLAYWRIGHT_REPO_REF
default: master
- choice:
name: PLAYWRIGHT_BROWSER
choices:
- chromium
- firefox
- webkit
- choice:
name: PLAYWRIGHT_HEADLESS
choices:
- "true"
- "false"
- string:
name: PLAYWRIGHT_DOCKER_IMAGE
default: mcr.microsoft.com/playwright/java:v1.58.0-jammy
dsl: !include-raw-verbatim: ../scripts/qa-runner.groovy dsl: !include-raw-verbatim: ../scripts/qa-runner.groovy
+2 -2
View File
@@ -33,7 +33,7 @@
description: "Базовый URL тестируемого сайта" description: "Базовый URL тестируемого сайта"
- string: - string:
name: EXECUTION_MODE name: EXECUTION_MODE
default: local default: selenoid
description: "Режим запуска (local|selenoid)" description: "Режим запуска (local|selenoid)"
- string: - string:
name: SELENOID_URL name: SELENOID_URL
@@ -42,7 +42,7 @@
- choice: - choice:
name: HEADLESS name: HEADLESS
choices: choices:
- "true"
- "false" - "false"
- "true"
description: "Headless режим" description: "Headless режим"
dsl: !include-raw-verbatim: ../scripts/qa-selenium-tests.groovy dsl: !include-raw-verbatim: ../scripts/qa-selenium-tests.groovy
@@ -0,0 +1,40 @@
---
- job:
name: qa-web-bdd-tests
description: "WEB BDD/Cucumber тесты (homework_1, branch homework_2)."
project-type: pipeline
concurrent: true
sandbox: true
properties:
- build-discarder:
num-to-keep: 50
parameters:
- string:
name: BDD_REPO_URL
default: https://git.kovbasa.ru/otus-autotests/homework_1.git
description: "Git URL репозитория с BDD/Cucumber web тестами"
- string:
name: BDD_REPO_REF
default: homework_2
description: "Git branch/tag/commit для checkout"
- string:
name: ALLURE_ADAPTER_VERSION
default: 2.29.1
description: "Версия allure-junit5 адаптера"
- choice:
name: BROWSER
choices:
- chrome
- firefox
- string:
name: BASE_URL
default: https://otus.ru
- string:
name: SELENOID_URL
default: http://host.docker.internal:4444/wd/hub
- choice:
name: HEADLESS
choices:
- "false"
- "true"
dsl: !include-raw-verbatim: ../scripts/qa-web-bdd-tests.groovy
+5
View File
@@ -41,9 +41,14 @@
filter-executors: true filter-executors: true
filter-queue: true filter-queue: true
job-name: job-name:
- qa-web-bdd-tests
- qa-selenium-tests - qa-selenium-tests
- qa-api-citrus-tests - qa-api-citrus-tests
- qa-api-contract-tests
- qa-mobile-appium-tests - qa-mobile-appium-tests
- qa-api-rest-tests
- qa-maven-extra-tests
- qa-playwright-tests
columns: columns:
- status - status
- weather - weather
@@ -0,0 +1,16 @@
{
"request": {
"method": "GET",
"urlPath": "/users/500"
},
"response": {
"status": 500,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": {
"error": "temporary_failure",
"message": "Mocked failing method for resilience checks"
}
}
}
@@ -0,0 +1,19 @@
{
"request": {
"method": "GET",
"urlPath": "/users/1"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": {
"id": 1,
"name": "Student 1",
"grade": "A",
"school_name": "OTUS School",
"city": "Moscow"
}
}
}
+24
View File
@@ -0,0 +1,24 @@
{
"request": {
"method": "GET",
"urlPath": "/users/get/all"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": [
{
"id": 1,
"name": "Ivan Ivanov",
"grade": "A"
},
{
"id": 2,
"name": "Petr Petrov",
"grade": "B"
}
]
}
}
+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus.projectwork</groupId>
<artifactId>contracts-tests</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.10.2</junit.version>
<restassured.version>5.5.1</restassured.version>
</properties>
<dependencies>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>${restassured.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<version>${restassured.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<systemPropertyVariables>
<baseUrl>${baseUrl}</baseUrl>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,52 @@
package ru.otus.contracts;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
class UsersContractTest {
@BeforeEach
void setUp() {
RestAssured.baseURI = System.getProperty("baseUrl", "http://127.0.0.1:18080");
}
@Test
void shouldReturnUsersArrayByContract() {
given()
.when()
.get("/users/get/all")
.then()
.statusCode(200)
.body(matchesJsonSchemaInClasspath("schemas/users-all.schema.json"))
.body("size()", greaterThanOrEqualTo(1));
}
@Test
void shouldReturnUserByIdByContract() {
given()
.pathParam("id", 1)
.when()
.get("/users/{id}")
.then()
.statusCode(200)
.body(matchesJsonSchemaInClasspath("schemas/user-by-id.schema.json"))
.body("id", equalTo(1));
}
@Test
void shouldMockFailingMethod() {
given()
.pathParam("id", 500)
.when()
.get("/users/{id}")
.then()
.statusCode(500)
.body("error", equalTo("temporary_failure"));
}
}
@@ -0,0 +1,23 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["id", "name", "grade", "school_name", "city"],
"additionalProperties": false,
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"grade": {
"type": "string"
},
"school_name": {
"type": "string"
},
"city": {
"type": "string"
}
}
}
@@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["id", "name", "grade"],
"additionalProperties": false,
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"grade": {
"type": "string"
}
}
}
}