fix: handle double-stringified merge response in _parse_merged_status #219
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
ldraney/claude-custom!219
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "216-fix-merge-parse"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Fixes false negative in
_parse_merged_status()where all 8 PR merges during session 2026-03-28 reported "Merge was not successful" despite the Forgejo API returningmerged: true. The root cause: Claude Code MCP wrapsmerge_approved_prresponses in double-stringified JSON that none of the existing 5 shapes could parse.Changes
hooks/forgejo-helper.sh-- Added Shape 4b to_parse_merged_status(): after parsing.tool_responseas a string (Shape 4), also parse.resultinside for.merged. Updated header comment to document all 6 shapes including 4b and the existing Shape 5.tests/test_parse_merged_status.sh-- Added 2 test cases for Shape 4b: double-stringifiedmerged: trueandmerged: false, using the exact payload format captured from/tmp/hook-debug-merge.json.Test Plan
bash tests/test_parse_merged_status.sh-- 15/15 pass (13 existing + 2 new)/tmp/hook-debug-merge.json(Python-generated replica)Review Checklist
_parse_merged_status()modified -- no other functions touchedRelated Notes
Related
Closes #216
Claude Code MCP wraps merge_approved_pr responses in double-stringified JSON: tool_response is a string containing {"result": "{...merged: true...}"}. Existing shapes 1-4 all missed this because none parsed through both string layers. Add Shape 4b to unwrap tool_response -> .result -> .merged. Closes #216 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>QA Review
VERDICT: APPROVED
Diff Analysis (2 files, +27/-0)
hooks/forgejo-helper.shtool_response->result->merged) matches the exact double-stringified structure from the real-world debug capturemerged=$()/if/echo/returnpattern)tests/test_parse_merged_status.shmerged: trueandmerged: false/tmp/hook-debug-merge.jsonChecklist
_parse_merged_status()modified -- no other functions touchedPR #219 Review
DOMAIN REVIEW
Tech stack: Bash shell scripting, jq JSON processing, unit tests (bash-based).
Shape 4b jq pipeline correctness: The three-stage pipeline is correct for the double-stringified case:
jq -r '.tool_response // empty'-- extracts the string value of.tool_response, outputting raw content (one level of JSON unescaping)jq -r '.result // empty'-- parses that raw content as JSON, extracts.resultstring, outputs rawjq -r '.merged // empty'-- parses the innermost string as JSON, extracts.mergedEach
jqinvocation acts as an implicitfromjsonfor the next stage becausejq -ron a string value outputs the raw string content, which the nextjqinstance re-parses. The pipeline correctly unwraps two levels of stringification.Shape ordering: Shape 4b is placed after Shape 4 and before Shape 5 (grep fallback). This is correct. Shape 4 handles single-stringified
.tool_responsewith.mergedat the top level. Shape 4b handles single-stringified.tool_responsewhere.mergedis nested inside a second stringified.result. Neither interferes with earlier shapes because Shapes 1-4 would all fail (return empty) before reaching 4b for this payload structure.No regression risk to Shapes 1-5: Shape 4b only executes when Shapes 1-4 have all failed to find
merged: true. Its jq pipeline with// emptyat each stage ensures any parsing failure produces empty strings rather than errors, falling through gracefully. The2>/dev/nullon each jq call suppresses parse errors for non-matching shapes.Error suppression pattern: Consistent with existing shapes -- each jq call has
2>/dev/nulland uses// emptyas a null coalescing fallback. This is the established pattern in this function.Observation on Shape 5 overlap: Shape 5 (grep fallback) likely already matches the double-stringified
merged: truecase in isolation, sincejq -r '.tool_response'would output the raw string content containing"merged": trueafter one level of unescaping. However, adding Shape 4b for explicit structured parsing is the right call -- grep is a blunt instrument and structured parsing is more reliable. If the real-world payload had additional escaping or whitespace that defeated grep, Shape 4b would still work.Header comment update: The diff correctly adds Shape 4b and Shape 5 to the header comment block. Shape 5 existed in code but was undocumented in the header -- this is a good documentation fix included alongside the feature.
BLOCKERS
None.
NITS
Shape 4 missing
merged:falsetest: The existing Shape 4 (line 73) only testsmerged:truebut notmerged:false. Shape 4b correctly tests both. Consider adding amerged:falsetest for Shape 4 in a follow-up for test parity across all shapes. (Pre-existing gap, not introduced by this PR.)Comment notation: The header comment says
.tool_response | strings | fromjson | .result | fromjson | .mergedbut the actual code usesjq -r '.tool_response // empty' | jq -r '.result // empty' | jq -r '.merged // empty'(three separate jq invocations with// empty). This is functionally equivalent but the comment notation implies a single jq pipeline. Consistent with how Shape 4 is already documented, so this is not a divergence from established convention.SOP COMPLIANCE
216-fix-merge-parsefollows{issue-number}-{kebab-case-purpose}_parse_merged_status()modified + corresponding testsPROCESS OBSERVATIONS
VERDICT: APPROVED