fix(reconciler): use mergedAt, not merged, in gh pr view --json#3651
Conversation
The Post-Merge reconciler check called `gh pr view --json state,merged` but `merged` is not a valid field on `gh pr view` (the available boolean-ish field is `mergedAt` — null when unmerged, ISO string when merged). Every call exited non-zero with "Unknown JSON field: merged", the catch handler logged at debug level (effectively /dev/null), and the feature was silently skipped every tick. Observed in this session: the 60-second reconciler timer fired 1168 times with 0 failures (because the catch swallows them) and 0 reconciliations across two PRs that had been merged for hours. The WebhookHealthCheck flagged "PR has been in review for 1023 min with no CI events" but nothing tied it back to the reconciler being broken. Fix: - gh args: `--json state,mergedAt` instead of `state,merged` - isMerged predicate: `state === 'MERGED' || mergedAt != null` - catch handler: bumped from `debug` to `warn` so this class of bug surfaces immediately if it happens again - updated tests + interface type The Phase 2 (branch-fallback) path I shipped in #3642 was correct on its own merits — but Phase 1 silently failing is what made Phase 2 appear necessary in the first place. Both paths now work as intended. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
📝 WalkthroughWalkthroughUpdated the GitHub PR polling reconciler to align with the actual ChangesPR merge detection update
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed: one or more packages not found in the registry. Comment |
Code Review — ? finding(s)
protoLabs Code Review Report
No findings recorded. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/server/tests/unit/services/post-merge-reconciler-check.test.ts (1)
226-230: ⚡ Quick winAdd explicit coverage for
OPENwith non-nullmergedAt.The reconciler now treats either signal as merged, but the suite still misses the defensive
state: 'OPEN'+mergedAttimestamp case.✅ Suggested test case
+ it('treats PR as merged when mergedAt is set even if state is OPEN', async () => { + const feature = makeFeature(); + mockFeatureLoaderGetAll.mockResolvedValue([feature]); + + const execFileAsync = makeExecFileAsync({ + 'protoLabsAI/mythxengine#184': { + stdout: JSON.stringify({ state: 'OPEN', mergedAt: '2026-05-22T12:00:00Z' }), + }, + }); + + const check = new PostMergeReconcilerCheck(makeLoader(), events as any, execFileAsync); + const result = await check.run('/home/josh/dev/labs/mythxengine'); + + expect(result.checked).toBe(1); + expect(result.reconciled).toBe(1); + expect(mockFeatureLoaderUpdate).toHaveBeenCalledTimes(1); + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/server/tests/unit/services/post-merge-reconciler-check.test.ts` around lines 226 - 230, The test suite is missing a case for the reconciler treating "state: 'OPEN'" with a non-null "mergedAt" as merged; add a new execFileAsync fixture (using makeExecFileAsync) that returns { state: 'OPEN', mergedAt: '<timestamp>' } for the same repo key (e.g., 'protoLabsAI/mythxengine#184') and add assertions in the corresponding test that the reconciler marks that PR as merged (same expectations as other merged signals); locate the existing execFileAsync setup in apps/server/tests/unit/services/post-merge-reconciler-check.test.ts and mirror its pattern so the reconciler path handling (the code under test) for OPEN+mergedAt is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@apps/server/tests/unit/services/post-merge-reconciler-check.test.ts`:
- Around line 226-230: The test suite is missing a case for the reconciler
treating "state: 'OPEN'" with a non-null "mergedAt" as merged; add a new
execFileAsync fixture (using makeExecFileAsync) that returns { state: 'OPEN',
mergedAt: '<timestamp>' } for the same repo key (e.g.,
'protoLabsAI/mythxengine#184') and add assertions in the corresponding test that
the reconciler marks that PR as merged (same expectations as other merged
signals); locate the existing execFileAsync setup in
apps/server/tests/unit/services/post-merge-reconciler-check.test.ts and mirror
its pattern so the reconciler path handling (the code under test) for
OPEN+mergedAt is covered.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 708e0961-3e14-4f26-a140-bd341e5bd26a
📒 Files selected for processing (2)
apps/server/src/services/maintenance/checks/post-merge-reconciler-check.tsapps/server/tests/unit/services/post-merge-reconciler-check.test.ts
Root cause
The Post-Merge reconciler check called `gh pr view --json state,merged` but `merged` is not a valid field on `gh pr view` (the available boolean-ish field is `mergedAt` — null when unmerged, ISO string when merged). Every call exited non-zero with `Unknown JSON field: "merged"`, the catch handler logged at `debug` level (effectively /dev/null), and the feature was silently skipped every tick.
Observed
The 60-second reconciler timer fired 1168 times with 0 failures (because the catch swallows them) and 0 reconciliations across two PRs that had been merged for hours. `WebhookHealthCheck` flagged `"PR has been in review for 1023 min with no CI events"` but nothing tied it back to the reconciler itself being broken.
Manual repro:
```
$ gh pr view 3645 --repo protoLabsAI/protoMaker --json state,merged
Unknown JSON field: "merged"
```
Fix
Note on #3642
The Phase 2 (branch-fallback) path I shipped in #3642 was correct on its own merits — but Phase 1 silently failing is what made Phase 2 appear necessary in the first place. Both paths now work as intended.
Verification
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
Bug Fixes
Improvements