Prow Job Analyze Test Failure
This skill analyzes test failures by downloading Prow CI artifacts, checking test logs, inspecting resources and events,
analyzing test source code, and optionally integrating cluster diagnostics from must-gather data.
Prerequisites
Identical with "Prow Job Analyze Resource" skill.
Input Format
The user will provide:
Prow job URL - gcsweb URL containing test-platform-results/
- Example:
https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/pr-logs/pull/openshift_hypershift/6731/pull-ci-openshift-hypershift-main-e2e-aws/1962527613477982208
- URL may or may not have trailing slash
Test name (optional) - specific test name that failed
- When provided, focus the analysis on that test's stack trace and logs
- When omitted, analyze all failed CI steps by inspecting the JUnit XML, build logs,
and step artifacts to identify the root cause — this is the common case for
multi-step CI workflows (e.g. HyperShift, bare-metal OADP jobs) where the failure
is in a step rather than a named unit test
- Examples:
TestKarpenter/EnsureHostedCluster/ValidateMetricsAreExposed
TestCreateClusterCustomConfig
The openshift-console downloads pods [apigroup:console.openshift.io] should be scheduled on different nodes
Optional flags (optional):
--fast - Skip must-gather extraction and analysis; continue with log/JUnit/step-artifact analysis only (scope is preserved: test-level when test-name is provided, step-level across all failed CI steps when omitted)
Implementation Steps
Step 1: Parse and Validate URL
Use the "Parse and Validate URL" steps from "Prow Job Analyze Resource" skill
Step 2: Create Working Directory
Check for existing artifacts first
- Check if
.work/prow-job-analyze-test-failure/{build_id}/logs/ directory exists and has content
- If it exists with content:
- Use AskUserQuestion tool to ask:
- Question: "Artifacts already exist for build {build_id}. Would you like to use the existing download or re-download?"
- Options:
- "Use existing" - Skip to step Analyze Test Failure
- "Re-download" - Continue to clean and re-download
- If user chooses "Re-download":
- Remove all existing content:
rm -rf .work/prow-job-analyze-test-failure/{build_id}/logs/
- Also remove tmp directory:
rm -rf .work/prow-job-analyze-test-failure/{build_id}/tmp/
- This ensures clean state before downloading new content
- If user chooses "Use existing":
- Skip directly to Step 4 (Analyze Test Failure)
- Still need to download prowjob.json if it doesn't exist
Create directory structure
mkdir -p .work/prow-job-analyze-test-failure/{build_id}/logs
mkdir -p .work/prow-job-analyze-test-failure/{build_id}/tmp
- Use
.work/prow-job-analyze-test-failure/ as the base directory (already in .gitignore)
- Use build_id as subdirectory name
- Create
logs/ subdirectory for all downloads
- Create
tmp/ subdirectory for temporary files (intermediate JSON, etc.)
- Working directory:
.work/prow-job-analyze-test-failure/{build_id}/
Step 3: Download and Validate prowjob.json
Use the fetch-prowjob-json skill to fetch the prowjob.json for this job. See plugins/ci/skills/fetch-prowjob-json/SKILL.md for complete implementation details.
- Fetch prowjob.json using the Prow job URL (convert to gcsweb URL per the
fetch-prowjob-json skill)
- Save locally to
.work/prow-job-analyze-test-failure/{build_id}/logs/prowjob.json
- Parse and validate
- Search for pattern:
--target=([a-zA-Z0-9-]+) in the ci-operator args
- If not found:
- Display: "This is not a ci-operator job. The prowjob cannot be analyzed by this skill."
- Explain: ci-operator jobs have a --target argument specifying the test target
- Exit skill
- Extract target name and JOB_NAME
Step 4: Analyze Test Failure
Step 4.0: Detect Aggregated Jobs
Aggregated jobs run the same job in parallel (typically 10 times) and perform statistical
analysis of test results. Detect aggregation by checking for aggregated- prefix in the job
name or an aggregator container/step in prowjob.json.
If the job is aggregated, the failure modes are different from normal jobs:
Statistically significant test failure — The test itself fails frequently enough across
runs to be flagged as a regression. This is a real regression that needs investigation.
Insufficient completed runs — Not enough runs completed successfully to perform the
statistical test (e.g., only 5 of 10 jobs produced results). This manifests as mass test
failures across many unrelated tests. The root cause is whatever prevented the other runs
from completing — this could be infrastructure issues, install failures, or an actual product
bug causing crashes. Investigate the underlying job runs that did not complete to determine
why.
Non-deterministic test presence — A test only ran in a small subset of completed jobs,
even though the other jobs completed successfully. The failure message will say something like
"Passed 1 times, failed 0 times, skipped 0 times: we require at least 6 attempts to have a
chance at success". Every test must produce results in every job run; it is a bug if it does
not. This is a regression — someone introduced a test that doesn't produce results
deterministically. Investigate which test is non-deterministic and why it only runs in some
jobs.
When analyzing an aggregated job failure, first determine which failure mode applies before
diving into individual test analysis. For mode 2, focus on why runs failed rather than
individual test results. For mode 3, investigate why the test only ran in some jobs.
Finding underlying job run URLs:
The aggregated junit XML contains links to every underlying job run. Download it from:
gs://test-platform-results/{bucket-path}/artifacts/release-analysis-aggregator/openshift-release-analysis-aggregator/artifacts/release-analysis-aggregator/{job-name}/{payload-tag}/junit-aggregated.xml
Each <testcase> element has a <system-out> with YAML-formatted data including passes:,
failures:, and skips: lists. Each entry has:
jobrunid: The build ID of the underlying job run
humanurl: Prow URL for the job run (e.g., https://prow.ci.openshift.org/view/gs/test-platform-results/logs/{job-name}/{jobrunid})
gcsartifacturl: Direct link to GCS artifacts
Use the humanurl links to investigate individual job run failures with the normal
(non-aggregated) analysis steps below.
Step 4.1: Download build-log.txt
gcloud storage cp gs://test-platform-results/{bucket-path}/build-log.txt .work/prow-job-analyze-test-failure/{build_id}/logs/build-log.txt --no-user-output-enabled
Step 4.2: Parse and validate
- Read
.work/prow-job-analyze-test-failure/{build_id}/logs/build-log.txt
Branch on whether {test_name} was provided:
Branch A: test_name provided
- Search build-log.txt for the exact test name string
- Gather the stack trace and surrounding context for that specific test
- Store the single failing test's error output for use in Steps 4.4 and 4.9
Branch B: test_name NOT provided (multi-step discovery)
This is the common case for multi-step CI workflows (e.g., HyperShift, bare-metal, OADP jobs)
where failures occur in CI steps rather than named unit tests.
Discover failed CI steps from JUnit XML
Use {JOB_NAME} (from Step 3, extracted from .spec.job) for all GCS artifact paths —
not {target}. On PR jobs these values differ and {target} will miss the artifacts.
Search for JUnit XML files under the artifacts directory:
gcloud storage ls "gs://test-platform-results/{bucket-path}/artifacts/{JOB_NAME}/**/junit*.xml" 2>/dev/null
Download all found JUnit XML files:
gcloud storage cp "gs://test-platform-results/{bucket-path}/artifacts/{JOB_NAME}/**/junit*.xml" \
.work/prow-job-analyze-test-failure/{build_id}/logs/ --no-user-output-enabled --recursive 2>/dev/null || true
Parse each downloaded XML file and collect every <testcase> element where:
- A
<failure> or <error> child element is present, OR
- The
<testcase> has attribute status="failed"
For each failed testcase record:
step_name: value of classname or name attribute (whichever identifies the CI step)
failure_message: text content of the <failure> or <error> element
junit_file: path of the XML file it came from
Classify each failed step by phase
The ci-operator JUnit XML (junit_operator.xml) includes phase-level testcases:
"Run multi-stage test pre phase" — setup/installation steps
"Run multi-stage test test phase" — functional test steps
"Run multi-stage test post phase" — gather/cleanup steps
Use the phase entries to classify each failed step_name:
- Look at the failed phase-level testcases (those with
"pre phase", "test phase",
or "post phase" in their name) — these tell you which phase failed
- Cross-reference the individual step testcases against their phase:
- Steps in the
pre phase → installation/setup steps
- Steps in the
test phase → functional test steps
- Steps in the
post phase → gather/cleanup steps (rarely the root cause)
If the phase-level testcases are absent from the JUnit XML (some jobs omit them),
fall back to the ci-operator-step-graph.json artifact, which lists all steps with
their dependencies and timing — use execution order and naming conventions as a
secondary signal. When still ambiguous, prefer classifying as a test step so
must-gather analysis is not skipped.
Route each failed step to the appropriate analysis path
pre phase steps (installation) → invoke the ci:analyze-prow-job-install-failure
skill for each such step, passing the same Prow job URL. Must-gather is not
attempted for pre phase failures: must-gather requires a live apiserver, and when
the installation phase fails the cluster is not fully up, so collection will fail.
Capture the delegated output and store it
as a per-step entry in the shared results collection used by Step 5 and Step 5.5:{
step_name: {step_name},
type: "installation",
summary: {one-line summary from ci:analyze-prow-job-install-failure},
evidence: {key error / stack trace from delegated analysis},
recommendation: {recommended action from delegated analysis}
}
test phase steps (functional tests) → proceed with the iteration in item 4
below (download step log, artifacts, extract stack trace) and include must-gather
analysis (Steps 4.4–4.9). Store findings in the same per-step results collection:{
step_name: {step_name},
type: "test",
summary: {one-line failure summary},
evidence: {stack trace / error output},
recommendation: {recommended action}
}
post phase steps (gather/cleanup) → treat as informational. Download the step
log and note the failure in the report, but do not perform must-gather analysis for
post-phase failures alone (they are usually a consequence of earlier failures).
Iterate over each test phase step (pre and post phase steps already routed above)
For each step_name in the test phase, perform the following sub-steps:
a. Download step-specific log
gcloud storage cp \
"gs://test-platform-results/{bucket-path}/artifacts/{JOB_NAME}/{step_name}/build-log.txt" \
.work/prow-job-analyze-test-failure/{build_id}/logs/{step_name}-build-log.txt \
--no-user-output-enabled 2>/dev/null || true
If the step log is not found at that path, fall back to scanning build-log.txt for
lines mentioning {step_name} and collect surrounding context (±50 lines).
b. Download step-specific artifacts
gcloud storage ls "gs://test-platform-results/{bucket-path}/artifacts/{JOB_NAME}/{step_name}/" \
2>/dev/null
Download any relevant artifacts (e.g., *.json, *.yaml, events*.txt) to
.work/prow-job-analyze-test-failure/{build_id}/logs/{step_name}/.
c. Extract stack trace and context
- Scan the step log for panic traces,
FAIL, Error:, fatal, or assertion failures
- Collect the full stack trace block (from the triggering line to the end of the trace)
- Note the failure message from the JUnit XML as additional context
d. Store per-step findings
Record for each step:
step_name
failure_message (from JUnit XML)
stack_trace (from step log or build-log.txt context)
artifacts (list of downloaded artifact paths)
Produce a multi-step failure summary
After iterating all failed steps, produce an ordered list of findings:
Failed Steps Discovered:
1. {step_name_1}: {one-line failure summary}
2. {step_name_2}: {one-line failure summary}
...
This list is used in Step 4.4 (evidence gathering) and Step 5 (report) to drive
per-step analysis rather than singular test analysis.
Step 4.3: Examine intervals files for cluster activity during E2E failures
- Search recursively for E2E timeline artifacts (known as "interval files") within the bucket-path:
gcloud storage ls 'gs://test-platform-results/{bucket-path}/**/e2e-timelines_spyglass_*json'
- The files can be nested at unpredictable levels below the bucket-path
- There could be as many as two matching files
- Download all matching interval files (use the full paths from the search results):
gcloud storage cp gs://test-platform-results/{bucket-path}/**/e2e-timelines_spyglass_*.json .work/prow-job-analyze-test-failure/{build_id}/logs/ --no-user-output-enabled
- If the wildcard copy doesn't work, copy each file individually using the full paths from the search results
- Scan interval files for test failure timing:
- Look for intervals where
source = "E2ETest" and message.annotations.status = "Failed"
- Note the
from and to timestamps on this interval - this indicates when the test was running
- Scan interval files for related cluster events:
- Look for intervals that overlap the timeframe when the failed test was running
- Filter for intervals with:
level = "Error" or level = "Warning"
source = "OperatorState"
- These events may indicate cluster issues that caused or contributed to the test failure
Step 4.3b: Check for Known Symptom Labels
The CI system may attach symptom labels to job runs — machine-detected patterns (e.g., "test failures during high CPU events") stored as JSON artifacts. These are not root causes but provide useful environmental context that may help explain failures when no other cause is found.
List the job_labels directory
gcloud storage ls "gs://test-platform-results/{bucket-path}/artifacts/job_labels/" 2>/dev/null
- If the directory does not exist or returns an error, skip this step silently
Download any JSON symptom files (exclude label-summary.html)
gcloud storage cp "gs://test-platform-results/{bucket-path}/artifacts/job_labels/*.json" \
.work/prow-job-analyze-test-failure/{build_id}/logs/job_labels/ --no-user-output-enabled 2>/dev/null || true
Parse symptom labels — each JSON file describes a detected symptom with a summary and explanation. Collect all symptom summaries for inclusion in the report.
Use symptoms as investigative context — symptoms are environmental observations, NOT definitive causes. They should inform your investigation (e.g., if 2 tests failed while high CPU was measured, CPU pressure could explain the failures if no other cause is found) but you must still perform thorough root cause analysis. Include them in the "Known Symptoms Seen" section of the report.
Step 4.4: Gather initial evidence
- Analyze stack traces from build-log.txt
- Analyze related code in the code repository
- Store artifacts from Prow CI job (json/yaml files) related to the failure under
.work/prow-job-analyze-test-failure/{build_id}/tmp
- Store logs under
.work/prow-job-analyze-test-failure/{build_id}/logs/
- Collect evidence from logs and events and other json/yaml files
- Check for CI step script errors: If the error is a scripting issue in a CI step (e.g., unbound variable, syntax error, missing command, bad exit code from a shell script) rather than a product bug, check for recent commits to that step's script in the
openshift/release repository. This is part of evidence gathering — identifying the responsible PR early informs the rest of the analysis. Include the responsible PR in your evidence.
Step 4.4b: Investigate crash-looping or failing containers
Be tenacious. When the build log or test output mentions crash-looping pods, container restarts, or deployments not becoming ready, you MUST trace the failure to its root cause. Never stop at "containers are crash-looping" — find out why.
Pursue all available log sources:
- Must-gather data (Step 4.6–4.7): Contains pod YAMLs with
containerStatuses (exitCode, lastState.terminated.reason, restartCount), container logs (current and previous), events, and operator conditions. This is often the richest source for diagnosing crash-looping pods.
- Gather-extra / gather-audit logs: Some jobs run additional gather steps that collect extra diagnostics. List the step artifacts directory for gather steps beyond
gather-must-gather (e.g., gather-extra, gather-audit-logs) and download relevant logs.
- Step-level build logs: The build log for the failing test step (downloaded in Step 4.2) often contains error output, stack traces, and timeout messages that reference specific pods or containers.
- Events from interval files (Step 4.3): Operator state transitions and warning events correlated with the failure window.
Follow the dependency chain. Always trace upstream to the originating error rather than stopping at the first symptom.
Step 4.5: Check for Must-Gather Availability
Parse optional flags
- Parse user input for
--fast flag
- If
--fast flag present:
- Skip must-gather detection and analysis entirely
- Proceed directly to Step 5 — scope is preserved:
- If
test_name was provided → produce singular test report (Branch A)
- If
test_name was omitted → produce multi-step report (Branch B)
- Do NOT prompt user about must-gather
Extract actual test name from prowjob.json
The artifacts directory uses the test name from prowjob.json, NOT the full URL path.
# Extract test name from prowjob.json (e.g., "e2e-aws-operator-serial-ote")
JOB_NAME=$(jq -r '.spec.job' .work/prow-job-analyze-test-failure/{build_id}/logs/prowjob.json)
# Note: For PR jobs, TARGET contains the full PR path like:
# pr-logs/pull/openshift_service-ca-operator/306/pull-ci-openshift-service-ca-operator-main-e2e-aws-operator-serial-ote
# But artifacts are stored under just the test name:
# e2e-aws-operator-serial-ote
Detect must-gather archive (only if --fast not present)
Use JOB_NAME (not TARGET) for artifact paths.
HyperShift jobs may have different must-gather patterns:
Pattern 1: Unified Archive (dump-management-cluster)
- Single archive with both management and hosted cluster data
- Used by: hypershift-aws-e2e-external workflow
Pattern 2: Dual Archives (gather-must-gather + dump)
- Standard must-gather for management cluster
- Separate hypershift-dump for additional data (may or may not have hosted cluster)
- Used by: hypershift-kubevirt-e2e-aws workflow
Pattern 3: Standard Only (gather-must-gather)
- Standard OpenShift must-gather only
- No HyperShift-specific dump
Detection logic:
# Check for Pattern 1: Unified archive (dump-management-cluster)
UNIFIED_DUMP=$(gcloud storage ls "gs://test-platform-results/{bucket-path}/artifacts/$JOB_NAME/dump-management-cluster/artifacts/artifacts.tar*" 2>/dev/null | head -1 || true)
# Check for Pattern 2/3: Standard must-gather
STANDARD_MG=$(gcloud storage ls "gs://test-platform-results/{bucket-path}/artifacts/$JOB_NAME/gather-must-gather/artifacts/must-gather.tar" 2>/dev/null || true)
# Check for Pattern 2: Additional hypershift-dump (multiple possible locations)
# Use wildcards to match all current and future HyperShift dump patterns:
# 1. **/artifacts/hypershift-dump.tar (covers dump/, hypershift-mce-dump/, etc.)
# 2. **/artifacts/**/hostedcluster.tar (covers all E2E test patterns)
HYPERSHIFT_DUMP=""
for pattern in \
"**/artifacts/hypershift-dump.tar" \
"**/artifacts/**/hostedcluster.tar"; do
FOUND=$(gcloud storage ls "gs://test-platform-results/{bucket-path}/artifacts/$JOB_NAME/$pattern" 2>/dev/null | head -1 || true)
if [ -n "$FOUND" ]; then
HYPERSHIFT_DUMP="$FOUND"
break
fi
done
# Determine pattern and check for hosted cluster data
if [ -n "$UNIFIED_DUMP" ]; then
# Pattern 1: Unified archive
PATTERN="unified"
# Download temporarily to check for hosted cluster data
TMP_CHECK="/tmp/check-unified-$$.tar"
gcloud storage cp "$UNIFIED_DUMP" "$TMP_CHECK" --no-user-output-enabled
# Check if archive contains hostedcluster-* directory
HAS_HOSTED_CLUSTER=$(tar -tf "$TMP_CHECK" 2>/dev/null | grep -q "hostedcluster-" && echo "true" || echo "false")
rm -f "$TMP_CHECK"
elif [ -n "$STANDARD_MG" ] && [ -n "$HYPERSHIFT_DUMP" ]; then
# Pattern 2: Dual archives
PATTERN="dual"
# Download hypershift-dump temporarily to check for hosted cluster
TMP_CHECK="/tmp/check-dump-$$.tar"
gcloud storage cp "$HYPERSHIFT_DUMP" "$TMP_CHECK" --no-user-output-enabled
# Check if hypershift-dump contains hostedcluster-* directory
HAS_HOSTED_CLUSTER=$(tar -tf "$TMP_CHECK" 2>/dev/null | grep -q "hostedcluster-" && echo "true" || echo "false")
rm -f "$TMP_CHECK"
elif [ -n "$STANDARD_MG" ]; then
# Pattern 3: Standard must-gather only
PATTERN="standard"
HAS_HOSTED_CLUSTER=false
else
# No must-gather found
PATTERN="none"
HAS_HOSTED_CLUSTER=false
fi
Possible outcomes:
- PATTERN="none": No must-gather found → Skip to Step 5 (silent, expected for some jobs)
- PATTERN="standard": Standard OpenShift must-gather only → Extract single cluster
- PATTERN="unified" + HAS_HOSTED_CLUSTER=false: HyperShift unified archive with management only
- PATTERN="unified" + HAS_HOSTED_CLUSTER=true: HyperShift unified archive with both clusters
- PATTERN="dual" + HAS_HOSTED_CLUSTER=false: Two archives but hosted cluster not in hypershift-dump
- PATTERN="dual" + HAS_HOSTED_CLUSTER=true: Two archives with hosted cluster in hypershift-dump
Important Pattern Notes:
- Pattern 1 (unified): Management at
logs/artifacts/output/, hosted at logs/artifacts/output/hostedcluster-{name}/
- Pattern 2 (dual): Management in standard must-gather, hosted MAY be in hypershift-dump.tar or hostedcluster.tar (not guaranteed)
- Wildcard patterns match all current and future dump locations
- Pattern 3 (standard): Management cluster only, no HyperShift-specific data
Ask user if they want must-gather analysis
- Only if must-gather(s) were found and --fast not present
- Use AskUserQuestion tool:
- Question: "Must-gather data is available. Include cluster diagnostics in the analysis?"
- Header: "Must-gather"
- Options:
- Label: "Yes - Extract and analyze must-gather (Recommended)"
Description: "Provides cluster-level diagnostics that may reveal root causes (pods, operators, nodes, events). Takes additional time to download and analyze."
- Label: "No - Skip must-gather (faster)"
Description: "Only analyze test-level artifacts (build-log, intervals). Faster but may miss cluster-level issues."
- If user chooses "No", skip to Step 5
Step 4.6: Extract Must-Gather (Conditional)
Only if user chose "Yes" in Step 4.5:
Determine extraction strategy
- If single must-gather detected → extract to
must-gather/logs/
- If dual must-gather detected (HyperShift) → extract both:
- Management cluster →
must-gather-mgmt/logs/
- Hosted cluster →
must-gather-hosted/logs/
Check for existing extraction
For single must-gather:
- Check if
.work/prow-job-analyze-test-failure/{build_id}/must-gather/logs/ exists with content
For dual must-gather (HyperShift):
- Check if
.work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs/ exists with content
- Check if
.work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/logs/ exists with content
If either exists:
- Use AskUserQuestion tool:
- Question: "Must-gather already extracted for this build. Use existing data?"
- Header: "Reuse"
- Options:
- Label: "Use existing"
Description: "Reuse previously extracted must-gather data (faster)"
- Label: "Re-extract"
Description: "Download and extract fresh must-gather data"
- If "Re-extract":
rm -rf .work/prow-job-analyze-test-failure/{build_id}/must-gather*/
- Continue to step 3 (fresh extraction)
- If "Use existing":
- Validate content directories exist and are not empty (see validation in step 4.6)
- If validation fails, fall back to re-extraction
- If validation succeeds, skip to Step 4.7
Create must-gather directories
Based on PATTERN from Step 4.5.3:
For Pattern 3 (standard only):
mkdir -p .work/prow-job-analyze-test-failure/{build_id}/must-gather/logs
mkdir -p .work/prow-job-analyze-test-failure/{build_id}/must-gather/tmp
For Pattern 1 (unified) or Pattern 2 (dual):
mkdir -p .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs
mkdir -p .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/tmp
mkdir -p .work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/logs
mkdir -p .work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/tmp
Download must-gather archives
Use JOB_NAME (from Step 4.5.2) for artifact paths, not {target}:
For Pattern 3 (standard only):
gcloud storage cp "$STANDARD_MG" \
.work/prow-job-analyze-test-failure/{build_id}/must-gather/tmp/must-gather.tar \
--no-user-output-enabled
For Pattern 1 (unified):
# Download unified archive (contains both management and hosted cluster data)
gcloud storage cp "$UNIFIED_DUMP" \
.work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/tmp/unified-dump.tar \
--no-user-output-enabled
For Pattern 2 (dual):
# Download management cluster must-gather
gcloud storage cp "$STANDARD_MG" \
.work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/tmp/must-gather.tar \
--no-user-output-enabled
# Download hypershift-dump (may or may not contain hosted cluster)
gcloud storage cp "$HYPERSHIFT_DUMP" \
.work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/tmp/hypershift-dump.tar \
--no-user-output-enabled
Extract archives
For Pattern 3 (standard only):
# Use existing extract_archives.py script for standard must-gather
python3 plugins/ci/skills/prow-job-extract-must-gather/extract_archives.py \
.work/prow-job-analyze-test-failure/{build_id}/must-gather/tmp/must-gather.tar \
.work/prow-job-analyze-test-failure/{build_id}/must-gather/logs
For Pattern 1 (unified):
# Extract unified archive to temporary location
TMP_EXTRACT=".work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/tmp/extracted"
mkdir -p "$TMP_EXTRACT"
# Handle both .tar and .tar.gz
if [[ "$UNIFIED_DUMP" == *.tar.gz ]]; then
tar -xzf .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/tmp/unified-dump.tar -C "$TMP_EXTRACT"
else
tar -xf .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/tmp/unified-dump.tar -C "$TMP_EXTRACT"
fi
# Find the output directory (may be at logs/artifacts/output or just output)
OUTPUT_DIR=$(find "$TMP_EXTRACT" -type d -name "output" | head -1)
if [ -z "$OUTPUT_DIR" ]; then
echo "ERROR: Could not find output directory in unified dump"
rm -rf "$TMP_EXTRACT"
# Clear variables to prevent subsequent usage
HAS_HOSTED_CLUSTER="false"
unset HOSTED_DIR
unset OUTPUT_DIR
# Skip to Step 5 - no must-gather analysis possible
else
# Move management cluster data (root level in output/)
# Exclude hostedcluster-* directories
for item in "$OUTPUT_DIR"/*; do
if [ -e "$item" ] && [[ ! "$(basename "$item")" =~ ^hostedcluster- ]]; then
mv "$item" .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs/
fi
done
# Move hosted cluster data (hostedcluster-* subdirectory)
if [ "$HAS_HOSTED_CLUSTER" = "true" ]; then
HOSTED_DIR=$(find "$OUTPUT_DIR" -maxdepth 1 -type d -name "hostedcluster-*" | head -1)
if [ -n "$HOSTED_DIR" ]; then
mv "$HOSTED_DIR"/* .work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/logs/
echo "✓ Hosted cluster data extracted from unified archive"
else
echo "WARNING: Expected hosted cluster data but hostedcluster-* directory not found"
fi
fi
# Cleanup temporary extraction directory
rm -rf "$TMP_EXTRACT"
fi
For Pattern 2 (dual):
# Extract management cluster must-gather (standard format)
python3 plugins/prow-job/skills/prow-job-extract-must-gather/extract_archives.py \
.work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/tmp/must-gather.tar \
.work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs
# Extract hypershift-dump (may contain hosted cluster)
TMP_EXTRACT=".work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/tmp/extracted"
mkdir -p "$TMP_EXTRACT"
tar -xf .work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/tmp/hypershift-dump.tar -C "$TMP_EXTRACT"
if [ "$HAS_HOSTED_CLUSTER" = "true" ]; then
# Look for hostedcluster-* directory in dump
HOSTED_DIR=$(find "$TMP_EXTRACT" -maxdepth 2 -type d -name "hostedcluster-*" | head -1)
if [ -n "$HOSTED_DIR" ]; then
mv "$HOSTED_DIR"/* .work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/logs/
echo "✓ Hosted cluster data extracted from hypershift-dump"
else
echo "WARNING: HAS_HOSTED_CLUSTER=true but no hostedcluster-* directory found in dump"
echo "Hypershift-dump likely contains only management cluster data"
HAS_HOSTED_CLUSTER=false # Update flag since hosted cluster not actually present
fi
else
echo "INFO: Hypershift-dump does not contain hosted cluster data (management cluster only)"
fi
# Cleanup temporary extraction directory
rm -rf "$TMP_EXTRACT"
Locate and validate content directories
For Pattern 3 (standard only):
# Check for content/ directory first (renamed by extraction script)
if [ -d ".work/prow-job-analyze-test-failure/{build_id}/must-gather/logs/content" ]; then
MUST_GATHER_PATH=".work/prow-job-analyze-test-failure/{build_id}/must-gather/logs/content"
else
# Fall back to finding the directory containing -ci- (e.g., registry-build09-ci-...)
MUST_GATHER_PATH=$(find .work/prow-job-analyze-test-failure/{build_id}/must-gather/logs -maxdepth 1 -type d -name "*-ci-*" | head -1)
fi
# Validate MUST_GATHER_PATH is set and directory exists
if [ -z "$MUST_GATHER_PATH" ] || [ ! -d "$MUST_GATHER_PATH" ]; then
echo "ERROR: Must-gather content directory not found after extraction"
# Skip to Step 5 (scope preserved: Branch A or Branch B per test_name presence)
elif [ -z "$(ls -A "$MUST_GATHER_PATH" 2>/dev/null)" ]; then
echo "ERROR: Must-gather content directory is empty"
# Skip to Step 5 (scope preserved: Branch A or Branch B per test_name presence)
else
echo "✓ Must-gather content located at: $MUST_GATHER_PATH"
# Continue to Step 4.7 with MUST_GATHER_PATH set
fi
For Pattern 1 (unified) or Pattern 2 (dual):
# Management cluster validation
if [ "$PATTERN" = "unified" ]; then
# Pattern 1: Data extracted directly to logs/ directory
MUST_GATHER_MGMT_PATH=".work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs"
else
# Pattern 2: Standard must-gather extraction (look for content/ or hash directory)
if [ -d ".work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs/content" ]; then
MUST_GATHER_MGMT_PATH=".work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs/content"
else
MUST_GATHER_MGMT_PATH=$(find .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs -maxdepth 1 -type d -name "*-ci-*" | head -1)
fi
fi
# Validate management cluster path
if [ -z "$MUST_GATHER_MGMT_PATH" ] || [ ! -d "$MUST_GATHER_MGMT_PATH" ]; then
echo "ERROR: Management cluster directory not found"
# Skip to Step 5 (scope preserved: Branch A or Branch B per test_name presence)
elif [ -z "$(ls -A "$MUST_GATHER_MGMT_PATH" 2>/dev/null)" ]; then
echo "ERROR: Management cluster directory is empty"
# Skip to Step 5 (scope preserved: Branch A or Branch B per test_name presence)
else
echo "✓ Management cluster data located at: $MUST_GATHER_MGMT_PATH"
fi
# Hosted cluster validation - only if HAS_HOSTED_CLUSTER is true
if [ "$HAS_HOSTED_CLUSTER" = "true" ]; then
MUST_GATHER_HOSTED_PATH=".work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/logs"
# Validate hosted cluster path
if [ ! -d "$MUST_GATHER_HOSTED_PATH" ]; then
echo "WARNING: Hosted cluster directory not found (expected based on archive detection)"
MUST_GATHER_HOSTED_PATH="" # Clear the path
elif [ -z "$(ls -A "$MUST_GATHER_HOSTED_PATH" 2>/dev/null)" ]; then
echo "WARNING: Hosted cluster directory is empty"
MUST_GATHER_HOSTED_PATH="" # Clear the path
else
echo "✓ Hosted cluster data located at: $MUST_GATHER_HOSTED_PATH"
fi
else
echo "✓ Management cluster must-gather located at: $MUST_GATHER_MGMT_PATH"
fi
# Only validate hosted cluster if HAS_HOSTED_CLUSTER is true
if [ "$HAS_HOSTED_CLUSTER" = "true" ]; then
if [ -z "$MUST_GATHER_HOSTED_PATH" ] || [ ! -d "$MUST_GATHER_HOSTED_PATH" ]; then
echo "ERROR: Hosted cluster must-gather content directory not found"
elif [ -z "$(ls -A "$MUST_GATHER_HOSTED_PATH" 2>/dev/null)" ]; then
echo "ERROR: Hosted cluster must-gather content directory is empty"
else
echo "✓ Hosted cluster must-gather located at: $MUST_GATHER_HOSTED_PATH"
echo "✓ Hosted cluster namespace: $HOSTED_NAMESPACE"
fi
fi
Step 4.7: Analyze Must-Gather (Conditional)
Only if Step 4.6 completed successfully:
Locate must-gather-analyzer scripts
The must-gather plugin provides analysis scripts. Locate the scripts directory:
# Try to find the must-gather-analyzer scripts in common locations
for SEARCH_PATH in \
"plugins/must-gather/skills/must-gather-analyzer/scripts" \
"~/.claude/plugins/cache/*/plugins/must-gather/skills/must-gather-analyzer/scripts" \
"$(find ~ -type d -path "*/must-gather/skills/must-gather-analyzer/scripts" 2>/dev/null | head -1)"; do
SCRIPTS_DIR=$(eval echo "$SEARCH_PATH")
if [ -d "$SCRIPTS_DIR" ] && [ -f "$SCRIPTS_DIR/analyze_clusteroperators.py" ]; then
break
fi
SCRIPTS_DIR=""
done
if [ -z "$SCRIPTS_DIR" ]; then
echo "WARNING: Must-gather analysis scripts not found."
echo "Install the must-gather plugin: /plugin install must-gather@ai-helpers"
# Continue to Step 5 without cluster analysis
fi
Run targeted cluster diagnostics
Focus on issues relevant to test failures (not full cluster analysis).
For single must-gather (standard OpenShift):
# Core diagnostics (only if scripts and path are available)
if [ -n "$SCRIPTS_DIR" ] && [ -n "$MUST_GATHER_PATH" ]; then
python3 "$SCRIPTS_DIR/analyze_clusteroperators.py" "$MUST_GATHER_PATH"
python3 "$SCRIPTS_DIR/analyze_pods.py" "$MUST_GATHER_PATH" --problems-only
python3 "$SCRIPTS_DIR/analyze_nodes.py" "$MUST_GATHER_PATH" --problems-only
python3 "$SCRIPTS_DIR/analyze_events.py" "$MUST_GATHER_PATH" --type Warning --count 50
else
echo "WARNING: Skipping must-gather analysis (scripts or path not available)"
fi
For dual must-gather (HyperShift):
# Management cluster diagnostics (only if scripts and path are available)
if [ -n "$SCRIPTS_DIR" ] && [ -n "$MUST_GATHER_MGMT_PATH" ]; then
echo "=== Analyzing Management Cluster ==="
python3 "$SCRIPTS_DIR/analyze_clusteroperators.py" "$MUST_GATHER_MGMT_PATH"
python3 "$SCRIPTS_DIR/analyze_pods.py" "$MUST_GATHER_MGMT_PATH" --problems-only
python3 "$SCRIPTS_DIR/analyze_nodes.py" "$MUST_GATHER_MGMT_PATH" --problems-only
python3 "$SCRIPTS_DIR/analyze_events.py" "$MUST_GATHER_MGMT_PATH" --type Warning --count 50
else
echo "WARNING: Skipping management cluster analysis (scripts or path not available)"
fi
# Hosted cluster diagnostics (only if scripts and path are available)
if [ -n "$SCRIPTS_DIR" ] && [ -n "$MUST_GATHER_HOSTED_PATH" ]; then
echo "=== Analyzing Hosted Cluster (Namespace: $HOSTED_NAMESPACE) ==="
python3 "$SCRIPTS_DIR/analyze_clusteroperators.py" "$MUST_GATHER_HOSTED_PATH"
python3 "$SCRIPTS_DIR/analyze_pods.py" "$MUST_GATHER_HOSTED_PATH" --problems-only
python3 "$SCRIPTS_DIR/analyze_nodes.py" "$MUST_GATHER_HOSTED_PATH" --problems-only
python3 "$SCRIPTS_DIR/analyze_events.py" "$MUST_GATHER_HOSTED_PATH" --type Warning --count 50
else
echo "INFO: Skipping hosted cluster analysis (scripts or path not available)"
fi
Run conditional diagnostics based on test context
# Network diagnostics (if test name suggests network issues)
if [[ "$JOB_NAME" =~ network|ovn|sdn|connectivity|route|ingress|egress ]]; then
if [ -n "$MUST_GATHER_PATH" ]; then
python3 "$SCRIPTS_DIR/analyze_network.py" "$MUST_GATHER_PATH"
fi
if [ -n "$MUST_GATHER_MGMT_PATH" ]; then
…(truncated)
1---2name: prow-job-analyze-test-failure3description: Analyze failed Prow CI tests by inspecting test code, downloading artifacts, and optionally integrating must-gather cluster diagnostics for root cause analysis Use when this capability is needed.4---56# Prow Job Analyze Test Failure78This skill analyzes test failures by downloading Prow CI artifacts, checking test logs, inspecting resources and events,9analyzing test source code, and optionally integrating cluster diagnostics from must-gather data.1011## Prerequisites1213Identical with "Prow Job Analyze Resource" skill.1415## Input Format1617The user will provide:18191. **Prow job URL** - gcsweb URL containing `test-platform-results/`2021 - Example: `https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/pr-logs/pull/openshift_hypershift/6731/pull-ci-openshift-hypershift-main-e2e-aws/1962527613477982208`22 - URL may or may not have trailing slash23242. **Test name** (optional) - specific test name that failed25 - When provided, focus the analysis on that test's stack trace and logs26 - When omitted, analyze all failed CI steps by inspecting the JUnit XML, build logs,27 and step artifacts to identify the root cause — this is the common case for28 multi-step CI workflows (e.g. HyperShift, bare-metal OADP jobs) where the failure29 is in a step rather than a named unit test30 - Examples:31 - `TestKarpenter/EnsureHostedCluster/ValidateMetricsAreExposed`32 - `TestCreateClusterCustomConfig`33 - `The openshift-console downloads pods [apigroup:console.openshift.io] should be scheduled on different nodes`34353. Optional flags (optional):36 - `--fast` - Skip must-gather extraction and analysis; continue with log/JUnit/step-artifact analysis only (scope is preserved: test-level when `test-name` is provided, step-level across all failed CI steps when omitted)3738## Implementation Steps3940### Step 1: Parse and Validate URL4142Use the "Parse and Validate URL" steps from "Prow Job Analyze Resource" skill4344### Step 2: Create Working Directory45461. **Check for existing artifacts first**4748 - Check if `.work/prow-job-analyze-test-failure/{build_id}/logs/` directory exists and has content49 - If it exists with content:50 - Use AskUserQuestion tool to ask:51 - Question: "Artifacts already exist for build {build_id}. Would you like to use the existing download or re-download?"52 - Options:53 - "Use existing" - Skip to step Analyze Test Failure54 - "Re-download" - Continue to clean and re-download55 - If user chooses "Re-download":56 - Remove all existing content: `rm -rf .work/prow-job-analyze-test-failure/{build_id}/logs/`57 - Also remove tmp directory: `rm -rf .work/prow-job-analyze-test-failure/{build_id}/tmp/`58 - This ensures clean state before downloading new content59 - If user chooses "Use existing":60 - Skip directly to Step 4 (Analyze Test Failure)61 - Still need to download prowjob.json if it doesn't exist62632. **Create directory structure**64 ```bash65 mkdir -p .work/prow-job-analyze-test-failure/{build_id}/logs66 mkdir -p .work/prow-job-analyze-test-failure/{build_id}/tmp67 ```68 - Use `.work/prow-job-analyze-test-failure/` as the base directory (already in .gitignore)69 - Use build_id as subdirectory name70 - Create `logs/` subdirectory for all downloads71 - Create `tmp/` subdirectory for temporary files (intermediate JSON, etc.)72 - Working directory: `.work/prow-job-analyze-test-failure/{build_id}/`7374### Step 3: Download and Validate prowjob.json7576Use the `fetch-prowjob-json` skill to fetch the prowjob.json for this job. See `plugins/ci/skills/fetch-prowjob-json/SKILL.md` for complete implementation details.77781. **Fetch prowjob.json** using the Prow job URL (convert to gcsweb URL per the `fetch-prowjob-json` skill)792. **Save locally** to `.work/prow-job-analyze-test-failure/{build_id}/logs/prowjob.json`803. **Parse and validate**81 - Search for pattern: `--target=([a-zA-Z0-9-]+)` in the ci-operator args82 - If not found:83 - Display: "This is not a ci-operator job. The prowjob cannot be analyzed by this skill."84 - Explain: ci-operator jobs have a --target argument specifying the test target85 - Exit skill864. **Extract target name and JOB_NAME**87 - Capture the target value (e.g., `e2e-aws-ovn`) from the `--target=` arg88 - Extract `JOB_NAME` from `.spec.job` in prowjob.json (the artifact directory key):89 ```bash90 JOB_NAME=$(jq -r '.spec.job' .work/prow-job-analyze-test-failure/{build_id}/logs/prowjob.json)91 ```92 - Note: on PR jobs `{target}` and `{JOB_NAME}` often differ. All artifact-path lookups93 (JUnit XML, step logs, step artifacts) must use `{JOB_NAME}`, not `{target}`9495### Step 4: Analyze Test Failure9697### Step 4.0: Detect Aggregated Jobs9899Aggregated jobs run the same job in parallel (typically 10 times) and perform statistical100analysis of test results. Detect aggregation by checking for `aggregated-` prefix in the job101name or an `aggregator` container/step in prowjob.json.102103If the job is aggregated, the failure modes are different from normal jobs:1041051. **Statistically significant test failure** — The test itself fails frequently enough across106 runs to be flagged as a regression. This is a real regression that needs investigation.1071082. **Insufficient completed runs** — Not enough runs completed successfully to perform the109 statistical test (e.g., only 5 of 10 jobs produced results). This manifests as mass test110 failures across many unrelated tests. The root cause is whatever prevented the other runs111 from completing — this could be infrastructure issues, install failures, or an actual product112 bug causing crashes. Investigate the underlying job runs that did not complete to determine113 why.1141153. **Non-deterministic test presence** — A test only ran in a small subset of completed jobs,116 even though the other jobs completed successfully. The failure message will say something like117 "Passed 1 times, failed 0 times, skipped 0 times: we require at least 6 attempts to have a118 chance at success". Every test must produce results in every job run; it is a bug if it does119 not. This is a regression — someone introduced a test that doesn't produce results120 deterministically. Investigate which test is non-deterministic and why it only runs in some121 jobs.122123When analyzing an aggregated job failure, first determine which failure mode applies before124diving into individual test analysis. For mode 2, focus on why runs failed rather than125individual test results. For mode 3, investigate why the test only ran in some jobs.126127**Finding underlying job run URLs:**128129The aggregated junit XML contains links to every underlying job run. Download it from:130```131gs://test-platform-results/{bucket-path}/artifacts/release-analysis-aggregator/openshift-release-analysis-aggregator/artifacts/release-analysis-aggregator/{job-name}/{payload-tag}/junit-aggregated.xml132```133134Each `<testcase>` element has a `<system-out>` with YAML-formatted data including `passes:`,135`failures:`, and `skips:` lists. Each entry has:136- `jobrunid`: The build ID of the underlying job run137- `humanurl`: Prow URL for the job run (e.g., `https://prow.ci.openshift.org/view/gs/test-platform-results/logs/{job-name}/{jobrunid}`)138- `gcsartifacturl`: Direct link to GCS artifacts139140Use the `humanurl` links to investigate individual job run failures with the normal141(non-aggregated) analysis steps below.142143### Step 4.1: Download build-log.txt144145```bash146gcloud storage cp gs://test-platform-results/{bucket-path}/build-log.txt .work/prow-job-analyze-test-failure/{build_id}/logs/build-log.txt --no-user-output-enabled147```148149### Step 4.2: Parse and validate150151- Read `.work/prow-job-analyze-test-failure/{build_id}/logs/build-log.txt`152153**Branch on whether {test_name} was provided:**154155#### Branch A: test_name provided156- Search build-log.txt for the exact test name string157- Gather the stack trace and surrounding context for that specific test158- Store the single failing test's error output for use in Steps 4.4 and 4.9159160#### Branch B: test_name NOT provided (multi-step discovery)161162This is the common case for multi-step CI workflows (e.g., HyperShift, bare-metal, OADP jobs)163where failures occur in CI steps rather than named unit tests.1641651. **Discover failed CI steps from JUnit XML**166167 Use `{JOB_NAME}` (from Step 3, extracted from `.spec.job`) for all GCS artifact paths —168 not `{target}`. On PR jobs these values differ and `{target}` will miss the artifacts.169170 Search for JUnit XML files under the artifacts directory:171 ```bash172 gcloud storage ls "gs://test-platform-results/{bucket-path}/artifacts/{JOB_NAME}/**/junit*.xml" 2>/dev/null173 ```174 Download all found JUnit XML files:175 ```bash176 gcloud storage cp "gs://test-platform-results/{bucket-path}/artifacts/{JOB_NAME}/**/junit*.xml" \177 .work/prow-job-analyze-test-failure/{build_id}/logs/ --no-user-output-enabled --recursive 2>/dev/null || true178 ```179 Parse each downloaded XML file and collect every `<testcase>` element where:180 - A `<failure>` or `<error>` child element is present, OR181 - The `<testcase>` has attribute `status="failed"`182183 For each failed testcase record:184 - `step_name`: value of `classname` or `name` attribute (whichever identifies the CI step)185 - `failure_message`: text content of the `<failure>` or `<error>` element186 - `junit_file`: path of the XML file it came from1871882. **Classify each failed step by phase**189190 The ci-operator JUnit XML (`junit_operator.xml`) includes phase-level testcases:191 - `"Run multi-stage test pre phase"` — setup/installation steps192 - `"Run multi-stage test test phase"` — functional test steps193 - `"Run multi-stage test post phase"` — gather/cleanup steps194195 Use the phase entries to classify each failed `step_name`:196197 - Look at the failed **phase-level** testcases (those with `"pre phase"`, `"test phase"`,198 or `"post phase"` in their name) — these tell you which phase failed199 - Cross-reference the individual step testcases against their phase:200 - Steps in the **`pre` phase** → installation/setup steps201 - Steps in the **`test` phase** → functional test steps202 - Steps in the **`post` phase** → gather/cleanup steps (rarely the root cause)203204 If the phase-level testcases are absent from the JUnit XML (some jobs omit them),205 fall back to the `ci-operator-step-graph.json` artifact, which lists all steps with206 their dependencies and timing — use execution order and naming conventions as a207 secondary signal. When still ambiguous, prefer classifying as a **test step** so208 must-gather analysis is not skipped.2092103. **Route each failed step to the appropriate analysis path**211212 - **`pre` phase steps (installation)** → invoke the `ci:analyze-prow-job-install-failure`213 skill for each such step, passing the same Prow job URL. Must-gather is **not**214 attempted for `pre` phase failures: must-gather requires a live apiserver, and when215 the installation phase fails the cluster is not fully up, so collection will fail.216 **Capture the delegated output** and store it217 as a per-step entry in the shared results collection used by Step 5 and Step 5.5:218 ```219 {220 step_name: {step_name},221 type: "installation",222 summary: {one-line summary from ci:analyze-prow-job-install-failure},223 evidence: {key error / stack trace from delegated analysis},224 recommendation: {recommended action from delegated analysis}225 }226 ```227 - **`test` phase steps (functional tests)** → proceed with the iteration in item 4228 below (download step log, artifacts, extract stack trace) and include must-gather229 analysis (Steps 4.4–4.9). Store findings in the same per-step results collection:230 ```231 {232 step_name: {step_name},233 type: "test",234 summary: {one-line failure summary},235 evidence: {stack trace / error output},236 recommendation: {recommended action}237 }238 ```239 - **`post` phase steps (gather/cleanup)** → treat as informational. Download the step240 log and note the failure in the report, but do not perform must-gather analysis for241 post-phase failures alone (they are usually a consequence of earlier failures).2422434. **Iterate over each `test` phase step** (`pre` and `post` phase steps already routed above)244245 For each `step_name` in the **`test` phase**, perform the following sub-steps:246247 a. **Download step-specific log**248 ```bash249 gcloud storage cp \250 "gs://test-platform-results/{bucket-path}/artifacts/{JOB_NAME}/{step_name}/build-log.txt" \251 .work/prow-job-analyze-test-failure/{build_id}/logs/{step_name}-build-log.txt \252 --no-user-output-enabled 2>/dev/null || true253 ```254 If the step log is not found at that path, fall back to scanning build-log.txt for255 lines mentioning `{step_name}` and collect surrounding context (±50 lines).256257 b. **Download step-specific artifacts**258 ```bash259 gcloud storage ls "gs://test-platform-results/{bucket-path}/artifacts/{JOB_NAME}/{step_name}/" \260 2>/dev/null261 ```262 Download any relevant artifacts (e.g., `*.json`, `*.yaml`, `events*.txt`) to263 `.work/prow-job-analyze-test-failure/{build_id}/logs/{step_name}/`.264265 c. **Extract stack trace and context**266 - Scan the step log for panic traces, `FAIL`, `Error:`, `fatal`, or assertion failures267 - Collect the full stack trace block (from the triggering line to the end of the trace)268 - Note the failure message from the JUnit XML as additional context269270 d. **Store per-step findings**271 Record for each step:272 - `step_name`273 - `failure_message` (from JUnit XML)274 - `stack_trace` (from step log or build-log.txt context)275 - `artifacts` (list of downloaded artifact paths)2762773. **Produce a multi-step failure summary**278279 After iterating all failed steps, produce an ordered list of findings:280 ```281 Failed Steps Discovered:282 1. {step_name_1}: {one-line failure summary}283 2. {step_name_2}: {one-line failure summary}284 ...285 ```286 This list is used in Step 4.4 (evidence gathering) and Step 5 (report) to drive287 per-step analysis rather than singular test analysis.288289### Step 4.3: Examine intervals files for cluster activity during E2E failures290291- Search recursively for E2E timeline artifacts (known as "interval files") within the bucket-path:292 ```bash293 gcloud storage ls 'gs://test-platform-results/{bucket-path}/**/e2e-timelines_spyglass_*json'294 ```295- The files can be nested at unpredictable levels below the bucket-path296- There could be as many as two matching files297- Download all matching interval files (use the full paths from the search results):298 ```bash299 gcloud storage cp gs://test-platform-results/{bucket-path}/**/e2e-timelines_spyglass_*.json .work/prow-job-analyze-test-failure/{build_id}/logs/ --no-user-output-enabled300 ```301- If the wildcard copy doesn't work, copy each file individually using the full paths from the search results302- **Scan interval files for test failure timing:**303 - Look for intervals where `source = "E2ETest"` and `message.annotations.status = "Failed"`304 - Note the `from` and `to` timestamps on this interval - this indicates when the test was running305- **Scan interval files for related cluster events:**306 - Look for intervals that overlap the timeframe when the failed test was running307 - Filter for intervals with:308 - `level = "Error"` or `level = "Warning"`309 - `source = "OperatorState"`310 - These events may indicate cluster issues that caused or contributed to the test failure311312### Step 4.3b: Check for Known Symptom Labels313314The CI system may attach **symptom labels** to job runs — machine-detected patterns (e.g., "test failures during high CPU events") stored as JSON artifacts. These are **not root causes** but provide useful environmental context that may help explain failures when no other cause is found.3153161. **List the job_labels directory**317 ```bash318 gcloud storage ls "gs://test-platform-results/{bucket-path}/artifacts/job_labels/" 2>/dev/null319 ```320 - If the directory does not exist or returns an error, skip this step silently3213222. **Download any JSON symptom files** (exclude `label-summary.html`)323 ```bash324 gcloud storage cp "gs://test-platform-results/{bucket-path}/artifacts/job_labels/*.json" \325 .work/prow-job-analyze-test-failure/{build_id}/logs/job_labels/ --no-user-output-enabled 2>/dev/null || true326 ```3273283. **Parse symptom labels** — each JSON file describes a detected symptom with a summary and explanation. Collect all symptom summaries for inclusion in the report.3293304. **Use symptoms as investigative context** — symptoms are environmental observations, NOT definitive causes. They should inform your investigation (e.g., if 2 tests failed while high CPU was measured, CPU pressure could explain the failures if no other cause is found) but you must still perform thorough root cause analysis. Include them in the "Known Symptoms Seen" section of the report.331332### Step 4.4: Gather initial evidence333334- Analyze stack traces from build-log.txt335- Analyze related code in the code repository336- Store artifacts from Prow CI job (json/yaml files) related to the failure under `.work/prow-job-analyze-test-failure/{build_id}/tmp`337- Store logs under `.work/prow-job-analyze-test-failure/{build_id}/logs/`338- Collect evidence from logs and events and other json/yaml files339- **Check for CI step script errors**: If the error is a scripting issue in a CI step (e.g., unbound variable, syntax error, missing command, bad exit code from a shell script) rather than a product bug, check for recent commits to that step's script in the `openshift/release` repository. This is part of evidence gathering — identifying the responsible PR early informs the rest of the analysis. Include the responsible PR in your evidence.340341### Step 4.4b: Investigate crash-looping or failing containers342343**Be tenacious.** When the build log or test output mentions crash-looping pods, container restarts, or deployments not becoming ready, you MUST trace the failure to its root cause. Never stop at "containers are crash-looping" — find out *why*.344345**Pursue all available log sources:**346347- **Must-gather data** (Step 4.6–4.7): Contains pod YAMLs with `containerStatuses` (`exitCode`, `lastState.terminated.reason`, `restartCount`), container logs (current and previous), events, and operator conditions. This is often the richest source for diagnosing crash-looping pods.348- **Gather-extra / gather-audit logs**: Some jobs run additional gather steps that collect extra diagnostics. List the step artifacts directory for gather steps beyond `gather-must-gather` (e.g., `gather-extra`, `gather-audit-logs`) and download relevant logs.349- **Step-level build logs**: The build log for the failing test step (downloaded in Step 4.2) often contains error output, stack traces, and timeout messages that reference specific pods or containers.350- **Events from interval files** (Step 4.3): Operator state transitions and warning events correlated with the failure window.351352**Follow the dependency chain.** Always trace upstream to the originating error rather than stopping at the first symptom.353354### Step 4.5: Check for Must-Gather Availability3553561. **Parse optional flags**357 - Parse user input for `--fast` flag358 - If `--fast` flag present:359 - Skip must-gather detection and analysis entirely360 - Proceed directly to Step 5 — scope is preserved:361 - If `test_name` was provided → produce singular test report (Branch A)362 - If `test_name` was omitted → produce multi-step report (Branch B)363 - Do NOT prompt user about must-gather3643652. **Extract actual test name from prowjob.json**366367 The artifacts directory uses the test name from prowjob.json, NOT the full URL path.368369 ```bash370 # Extract test name from prowjob.json (e.g., "e2e-aws-operator-serial-ote")371 JOB_NAME=$(jq -r '.spec.job' .work/prow-job-analyze-test-failure/{build_id}/logs/prowjob.json)372373 # Note: For PR jobs, TARGET contains the full PR path like:374 # pr-logs/pull/openshift_service-ca-operator/306/pull-ci-openshift-service-ca-operator-main-e2e-aws-operator-serial-ote375 # But artifacts are stored under just the test name:376 # e2e-aws-operator-serial-ote377 ```3783793. **Detect must-gather archive** (only if --fast not present)380381 Use JOB_NAME (not TARGET) for artifact paths.382383 HyperShift jobs may have **different must-gather patterns**:384385 **Pattern 1: Unified Archive** (dump-management-cluster)386 - Single archive with both management and hosted cluster data387 - Used by: hypershift-aws-e2e-external workflow388389 **Pattern 2: Dual Archives** (gather-must-gather + dump)390 - Standard must-gather for management cluster391 - Separate hypershift-dump for additional data (may or may not have hosted cluster)392 - Used by: hypershift-kubevirt-e2e-aws workflow393394 **Pattern 3: Standard Only** (gather-must-gather)395 - Standard OpenShift must-gather only396 - No HyperShift-specific dump397398 **Detection logic**:399 ```bash400 # Check for Pattern 1: Unified archive (dump-management-cluster)401 UNIFIED_DUMP=$(gcloud storage ls "gs://test-platform-results/{bucket-path}/artifacts/$JOB_NAME/dump-management-cluster/artifacts/artifacts.tar*" 2>/dev/null | head -1 || true)402403 # Check for Pattern 2/3: Standard must-gather404 STANDARD_MG=$(gcloud storage ls "gs://test-platform-results/{bucket-path}/artifacts/$JOB_NAME/gather-must-gather/artifacts/must-gather.tar" 2>/dev/null || true)405406 # Check for Pattern 2: Additional hypershift-dump (multiple possible locations)407 # Use wildcards to match all current and future HyperShift dump patterns:408 # 1. **/artifacts/hypershift-dump.tar (covers dump/, hypershift-mce-dump/, etc.)409 # 2. **/artifacts/**/hostedcluster.tar (covers all E2E test patterns)410 HYPERSHIFT_DUMP=""411 for pattern in \412 "**/artifacts/hypershift-dump.tar" \413 "**/artifacts/**/hostedcluster.tar"; do414 FOUND=$(gcloud storage ls "gs://test-platform-results/{bucket-path}/artifacts/$JOB_NAME/$pattern" 2>/dev/null | head -1 || true)415 if [ -n "$FOUND" ]; then416 HYPERSHIFT_DUMP="$FOUND"417 break418 fi419 done420421 # Determine pattern and check for hosted cluster data422 if [ -n "$UNIFIED_DUMP" ]; then423 # Pattern 1: Unified archive424 PATTERN="unified"425426 # Download temporarily to check for hosted cluster data427 TMP_CHECK="/tmp/check-unified-$$.tar"428 gcloud storage cp "$UNIFIED_DUMP" "$TMP_CHECK" --no-user-output-enabled429430 # Check if archive contains hostedcluster-* directory431 HAS_HOSTED_CLUSTER=$(tar -tf "$TMP_CHECK" 2>/dev/null | grep -q "hostedcluster-" && echo "true" || echo "false")432 rm -f "$TMP_CHECK"433434 elif [ -n "$STANDARD_MG" ] && [ -n "$HYPERSHIFT_DUMP" ]; then435 # Pattern 2: Dual archives436 PATTERN="dual"437438 # Download hypershift-dump temporarily to check for hosted cluster439 TMP_CHECK="/tmp/check-dump-$$.tar"440 gcloud storage cp "$HYPERSHIFT_DUMP" "$TMP_CHECK" --no-user-output-enabled441442 # Check if hypershift-dump contains hostedcluster-* directory443 HAS_HOSTED_CLUSTER=$(tar -tf "$TMP_CHECK" 2>/dev/null | grep -q "hostedcluster-" && echo "true" || echo "false")444 rm -f "$TMP_CHECK"445446 elif [ -n "$STANDARD_MG" ]; then447 # Pattern 3: Standard must-gather only448 PATTERN="standard"449 HAS_HOSTED_CLUSTER=false450451 else452 # No must-gather found453 PATTERN="none"454 HAS_HOSTED_CLUSTER=false455 fi456 ```457458 Possible outcomes:459 - **PATTERN="none"**: No must-gather found → Skip to Step 5 (silent, expected for some jobs)460 - **PATTERN="standard"**: Standard OpenShift must-gather only → Extract single cluster461 - **PATTERN="unified" + HAS_HOSTED_CLUSTER=false**: HyperShift unified archive with management only462 - **PATTERN="unified" + HAS_HOSTED_CLUSTER=true**: HyperShift unified archive with both clusters463 - **PATTERN="dual" + HAS_HOSTED_CLUSTER=false**: Two archives but hosted cluster not in hypershift-dump464 - **PATTERN="dual" + HAS_HOSTED_CLUSTER=true**: Two archives with hosted cluster in hypershift-dump465466 **Important Pattern Notes**:467 - **Pattern 1 (unified)**: Management at `logs/artifacts/output/`, hosted at `logs/artifacts/output/hostedcluster-{name}/`468 - **Pattern 2 (dual)**: Management in standard must-gather, hosted MAY be in hypershift-dump.tar or hostedcluster.tar (not guaranteed)469 - Wildcard patterns match all current and future dump locations470 - **Pattern 3 (standard)**: Management cluster only, no HyperShift-specific data4714724. **Ask user if they want must-gather analysis**473 - Only if must-gather(s) were found and --fast not present474 - Use AskUserQuestion tool:475 - Question: "Must-gather data is available. Include cluster diagnostics in the analysis?"476 - Header: "Must-gather"477 - Options:478 - Label: "Yes - Extract and analyze must-gather (Recommended)"479 Description: "Provides cluster-level diagnostics that may reveal root causes (pods, operators, nodes, events). Takes additional time to download and analyze."480 - Label: "No - Skip must-gather (faster)"481 Description: "Only analyze test-level artifacts (build-log, intervals). Faster but may miss cluster-level issues."482 - If user chooses "No", skip to Step 5483484### Step 4.6: Extract Must-Gather (Conditional)485486Only if user chose "Yes" in Step 4.5:4874881. **Determine extraction strategy**489 - If single must-gather detected → extract to `must-gather/logs/`490 - If dual must-gather detected (HyperShift) → extract both:491 - Management cluster → `must-gather-mgmt/logs/`492 - Hosted cluster → `must-gather-hosted/logs/`4934942. **Check for existing extraction**495496 For single must-gather:497 - Check if `.work/prow-job-analyze-test-failure/{build_id}/must-gather/logs/` exists with content498499 For dual must-gather (HyperShift):500 - Check if `.work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs/` exists with content501 - Check if `.work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/logs/` exists with content502503 If either exists:504 - Use AskUserQuestion tool:505 - Question: "Must-gather already extracted for this build. Use existing data?"506 - Header: "Reuse"507 - Options:508 - Label: "Use existing"509 Description: "Reuse previously extracted must-gather data (faster)"510 - Label: "Re-extract"511 Description: "Download and extract fresh must-gather data"512 - If "Re-extract":513 - `rm -rf .work/prow-job-analyze-test-failure/{build_id}/must-gather*/`514 - Continue to step 3 (fresh extraction)515 - If "Use existing":516 - Validate content directories exist and are not empty (see validation in step 4.6)517 - If validation fails, fall back to re-extraction518 - If validation succeeds, skip to Step 4.75195203. **Create must-gather directories**521522 Based on PATTERN from Step 4.5.3:523524 For Pattern 3 (standard only):525 ```bash526 mkdir -p .work/prow-job-analyze-test-failure/{build_id}/must-gather/logs527 mkdir -p .work/prow-job-analyze-test-failure/{build_id}/must-gather/tmp528 ```529530 For Pattern 1 (unified) or Pattern 2 (dual):531 ```bash532 mkdir -p .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs533 mkdir -p .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/tmp534 mkdir -p .work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/logs535 mkdir -p .work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/tmp536 ```5375384. **Download must-gather archives**539540 Use JOB_NAME (from Step 4.5.2) for artifact paths, not {target}:541542 For Pattern 3 (standard only):543 ```bash544 gcloud storage cp "$STANDARD_MG" \545 .work/prow-job-analyze-test-failure/{build_id}/must-gather/tmp/must-gather.tar \546 --no-user-output-enabled547 ```548549 For Pattern 1 (unified):550 ```bash551 # Download unified archive (contains both management and hosted cluster data)552 gcloud storage cp "$UNIFIED_DUMP" \553 .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/tmp/unified-dump.tar \554 --no-user-output-enabled555 ```556557 For Pattern 2 (dual):558 ```bash559 # Download management cluster must-gather560 gcloud storage cp "$STANDARD_MG" \561 .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/tmp/must-gather.tar \562 --no-user-output-enabled563564 # Download hypershift-dump (may or may not contain hosted cluster)565 gcloud storage cp "$HYPERSHIFT_DUMP" \566 .work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/tmp/hypershift-dump.tar \567 --no-user-output-enabled568 ```5695705. **Extract archives**571572 For Pattern 3 (standard only):573 ```bash574 # Use existing extract_archives.py script for standard must-gather575 python3 plugins/ci/skills/prow-job-extract-must-gather/extract_archives.py \576 .work/prow-job-analyze-test-failure/{build_id}/must-gather/tmp/must-gather.tar \577 .work/prow-job-analyze-test-failure/{build_id}/must-gather/logs578 ```579580 For Pattern 1 (unified):581 ```bash582 # Extract unified archive to temporary location583 TMP_EXTRACT=".work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/tmp/extracted"584 mkdir -p "$TMP_EXTRACT"585586 # Handle both .tar and .tar.gz587 if [[ "$UNIFIED_DUMP" == *.tar.gz ]]; then588 tar -xzf .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/tmp/unified-dump.tar -C "$TMP_EXTRACT"589 else590 tar -xf .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/tmp/unified-dump.tar -C "$TMP_EXTRACT"591 fi592593 # Find the output directory (may be at logs/artifacts/output or just output)594 OUTPUT_DIR=$(find "$TMP_EXTRACT" -type d -name "output" | head -1)595596 if [ -z "$OUTPUT_DIR" ]; then597 echo "ERROR: Could not find output directory in unified dump"598 rm -rf "$TMP_EXTRACT"599 # Clear variables to prevent subsequent usage600 HAS_HOSTED_CLUSTER="false"601 unset HOSTED_DIR602 unset OUTPUT_DIR603 # Skip to Step 5 - no must-gather analysis possible604 else605 # Move management cluster data (root level in output/)606 # Exclude hostedcluster-* directories607 for item in "$OUTPUT_DIR"/*; do608 if [ -e "$item" ] && [[ ! "$(basename "$item")" =~ ^hostedcluster- ]]; then609 mv "$item" .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs/610 fi611 done612613 # Move hosted cluster data (hostedcluster-* subdirectory)614 if [ "$HAS_HOSTED_CLUSTER" = "true" ]; then615 HOSTED_DIR=$(find "$OUTPUT_DIR" -maxdepth 1 -type d -name "hostedcluster-*" | head -1)616 if [ -n "$HOSTED_DIR" ]; then617 mv "$HOSTED_DIR"/* .work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/logs/618 echo "✓ Hosted cluster data extracted from unified archive"619 else620 echo "WARNING: Expected hosted cluster data but hostedcluster-* directory not found"621 fi622 fi623624 # Cleanup temporary extraction directory625 rm -rf "$TMP_EXTRACT"626 fi627 ```628629 For Pattern 2 (dual):630 ```bash631 # Extract management cluster must-gather (standard format)632 python3 plugins/prow-job/skills/prow-job-extract-must-gather/extract_archives.py \633 .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/tmp/must-gather.tar \634 .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs635636 # Extract hypershift-dump (may contain hosted cluster)637 TMP_EXTRACT=".work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/tmp/extracted"638 mkdir -p "$TMP_EXTRACT"639 tar -xf .work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/tmp/hypershift-dump.tar -C "$TMP_EXTRACT"640641 if [ "$HAS_HOSTED_CLUSTER" = "true" ]; then642 # Look for hostedcluster-* directory in dump643 HOSTED_DIR=$(find "$TMP_EXTRACT" -maxdepth 2 -type d -name "hostedcluster-*" | head -1)644 if [ -n "$HOSTED_DIR" ]; then645 mv "$HOSTED_DIR"/* .work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/logs/646 echo "✓ Hosted cluster data extracted from hypershift-dump"647 else648 echo "WARNING: HAS_HOSTED_CLUSTER=true but no hostedcluster-* directory found in dump"649 echo "Hypershift-dump likely contains only management cluster data"650 HAS_HOSTED_CLUSTER=false # Update flag since hosted cluster not actually present651 fi652 else653 echo "INFO: Hypershift-dump does not contain hosted cluster data (management cluster only)"654 fi655656 # Cleanup temporary extraction directory657 rm -rf "$TMP_EXTRACT"658 ```6596606. **Locate and validate content directories**661662 For Pattern 3 (standard only):663 ```bash664 # Check for content/ directory first (renamed by extraction script)665 if [ -d ".work/prow-job-analyze-test-failure/{build_id}/must-gather/logs/content" ]; then666 MUST_GATHER_PATH=".work/prow-job-analyze-test-failure/{build_id}/must-gather/logs/content"667 else668 # Fall back to finding the directory containing -ci- (e.g., registry-build09-ci-...)669 MUST_GATHER_PATH=$(find .work/prow-job-analyze-test-failure/{build_id}/must-gather/logs -maxdepth 1 -type d -name "*-ci-*" | head -1)670 fi671672 # Validate MUST_GATHER_PATH is set and directory exists673 if [ -z "$MUST_GATHER_PATH" ] || [ ! -d "$MUST_GATHER_PATH" ]; then674 echo "ERROR: Must-gather content directory not found after extraction"675 # Skip to Step 5 (scope preserved: Branch A or Branch B per test_name presence)676 elif [ -z "$(ls -A "$MUST_GATHER_PATH" 2>/dev/null)" ]; then677 echo "ERROR: Must-gather content directory is empty"678 # Skip to Step 5 (scope preserved: Branch A or Branch B per test_name presence)679 else680 echo "✓ Must-gather content located at: $MUST_GATHER_PATH"681 # Continue to Step 4.7 with MUST_GATHER_PATH set682 fi683 ```684685 For Pattern 1 (unified) or Pattern 2 (dual):686 ```bash687 # Management cluster validation688 if [ "$PATTERN" = "unified" ]; then689 # Pattern 1: Data extracted directly to logs/ directory690 MUST_GATHER_MGMT_PATH=".work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs"691 else692 # Pattern 2: Standard must-gather extraction (look for content/ or hash directory)693 if [ -d ".work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs/content" ]; then694 MUST_GATHER_MGMT_PATH=".work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs/content"695 else696 MUST_GATHER_MGMT_PATH=$(find .work/prow-job-analyze-test-failure/{build_id}/must-gather-mgmt/logs -maxdepth 1 -type d -name "*-ci-*" | head -1)697 fi698 fi699700 # Validate management cluster path701 if [ -z "$MUST_GATHER_MGMT_PATH" ] || [ ! -d "$MUST_GATHER_MGMT_PATH" ]; then702 echo "ERROR: Management cluster directory not found"703 # Skip to Step 5 (scope preserved: Branch A or Branch B per test_name presence)704 elif [ -z "$(ls -A "$MUST_GATHER_MGMT_PATH" 2>/dev/null)" ]; then705 echo "ERROR: Management cluster directory is empty"706 # Skip to Step 5 (scope preserved: Branch A or Branch B per test_name presence)707 else708 echo "✓ Management cluster data located at: $MUST_GATHER_MGMT_PATH"709 fi710711 # Hosted cluster validation - only if HAS_HOSTED_CLUSTER is true712 if [ "$HAS_HOSTED_CLUSTER" = "true" ]; then713 MUST_GATHER_HOSTED_PATH=".work/prow-job-analyze-test-failure/{build_id}/must-gather-hosted/logs"714715 # Validate hosted cluster path716 if [ ! -d "$MUST_GATHER_HOSTED_PATH" ]; then717 echo "WARNING: Hosted cluster directory not found (expected based on archive detection)"718 MUST_GATHER_HOSTED_PATH="" # Clear the path719 elif [ -z "$(ls -A "$MUST_GATHER_HOSTED_PATH" 2>/dev/null)" ]; then720 echo "WARNING: Hosted cluster directory is empty"721 MUST_GATHER_HOSTED_PATH="" # Clear the path722 else723 echo "✓ Hosted cluster data located at: $MUST_GATHER_HOSTED_PATH"724 fi725 else726 echo "✓ Management cluster must-gather located at: $MUST_GATHER_MGMT_PATH"727 fi728729 # Only validate hosted cluster if HAS_HOSTED_CLUSTER is true730 if [ "$HAS_HOSTED_CLUSTER" = "true" ]; then731 if [ -z "$MUST_GATHER_HOSTED_PATH" ] || [ ! -d "$MUST_GATHER_HOSTED_PATH" ]; then732 echo "ERROR: Hosted cluster must-gather content directory not found"733 elif [ -z "$(ls -A "$MUST_GATHER_HOSTED_PATH" 2>/dev/null)" ]; then734 echo "ERROR: Hosted cluster must-gather content directory is empty"735 else736 echo "✓ Hosted cluster must-gather located at: $MUST_GATHER_HOSTED_PATH"737 echo "✓ Hosted cluster namespace: $HOSTED_NAMESPACE"738 fi739 fi740 ```741742### Step 4.7: Analyze Must-Gather (Conditional)743744Only if Step 4.6 completed successfully:7457461. **Locate must-gather-analyzer scripts**747748 The must-gather plugin provides analysis scripts. Locate the scripts directory:749750 ```bash751 # Try to find the must-gather-analyzer scripts in common locations752 for SEARCH_PATH in \753 "plugins/must-gather/skills/must-gather-analyzer/scripts" \754 "~/.claude/plugins/cache/*/plugins/must-gather/skills/must-gather-analyzer/scripts" \755 "$(find ~ -type d -path "*/must-gather/skills/must-gather-analyzer/scripts" 2>/dev/null | head -1)"; do756 SCRIPTS_DIR=$(eval echo "$SEARCH_PATH")757 if [ -d "$SCRIPTS_DIR" ] && [ -f "$SCRIPTS_DIR/analyze_clusteroperators.py" ]; then758 break759 fi760 SCRIPTS_DIR=""761 done762763 if [ -z "$SCRIPTS_DIR" ]; then764 echo "WARNING: Must-gather analysis scripts not found."765 echo "Install the must-gather plugin: /plugin install must-gather@ai-helpers"766 # Continue to Step 5 without cluster analysis767 fi768 ```7697702. **Run targeted cluster diagnostics**771772 Focus on issues relevant to test failures (not full cluster analysis).773774 **For single must-gather (standard OpenShift):**775776 ```bash777 # Core diagnostics (only if scripts and path are available)778 if [ -n "$SCRIPTS_DIR" ] && [ -n "$MUST_GATHER_PATH" ]; then779 python3 "$SCRIPTS_DIR/analyze_clusteroperators.py" "$MUST_GATHER_PATH"780 python3 "$SCRIPTS_DIR/analyze_pods.py" "$MUST_GATHER_PATH" --problems-only781 python3 "$SCRIPTS_DIR/analyze_nodes.py" "$MUST_GATHER_PATH" --problems-only782 python3 "$SCRIPTS_DIR/analyze_events.py" "$MUST_GATHER_PATH" --type Warning --count 50783 else784 echo "WARNING: Skipping must-gather analysis (scripts or path not available)"785 fi786 ```787788 **For dual must-gather (HyperShift):**789790 ```bash791 # Management cluster diagnostics (only if scripts and path are available)792 if [ -n "$SCRIPTS_DIR" ] && [ -n "$MUST_GATHER_MGMT_PATH" ]; then793 echo "=== Analyzing Management Cluster ==="794 python3 "$SCRIPTS_DIR/analyze_clusteroperators.py" "$MUST_GATHER_MGMT_PATH"795 python3 "$SCRIPTS_DIR/analyze_pods.py" "$MUST_GATHER_MGMT_PATH" --problems-only796 python3 "$SCRIPTS_DIR/analyze_nodes.py" "$MUST_GATHER_MGMT_PATH" --problems-only797 python3 "$SCRIPTS_DIR/analyze_events.py" "$MUST_GATHER_MGMT_PATH" --type Warning --count 50798 else799 echo "WARNING: Skipping management cluster analysis (scripts or path not available)"800 fi801802 # Hosted cluster diagnostics (only if scripts and path are available)803 if [ -n "$SCRIPTS_DIR" ] && [ -n "$MUST_GATHER_HOSTED_PATH" ]; then804 echo "=== Analyzing Hosted Cluster (Namespace: $HOSTED_NAMESPACE) ==="805 python3 "$SCRIPTS_DIR/analyze_clusteroperators.py" "$MUST_GATHER_HOSTED_PATH"806 python3 "$SCRIPTS_DIR/analyze_pods.py" "$MUST_GATHER_HOSTED_PATH" --problems-only807 python3 "$SCRIPTS_DIR/analyze_nodes.py" "$MUST_GATHER_HOSTED_PATH" --problems-only808 python3 "$SCRIPTS_DIR/analyze_events.py" "$MUST_GATHER_HOSTED_PATH" --type Warning --count 50809 else810 echo "INFO: Skipping hosted cluster analysis (scripts or path not available)"811 fi812 ```8138143. **Run conditional diagnostics based on test context**815816 ```bash817 # Network diagnostics (if test name suggests network issues)818 if [[ "$JOB_NAME" =~ network|ovn|sdn|connectivity|route|ingress|egress ]]; then819 if [ -n "$MUST_GATHER_PATH" ]; then820 python3 "$SCRIPTS_DIR/analyze_network.py" "$MUST_GATHER_PATH"821 fi822 if [ -n "$MUST_GATHER_MGMT_PATH" ]; then823824825…(truncated)