fix: feat: per-workflow/per-step CI diagnostics in agent fix prompts (implements #1050) (#1051) (#1052)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful

Closes #1051. Implements the fix sketched in #1050.
This commit is contained in:
disinto-admin 2026-04-19 19:08:16 +00:00
commit e9aed747b5
2 changed files with 117 additions and 10 deletions

View file

@ -247,6 +247,31 @@ ci_promote() {
echo "$new_num" echo "$new_num"
} }
# ci_get_step_logs <pipeline_num> <step_id>
# Fetches logs for a single CI step via the Woodpecker API.
# Requires: WOODPECKER_REPO_ID, woodpecker_api() (from env.sh)
# Returns: 0 on success, 1 on failure. Outputs log text to stdout.
#
# Usage:
# ci_get_step_logs 1423 5 # Get logs for step ID 5 in pipeline 1423
ci_get_step_logs() {
local pipeline_num="$1" step_id="$2"
if [ -z "$pipeline_num" ] || [ -z "$step_id" ]; then
echo "Usage: ci_get_step_logs <pipeline_num> <step_id>" >&2
return 1
fi
if [ -z "${WOODPECKER_REPO_ID:-}" ] || [ "${WOODPECKER_REPO_ID}" = "0" ]; then
echo "ERROR: WOODPECKER_REPO_ID not set or zero" >&2
return 1
fi
woodpecker_api "/repos/${WOODPECKER_REPO_ID}/logs/${pipeline_num}/${step_id}" \
--max-time 15 2>/dev/null \
| jq -r '.[].data // empty' 2>/dev/null
}
# ci_get_logs <pipeline_number> [--step <step_name>] # ci_get_logs <pipeline_number> [--step <step_name>]
# Reads CI logs from the Woodpecker SQLite database. # Reads CI logs from the Woodpecker SQLite database.
# Requires: WOODPECKER_DATA_DIR env var or mounted volume at /woodpecker-data # Requires: WOODPECKER_DATA_DIR env var or mounted volume at /woodpecker-data

View file

@ -429,19 +429,100 @@ pr_walk_to_merge() {
_prl_log "CI failed — invoking agent (attempt ${ci_fix_count}/${max_ci_fixes})" _prl_log "CI failed — invoking agent (attempt ${ci_fix_count}/${max_ci_fixes})"
# Get CI logs from SQLite database if available # Build per-workflow/per-step CI diagnostics prompt
local ci_logs="" local ci_prompt_body=""
if [ -n "$_PR_CI_PIPELINE" ] && [ -n "${FACTORY_ROOT:-}" ]; then local passing_workflows=""
ci_logs=$(ci_get_logs "$_PR_CI_PIPELINE" 2>/dev/null | tail -50) || ci_logs="" local built_diagnostics=false
if [ -n "$_PR_CI_PIPELINE" ] && [ -n "${WOODPECKER_REPO_ID:-}" ]; then
local pip_json
pip_json=$(woodpecker_api "/repos/${WOODPECKER_REPO_ID}/pipelines/${_PR_CI_PIPELINE}" 2>/dev/null) || pip_json=""
if [ -n "$pip_json" ]; then
local wf_count
wf_count=$(printf '%s' "$pip_json" | jq '[.workflows[]?] | length' 2>/dev/null) || wf_count=0
if [ "$wf_count" -gt 0 ]; then
built_diagnostics=true
local wf_idx=0
while [ "$wf_idx" -lt "$wf_count" ]; do
local wf_name wf_state
wf_name=$(printf '%s' "$pip_json" | jq -r ".workflows[$wf_idx].name // \"workflow-$wf_idx\"" 2>/dev/null)
wf_state=$(printf '%s' "$pip_json" | jq -r ".workflows[$wf_idx].state // \"unknown\"" 2>/dev/null)
if [ "$wf_state" = "failure" ] || [ "$wf_state" = "error" ] || [ "$wf_state" = "killed" ]; then
# Collect failed children for this workflow
local failed_children
failed_children=$(printf '%s' "$pip_json" | jq -r "
.workflows[$wf_idx].children[]? |
select(.state == \"failure\" or .state == \"error\" or .state == \"killed\") |
\"\(.name)\t\(.exit_code)\t\(.pid)\"" 2>/dev/null) || failed_children=""
ci_prompt_body="${ci_prompt_body}
--- Failed workflow: ${wf_name} ---"
if [ -n "$failed_children" ]; then
while IFS=$'\t' read -r step_name step_exit step_pid; do
[ -z "$step_name" ] && continue
local exit_annotation=""
case "$step_exit" in
126) exit_annotation=" (permission denied or not executable)" ;;
127) exit_annotation=" (command not found)" ;;
128) exit_annotation=" (invalid exit argument / signal+128)" ;;
esac
ci_prompt_body="${ci_prompt_body}
Step: ${step_name}
Exit code: ${step_exit}${exit_annotation}"
# Fetch per-step logs
if [ -n "$step_pid" ] && [ "$step_pid" != "null" ]; then
local step_logs
step_logs=$(ci_get_step_logs "$_PR_CI_PIPELINE" "$step_pid" 2>/dev/null | tail -50) || step_logs=""
if [ -n "$step_logs" ]; then
ci_prompt_body="${ci_prompt_body}
Log tail (last 50 lines):
\`\`\`
${step_logs}
\`\`\`"
fi
fi
done <<< "$failed_children"
else
ci_prompt_body="${ci_prompt_body}
(no failed step details available)"
fi
else
# Track passing/other workflows
if [ -n "$passing_workflows" ]; then
passing_workflows="${passing_workflows}, ${wf_name}"
else
passing_workflows="${wf_name}"
fi
fi
wf_idx=$((wf_idx + 1))
done
fi
fi
fi fi
local logs_section="" # Fallback: use legacy log fetch if per-workflow diagnostics unavailable
if [ -n "$ci_logs" ]; then if [ "$built_diagnostics" = false ]; then
logs_section=" local ci_logs=""
if [ -n "$_PR_CI_PIPELINE" ] && [ -n "${FACTORY_ROOT:-}" ]; then
ci_logs=$(ci_get_logs "$_PR_CI_PIPELINE" 2>/dev/null | tail -50) || ci_logs=""
fi
if [ -n "$ci_logs" ]; then
ci_prompt_body="
CI Log Output (last 50 lines): CI Log Output (last 50 lines):
\`\`\` \`\`\`
${ci_logs} ${ci_logs}
\`\`\` \`\`\`"
fi
fi
local passing_line=""
if [ -n "$passing_workflows" ]; then
passing_line="
Passing workflows (do not modify): ${passing_workflows}
" "
fi fi
@ -450,9 +531,10 @@ ${ci_logs}
Pipeline: #${_PR_CI_PIPELINE:-?} Pipeline: #${_PR_CI_PIPELINE:-?}
Failure type: ${_PR_CI_FAILURE_TYPE:-unknown} Failure type: ${_PR_CI_FAILURE_TYPE:-unknown}
${passing_line}
Error log: Error log:
${_PR_CI_ERROR_LOG:-No logs available.}${logs_section} ${_PR_CI_ERROR_LOG:-No logs available.}
${ci_prompt_body}
Fix the issue, run tests, commit, rebase on ${PRIMARY_BRANCH}, and push: Fix the issue, run tests, commit, rebase on ${PRIMARY_BRANCH}, and push:
git fetch ${remote} ${PRIMARY_BRANCH} && git rebase ${remote}/${PRIMARY_BRANCH} git fetch ${remote} ${PRIMARY_BRANCH} && git rebase ${remote}/${PRIMARY_BRANCH}