Route stale-PR follow-up by provider so merged GitLab MRs poll correctly#242
Merged
dhilgaertner merged 1 commit intomainfrom May 5, 2026
Merged
Conversation
The 60s polling cycle was hardcoding `gh api graphql` for every stale PR
URL — including GitLab MRs. One GitLab URL in the batch failed the whole
gh call (`Could not resolve to a Repository with the name 'big-bang/product'`),
so `fetchStalePRStates` returned nil and no PR (GitHub or GitLab) got its
merged state applied. That's why merged PRs only flipped to purple on
app launch.
Split URLs by provider: GitHub PRs go through the existing batched aliased
GraphQL query; GitLab MRs go through one `glab api projects/{slug}/merge_requests/{iid}`
per MR with `GITLAB_HOST` set per host. Return a small `StalePRFetchResult`
struct so a single-provider failure no longer masks the other provider's
PRs — partial-success now flows through to `applyPRStatuses` while
`prDataComplete=false` still blocks auto-completion on degraded data.
Closes #241
dgershman
approved these changes
May 4, 2026
Collaborator
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues
None found.
Security Review
Strengths:
- URL-encoded slug passed to the GitLab API endpoint (
addingPercentEncoding(withAllowedCharacters: .alphanumerics)) prevents path traversal via malicious slug strings. - Error output is truncated (
.prefix(200)) in log messages, preventing large stderr from flooding logs. GITLAB_HOSTis set per-host in the process environment, mirroring the existingfetchGitLabMRsForReconcilepattern — no credential leak across hosts.NSHomeDirectory()as the cwd avoids the non-repo-cwd failure that #233 fixed.
Concerns:
- None. The attack surface is limited to locally-stored session URLs that originate from the user's own linked PRs.
Code Quality
Well done:
- The
StalePRFetchResultstruct cleanly replaces the[ViewerPR]?optional with explicit partial-success semantics — this is a meaningful improvement in clarity over nil-means-failure. - Extracting
normalizeGitLabPRStateand reusing it infetchGitLabMRsForReconcileeliminates the inline switch duplication. parseGitLabMRURLhandles nested groups correctly (the two-segment heuristic inparseTicketURLComponentswould truncate), and the test suite explicitly guards this regression.- The host guard at line 699 (
host != "github.com") is a sensible safety net — unknown hosts that don't match the/-/merge_requests/pattern won't get blindly routed togh. - Test coverage is comprehensive: URL parsing (top-level, nested, rejection cases), state normalization, JSON parsing (merged/opened/closed/malformed/legacy WIP flag), and dedup integration.
Minor observations (non-blocking):
- The
for (host, parsed) in gitlabParsedByHostloop at line 721 runs sequentially. For the stated assumption ("stale set is usually tiny") this is fine, but if it ever grows, these could be parallelized with a TaskGroup since they're independent per-host. parseGitLabStaleMRResponsereturnsnil(marking the cycle incomplete) on unparseable JSON, but this could also happen if the MR was deleted (404 returning an error body). The error-vs-parse-failure distinction is already handled by the try/catch in the caller, so this is fine in practice.
Summary Table
| Priority | Issue |
|---|---|
| Green | Sequential per-host GitLab fetches are fine for current scale; consider TaskGroup if stale sets grow |
| Green | 404 vs. malformed JSON both result in ok = false — correct behavior, just worth noting for future debugging |
Recommendation: Approve. The change is well-structured, correctly isolates provider failures so one bad GitLab host can't suppress GitHub PR updates, and the test suite thoroughly covers the new parsing/normalization logic. The architecture matches existing patterns in the codebase (reconcile path) and the PR description accurately reflects what was done.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
IssueTracker.fetchStalePRStateshardcodedgh api graphqlfor every stale PR URL. One GitLab MR in the batch (e.g.big-bang/productonrepo1.dso.mil) failed theghcall, returnednil, and wiped the whole cycle's merged-state update — so merged PRs (GitHub or GitLab) only turned purple on app launch.gh; GitLab MRs → oneglab api projects/{slug}/merge_requests/{iid}per MR withGITLAB_HOSTset per host (mirroring the pattern infetchGitLabMRsForReconcile).[ViewerPR]?return with aStalePRFetchResult { prs, complete }struct so a single-provider failure no longer masks the other provider's PRs — partial success contributes its PRs toapplyPRStatuseswhileprDataComplete=falsestill blocks auto-completion on degraded data.parseGitLabMRURL) that handles nested groups, plus a sharednormalizeGitLabPRStatehelper used by both stale-fetch and reconcile.Closes #241
Test plan
make build— cleanswift test --filter IssueTrackerStalePRTests— 17 new tests pass (URL parse, state normalization, JSON parse, mixed-provider dedup)swift test— all IssueTracker suites green; one pre-existing flakytmuxBackendDefaultIsOffrace unrelated to this change[IssueTracker] Stale-PR follow-up failed (exit 1)log lines appear🤖 Generated with Claude Code