diff --git a/email-notification/README-email-notification-json.md b/email-notification/README-email-notification-json.md index 59b5564..6ec2487 100644 --- a/email-notification/README-email-notification-json.md +++ b/email-notification/README-email-notification-json.md @@ -119,7 +119,7 @@ echo "$json_content" # Or use exported variables echo "JSON file: $JSON_FILE" -echo "Content: $GENERATED_JSON" + # File will be saved to: ../email-notification-generated/email-notification-results-generated.json ``` diff --git a/email-notification/calculate-email-notification-variables.sh b/email-notification/calculate-email-notification-variables.sh index fe57282..445b171 100644 --- a/email-notification/calculate-email-notification-variables.sh +++ b/email-notification/calculate-email-notification-variables.sh @@ -19,15 +19,15 @@ log_info() { log_success() { echo "✅ $1" } - +# shellcheck disable=SC2329 log_warning() { echo "⚠️ $1" } - +# shellcheck disable=SC2329 log_error() { echo "❌ $1" } - +# shellcheck disable=SC2034 # Get script directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -74,7 +74,8 @@ for result_file in "$ALLURE_RESULTS_DIR"/*-result.json; do # Extract test status using jq status=$(jq -r '.status' "$result_file" 2>/dev/null || echo "unknown") test_name=$(jq -r '.name' "$result_file" 2>/dev/null || echo "Unknown Test") - + test_name=$(printf '%s' "$test_name" | jq -R .) + test_name=${test_name:1:-1} case "$status" in "passed") passed_tests=$((passed_tests + 1)) @@ -113,8 +114,10 @@ if [ "$BC_AVAILABLE" = true ]; then pass_rate_rounded=$(echo "scale=0; $passed_tests * 100 / $total_tests" | bc) else # Use awk for calculations if bc is not available - pass_rate=$(awk "BEGIN {printf \"%.2f\", $passed_tests * 100 / $total_tests}") - pass_rate_rounded=$(awk "BEGIN {printf \"%.0f\", $passed_tests * 100 / $total_tests}") + pass_rate=$(awk -v p="$passed_tests" -v t="$total_tests" \ + 'BEGIN { if (t > 0) printf "%.2f", p * 100 / t; else print "0.00" }') + pass_rate_rounded=$(awk -v p="$passed_tests" -v t="$total_tests" \ + 'BEGIN { if (t > 0) printf "%.0f", p * 100 / t; else print "0" }') fi # Determine overall status @@ -135,8 +138,8 @@ export TEST_FAILED_COUNT="$failed_tests" export TEST_SKIPPED_COUNT="$skipped_tests" export TEST_OVERALL_STATUS="$overall_status" -# Create test details string TEST_DETAILS_STRING="" +# Create test details string for test_detail in "${test_details[@]}"; do if [ -n "$TEST_DETAILS_STRING" ]; then TEST_DETAILS_STRING="$TEST_DETAILS_STRING\n$test_detail" @@ -144,7 +147,6 @@ for test_detail in "${test_details[@]}"; do TEST_DETAILS_STRING="$test_detail" fi done -export TEST_DETAILS_STRING="$TEST_DETAILS_STRING" # Display summary echo "" diff --git a/email-notification/generate-email-notification-file.sh b/email-notification/generate-email-notification-file.sh index 3eddb17..350fc30 100644 --- a/email-notification/generate-email-notification-file.sh +++ b/email-notification/generate-email-notification-file.sh @@ -207,4 +207,4 @@ generate_email_notification_file() { # Return the message content # echo "$message_content" -} +} \ No newline at end of file diff --git a/email-notification/generate-email-notification-json.sh b/email-notification/generate-email-notification-json.sh index 41e742f..4dfa401 100644 --- a/email-notification/generate-email-notification-json.sh +++ b/email-notification/generate-email-notification-json.sh @@ -22,17 +22,17 @@ generate_email_notification_json() { log_success() { echo "✅ $1" } - + # shellcheck disable=SC2329 log_warning() { echo "⚠️ $1" } - + # shellcheck disable=SC2329 log_error() { echo "❌ $1" } - # Get script directory - local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + local SCRIPT_DIR + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Set allure results directory to default location local allure_results_dir="/tmp/clone/allure-results" @@ -50,7 +50,8 @@ generate_email_notification_json() { # Calculate additional metrics if [ -n "${TEST_TOTAL_COUNT:-}" ] && [ "$TEST_TOTAL_COUNT" -gt 0 ]; then - TEST_FAILURE_RATE=$(awk "BEGIN {printf \"%.2f\", $TEST_FAILED_COUNT * 100 / $TEST_TOTAL_COUNT}") + TEST_FAILURE_RATE=$(awk -v failed="$TEST_FAILED_COUNT" -v total="$TEST_TOTAL_COUNT" \ + 'BEGIN { if (total > 0) printf "%.2f", failed * 100 / total; else print "0.00" }') else TEST_FAILURE_RATE="0.00" fi @@ -82,6 +83,8 @@ generate_email_notification_json() { local status_part="${BASH_REMATCH[1]// /}" # Keep test name exactly as is (no formatting applied) local test_name="${BASH_REMATCH[2]}" + test_name=$(printf '%s' "$test_name" | jq -R .) + test_name=${test_name:1:-1} # Determine status and emoji @@ -118,8 +121,12 @@ generate_email_notification_json() { for result_file in "$allure_results_dir"/*-result.json; do if [ -f "$result_file" ]; then # Extract test status and name using jq - local status=$(jq -r '.status' "$result_file" 2>/dev/null || echo "unknown") - local test_name=$(jq -r '.name' "$result_file" 2>/dev/null || echo "Unknown Test") + local status + status=$(jq -r '.status' "$result_file" 2>/dev/null || echo "unknown") + local test_name + test_name=$(jq -r '.name' "$result_file" 2>/dev/null || echo "Unknown Test") + test_name=$(printf '%s' "$test_name" | jq -R .) + test_name=${test_name:1:-1} # Determine status and emoji local emoji="❓" @@ -230,10 +237,11 @@ generate_email_notification_json() { # Export the JSON content as environment variable for use in other scripts - export GENERATED_JSON="$json_content" export JSON_FILE="$output_file" - log_info "Environment variables exported: GENERATED_JSON, JSON_FILE" + log_info "Environment variables exported: JSON_FILE" + echo "$output_file" + return 0 # Return the JSON content # echo "$json_content" diff --git a/envgene.sh b/envgene.sh new file mode 100644 index 0000000..657d724 --- /dev/null +++ b/envgene.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +load_envgene() { + if [ -z "${ATP_ENVGENE_CONFIGURATION:-}" ]; then + echo "ATP_ENVGENE_CONFIGURATION is empty, skipping EnvGene mapping" + return 0 + fi + + echo "Loading EnvGene configuration..." + + to_env_name() { + printf '%s\n' "$1" \ + | tr '[:lower:]' '[:upper:]' \ + | sed 's/[^A-Z0-9]/_/g' + } + + derive_namespace_and_hostname() { + local url="$1" + local host namespace_part + + if [ -z "$url" ]; then + export NAMESPACE="unknown" + export SERVER_HOSTNAME="unknown" + return 0 + fi + + host=$(printf '%s\n' "$url" | sed -E 's#https?://([^/]+).*#\1#') + + if [[ "$host" == public-gateway-*.* ]]; then + namespace_part="${host#public-gateway-}" + export NAMESPACE="${namespace_part%%.*}" + + if [[ "$host" == public-gateway-"$NAMESPACE".* ]]; then + export SERVER_HOSTNAME="${host#public-gateway-$NAMESPACE.}" + else + export SERVER_HOSTNAME="$host" + fi + else + export NAMESPACE="unknown" + export SERVER_HOSTNAME="$host" + fi + } + + while IFS=$'\t' read -r system_name field_name field_value; do + [ -z "$system_name" ] && continue + [ -z "$field_name" ] && continue + [ -z "$field_value" ] && continue + + env_name="$(to_env_name "${system_name}_${field_name}")" + export "${env_name}=${field_value}" + + echo " ${env_name}=${field_value}" + done < <( + jq -r ' + .systems[] + | to_entries[] + | .key as $system_name + | .value.connections[0].HTTP? // empty + | to_entries[] + | select(.key == "url" or .key == "login" or .key == "password") + | select(.value != null and .value != "") + | [$system_name, .key, (.value | tostring)] + | @tsv + ' <<< "$ATP_ENVGENE_CONFIGURATION" + ) + + derive_namespace_and_hostname "${PUBLIC_GATEWAY_URL:-}" + + echo "Derived values:" + echo " NAMESPACE=${NAMESPACE}" + echo " SERVER_HOSTNAME=${SERVER_HOSTNAME}" +} \ No newline at end of file diff --git a/git-clone.sh b/git-clone.sh index 7daef1d..7aef14d 100644 --- a/git-clone.sh +++ b/git-clone.sh @@ -4,195 +4,265 @@ clone_repository() { if [ -d "$TMP_DIR" ] && [ "$(ls -A "$TMP_DIR" 2>/dev/null)" ]; then echo "ℹ️ Cloning tests repository is not required, because tests are already in image..." - else - echo "📥 Cloning tests repository..." + return 0 + fi - # ============================================ - # Pre-flight validation - # ============================================ - if [ -z "${ATP_TESTS_GIT_TOKEN:-}" ]; then - echo "❌ ERROR: ATP_TESTS_GIT_TOKEN is not set (required to download repository archive)" - exit 1 - fi - if [ -z "${ATP_TESTS_GIT_REPO_URL:-}" ]; then - echo "❌ ERROR: ATP_TESTS_GIT_REPO_URL is not set" - exit 1 - fi - if [ -z "${ATP_TESTS_GIT_REPO_BRANCH:-}" ]; then - echo "❌ ERROR: ATP_TESTS_GIT_REPO_BRANCH is not set" - exit 1 + echo "📥 Preparing tests repository..." + + # ============================================ + # Pre-flight validation + # ============================================ + if [ -z "${ATP_TESTS_GIT_TOKEN:-}" ]; then + echo "❌ ERROR: ATP_TESTS_GIT_TOKEN is not set (required to download repository archive)" + exit 1 + fi + if [ -z "${ATP_TESTS_GIT_REPO_URL:-}" ]; then + echo "❌ ERROR: ATP_TESTS_GIT_REPO_URL is not set" + exit 1 + fi + if [ -z "${ATP_TESTS_GIT_REPO_BRANCH:-}" ]; then + echo "❌ ERROR: ATP_TESTS_GIT_REPO_BRANCH is not set" + exit 1 + fi + if [ -z "${TMP_DIR:-}" ]; then + echo "❌ ERROR: TMP_DIR is not set" + exit 1 + fi + + # Basic URL sanity check (no whitespace, http/https) + if [[ "$ATP_TESTS_GIT_REPO_URL" =~ [[:space:]] ]] || [[ ! "$ATP_TESTS_GIT_REPO_URL" =~ ^https?:// ]]; then + echo "❌ ERROR: ATP_TESTS_GIT_REPO_URL is invalid URL: $ATP_TESTS_GIT_REPO_URL" + exit 1 + fi + + # Strip .git from URL and extract repo name + REPO_PATH=$(echo "$ATP_TESTS_GIT_REPO_URL" | sed 's|\.git$||') + GIT_BRANCH_CLEANED=$(echo "$ATP_TESTS_GIT_REPO_BRANCH" | sed 's|/|-|') + REPO_NAME=$(basename "$REPO_PATH") + ARCHIVE_URL="${REPO_PATH}/-/archive/${ATP_TESTS_GIT_REPO_BRANCH}/${REPO_NAME}-${GIT_BRANCH_CLEANED}.zip" + + mkdir -p "$TMP_DIR" 2>/dev/null || true + ZIP_PATH="$TMP_DIR/repo.zip" + CURL_ERR_PATH="$TMP_DIR/curl_download.err" + + fetch_by_clone_with_submodules() { + if ! command -v git >/dev/null 2>&1; then + echo "❌ git is not installed in the container, cannot clone repository with submodules" + return 1 fi - # Basic URL sanity check (no whitespace, http/https) - if [[ "$ATP_TESTS_GIT_REPO_URL" =~ [[:space:]] ]] || [[ ! "$ATP_TESTS_GIT_REPO_URL" =~ ^https?:// ]]; then - echo "❌ ERROR: ATP_TESTS_GIT_REPO_URL is invalid URL: $ATP_TESTS_GIT_REPO_URL" - exit 1 + + echo "📥 .gitmodules detected — switching to git clone with submodules..." + + rm -rf "$TMP_DIR" + + AUTH_REPO_URL="$ATP_TESTS_GIT_REPO_URL" + if [[ "$AUTH_REPO_URL" =~ ^https:// ]]; then + AUTH_REPO_URL=$(echo "$AUTH_REPO_URL" | sed "s|^https://|https://oauth2:${ATP_TESTS_GIT_TOKEN}@|") fi - # Strip .git from URL and extract repo name - REPO_PATH=$(echo "$ATP_TESTS_GIT_REPO_URL" | sed 's|\.git$||') - GIT_BRANCH_CLEANED=$(echo "$ATP_TESTS_GIT_REPO_BRANCH" | sed 's|/|-|') - REPO_NAME=$(basename "$REPO_PATH") - ARCHIVE_URL="${REPO_PATH}/-/archive/${ATP_TESTS_GIT_REPO_BRANCH}/${REPO_NAME}-${GIT_BRANCH_CLEANED}.zip" + git clone \ + --branch "$ATP_TESTS_GIT_REPO_BRANCH" \ + --single-branch \ + "$AUTH_REPO_URL" \ + "$TMP_DIR" || return 1 - if [ -z "${TMP_DIR:-}" ]; then - echo "❌ ERROR: TMP_DIR is not set" - exit 1 + cd "$TMP_DIR" || return 1 + + if [ -f .gitmodules ]; then + echo "🔧 Rewriting submodule URLs to use token authentication..." + + git config --file=.gitmodules --get-regexp '^submodule\..*\.url$' | while read -r key url; do + if [[ "$url" =~ ^https://git\.netcracker\.com/ ]]; then + auth_url=$(echo "$url" | sed "s|^https://|https://oauth2:${ATP_TESTS_GIT_TOKEN}@|") + echo " $key -> authenticated git.netcracker.com URL" + git config --file=.gitmodules "$key" "$auth_url" + fi + done fi - echo "📥 Downloading archive from: $ARCHIVE_URL" - mkdir -p "$TMP_DIR" 2>/dev/null || true - ZIP_PATH="$TMP_DIR/repo.zip" - CURL_ERR_PATH="$TMP_DIR/curl_download.err" - - # ============================================ - # Download archive with enhanced error handling - # - connect timeout: 30s - # - max time: 120s - # ============================================ - HTTP_CODE="$( - curl -sS -L \ - --connect-timeout 30 \ - --max-time 120 \ - --fail \ - -H "PRIVATE-TOKEN: ${ATP_TESTS_GIT_TOKEN}" \ - "$ARCHIVE_URL" \ - -o "$ZIP_PATH" \ - -w "%{http_code}" \ - 2>"$CURL_ERR_PATH" - )" - CURL_EXIT_CODE=$? - - # ============================================ - # Error detection and messaging - # ============================================ - if [ "$CURL_EXIT_CODE" -ne 0 ]; then - CURL_ERR_MSG="" - if [ -f "$CURL_ERR_PATH" ]; then - CURL_ERR_MSG=$(tr '\n' ' ' < "$CURL_ERR_PATH" | sed 's/[[:space:]]\+/ /g' | sed 's/[[:space:]]*$//') - fi - case "$CURL_EXIT_CODE" in - 6) - echo "❌ ERROR: Couldn't resolve host while downloading repository archive." - ;; - 7) - echo "❌ ERROR: Failed to connect to host while downloading repository archive." - ;; - 22) - # HTTP error (4xx/5xx). We'll handle using HTTP_CODE below. - ;; - 28) - echo "❌ ERROR: Download timed out (connect-timeout=30s, max-time=120s)." - ;; - 35) - echo "❌ ERROR: SSL/TLS connection error while downloading repository archive." - ;; - 52) - echo "❌ ERROR: Empty reply from server while downloading repository archive." - ;; - 56) - echo "❌ ERROR: Network receive error while downloading repository archive." - ;; - *) - echo "❌ ERROR: Network error while downloading repository archive (curl exit code: $CURL_EXIT_CODE)." - ;; - esac - - if [ -n "$CURL_ERR_MSG" ]; then - echo " curl: $CURL_ERR_MSG" - fi + git submodule sync --recursive + git submodule update --init --recursive || return 1 - # For non-HTTP curl failures, stop here (HTTP_CODE may be empty/undefined). - if [ "$CURL_EXIT_CODE" -ne 22 ]; then - exit 1 - fi + echo "📋 Submodule status after initialization:" + git submodule status || true + + echo "✅ Repository cloned with submodules to: $TMP_DIR" + } + + echo "📥 Downloading archive from: $ARCHIVE_URL" + + # ============================================ + # Download archive with enhanced error handling + # - connect timeout: 30s + # - max time: 120s + # ============================================ + HTTP_CODE="$( + curl -sS -L \ + --connect-timeout 30 \ + --max-time 120 \ + --fail \ + -H "PRIVATE-TOKEN: ${ATP_TESTS_GIT_TOKEN}" \ + "$ARCHIVE_URL" \ + -o "$ZIP_PATH" \ + -w "%{http_code}" \ + 2>"$CURL_ERR_PATH" + )" + CURL_EXIT_CODE=$? + + # ============================================ + # Error detection and messaging + # ============================================ + if [ "$CURL_EXIT_CODE" -ne 0 ]; then + CURL_ERR_MSG="" + if [ -f "$CURL_ERR_PATH" ]; then + CURL_ERR_MSG=$(tr '\n' ' ' < "$CURL_ERR_PATH" | sed 's/[[:space:]]\+/ /g' | sed 's/[[:space:]]*$//') fi - # HTTP code interpretation (also covers curl --fail exit 22) - if [ "$HTTP_CODE" != "200" ]; then - case "$HTTP_CODE" in - 200) - # ok - ;; - 000) - echo "❌ ERROR: No HTTP response received from server (HTTP 000)." - echo " Check network connectivity and URL: $ARCHIVE_URL" - exit 1 - ;; - 401|403) - echo "❌ ERROR: Authentication failed (HTTP $HTTP_CODE)." - echo " Check that ATP_TESTS_GIT_TOKEN is valid and has access to the repository." - exit 1 - ;; - 404) - echo "❌ ERROR: Repository or branch not found (HTTP 404)." - echo " Check URL and branch:" - echo " - URL: $ATP_TESTS_GIT_REPO_URL" - echo " - Branch: $ATP_TESTS_GIT_REPO_BRANCH" - echo " - Archive URL: $ARCHIVE_URL" - exit 1 - ;; - 429) - echo "❌ ERROR: Rate limited by the server (HTTP 429)." - echo " Try again later or reduce request frequency." - exit 1 - ;; - 5??) - echo "❌ ERROR: Server error while downloading archive (HTTP $HTTP_CODE)." - echo " The Git server may be temporarily unavailable." - exit 1 - ;; - *) - echo "❌ ERROR: Failed to download repository archive (HTTP $HTTP_CODE, curl exit code: $CURL_EXIT_CODE)." - echo " Archive URL: $ARCHIVE_URL" - exit 1 - ;; - esac + case "$CURL_EXIT_CODE" in + 6) + echo "❌ ERROR: Couldn't resolve host while downloading repository archive." + ;; + 7) + echo "❌ ERROR: Failed to connect to host while downloading repository archive." + ;; + 22) + # HTTP error (4xx/5xx). We'll handle using HTTP_CODE below. + ;; + 28) + echo "❌ ERROR: Download timed out (connect-timeout=30s, max-time=120s)." + ;; + 35) + echo "❌ ERROR: SSL/TLS connection error while downloading repository archive." + ;; + 52) + echo "❌ ERROR: Empty reply from server while downloading repository archive." + ;; + 56) + echo "❌ ERROR: Network receive error while downloading repository archive." + ;; + *) + echo "❌ ERROR: Network error while downloading repository archive (curl exit code: $CURL_EXIT_CODE)." + ;; + esac + + if [ -n "$CURL_ERR_MSG" ]; then + echo " curl: $CURL_ERR_MSG" fi - # ============================================ - # Downloaded archive validation - # ============================================ - if [ ! -f "$ZIP_PATH" ] || [ ! -s "$ZIP_PATH" ]; then - echo "❌ ERROR: Downloaded file is missing or empty: $ZIP_PATH" + # For non-HTTP curl failures, stop here (HTTP_CODE may be empty/undefined). + if [ "$CURL_EXIT_CODE" -ne 22 ]; then exit 1 fi + fi - if command -v file >/dev/null 2>&1; then - FILE_TYPE="$(file "$ZIP_PATH" 2>/dev/null || true)" - if ! printf '%s' "$FILE_TYPE" | grep -qi "zip archive"; then - # Git servers sometimes return an HTML login page when the token is invalid/expired. - if printf '%s' "$FILE_TYPE" | grep -qi "html"; then - echo "❌ ERROR: Downloaded an HTML login page instead of a repository." - echo " Check that ATP_TESTS_GIT_TOKEN is valid and has access to the repository." - echo " Check that the repository URL is correct." - else - echo "❌ ERROR: Downloaded repository is not recognized as a zip archive." - fi - echo " File type: $FILE_TYPE" + # HTTP code interpretation (also covers curl --fail exit 22) + if [ "$HTTP_CODE" != "200" ]; then + case "$HTTP_CODE" in + 200) + ;; + 000) + echo "❌ ERROR: No HTTP response received from server (HTTP 000)." + echo " Check network connectivity and URL: $ARCHIVE_URL" + exit 1 + ;; + 401|403) + echo "❌ ERROR: Authentication failed (HTTP $HTTP_CODE)." + echo " Check that ATP_TESTS_GIT_TOKEN is valid and has access to the repository." + exit 1 + ;; + 404) + echo "❌ ERROR: Repository or branch not found (HTTP 404)." + echo " Check URL and branch:" + echo " - URL: $ATP_TESTS_GIT_REPO_URL" + echo " - Branch: $ATP_TESTS_GIT_REPO_BRANCH" + echo " - Archive URL: $ARCHIVE_URL" + exit 1 + ;; + 429) + echo "❌ ERROR: Rate limited by the server (HTTP 429)." + echo " Try again later or reduce request frequency." exit 1 + ;; + 5??) + echo "❌ ERROR: Server error while downloading archive (HTTP $HTTP_CODE)." + echo " The Git server may be temporarily unavailable." + exit 1 + ;; + *) + echo "❌ ERROR: Failed to download repository archive (HTTP $HTTP_CODE, curl exit code: $CURL_EXIT_CODE)." + echo " Archive URL: $ARCHIVE_URL" + exit 1 + ;; + esac + fi + + # ============================================ + # Downloaded archive validation + # ============================================ + if [ ! -f "$ZIP_PATH" ] || [ ! -s "$ZIP_PATH" ]; then + echo "❌ ERROR: Downloaded file is missing or empty: $ZIP_PATH" + exit 1 + fi + + if command -v file >/dev/null 2>&1; then + FILE_TYPE="$(file "$ZIP_PATH" 2>/dev/null || true)" + if ! printf '%s' "$FILE_TYPE" | grep -qi "zip archive"; then + # Git servers sometimes return an HTML login page when the token is invalid/expired. + if printf '%s' "$FILE_TYPE" | grep -qi "html"; then + echo "❌ ERROR: Downloaded an HTML login page instead of a repository." + echo " Check that ATP_TESTS_GIT_TOKEN is valid and has access to the repository." + echo " Check that the repository URL is correct." + else + echo "❌ ERROR: Downloaded repository is not recognized as a zip archive." fi - else - echo "⚠️ 'file' command not available; skipping zip magic-byte validation." + echo " File type: $FILE_TYPE" + exit 1 fi + else + echo "⚠️ 'file' command not available; skipping zip magic-byte validation." + fi - if command -v unzip >/dev/null 2>&1; then - if ! unzip -t "$ZIP_PATH" > /dev/null 2>&1; then - echo "❌ ERROR: Archive integrity test failed (unzip -t). Please retry the operation." - exit 1 - fi - else - echo "❌ ERROR: 'unzip' command is not available; cannot validate/extract archive." + if command -v unzip >/dev/null 2>&1; then + if ! unzip -t "$ZIP_PATH" > /dev/null 2>&1; then + echo "❌ ERROR: Archive integrity test failed (unzip -t). Please retry the operation." exit 1 fi + else + echo "❌ ERROR: 'unzip' command is not available; cannot validate/extract archive." + exit 1 + fi + + echo "📦 Unzipping..." + unzip -q "$ZIP_PATH" -d "$TMP_DIR" || { + echo "❌ Failed to unzip repository archive" + exit 1 + } - echo "📦 Unzipping..." - unzip -q "$ZIP_PATH" -d "$TMP_DIR" - mv "$TMP_DIR"/${REPO_NAME}-${GIT_BRANCH_CLEANED}/* "$TMP_DIR" + extracted_dir="$TMP_DIR/${REPO_NAME}-${GIT_BRANCH_CLEANED}" + + if [ -f "$extracted_dir/.gitmodules" ]; then + rm -rf "$extracted_dir" + rm -f "$ZIP_PATH" + + fetch_by_clone_with_submodules || { + echo "❌ Failed to clone repository with submodules" + exit 1 + } + else + shopt -s dotglob + mv "$extracted_dir"/* "$TMP_DIR" || { + shopt -u dotglob + echo "❌ Failed to move extracted repository contents" + exit 1 + } + shopt -u dotglob + + rm -rf "$extracted_dir" + rm -f "$ZIP_PATH" echo "✅ Repository extracted to: $TMP_DIR" fi - # Check for either 'app/' nor 'tests/' nor 'collections/' directory (for different runtime types) if [ -d "$TMP_DIR/app" ]; then echo "✅ Validation successful. Found 'app/' directory in the repo." elif [ -d "$TMP_DIR/tests" ]; then @@ -203,13 +273,11 @@ clone_repository() { echo "✅ Validation successful. Found 'collections/' directory in the repo." else echo "❌ ERROR: Neither 'app/' nor 'tests/' nor 'collections/' directory nor 'postman_collection' file found in the cloned repo!" - exit 1 + exit 1 fi - # Move into the work directory - cd "$TMP_DIR" + cd "$TMP_DIR" || exit 1 - # List contents to verify if [ -d "$TMP_DIR/app" ]; then echo "📋 Contents of $TMP_DIR/app directory:" ls -la app @@ -218,9 +286,18 @@ clone_repository() { ls -la tests fi + if [ -f "$TMP_DIR/.gitmodules" ]; then + echo "📋 .gitmodules detected:" + cat "$TMP_DIR/.gitmodules" + + if command -v git >/dev/null 2>&1; then + echo "📋 Submodule status:" + git submodule status || true + fi + fi # Clear Git token from environment for security unset ATP_TESTS_GIT_TOKEN echo "🔐 Git token cleared from environment" echo "✅ Tests repository prepared successfully" -} +} \ No newline at end of file diff --git a/test-runner.sh b/test-runner.sh index 7f49767..3427419 100644 --- a/test-runner.sh +++ b/test-runner.sh @@ -1,36 +1,376 @@ #!/bin/bash -# Test execution module run_tests() { - echo "▶ Starting test execution..." + echo "▶ Starting test execution..." + + set -o pipefail + + # shellcheck disable=SC1091 + if [ -f "/app/scripts/upload-monitor.sh" ]; then + source "/app/scripts/upload-monitor.sh" + elif [ -f "/scripts/upload-monitor.sh" ]; then + source "/scripts/upload-monitor.sh" + else + echo "❌ upload-monitor.sh not found!" + exit 1 + fi + + echo "📁 Creating Allure results directory..." + mkdir -p "$TMP_DIR/allure-results" + + echo "🔐 Clearing sensitive environment variables before tests..." + clear_sensitive_vars + + echo "🚀 Running test suite..." + + if [ -f "./start_tests.sh" ]; then + chmod +x "./start_tests.sh" + ./start_tests.sh + TEST_EXIT_CODE=$? + elif [ -d "./collections" ]; then + echo "ℹ️ collections/ detected — running Bruno runner" + run_bruno_from_test_params + TEST_EXIT_CODE=$? + else + echo "❌ Neither start_tests.sh nor collections/ directory found" + exit 1 + fi + + TEST_EXIT_CODE=${TEST_EXIT_CODE:-0} + echo "ℹ️ Test script exited with code: $TEST_EXIT_CODE" + echo "✅ Test execution completed" + + return "$TEST_EXIT_CODE" +} + + +run_collection_body() { + + collection_dir="$1" + collection_path="${TMP_DIR}/${collection_dir}" + + + if [ -n "$BRUNO_FOLDERS_STR" ]; then + mapfile -t BRUNO_FOLDERS_ARRAY <<< "$BRUNO_FOLDERS_STR" + else + BRUNO_FOLDERS_ARRAY=() + fi + + echo "➡️ Processing collection: $collection_path" + + if [ -d "$collection_path" ]; then + collection_name=$(basename "$collection_dir") + bruno_report_path="${PATH_TO_ATTACHMENTS_DIR}/${collection_name}-result.json" + raw_log_path="${PATH_TO_ATTACHMENTS_DIR}/${collection_name}.raw.log" + + collection_start_ts=$(date +%s) + echo "🚀 START collection=$collection_name pid=$$ time=$(date '+%H:%M:%S')" + + pushd "$collection_path" > /dev/null || return 1 - # Import upload monitoring module for security functions - # A temporary solution: after moving all runner files to the app directory, you need to delete /scripts/upload-monitor.sh in all runners and leave only /app/scripts/upload-monitor.sh - # shellcheck disable=1091 - if [ -f "/app/scripts/upload-monitor.sh" ]; then - source "/app/scripts/upload-monitor.sh" - elif [ -f "/scripts/upload-monitor.sh" ]; then - source "/scripts/upload-monitor.sh" + RESOLVED_FOLDERS=() + + if [ ${#BRUNO_FOLDERS_ARRAY[@]} -gt 0 ]; then + + for folder in "${BRUNO_FOLDERS_ARRAY[@]}"; do + found_any=false + + while IFS= read -r found; do + echo "✔ Found folder in $collection_name: $found" + RESOLVED_FOLDERS+=("$found") + found_any=true + done < <(find . -maxdepth 5 -type d -name "$folder" -not -path "*/.git/*" -not -path "*/node_modules/*") + + if [ "$found_any" = false ]; then + echo "⚠ Folder not found in $collection_name: $folder" + fi + done + + fi + + + if [ ${#BRUNO_FOLDERS_ARRAY[@]} -eq 0 ]; then + + echo "➡ Running full collection" + echo "▶ BRUNO RUN START collection=$collection_name pid=$$ mode=full time=$(date '+%H:%M:%S')" + + COLLECTION_TIMEOUT="${COLLECTION_TIMEOUT:-3600}" + + if timeout --signal=TERM --kill-after=30s "${COLLECTION_TIMEOUT}s" \ + ${BRU_BIN}/bru.js run ${BRUNO_FLAGS_CLI} \ + --env "${BRUNO_ENV_STR}" \ + ${BRUNO_ENV_VARS_CLI} \ + --reporter-json "${bruno_report_path}" \ + 2>&1 | tee "${raw_log_path}"; then + echo "✅ SUCCESS: $collection_name" + else + rc=${PIPESTATUS[0]} + echo "❌ FAILED: $collection_name rc=$rc" + echo "----- LAST 200 LINES: $collection_name -----" + tail -n 200 "${raw_log_path}" || true + echo "--------------------------------------------" + TOTAL_FAILED=1 + fi + + echo "◀ BRUNO RUN END collection=$collection_name pid=$$ time=$(date '+%H:%M:%S')" else - echo "❌ upload-monitor.sh not found!" - exit 1 + + if [ ${#RESOLVED_FOLDERS[@]} -eq 0 ]; then + echo "⚠ No matching folders found — skipping collection" + popd > /dev/null + uuid=$(cat /proc/sys/kernel/random/uuid) + + cat > "$PATH_TO_ALLURE_RESULTS/${uuid}-result.json" <&1 | tee "${raw_log_path}"; then + echo "✅ SUCCESS: $collection_name" + else + rc=${PIPESTATUS[0]} + echo "❌ FAILED: $collection_name rc=$rc" + echo "----- LAST 200 LINES: $collection_name -----" + tail -n 200 "${raw_log_path}" || true + echo "--------------------------------------------" + TOTAL_FAILED=1 + fi + + echo "◀ BRUNO RUN END collection=$collection_name pid=$$ time=$(date '+%H:%M:%S')" fi - - # Create Allure results directory - echo "📁 Creating Allure results directory..." - mkdir -p "$TMP_DIR"/allure-results - # Clear sensitive variables before tests - echo "🔐 Clearing sensitive environment variables before tests..." - clear_sensitive_vars + popd > /dev/null || return 1 - # Execute test suite - echo "🚀 Running test suite..." - chmod +x start_tests.sh - ./start_tests.sh || TEST_EXIT_CODE=$? + collection_end_ts=$(date +%s) - TEST_EXIT_CODE=${TEST_EXIT_CODE:-0} - echo "ℹ️ Test script exited with code: $TEST_EXIT_CODE (but continuing...)" - - echo "✅ Test execution completed" -} \ No newline at end of file + echo "🏁 BRUNO EXECUTION FINISHED =$collection_name pid=$$ duration=$((collection_end_ts-collection_start_ts))s time=$(date '+%H:%M:%S')" + + if [ -f "$bruno_report_path" ]; then + echo "📦 Parsing report: $bruno_report_path" + + count=$(jq 'if type=="array" then (if (.[0]?|type)=="object" and (.[0]?|has("results")) then ([.[].results[]]|length) else length end) elif type=="object" and has("results") then (.results|length) else 0 end' "$bruno_report_path") + echo "📊 $collection_name → $count tests" + printf "%s,%s\n" "$collection_name" "$count" >> "$TMP_DIR/tests_count.csv" + node /scripts/tools/bruno-to-allure.js \ + "$bruno_report_path" \ + "$PATH_TO_ALLURE_RESULTS" \ + "$collection_name" + echo "✅ COLLECTION FULLY FINISHED collection=$collection_name pid=$$ time=$(date '+%H:%M:%S')" + else + + echo "⚠ Bruno report missing — writing broken test to Allure" + + uuid=$(cat /proc/sys/kernel/random/uuid) + + cat > "$PATH_TO_ALLURE_RESULTS/${uuid}-result.json" < "$skipped_file" < "$TMP_DIR/tests_count.csv" + + { + echo "BRUNO_ENV=$BRUNO_ENV_STR" + + while IFS= read -r key; do + case "$key" in + *_URL|*_LOGIN|*_PASSWORD|NAMESPACE|SERVER_HOSTNAME) + printf '%s=%s\n' "$key" "${!key}" + ;; + esac + done < <(compgen -e | sort) + } > "$PATH_TO_ALLURE_RESULTS/environment.properties" + + echo "Env vars exported to Bruno child processes:" + while IFS= read -r key; do + case "$key" in + *_URL|*_LOGIN|*_PASSWORD|NAMESPACE|SERVER_HOSTNAME) + export "$key" + echo " - $key" + ;; + esac + done < <(compgen -e) + + TOTAL_FAILED=0 + export -f run_collection_body + + export TMP_DIR + export PATH_TO_ATTACHMENTS_DIR + export PATH_TO_ALLURE_RESULTS + export BRU_BIN + export BRUNO_ENV_STR + export BRUNO_ENV_VARS_CLI + export BRUNO_FLAGS_CLI + + if [ ${#BRUNO_FOLDERS_ARRAY[@]} -gt 0 ]; then + BRUNO_FOLDERS_STR=$(printf "%s\n" "${BRUNO_FOLDERS_ARRAY[@]}") + else + BRUNO_FOLDERS_STR="" + fi + +export BRUNO_FOLDERS_STR + + PARALLELISM=${PARALLELISM:-4} + echo "Collections to run:" + printf "%s\n" "${BRUNO_COLLECTIONS_ARRAY[@]}" + echo "Total collections: ${#BRUNO_COLLECTIONS_ARRAY[@]}" + + echo "⚡ Running ${#BRUNO_COLLECTIONS_ARRAY[@]} collections with parallelism=$PARALLELISM" + + printf "%s\n" "${BRUNO_COLLECTIONS_ARRAY[@]}" > "$PATH_TO_ALLURE_RESULTS/collections.txt" + + parallel_start_ts=$(date +%s) + echo "✅ PARALLEL PHASE START time=$(date '+%H:%M:%S')" + + running_jobs=0 + active_collection_pids=() + + wait_for_collection_slot() { + while true; do + for idx in "${!active_collection_pids[@]}"; do + local pid="${active_collection_pids[$idx]}" + + if ! kill -0 "$pid" 2>/dev/null; then + wait "$pid" || true + unset 'active_collection_pids[idx]' + active_collection_pids=("${active_collection_pids[@]}") + return 0 + fi + done + + sleep 1 + done + } + + for collection in "${BRUNO_COLLECTIONS_ARRAY[@]}"; do + bash -c 'run_collection_body "$1"' _ "$collection" & + active_collection_pids+=("$!") + running_jobs=$((running_jobs + 1)) + + if [ "$running_jobs" -ge "$PARALLELISM" ]; then + wait_for_collection_slot + running_jobs=$((running_jobs - 1)) + fi + done + + while [ "$running_jobs" -gt 0 ]; do + wait_for_collection_slot + running_jobs=$((running_jobs - 1)) + done + + parallel_end_ts=$(date +%s) + + echo "✅ PARALLEL PHASE END time=$(date '+%H:%M:%S') took=$((parallel_end_ts-parallel_start_ts))s" + + + echo "==== TEST COUNT BY COLLECTION ====" + sort "$TMP_DIR/tests_count.csv" + echo "-----------------------------------------" + + return 0 +} diff --git a/tools/bru_tools.sh b/tools/bru_tools.sh new file mode 100644 index 0000000..3170e99 --- /dev/null +++ b/tools/bru_tools.sh @@ -0,0 +1,241 @@ +#!/usr/bin/env bash +# ============================================ +# check_env_var — ensure an env var is set or compute & export it. +# +# Usage: +# check_env_var VAR_NAME [COMPUTE_EXPR] +# +# Args: +# VAR_NAME Name of the environment variable to check. +# COMPUTE_EXPR (optional) Bash expression/command to compute the value if +# VAR_NAME is empty. Evaluated with `eval`. +# +# Behavior: +# - If VAR_NAME is unset/empty: +# * If COMPUTE_EXPR is provided: evaluates it, exports the result to +# VAR_NAME, and echoes "VAR_NAME = VALUE (Computed)". +# * If COMPUTE_EXPR is missing: prints an error to stderr and exits 1. +# - If VAR_NAME is already set: echoes "VAR_NAME = VALUE". +# ============================================ +check_env_var() { + local var_name="$1" + local compute_expr="$2" + local computed_value # Announcing in advance + + # Check if the variable exists and if it is not empty + if [[ -z "${!var_name:-}" ]]; then + if [[ -z "$compute_expr" ]]; then + echo "❗Error: Variable $var_name must be specified!" >&2 + exit 1 + else + # Calculate the value + if ! computed_value=$(eval "$compute_expr" 2>/dev/null); then + echo "❗Error calculating the value for $var_name" >&2 + exit 1 + fi + + # Export variable + declare -gx "$var_name"="$computed_value" + echo "${var_name} = ${computed_value} (Computed)" + fi + else + echo "${var_name} = ${!var_name}" + fi +} + +# ============================================ +# Extract Bruno environment file from TEST_PARAMS: section "env" +# and convert them to a string like "environments/test_environment.json" +# Parameters: +# $1 - JSON input string +# $2 - Name of the output variable to store the result (optional) +# ============================================ +extract_bruno_env() { + local json_input="$1" + local output_var_name="$2" + local result="" + + # Extract the path to the environment + result=$(echo "$json_input" | jq -r '.env') + echo "➡️ Extracted Bruno environment: $result" + + # Export the result to a variable with a specified name + eval "$output_var_name=\"$result\"" +} + +# ============================================ +# Extract Bruno collections from TEST_PARAMS: section "collections" +# and convert them to an array like:" +# "collections/system1/system1.postman_collection.json", +# "collections/system2/system2.postman_collection.json" +# Parameters: +# $1 - JSON input string +# $2 - Name of the output variable to store the result (optional) +# ============================================ +extract_bruno_collections() { + local json_input="$1" + local output_var_name="$2" + local result_array=() + + # Retrieve the array of collections and save it to a temporary array + readarray -t result_array < <(echo "$json_input" | jq -r '.collections[]?') + + # Export the array to a variable with a specified name + q='' + for x in "${result_array[@]}"; do + q+=$(printf ' %q' "$x") + done + eval "$output_var_name=(${q# })" + + # Log the result + local output_message="➡️ Extracted Bruno collections:" + for collection in "${result_array[@]}"; do + output_message+="\n - $collection" + done + echo -e "$output_message" +} + +# ============================================ +# Extract Bruno flags from TEST_PARAMS: section "flags" +# and convert them to a string like "--insecure --iteration-count 5" +# Parameters: +# $1 - JSON input string +# $2 - Name of the output variable to store the result (optional) +# ============================================ +extract_bruno_flags() { + local json_input="$1" + local output_var_name="$2" + local result="" + + # Extract the flags array from JSON and convert it to a string + result=$(echo "$json_input" | jq -r '.flags | join(" ")') + echo "➡️ Extracted Bruno flags: $result" + + # Export the result to a variable with a specified name + eval "$output_var_name=\"$result\"" +} + +# ============================================ +# Extract Bruno environment variables from TEST_PARAMS: section "env_vars" +# and convert them to a string like "--env-var key1=value1 --env-var key2=value2" +# Parameters: +# $1 - JSON input string +# $2 - Name of the output variable to store the result (optional) +# ============================================ +extract_bruno_env_vars() { + local json_input="$1" + local output_var_name="$2" + local result="" + + # Get the number of elements in env_vars + local count + count=$(jq '.env_vars | length' <<< "$json_input") + + # We go through the indices from 0 to count-1 + for ((i=0; i/dev/null 2>&1; then + echo "➡️ No folders defined in TEST_PARAMS" + eval "$output_var_name=()" + return + fi + + readarray -t result_array < <(echo "$json_input" | jq -r '.folders[]') + + q='' + for x in "${result_array[@]}"; do + q+=$(printf ' %q' "$x") + done + eval "$output_var_name=(${q# })" + + local output_message="➡️ Extracted Bruno folders:" + for folder in "${result_array[@]}"; do + output_message+="\n - $folder" + done + echo -e "$output_message" +} + +# Return: +# 0 — if LOCAL_RUN=true +# 1 — if LOCAL_RUN=false or value not set/empty +# 2 — if incorrect value (not true, not false) +local_run_enabled() { + + local val="${LOCAL_RUN:-}" + + if [ -z "$val" ]; then + return 1 + fi + + # reduce it to lowercase + val="$(printf '%s' "$val" | tr '[:upper:]' '[:lower:]')" + + case "$val" in + true) return 0 ;; + false) return 1 ;; + *) + printf '❌ Incorrect value LOCAL_RUN=%s (expected true/false)\n' "$LOCAL_RUN" >&2 + return 2 + ;; + esac +} + +# Local test execution module +local_run_tests() { + cd "$TMP_DIR" || return 1 + + # Create Allure results directory + echo "▶ Starting test execution..." + export NODE_PATH=/app/node_modules + + cp -r "$WORK_DIR/tools" "$TMP_DIR/tools" + + # Create Allure results directory + echo "📁 Creating Allure results directory..." + mkdir -p "$TMP_DIR/allure-results" + + # Execute test suite + echo "🚀 Running test suite..." + chmod +x start_tests.sh + ./start_tests.sh || TEST_EXIT_CODE=$? + + TEST_EXIT_CODE=${TEST_EXIT_CODE:-0} + echo "ℹ️ Test script exited with code: $TEST_EXIT_CODE (but continuing...)" + + echo "✅ Test execution completed" + + cd "$WORK_DIR" || return 1 +} diff --git a/tools/bruno-to-allure.js b/tools/bruno-to-allure.js new file mode 100644 index 0000000..ff80c32 --- /dev/null +++ b/tools/bruno-to-allure.js @@ -0,0 +1,209 @@ +#!/usr/bin/env node +/* global require, process, __dirname, console */ + +const fs = require("fs"); +const path = require("path"); +const { randomUUID } = require("node:crypto"); +const { URL } = require("node:url"); + +const args = process.argv.slice(2); +const brunoReportPath = args[0]; +const allureResultsDir = args[1] || path.join(__dirname, "allure-results"); +const collectionName = args[2] || "unknown-collection"; + +// ensure dir +if (!fs.existsSync(allureResultsDir)) fs.mkdirSync(allureResultsDir, { recursive: true }); + +// split Bruno "path" into folder parts (preserve every level) +function splitPathParts(requestPath) { + if (!requestPath) return ["uncategorized"]; + return requestPath.replace(/\/+|\\+/g, "/").split("/").map(p => p.trim()).filter(Boolean); +} + +// Create steps: Request/Response + Assertion steps +function createSteps(test, id) { + const requestFilename = `${id}-request.json`; + const requestHeadersFilename = `${id}-request-headers.json`; + const responseFilename = `${id}-response.json`; + const responseHeadersFilename = `${id}-response-headers.json`; + + const requestHeaders = test.request?.headers || {}; + const requestBody = test.request?.data !== undefined + ? (typeof test.request.data === "string" ? test.request.data : JSON.stringify(test.request.data, null, 2)) + : "/* no request body */"; + + fs.writeFileSync(path.join(allureResultsDir, requestHeadersFilename), JSON.stringify(requestHeaders, null, 2)); + fs.writeFileSync(path.join(allureResultsDir, requestFilename), requestBody, "utf8"); + + const response = test.response || {}; + const responseHeaders = response.headers || {}; + const responseBody = response.data !== undefined + ? (typeof response.data === "string" ? response.data : JSON.stringify(response.data, null, 2)) + : "/* no response body */"; + + fs.writeFileSync(path.join(allureResultsDir, responseHeadersFilename), JSON.stringify(responseHeaders, null, 2)); + fs.writeFileSync(path.join(allureResultsDir, responseFilename), responseBody, "utf8"); + + const steps = []; + + // --- Assertions --- + const allAssertions = [ + ...(test.preRequestTestResults || []), + ...(test.testResults || []), + ...(test.postResponseTestResults || []) + ]; + + let assertionsFailed = false; + const failedAssertions = []; + + if (allAssertions.length > 0) { + for (const ar of allAssertions) { + const isFail = String(ar.status).toLowerCase() !== "pass"; + if (isFail) { + assertionsFailed = true; + failedAssertions.push(ar); + } + + steps.push({ + name: ar.description || "Assertion", + status: isFail ? "failed" : "passed", + stage: "finished", + statusDetails: isFail ? { + message: ar.description || "Assertion failed", + trace: ar.error || "No description" + }: undefined + }); + } + } + + // Request Headers step + steps.push({ + name: "Request Headers", + status: "passed", + stage: "finished", + attachments: [{ name: "Request Headers", source: requestHeadersFilename, type: "application/json" }], + parameters: Object.entries(requestHeaders).map(([k, v]) => ({ name: k, value: String(v) })) + }); + + // Request Body step + steps.push({ + name: "Request Body", + status: "passed", + stage: "finished", + attachments: [{ name: "Request Body", source: requestFilename, type: "application/json" }] + }); + + // Response Headers step + steps.push({ + name: "Response Headers", + status: "passed", + stage: "finished", + attachments: [{ name: "Response Headers", source: responseHeadersFilename, type: "application/json" }], + parameters: Object.entries(responseHeaders).map(([k, v]) => ({ name: k, value: String(v) })) + }); + + // Response Body step + steps.push({ + name: "Response Body", + status: assertionsFailed ? "failed" : "passed", + stage: "finished", + attachments: [{ name: "Response Body", source: responseFilename, type: "application/json" }] + }); + + return { steps, assertionsFailed, failedAssertions }; +} + +try { + const raw = fs.readFileSync(brunoReportPath, "utf8"); + const brunoReport = JSON.parse(raw); + let results = []; + + if (Array.isArray(brunoReport)) { + if (brunoReport.every(item => item && Array.isArray(item.results))) { + results = brunoReport.flatMap(item => item.results); + } else { + results = brunoReport; + } + } else if (brunoReport && Array.isArray(brunoReport.results)) { + results = brunoReport.results; + } else { + throw new Error("Invalid Bruno report format"); + } + + const children = []; + for (const test of results) { + const id = randomUUID(); + const timestamp = test.timestamp ? new Date(test.timestamp).getTime() : Date.now(); + const duration = test.response?.responseTime ?? test.duration ?? 0; + + const parts = splitPathParts(test.path); + + const parentSuite = "Backend (Bruno)"; + const suite = collectionName; + const subSuite = parts.length > 1 ? parts.slice(0, -1).join(" / ") : undefined; + const packageName = `${collectionName}.${parts.join(".")}`; + + const { steps, assertionsFailed, failedAssertions } = createSteps(test, id, timestamp, duration); + + const initialStatus = + test.status === "pass" ? "passed" : "failed"; + + const finalStatus = assertionsFailed ? "failed" : initialStatus; + + const allureResult = { + uuid: id, + historyId: randomUUID(), + name: test.name || `${test.request?.method || "GET"} ${test.request?.url || ""}`, + fullName: `${packageName}.${test.name || "test"}`, + status: finalStatus, + statusDetails: finalStatus === "failed" ? { + message: failedAssertions?.map(r => + `${r.description || "Test"}: ${r.error || ''}` + ).join("\n") || "Test failed", + trace: failedAssertions?.map(r => + `Status: ${r.status || "Failed"}\nDescription: ${r.description || "No description"}\nError: ${r.error || "No details"}\nActual: ${r.actual}\nExpected: ${r.expected}` + ).join("\n") || "No details" + } : undefined, + steps: steps, + parameters: [ + { name: "Method", value: test.request?.method || "GET" }, + { name: "URL", value: test.request?.url || "n/a" }, + { name: "Response Code", value: test.response?.status || "n/a" } + ], + start: timestamp, + stop: timestamp + duration, + labels: [ + { name: "parentSuite", value: parentSuite }, + { name: "suite", value: suite }, + ...(subSuite ? [{ name: "subSuite", value: subSuite }] : []), + { name: "package", value: packageName }, + { name: "host", value: (() => { try { return new URL(test.request?.url).host } catch { return "n/a"; } })() }, + { name: "framework", value: "bruno" }, + { name: "language", value: "javascript" } + ].filter(l => l.value !== undefined), + description: test.description || test.name || "No description provided", + descriptionHtml: test.description || test.name || "No description provided" + }; + + fs.writeFileSync(path.join(allureResultsDir, `${id}-result.json`), JSON.stringify(allureResult, null, 2)); + children.push(id); + } + + const container = { + uuid: randomUUID(), + children: children, + befores: [], + afters: [], + start: Date.now(), + stop: Date.now() + }; + fs.writeFileSync( + path.join(allureResultsDir, `${randomUUID()}-container.json`), + JSON.stringify(container, null, 2) + ); + console.log(`✅ Successfully converted Bruno report to Allure format. Results saved in: ${allureResultsDir}`); +} catch (error) { + console.error(`❌ Error processing Bruno report: ${error.message}`); + process.exit(1); +} + diff --git a/upload-monitor.sh b/upload-monitor.sh index 0037ae6..edd1444 100644 --- a/upload-monitor.sh +++ b/upload-monitor.sh @@ -191,4 +191,4 @@ final_cleanup() { unset _LOCAL_S3_SECRET unset _BACKGROUND_S3_KEY unset _BACKGROUND_S3_SECRET -} +} \ No newline at end of file