1717import uuid
1818from datetime import datetime
1919
20- from sqlalchemy import func , or_ , select
20+ from sqlalchemy import and_ , func , or_ , select
2121from sqlalchemy .ext .asyncio import AsyncSession
2222
2323from app .models .pull_request import PRState , PullRequest
@@ -172,22 +172,40 @@ async def list_open_for_bud_with_repo(
172172 ) -> list [tuple [PullRequest , TrackedRepository | None ]]:
173173 """List open PRs for a BUD joined with their tracked repository.
174174
175- When ``impacted_repo_ids`` is provided, the result includes any open
176- PR that is either linked to ``bud_id`` directly OR targets one of
177- the impacted repos. Used by release-stage views that need to surface
178- open PRs in repos affected by the BUD even if the PR forgot to set
179- a ``bud_id``.
175+ Three-way predicate so the release-stage views (UAT / PROD tabs)
176+ only surface PRs that genuinely relate to ``bud_id``:
177+
178+ * ``bud_id == X`` — the PR is directly linked to this BUD.
179+ * ``bud_id IS NULL AND repo_id IN impacted_repo_ids`` — aggregate
180+ release PRs like ``develop → main`` legitimately carry no single
181+ owning BUD; we keep them visible on the impacted repo's stage tab
182+ because the SHA-walk in the release detector uses them to attribute
183+ merges back to multiple BUDs.
184+
185+ A plain ``OR(bud_id == X, repo_id IN impacted)`` would also let
186+ through PRs linked to a **different** BUD that happens to touch the
187+ same impacted repo — which is the over-matching bug this method now
188+ prevents.
180189
181190 Args:
182191 bud_id: The BUD UUID to filter on.
183- impacted_repo_ids: Additional repo UUIDs to include open PRs for.
192+ impacted_repo_ids: Repo UUIDs whose unlinked release PRs should
193+ stay visible. When ``None`` / empty, only directly-linked
194+ PRs are returned.
184195
185196 Returns:
186197 List of ``(PullRequest, TrackedRepository | None)`` tuples.
187198 """
188- filters = [PullRequest .bud_id == bud_id ]
189199 if impacted_repo_ids :
190- filters .append (PullRequest .repo_id .in_ (impacted_repo_ids ))
200+ bud_predicate = or_ (
201+ PullRequest .bud_id == bud_id ,
202+ and_ (
203+ PullRequest .bud_id .is_ (None ),
204+ PullRequest .repo_id .in_ (impacted_repo_ids ),
205+ ),
206+ )
207+ else :
208+ bud_predicate = PullRequest .bud_id == bud_id
191209
192210 stmt = self ._scoped (
193211 select (PullRequest , TrackedRepository )
@@ -198,7 +216,7 @@ async def list_open_for_bud_with_repo(
198216 )
199217 .where (
200218 PullRequest .state == PRState .OPEN ,
201- or_ ( * filters ) ,
219+ bud_predicate ,
202220 )
203221 )
204222 result = await self ._db .execute (stmt )
0 commit comments