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
@@ -23,6 +23,9 @@ pipeline {
set -eux
docker version
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-api:1.0.0
docker pull localhost:5005/otus/test-mobile:1.0.0
@@ -33,8 +36,8 @@ pipeline {
steps {
sh '''
set -eux
HW8_ROOT_PATH="${HW8_ROOT:-/workspace/hw8}"
test -f "${HW8_ROOT_PATH}/config/jobs/global.yaml"
PROJECT_ROOT_PATH="${OTUS_WORKSPACE_ROOT:-/workspace/projectwork}"
test -f "${PROJECT_ROOT_PATH}/config/jobs/global.yaml"
'''
}
}
+56 -1
View File
@@ -22,6 +22,7 @@ pipeline {
steps {
sh '''
set -eux
git config --global --add safe.directory '*' || true
rm -rf ./sources
git clone "${QA_REPO_URL}" ./sources
git -C ./sources checkout "${QA_REPO_REF}"
@@ -40,7 +41,7 @@ pipeline {
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")"
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() {
docker rm -f "${CID}" >/dev/null 2>&1 || true
}
@@ -50,6 +51,53 @@ pipeline {
docker start -a "${CID}"
TEST_RC=$?
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
docker rm -f "${CID}" || true
exit "${TEST_RC}"
@@ -59,6 +107,13 @@ pipeline {
}
post {
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'
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 {
sh '''
set -eux
git config --global --add safe.directory '*' || true
rm -rf ./project
git clone "${MOBILE_REPO_URL}" ./project
git -C ./project checkout "${MOBILE_REPO_REF}"
@@ -68,8 +69,8 @@ pipeline {
dir('project') {
sh '''
set -eux
HW8_ROOT_PATH="${HW8_ROOT:-/workspace/hw8}"
COMPOSE_FILE="${HW8_ROOT_PATH}/config/compose/mobile-ci.compose.yml"
PROJECT_ROOT_PATH="${OTUS_WORKSPACE_ROOT:-/workspace/projectwork}"
COMPOSE_FILE="${PROJECT_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
@@ -96,6 +97,42 @@ pipeline {
compose_cmd down -v --remove-orphans || true
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=""
SELECTED=0
for service in android-emulator-1 android-emulator-2; do
@@ -205,6 +242,39 @@ pipeline {
done
exit 1
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() {
class_name="$1"
@@ -410,8 +480,8 @@ EOF
dir('project') {
sh '''
set +e
HW8_ROOT_PATH="${HW8_ROOT:-/workspace/hw8}"
COMPOSE_FILE="${HW8_ROOT_PATH}/config/compose/mobile-ci.compose.yml"
PROJECT_ROOT_PATH="${OTUS_WORKSPACE_ROOT:-/workspace/projectwork}"
COMPOSE_FILE="${PROJECT_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
+148 -4
View File
@@ -8,12 +8,24 @@ pipeline {
stage('Prepare Metadata') {
steps {
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']) {
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}"
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 {
script {
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'] = {
def run = build job: 'qa-selenium-tests',
@@ -73,6 +112,50 @@ pipeline {
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
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 mobileRun = parseRun('downstream-mobile.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 = []
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 << "bdd_repo_url=${params.BDD_REPO_URL}"
lines << "bdd_refs=${bddRefs.join(',')}"
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 << "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_result=${seleniumRun.result}"
lines << "mobile_build=${mobileRun.number}"
lines << "mobile_result=${mobileRun.result}"
lines << "api_build=${apiRun.number}"
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 << "mobile_link=/job/qa-mobile-appium-tests/${mobileRun.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 << "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_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'
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.")
def criticalRuns = [mobileRun, apiRun, apiContractRun, apiRestRun, extraRun]
def optionalRuns = []
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 {
sh '''
set -eux
git config --global --add safe.directory '*' || true
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)"
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
awk -v ver="${ALLURE_ADAPTER_VERSION}" '
index($0, "</dependencies>") && !done {
@@ -64,11 +69,34 @@ pipeline {
if [ "${BROWSER}" = "chrome" ]; then
EXTRA_ARGS="-Dchrome.binary=/usr/bin/chromium"
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 \
--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} -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() {
docker rm -f "${CID}" >/dev/null 2>&1 || true
}
@@ -79,6 +107,83 @@ pipeline {
TEST_RC=$?
docker cp "${CID}:/workspace/target" "./artifacts" || true
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 "build=${BUILD_NUMBER}"
@@ -91,6 +196,7 @@ pipeline {
echo "headless=${HEADLESS}"
echo "execution.mode=${EXECUTION_MODE}"
echo "base.url=${BASE_URL}"
echo "video.selection=all-new-from-selenoid"
} > ./artifacts/target/allure-results/environment.properties
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/**'
}
}
}