From a2f6a797067832a0080455695e3764878681b993 Mon Sep 17 00:00:00 2001 From: Elai Shalev Date: Sun, 22 Mar 2026 16:35:37 +0200 Subject: [PATCH 1/4] Error handling - error from python log --- .../x2a-backend/templates/x2a-job-script.sh | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/workspaces/x2a/plugins/x2a-backend/templates/x2a-job-script.sh b/workspaces/x2a/plugins/x2a-backend/templates/x2a-job-script.sh index 39f9e22bec..57756f775a 100644 --- a/workspaces/x2a/plugins/x2a-backend/templates/x2a-job-script.sh +++ b/workspaces/x2a/plugins/x2a-backend/templates/x2a-job-script.sh @@ -74,6 +74,42 @@ sanitize_secrets() { fi } +# Run an x2a tool command, capturing output for error reporting. +# On failure, extracts the error message from the tool's output and sets ERROR_MESSAGE. +# On success, clears ERROR_MESSAGE. +# The captured output is stored in X2A_OUTPUT for callers that need to parse it. +# Usage: run_x2a uv run app.py [args...] +run_x2a() { + local rc + local tmpfile + tmpfile=$(mktemp) + + # Stream output in real-time via tee while also capturing it to a file + set +e + "$@" 2>&1 | tee "${tmpfile}" + rc=${PIPESTATUS[0]} + set -e + + X2A_OUTPUT=$(cat "${tmpfile}") + rm -f "${tmpfile}" + + if [ ${rc} -ne 0 ]; then + # Extract the error block from x2a-convertor output. + # The convertor prints: \n\nError: \n (potentially multi-line, always last thing before exit) + # Find the last "Error: " line number, then grab everything from there to the end. + local error_block="" + local last_error_line + last_error_line=$(echo "${X2A_OUTPUT}" | grep -n "^Error: " | tail -1 | cut -d: -f1) + if [ -n "${last_error_line}" ]; then + error_block=$(echo "${X2A_OUTPUT}" | tail -n +"${last_error_line}") + fi + ERROR_MESSAGE="${error_block:-Unexpected error during ${PHASE} phase. See the job log for details.}" + exit ${rc} + fi + + ERROR_MESSAGE="" +} + # Cleanup trap: fires on every exit (success or failure). # Guarantees exactly one report_result call regardless of how the script ends. cleanup() { @@ -227,9 +263,7 @@ case "${PHASE}" in # --source-dir DIRECTORY Source directory to analyze USER_REQ="${USER_PROMPT:-Analyze the Chef cookbooks and create a migration plan}" echo "Command: uv run app.py init --source-dir ${SOURCE_BASE} \"${USER_REQ}\"" - ERROR_MESSAGE="Unexpected error during init phase. See the job log for details." - uv run app.py init --source-dir "${SOURCE_BASE}" "${USER_REQ}" - ERROR_MESSAGE="" + run_x2a uv run app.py init --source-dir "${SOURCE_BASE}" "${USER_REQ}" # Copy output to target location # Note: x2a tool writes files to the source directory (--source-dir) @@ -291,9 +325,7 @@ case "${PHASE}" in USER_REQ="${USER_PROMPT:-Analyze the module '${MODULE_NAME}' for migration to Ansible}" echo "Command: uv run app.py analyze --source-dir ${SOURCE_BASE} \"${USER_REQ}\"" - ERROR_MESSAGE="Unexpected error during analyze phase. See the job log for details." - uv run app.py analyze --source-dir "${SOURCE_BASE}" "${USER_REQ}" - ERROR_MESSAGE="" + run_x2a uv run app.py analyze --source-dir "${SOURCE_BASE}" "${USER_REQ}" # Copy output to target location # Note: x2a tool produces migration-plan-{module_name}.md (spaces replaced with underscores) @@ -348,14 +380,12 @@ case "${PHASE}" in USER_REQ="${USER_PROMPT:-Migrate this module to Ansible}" echo "Command: uv run app.py migrate --source-dir ${SOURCE_BASE} --source-technology Chef --high-level-migration-plan ${PROJECT_PATH}/migration-plan.md --module-migration-plan ${OUTPUT_DIR}/migration-plan-${MODULE_NAME_SANITIZED}.md \"${USER_REQ}\"" - ERROR_MESSAGE="Unexpected error during migrate phase. See the job log for details." - uv run app.py migrate \ + run_x2a uv run app.py migrate \ --source-dir "${SOURCE_BASE}" \ --source-technology Chef \ --high-level-migration-plan "${PROJECT_PATH}/migration-plan.md" \ --module-migration-plan "${OUTPUT_DIR}/migration-plan-${MODULE_NAME_SANITIZED}.md" \ "${USER_REQ}" - ERROR_MESSAGE="" # Copy output to target location # Note: x2a tool writes to ansible/roles/{module}/ in the source directory @@ -403,9 +433,7 @@ case "${PHASE}" in # and writes to {project_id}/ansible-project/ # It operates relative to CWD, so we run from TARGET_BASE pushd "${TARGET_BASE}" - ERROR_MESSAGE="Unexpected error during publish phase (publish-project). See the job log for details." - uv run --project /app /app/app.py publish-project "${PROJECT_DIR}" "${MODULE_NAME}" - ERROR_MESSAGE="" + run_x2a uv run --project /app /app/app.py publish-project "${PROJECT_DIR}" "${MODULE_NAME}" popd # Verify ansible-project was created @@ -424,15 +452,13 @@ case "${PHASE}" in echo "=== Step 2: Publishing to AAP ===" echo "Command: uv run app.py publish-aap --target-repo ${TARGET_REPO_URL} --target-branch ${TARGET_REPO_BRANCH} --project-id ${PROJECT_DIR}" cd /app - ERROR_MESSAGE="Unexpected error during publish phase (publish-aap). See the job log for details." - PUBLISH_OUTPUT=$(uv run app.py publish-aap \ + run_x2a uv run app.py publish-aap \ --target-repo "${TARGET_REPO_URL}" \ --target-branch "${TARGET_REPO_BRANCH}" \ - --project-id "${PROJECT_DIR}" 2>&1 | tee /dev/stderr) - ERROR_MESSAGE="" + --project-id "${PROJECT_DIR}" - # Parse AAP project ID from output and construct URL - AAP_PROJECT_ID=$(echo "${PUBLISH_OUTPUT}" | grep -oP 'ID: \K[0-9]+' | tail -1) + # Parse AAP project ID from output (captured by run_x2a in X2A_OUTPUT) + AAP_PROJECT_ID=$(echo "${X2A_OUTPUT}" | grep -oP 'ID: \K[0-9]+' | tail -1) if [ -n "${AAP_PROJECT_ID}" ]; then ARTIFACTS+=("ansible_project:${AAP_CONTROLLER_URL}/execution/projects/${AAP_PROJECT_ID}/details") else From eeffac3921b24fe3fe03ceb283b60fea03548280 Mon Sep 17 00:00:00 2001 From: Elai Shalev Date: Mon, 23 Mar 2026 14:07:27 +0200 Subject: [PATCH 2/4] Simplify reporting by reading from file --- .../x2a-backend/templates/x2a-job-script.sh | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/workspaces/x2a/plugins/x2a-backend/templates/x2a-job-script.sh b/workspaces/x2a/plugins/x2a-backend/templates/x2a-job-script.sh index 57756f775a..9ab997d8fe 100644 --- a/workspaces/x2a/plugins/x2a-backend/templates/x2a-job-script.sh +++ b/workspaces/x2a/plugins/x2a-backend/templates/x2a-job-script.sh @@ -11,6 +11,9 @@ PUSH_FAILED="" TERMINATED=false COMMIT_ID="" +# Path where x2a-convertor writes error details on failure +export X2A_ERROR_FILE="/tmp/x2a-error.txt" + # Report job result back to the backend. report_result() { local status="$1" @@ -74,36 +77,24 @@ sanitize_secrets() { fi } -# Run an x2a tool command, capturing output for error reporting. -# On failure, extracts the error message from the tool's output and sets ERROR_MESSAGE. +# Run an x2a tool command with error reporting. +# On failure, reads the error details file written by x2a-convertor and sets ERROR_MESSAGE. # On success, clears ERROR_MESSAGE. -# The captured output is stored in X2A_OUTPUT for callers that need to parse it. # Usage: run_x2a uv run app.py [args...] run_x2a() { - local rc - local tmpfile - tmpfile=$(mktemp) + rm -f "${X2A_ERROR_FILE}" - # Stream output in real-time via tee while also capturing it to a file set +e - "$@" 2>&1 | tee "${tmpfile}" - rc=${PIPESTATUS[0]} + "$@" + local rc=$? set -e - X2A_OUTPUT=$(cat "${tmpfile}") - rm -f "${tmpfile}" - if [ ${rc} -ne 0 ]; then - # Extract the error block from x2a-convertor output. - # The convertor prints: \n\nError: \n (potentially multi-line, always last thing before exit) - # Find the last "Error: " line number, then grab everything from there to the end. - local error_block="" - local last_error_line - last_error_line=$(echo "${X2A_OUTPUT}" | grep -n "^Error: " | tail -1 | cut -d: -f1) - if [ -n "${last_error_line}" ]; then - error_block=$(echo "${X2A_OUTPUT}" | tail -n +"${last_error_line}") + if [ -f "${X2A_ERROR_FILE}" ]; then + ERROR_MESSAGE=$(cat "${X2A_ERROR_FILE}") + else + ERROR_MESSAGE="Unexpected error during ${PHASE} phase. See the job log for details." fi - ERROR_MESSAGE="${error_block:-Unexpected error during ${PHASE} phase. See the job log for details.}" exit ${rc} fi @@ -452,13 +443,21 @@ case "${PHASE}" in echo "=== Step 2: Publishing to AAP ===" echo "Command: uv run app.py publish-aap --target-repo ${TARGET_REPO_URL} --target-branch ${TARGET_REPO_BRANCH} --project-id ${PROJECT_DIR}" cd /app - run_x2a uv run app.py publish-aap \ + rm -f "${X2A_ERROR_FILE}" + PUBLISH_OUTPUT=$(uv run app.py publish-aap \ --target-repo "${TARGET_REPO_URL}" \ --target-branch "${TARGET_REPO_BRANCH}" \ - --project-id "${PROJECT_DIR}" + --project-id "${PROJECT_DIR}" 2>&1 | tee /dev/stderr) || { + if [ -f "${X2A_ERROR_FILE}" ]; then + ERROR_MESSAGE=$(cat "${X2A_ERROR_FILE}") + else + ERROR_MESSAGE="Unexpected error during publish phase (publish-aap). See the job log for details." + fi + exit 1 + } - # Parse AAP project ID from output (captured by run_x2a in X2A_OUTPUT) - AAP_PROJECT_ID=$(echo "${X2A_OUTPUT}" | grep -oP 'ID: \K[0-9]+' | tail -1) + # Parse AAP project ID from output + AAP_PROJECT_ID=$(echo "${PUBLISH_OUTPUT}" | grep -oP 'ID: \K[0-9]+' | tail -1) if [ -n "${AAP_PROJECT_ID}" ]; then ARTIFACTS+=("ansible_project:${AAP_CONTROLLER_URL}/execution/projects/${AAP_PROJECT_ID}/details") else From d5c0b2d3c05c8e371e488dfdc1c0a6942af99387 Mon Sep 17 00:00:00 2001 From: Elai Shalev Date: Mon, 30 Mar 2026 15:27:22 +0300 Subject: [PATCH 3/4] applied review --- .../x2a-backend/templates/x2a-job-script.sh | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/workspaces/x2a/plugins/x2a-backend/templates/x2a-job-script.sh b/workspaces/x2a/plugins/x2a-backend/templates/x2a-job-script.sh index 9ab997d8fe..16aaed6c4d 100644 --- a/workspaces/x2a/plugins/x2a-backend/templates/x2a-job-script.sh +++ b/workspaces/x2a/plugins/x2a-backend/templates/x2a-job-script.sh @@ -80,20 +80,28 @@ sanitize_secrets() { # Run an x2a tool command with error reporting. # On failure, reads the error details file written by x2a-convertor and sets ERROR_MESSAGE. # On success, clears ERROR_MESSAGE. +# Captured output is stored in X2A_OUTPUT for callers that need to parse it. # Usage: run_x2a uv run app.py [args...] run_x2a() { rm -f "${X2A_ERROR_FILE}" + echo "Command: $*" + + local tmpfile + tmpfile=$(mktemp) + set +e - "$@" - local rc=$? + "$@" 2>&1 | tee "${tmpfile}" + local rc=${PIPESTATUS[0]} set -e + X2A_OUTPUT=$(cat "${tmpfile}") + rm -f "${tmpfile}" + if [ ${rc} -ne 0 ]; then + ERROR_MESSAGE="Unexpected error during ${PHASE} phase. See the job log for details." if [ -f "${X2A_ERROR_FILE}" ]; then - ERROR_MESSAGE=$(cat "${X2A_ERROR_FILE}") - else - ERROR_MESSAGE="Unexpected error during ${PHASE} phase. See the job log for details." + ERROR_MESSAGE+=" Message: $(cat "${X2A_ERROR_FILE}")" fi exit ${rc} fi @@ -253,7 +261,6 @@ case "${PHASE}" in # Usage: app.py init [OPTIONS] USER_REQUIREMENTS # --source-dir DIRECTORY Source directory to analyze USER_REQ="${USER_PROMPT:-Analyze the Chef cookbooks and create a migration plan}" - echo "Command: uv run app.py init --source-dir ${SOURCE_BASE} \"${USER_REQ}\"" run_x2a uv run app.py init --source-dir "${SOURCE_BASE}" "${USER_REQ}" # Copy output to target location @@ -315,7 +322,6 @@ case "${PHASE}" in echo "Working directory: $(pwd)" USER_REQ="${USER_PROMPT:-Analyze the module '${MODULE_NAME}' for migration to Ansible}" - echo "Command: uv run app.py analyze --source-dir ${SOURCE_BASE} \"${USER_REQ}\"" run_x2a uv run app.py analyze --source-dir "${SOURCE_BASE}" "${USER_REQ}" # Copy output to target location @@ -370,7 +376,6 @@ case "${PHASE}" in echo "Working directory: $(pwd)" USER_REQ="${USER_PROMPT:-Migrate this module to Ansible}" - echo "Command: uv run app.py migrate --source-dir ${SOURCE_BASE} --source-technology Chef --high-level-migration-plan ${PROJECT_PATH}/migration-plan.md --module-migration-plan ${OUTPUT_DIR}/migration-plan-${MODULE_NAME_SANITIZED}.md \"${USER_REQ}\"" run_x2a uv run app.py migrate \ --source-dir "${SOURCE_BASE}" \ --source-technology Chef \ @@ -418,8 +423,6 @@ case "${PHASE}" in # Step 1: publish-project — assemble Ansible project from migrated role echo "=== Step 1: Assembling Ansible project ===" - echo "Command: uv run app.py publish-project ${PROJECT_DIR} ${MODULE_NAME}" - # publish-project reads from {project_id}/modules/{module_name}/ansible/roles/{module_name}/ # and writes to {project_id}/ansible-project/ # It operates relative to CWD, so we run from TARGET_BASE @@ -441,23 +444,14 @@ case "${PHASE}" in # Step 2: publish-aap — register with AAP and sync echo "" echo "=== Step 2: Publishing to AAP ===" - echo "Command: uv run app.py publish-aap --target-repo ${TARGET_REPO_URL} --target-branch ${TARGET_REPO_BRANCH} --project-id ${PROJECT_DIR}" cd /app - rm -f "${X2A_ERROR_FILE}" - PUBLISH_OUTPUT=$(uv run app.py publish-aap \ + run_x2a uv run app.py publish-aap \ --target-repo "${TARGET_REPO_URL}" \ --target-branch "${TARGET_REPO_BRANCH}" \ - --project-id "${PROJECT_DIR}" 2>&1 | tee /dev/stderr) || { - if [ -f "${X2A_ERROR_FILE}" ]; then - ERROR_MESSAGE=$(cat "${X2A_ERROR_FILE}") - else - ERROR_MESSAGE="Unexpected error during publish phase (publish-aap). See the job log for details." - fi - exit 1 - } + --project-id "${PROJECT_DIR}" - # Parse AAP project ID from output - AAP_PROJECT_ID=$(echo "${PUBLISH_OUTPUT}" | grep -oP 'ID: \K[0-9]+' | tail -1) + # Parse AAP project ID from captured output + AAP_PROJECT_ID=$(echo "${X2A_OUTPUT}" | grep -oP 'ID: \K[0-9]+' | tail -1) if [ -n "${AAP_PROJECT_ID}" ]; then ARTIFACTS+=("ansible_project:${AAP_CONTROLLER_URL}/execution/projects/${AAP_PROJECT_ID}/details") else From 65e349fbcabd986678ac8aa566ae03cac82d1204 Mon Sep 17 00:00:00 2001 From: Elai Shalev Date: Mon, 30 Mar 2026 15:30:42 +0300 Subject: [PATCH 4/4] changeset --- workspaces/x2a/.changeset/error-propagation-fixes.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 workspaces/x2a/.changeset/error-propagation-fixes.md diff --git a/workspaces/x2a/.changeset/error-propagation-fixes.md b/workspaces/x2a/.changeset/error-propagation-fixes.md new file mode 100644 index 0000000000..eb51961053 --- /dev/null +++ b/workspaces/x2a/.changeset/error-propagation-fixes.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-x2a-backend': patch +--- + +Improve error propagation in job script: consolidate error handling into run_x2a function with default message and appended error details, add command logging, and refactor publish-aap to use the shared error handler.