plugins/ci/skills/detect-permafail/SKILL.md
Analyze consecutive job failures to determine if they represent a permafail pattern versus flaky failures
npx skillsauth add openshift-eng/ai-helpers detect-permafailInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill when you have 2-10 consecutive failures of the same job and need to determine if the failures represent a systematic/permanent failure (permafail) versus a flaky failure. This is critical for CI/CD pipeline analysis to distinguish between:
Bash tool for running plugins/ci/scripts/classify-job-failures.pyrequests package available for artifact fetchingfetch-prowjob-json and prow-job-artifact-search skills)Standard Mode (URL-based):
Verify that all required inputs are present with expected types and constraints:
failure_urls: Array of 2-10 strings matching Prow job URL pattern https://prow.ci.openshift.org/view/gs/<bucket>/<path>/<job-name>/<build-id> where path may be logs/, pr-logs/pull/, or other GCS paths (must be consecutive runs, ordered newest to oldest)job_name: Non-empty string identifier of the job being analyzedpr_info: Object containing PR number (integer) and repository context (string)Reject requests if:
Notes:
Offline/Eval Mode (Pre-normalized Signatures):
For testing or offline analysis, the skill can accept pre-normalized failure signatures directly instead of fetching from URLs. This mode skips Steps 2-4 (artifact fetching and classification) and proceeds directly to Step 5 (threshold analysis).
Input format:
signatures: Array of 2-10 pre-normalized signature objects (see Step 4 output format){type: "test_failure", url: "...", tests: [...], test_count: N} or infra_failure format {type: "infra_failure", url: "...", error: "...", error_hash: "..."}When signatures are provided, skip to Step 5 and apply threshold logic directly. Use this mode only for evals or when signatures have been extracted by another tool.
For standard URL-based analysis, call the deterministic classifier script to fetch artifacts and produce normalized signatures:
"$SKILL_DIR/../../scripts/classify-job-failures.py" --json-input '{
"failure_urls": ["https://prow.ci.openshift.org/view/gs/..."],
"job_name": "pull-ci-openshift-origin-master-e2e-aws",
"pr_info": {"pr_number": 12345, "repository": "openshift/origin"}
}'
The script performs the artifact-based work that does not require AI judgment:
prowjob.json from gcsweb.test_failure or infra_failure based on artifacts, not error text alone.If the script exits non-zero, report its JSON error and do not continue to threshold analysis unless at least two valid signatures are available from another trusted source.
The classifier follows these rules:
openshift-e2e-test/, e2e-*, junit/, junit XML files, openshift-tests-*, or monitor-test-*.Use the script output directly as the normalized signature array.
For test failures:
{
"type": "test_failure",
"url": "job_url",
"tests": ["failing_test_name1", "failing_test_name2"],
"test_count": 2
}
For infrastructure failures:
{
"type": "infra_failure",
"url": "job_url",
"error": "normalized_error_message",
"error_hash": "md5_of_error_message"
}
Apply permafail detection logic using failure-type-based thresholds - the denominator is the count of comparable runs (same failure type), not the matching group size:
Detection Thresholds (based on comparable run count):
For Test Failures:
Example: 8 test_failure signatures, "TestNetworkPolicy" appears in 6 → 6/8 = 75% → needs ceil(8×0.7)=6 → 6≥6 ✓ PERMAFAIL
For Infrastructure Failures:
similarity = 1 - (levenshtein_distance(a, b) / max(len(a), len(b))). Treat two normalized errors as similar when similarity > 0.70. If both strings are empty, do not group them as similar; require a non-empty normalized error.)Example: 5 infra_failure signatures - 3 have "operator X timeout", 2 have random errors → 3/5 = 60% → needs 4/5 (80%) → 3<4 → NOT PERMAFAIL
For Mixed Failure Types:
Example Scenario 1: PERMAFAIL (4 test, 3 infra):
Analysis:
Example Scenario 2: NOT PERMAFAIL (1 test, 6 diverse infra):
Analysis:
Construct the final response object with:
permafail (true/false)7/10, 4/4, 5/6, or 2/10. Do not write only 7 of 10; include 7/10."high", "medium", or "0.95". For example, a clearly non-permafail 2/10 pattern can still have confidence around 0.70 because the verdict is well supported.failure_type value:
test_failure when the final verdict is driven by test-failure analysis, or only test failures are presentinfra_failure when the final verdict is driven by infrastructure-failure analysis, or only infrastructure failures are presentmixed when both test and infrastructure failures are present and no single type meets the permafail thresholdmatch_ratio: Slash-format string for the dominant pattern, such as "7/10", "4/4", "5/6", or "2/10".threshold_required: Integer count required for the comparable-run group to qualify as permafail.matching_runs: Integer numerator for the dominant or strongest pattern.comparable_runs: Integer denominator for the comparable-run group.The final JSON must be machine-checkable. The ratio-bearing fields must be top-level fields, not nested under an analysis object. Use this exact style:
{
"permafail": true,
"confidence": 0.95,
"reason": "7/10 test_failure runs failed TestNetworkPolicy, meeting the required 7/10 threshold.",
"failure_type": "test_failure",
"match_ratio": "7/10",
"matching_runs": 7,
"comparable_runs": 10,
"threshold_required": 7,
"signatures": []
}
For a mixed non-permafail, use failure_type: "mixed" and include the word insufficient in the reason when one failure type has fewer than 2 comparable runs.
Do not set confidence equal to the match percentage. Confidence means confidence in the verdict:
2/10, should use confidence: 0.70 or higher.7/10 test failures or 5/6 infra failures, should use confidence: 0.85 or higher.4/4, should use confidence: 0.99.The skill returns a JSON object with this schema:
{
"permafail": true,
"confidence": 0.95,
"reason": "3/3 test_failure runs show the same failing test 'test_node_scale' - consistent permanent failure",
"failure_type": "test_failure",
"match_ratio": "3/3",
"matching_runs": 3,
"comparable_runs": 3,
"threshold_required": 3,
"signatures": [
{
"type": "test_failure",
"url": "https://prow.ci.openshift.org/view/gs/bucket/logs/...",
"tests": ["test_node_scale"],
"test_count": 1
},
{
"type": "test_failure",
"url": "https://prow.ci.openshift.org/view/gs/bucket/logs/...",
"tests": ["test_node_scale"],
"test_count": 1
},
{
"type": "test_failure",
"url": "https://prow.ci.openshift.org/view/gs/bucket/logs/...",
"tests": ["test_node_scale"],
"test_count": 1
}
],
"common_tests": ["test_node_scale"]
}
{
"permafail": true,
"confidence": 0.92,
"reason": "All 3 runs fail at cluster creation with identical error: 'Insufficient quota for machine type n1-standard-4'",
"failure_type": "infra_failure",
"signatures": [
{
"type": "infra_failure",
"url": "https://prow.ci.openshift.org/view/gs/bucket/logs/...",
"error": "Insufficient quota for machine type n1-standard-4",
"error_hash": "a1b2c3d4e5f6g7h8"
},
{
"type": "infra_failure",
"url": "https://prow.ci.openshift.org/view/gs/bucket/logs/...",
"error": "Insufficient quota for machine type n1-standard-4",
"error_hash": "a1b2c3d4e5f6g7h8"
},
{
"type": "infra_failure",
"url": "https://prow.ci.openshift.org/view/gs/bucket/logs/...",
"error": "Insufficient quota for machine type n1-standard-4",
"error_hash": "a1b2c3d4e5f6g7h8"
}
]
}
{
"permafail": false,
"confidence": 0.88,
"reason": "Mixed failure types detected: 2/3 runs are test_failure, 1/3 is infra_failure. Inconsistent pattern indicates flaky behavior, not a systematic permafail. Test failures: 2/3 (threshold: 2/3). Infra failures: 1/3 (threshold: 1/3).",
"failure_type": "mixed",
"match_ratio": "2/3",
"matching_runs": 2,
"comparable_runs": 3,
"threshold_required": 2,
"signatures": [
{
"type": "test_failure",
"url": "...",
"tests": ["test_networking"],
"test_count": 1
},
{
"type": "infra_failure",
"url": "...",
"error": "Pod evicted due to memory pressure",
"error_hash": "x1y2z3a4b5c6d7e8"
},
{
"type": "test_failure",
"url": "...",
"tests": ["test_storage", "test_deployment"],
"test_count": 2
}
]
}
{
"type": "test_failure",
"url": "string (job URL)",
"tests": ["array", "of", "failing_test_names"],
"test_count": "integer (length of tests array)"
}
Fields:
type: Always "test_failure"url: The Prow job URL for this runtests: Array of test names extracted from failure logs (deduplicated)test_count: Count of unique failing tests{
"type": "infra_failure",
"url": "string (job URL)",
"error": "string (normalized error message)",
"error_hash": "string (MD5 hash of normalized error)"
}
Fields:
type: Always "infra_failure"url: The Prow job URL for this runerror: Normalized error message with timestamps and build IDs removederror_hash: MD5 hash for fast similarity comparisonApply the threshold rules from Step 5 based on the number of test_failure signatures (the comparable run count):
reason, include the strongest test ratio in slash form, for example 7/10 test_failure runs failed TestNetworkPolicy or 2/10 test_failure runs failed TestNetworkPolicy.match_ratio, matching_runs, comparable_runs, and threshold_required fields for the strongest test pattern.Apply the threshold rules from Step 5 based on the number of infra_failure signatures (the comparable run count):
reason, include the strongest infra ratio in slash form, for example 5/6 infra_failure runs share operator authentication timeout or 1/6 infra_failure runs share the strongest error.match_ratio, matching_runs, comparable_runs, and threshold_required fields for the strongest infra pattern.When both test_failure and infra_failure types are present:
failure_type to the type that triggered the permafail: test_failure or infra_failurefailure_type: "test_failure", match_ratio: "4/4", matching_runs: 4, comparable_runs: 4, and the test threshold.failure_type to mixed, not flakyreason, such as 1/1 test_failure runs and 1/6 infra_failure runs.Key Principle: Infrastructure failures (cluster setup, resource quota, network issues) are orthogonal to test failures. A PR can have a systematic test failure (permafail) even if some runs fail during infrastructure setup. Analyze each type separately and detect permafails in either category.
If artifact fetching fails for a job URL:
Response:
{
"status": "error",
"error": "Failed to fetch artifacts for job: 404 Not Found at gcsweb URL",
"action": "verify_job_url_validity"
}
If the classifier script times out or returns an incomplete result:
Response:
{
"status": "error",
"error": "Analysis timeout: Only 2 of 3 jobs analyzed successfully. Insufficient data for permafail determination.",
"completed_jobs": 2,
"action": "retry_with_single_job"
}
If URL validation fails:
Response:
{
"status": "error",
"error": "Invalid job URL format: 'url3' is not a valid Prow job URL",
"invalid_url": "url3",
"action": "provide_valid_urls"
}
If the job_name parameter doesn't match the actual job names extracted from URLs:
Response:
{
"status": "error",
"error": "Job name mismatch. Expected 'pull-ci-job-xyz' but found 'pull-ci-job-abc' in run 2",
"expected_job": "pull-ci-job-xyz",
"actual_job": "pull-ci-job-abc",
"action": "provide_matching_job_urls"
}
If analysis completes but there are insufficient comparable runs for permafail determination (e.g., 1 test_failure + 1 infra_failure):
Response:
{
"permafail": false,
"confidence": 0.70,
"reason": "Insufficient comparable runs: 1/2 test failures, 1/2 infra failures. Cannot establish a permafail pattern with only one run of each type. Need at least 2 runs of the same failure type to determine if failures are systematic. Test: 1/2 (threshold: 2/2). Infra: 1/2 (threshold: 2/2).",
"failure_type": "mixed",
"match_ratio": "1/2",
"matching_runs": 1,
"comparable_runs": 2,
"threshold_required": 2,
"signatures": [
{
"type": "test_failure",
"url": "...",
"tests": ["[sig-network] test"]
},
{
"type": "infra_failure",
"url": "...",
"error": "cluster creation failed"
}
]
}
If the classifier output doesn't contain expected failure information:
Response:
{
"status": "incomplete",
"warning": "Run 1 analysis incomplete: could not extract failure details",
"completed_jobs": 2,
"incomplete_jobs": 1,
"permafail": "unknown",
"recommendation": "Review job logs manually or retry analysis"
}
Input:
{
"failure_urls": [
"https://prow.ci.openshift.org/view/gs/..../logs/pull-ci-openshift-origin-master-e2e-aws/1234567",
"https://prow.ci.openshift.org/view/gs/..../logs/pull-ci-openshift-origin-master-e2e-aws/1234568",
"https://prow.ci.openshift.org/view/gs/..../logs/pull-ci-openshift-origin-master-e2e-aws/1234569"
],
"job_name": "pull-ci-openshift-origin-master-e2e-aws",
"pr_info": {
"pr_number": 12345,
"repository": "openshift/origin"
}
}
Subagent analysis results for all 3 runs:
Output:
{
"permafail": true,
"confidence": 0.99,
"reason": "3/3 consecutive runs fail with identical test: '[sig-api] API discovery should provide capability information'. This is a systematic permanent failure.",
"failure_type": "test_failure",
"match_ratio": "3/3",
"matching_runs": 3,
"comparable_runs": 3,
"threshold_required": 3,
"signatures": [
{
"type": "test_failure",
"url": "https://prow.ci.openshift.org/view/gs/..../logs/pull-ci-openshift-origin-master-e2e-aws/1234567",
"tests": ["[sig-api] API discovery should provide capability information"],
"test_count": 1
},
{
"type": "test_failure",
"url": "https://prow.ci.openshift.org/view/gs/..../logs/pull-ci-openshift-origin-master-e2e-aws/1234568",
"tests": ["[sig-api] API discovery should provide capability information"],
"test_count": 1
},
{
"type": "test_failure",
"url": "https://prow.ci.openshift.org/view/gs/..../logs/pull-ci-openshift-origin-master-e2e-aws/1234569",
"tests": ["[sig-api] API discovery should provide capability information"],
"test_count": 1
}
],
"common_tests": ["[sig-api] API discovery should provide capability information"]
}
Input:
{
"failure_urls": [
"https://prow.ci.openshift.org/view/gs/..../logs/periodic-ci-openshift-release-main-ci-4.19-e2e-aws-upgrade-ovn-single-node/1111",
"https://prow.ci.openshift.org/view/gs/..../logs/periodic-ci-openshift-release-main-ci-4.19-e2e-aws-upgrade-ovn-single-node/1112",
"https://prow.ci.openshift.org/view/gs/..../logs/periodic-ci-openshift-release-main-ci-4.19-e2e-aws-upgrade-ovn-single-node/1113",
"https://prow.ci.openshift.org/view/gs/..../logs/periodic-ci-openshift-release-main-ci-4.19-e2e-aws-upgrade-ovn-single-node/1114",
"https://prow.ci.openshift.org/view/gs/..../logs/periodic-ci-openshift-release-main-ci-4.19-e2e-aws-upgrade-ovn-single-node/1115",
"https://prow.ci.openshift.org/view/gs/..../logs/periodic-ci-openshift-release-main-ci-4.19-e2e-aws-upgrade-ovn-single-node/1116",
"https://prow.ci.openshift.org/view/gs/..../logs/periodic-ci-openshift-release-main-ci-4.19-e2e-aws-upgrade-ovn-single-node/1117"
],
"job_name": "periodic-ci-openshift-release-main-ci-4.19-e2e-aws-upgrade-ovn-single-node",
"pr_info": {
"pr_number": 3186,
"repository": "openshift/ovn-kubernetes"
}
}
Subagent analysis results:
Analysis:
Output:
{
"permafail": true,
"confidence": 0.99,
"reason": "All 4 runs that reached e2e tests failed on '[sig-network] Networking should provide connectivity' (100% match). 3 additional runs failed during infrastructure setup (cluster creation, AWS quota) and are not relevant to this test failure pattern. This is a systematic test failure caused by the PR changes.",
"failure_type": "test_failure",
"signatures": [
{
"type": "infra_failure",
"url": "https://prow.ci.openshift.org/view/gs/..../logs/periodic-ci-openshift-release-main-ci-4.19-e2e-aws-upgrade-ovn-single-node/1111",
"error": "Cluster creation timeout",
"error_hash": "a1b2c3d4"
},
{
"type": "test_failure",
"url": "https://prow.ci.openshift.org/view/gs/..../logs/periodic-ci-openshift-release-main-ci-4.19-e2e-aws-upgrade-ovn-single-node/1112",
"tests": ["[sig-network] Networking should provide connectivity"],
"test_count": 1
},
{
"type": "infra_failure",
"url": "https://prow.ci.openshift.org/view/gs/..../logs/periodic-ci-openshift-release-main-ci-4.19-e2e-aws-upgrade-ovn-single-node/1113",
"error": "AWS quota exceeded",
"error_hash": "e5f6g7h8"
},
{
"type": "test_failure",
"url": "https://prow.ci.openshift.org/view/gs/..../logs/periodic-ci-openshift-release-main-ci-4.19-e2e-aws-upgrade-ovn-single-node/1114",
"tests": ["[sig-network] Networking should provide connectivity"],
"test_count": 1
},
{
"type": "test_failure",
"url": "https://prow.ci.openshift.org/view/gs/..../logs/periodic-ci-openshift-release-main-ci-4.19-e2e-aws-upgrade-ovn-single-node/1115",
"tests": ["[sig-network] Networking should provide connectivity"],
"test_count": 1
},
{
"type": "infra_failure",
"url": "https://prow.ci.openshift.org/view/gs/..../logs/periodic-ci-openshift-release-main-ci-4.19-e2e-aws-upgrade-ovn-single-node/1116",
"error": "Cluster creation timeout",
"error_hash": "a1b2c3d4"
},
{
"type": "test_failure",
"url": "https://prow.ci.openshift.org/view/gs/..../logs/periodic-ci-openshift-release-main-ci-4.19-e2e-aws-upgrade-ovn-single-node/1117",
"tests": ["[sig-network] Networking should provide connectivity"],
"test_count": 1
}
],
"common_tests": ["[sig-network] Networking should provide connectivity"]
}
Input: 5 runs where 3 have identical operator installation failure, 2 have random infra issues
Subagent analysis results:
Analysis:
Output:
{
"permafail": false,
"confidence": 0.70,
"reason": "Infrastructure failures are not consistent enough. 3/5 runs failed with 'operator authentication timeout waiting for operator to reach Available=True' (60% match), but with 5 infra failures at least 4/5 must match (80% required). The presence of 2 different random errors indicates flaky infrastructure rather than systematic failure.",
"failure_type": "infra_failure",
"match_ratio": "3/5",
"matching_runs": 3,
"comparable_runs": 5,
"threshold_required": 4,
"signatures": [
{
"type": "infra_failure",
"url": "...",
"error": "operator authentication timeout waiting for operator to reach Available=True",
"error_hash": "a1b2c3d4"
},
{
"type": "infra_failure",
"url": "...",
"error": "AWS quota exceeded for instance type m5.xlarge",
"error_hash": "e5f6g7h8"
},
{
"type": "infra_failure",
"url": "...",
"error": "operator authentication timeout waiting for operator to reach Available=True",
"error_hash": "a1b2c3d4"
},
{
"type": "infra_failure",
"url": "...",
"error": "operator authentication timeout waiting for operator to reach Available=True",
"error_hash": "a1b2c3d4"
},
{
"type": "infra_failure",
"url": "...",
"error": "Pod evicted due to memory pressure on node ip-10-0-1-2",
"error_hash": "i9j0k1l2"
}
]
}
Input: 3 runs with different test failures
Subagent analysis results:
Output:
{
"permafail": false,
"confidence": 0.70,
"reason": "No consistent failure pattern detected. 0/3 runs show matching tests - each of the 3 runs failed with different tests: networking, storage, API discovery. This indicates flaky/non-deterministic behavior rather than a systematic permafail.",
"failure_type": "test_failure",
"match_ratio": "0/3",
"matching_runs": 0,
"comparable_runs": 3,
"threshold_required": 3,
"signatures": [
{
"type": "test_failure",
"url": "...",
"tests": ["[sig-network] networking should support networking"],
"test_count": 1
},
{
"type": "test_failure",
"url": "...",
"tests": ["[sig-storage] storage should support volumes"],
"test_count": 1
},
{
"type": "test_failure",
"url": "...",
"tests": ["[sig-api] API discovery should work"],
"test_count": 1
}
]
}
Table-driven test cases demonstrating correct threshold behavior:
| Total URLs | Test Failures | Infra Failures | Matching Test Count | Verdict | Reason | |------------|---------------|----------------|---------------------|---------|---------| | 10 | 10 | 0 | 2 same test | NOT permafail | 2/10 = 20% < 70% threshold (need ≥7) | | 10 | 10 | 0 | 7 same test | PERMAFAIL | 7/10 = 70% ≥ 70% threshold ✓ | | 7 | 4 | 3 | 4 same test (in test bucket) | PERMAFAIL | 4/4 = 100% ≥ 80% threshold (4-5 runs need ≥4) ✓ | | 7 | 1 | 6 | 1 test, all 6 infra diverse | NOT permafail | Only 1 test_failure (need ≥2), and infra errors are all different | | 7 | 1 | 6 | 1 test, 5 same infra error | PERMAFAIL | 5/6 = 83% ≥ ceil(6×0.7)=5 (70% threshold) → infra bucket meets threshold ✓ |
This skill analyzes Prow job artifacts directly using techniques from existing CI skills:
plugins/ci/scripts/classify-job-failures.py to fetch prowjob.json and browse artifactsThis approach follows patterns from fetch-prowjob-json, prow-job-artifact-search, and prow-job-analyze-test-failure skills.
The classifier script performs deterministic artifact analysis before AI threshold reasoning:
failure_urls ━━━ classify-job-failures.py ━━━ normalized signatures ━━━ Step 5 threshold analysis
Benefits:
Synchronization:
Normalize infrastructure error messages for comparison:
2025-05-12T14:32:10Z → ""build-12345-xyz → ""pod-abc123xyz → "pod-*"Example normalization:
Input: "Pod evicted at 2025-05-12T14:32:10Z (build-12345): insufficient memory (512M < 1Gi required)"
Output: "Pod evicted: insufficient memory (* < *Gi required)"
Confidence reflects how certain the permafail verdict is:
Use confidence to determine remediation priority:
tools
Analyze a JIRA issue and create a pull request to solve it. Use when the user wants to implement a fix or feature described in a Jira issue, push a branch, and open a draft PR.
development
Use when a deeper level of code review is requested. Multi-agent panel code review with specialist reviewers and forced runtime reproducers for all BLOCKING bug findings. Optionally posts to GitHub/GitLab as a PENDING review.
development
Review agentic documentation — verify claims locally against source code first, then use chai-bot for cross-repo and cross-functional verification
development
Use this skill when debugging a failed Prow CI job.