Coverage for extra_codeowners/service.py: 78%

571 statements  

« prev     ^ index     » next       coverage.py v7.15.1, created at 2026-07-13 09:46 +0000

1"""GitHub evidence collection and policy evaluation orchestration.""" 

2 

3from __future__ import annotations 

4 

5import asyncio 

6from collections.abc import AsyncIterator 

7from contextlib import asynccontextmanager, suppress 

8from datetime import UTC, datetime, timedelta 

9from typing import Any, Final 

10 

11import structlog 

12from pydantic import ValidationError 

13 

14from extra_codeowners.codeowners import CodeownersDocument, parse_codeowners 

15from extra_codeowners.database import ( 

16 AuthorityRequest, 

17 CheckWriteGuard, 

18 ClaimedAuthorityJob, 

19 ClaimedJob, 

20 JobRequest, 

21 QueueStore, 

22 normalize_repository_full_name, 

23) 

24from extra_codeowners.evaluator import evaluate 

25from extra_codeowners.github import ( 

26 MAX_CODEOWNERS_BYTES, 

27 MAX_PULL_FILES, 

28 GitHubClient, 

29 GitHubError, 

30 GitHubRateLimitError, 

31 PullRequestTooLargeError, 

32) 

33from extra_codeowners.metrics import ( 

34 DEAD_JOBS, 

35 EVALUATION_SECONDS, 

36 EVALUATIONS, 

37 QUEUE_DEPTH, 

38 RECONCILIATION_LAST_SUCCESS, 

39 RECONCILIATIONS, 

40) 

41from extra_codeowners.models import ( 

42 ActorKind, 

43 ChangedFile, 

44 ChangedFileStatus, 

45 EnrolledApplication, 

46 EvaluationConclusion, 

47 EvaluationInput, 

48 EvaluationMessage, 

49 EvaluationOptions, 

50 EvaluationResult, 

51 OrganizationPolicy, 

52 PullRequestReview, 

53 RepositoryPolicy, 

54 ReviewActor, 

55 ReviewState, 

56) 

57from extra_codeowners.policy import BUILTIN_NON_DELEGABLE_PATHS 

58from extra_codeowners.settings import Settings 

59 

60CODEOWNERS_LOCATIONS: Final = ( 

61 ".github/CODEOWNERS", 

62 "CODEOWNERS", 

63 "docs/CODEOWNERS", 

64) 

65log = structlog.get_logger() 

66MAX_TEAM_MEMBERSHIP_LOOKUPS: Final = 250 

67MAX_PATH_MATCH_OPERATIONS: Final = 2_000_000 

68 

69 

70class EvidenceLimitError(GitHubError): 

71 """Trusted evidence exceeds a bounded evaluation budget.""" 

72 

73 

74class AuthorityChangePendingError(RuntimeError): 

75 """An accepted authority change must fan out before evaluation can finish.""" 

76 

77 

78def _failure(code: str, message: str) -> EvaluationResult: 

79 return EvaluationResult( 

80 conclusion=EvaluationConclusion.FAILURE, 

81 summary="Extra CODEOWNERS could not evaluate safely; approval is denied.", 

82 errors=(EvaluationMessage(code=code, message=message),), 

83 ) 

84 

85 

86def _required_object(value: Any, field: str) -> dict[str, Any]: 

87 if not isinstance(value, dict): 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true

88 msg = f"GitHub response omitted {field}" 

89 raise GitHubError(msg) 

90 return value 

91 

92 

93def _required_string(value: Any, field: str) -> str: 

94 if not isinstance(value, str) or not value: 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true

95 msg = f"GitHub response omitted {field}" 

96 raise GitHubError(msg) 

97 return value 

98 

99 

100def _required_nonnegative_int(value: Any, field: str) -> int: 

101 if isinstance(value, bool) or not isinstance(value, int) or value < 0: 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true

102 msg = f"GitHub response omitted {field}" 

103 raise GitHubError(msg) 

104 return int(value) 

105 

106 

107def _label_names(pull: dict[str, Any]) -> frozenset[str]: 

108 labels = pull.get("labels") 

109 if not isinstance(labels, list) or any( 

110 not isinstance(item, dict) or not isinstance(item.get("name"), str) for item in labels 

111 ): 

112 raise GitHubError("GitHub pull response omitted a valid labels list") 

113 return frozenset(str(item["name"]).lower() for item in labels) 

114 

115 

116class EvaluationService: 

117 """Collect trusted evidence, run the pure evaluator, and publish one check.""" 

118 

119 def __init__(self, settings: Settings, github: GitHubClient, store: QueueStore) -> None: 

120 self.settings = settings 

121 self.github = github 

122 self.store = store 

123 self._cleanup_tasks: set[asyncio.Task[None]] = set() 

124 

125 @asynccontextmanager 

126 async def _check_write_guard(self, installation_id: int, head_sha: str) -> AsyncIterator[None]: 

127 """Serialize commit-scoped check writes across every service replica.""" 

128 acquisition = asyncio.create_task( 

129 asyncio.to_thread( 

130 self.store.acquire_check_write_guard, 

131 f"installation:{installation_id}", 

132 head_sha, 

133 30.0, 

134 ) 

135 ) 

136 try: 

137 guard = await asyncio.shield(acquisition) 

138 except asyncio.CancelledError: 

139 # asyncio cannot cancel a running executor thread. Arrange to 

140 # release any guard it eventually returns instead of leaking a 

141 # PostgreSQL session advisory lock. 

142 cleanup = asyncio.create_task(self._release_abandoned_guard(acquisition)) 

143 self._cleanup_tasks.add(cleanup) 

144 cleanup.add_done_callback(self._cleanup_tasks.discard) 

145 raise 

146 if guard is None: 146 ↛ 147line 146 didn't jump to line 147 because the condition on line 146 was never true

147 raise GitHubError("timed out waiting for the pull request check writer") 

148 try: 

149 yield 

150 finally: 

151 await asyncio.to_thread(self.store.release_check_write_guard, guard) 

152 

153 @asynccontextmanager 

154 async def _authority_publish_guard(self, installation_id: int) -> AsyncIterator[None]: 

155 """Order final publications against accepted installation authority changes.""" 

156 acquisition = asyncio.create_task( 

157 asyncio.to_thread( 

158 self.store.acquire_authority_guard, 

159 installation_id, 

160 shared=True, 

161 timeout_seconds=30.0, 

162 ) 

163 ) 

164 try: 

165 guard = await asyncio.shield(acquisition) 

166 except asyncio.CancelledError: 

167 cleanup = asyncio.create_task(self._release_abandoned_guard(acquisition)) 

168 self._cleanup_tasks.add(cleanup) 

169 cleanup.add_done_callback(self._cleanup_tasks.discard) 

170 raise 

171 if guard is None: 171 ↛ 172line 171 didn't jump to line 172 because the condition on line 171 was never true

172 raise GitHubError("timed out waiting for the authority publication guard") 

173 try: 

174 yield 

175 finally: 

176 await asyncio.to_thread(self.store.release_check_write_guard, guard) 

177 

178 async def _release_abandoned_guard( 

179 self, acquisition: asyncio.Task[CheckWriteGuard | None] 

180 ) -> None: 

181 try: 

182 guard = await acquisition 

183 if guard is not None: 

184 await asyncio.to_thread(self.store.release_check_write_guard, guard) 

185 except Exception: 

186 log.exception("abandoned_check_writer_cleanup_failed") 

187 

188 async def _find_codeowners( 

189 self, installation_id: int, repository: str, base_sha: str 

190 ) -> tuple[str, str] | None: 

191 for path in CODEOWNERS_LOCATIONS: 

192 content = await self.github.get_file_text( 

193 installation_id, 

194 repository, 

195 path, 

196 ref=base_sha, 

197 max_bytes=MAX_CODEOWNERS_BYTES, 

198 ) 

199 if content is not None: 

200 return path, content 

201 return None 

202 

203 async def _load_organization_policy( 

204 self, 

205 installation_id: int, 

206 repository: str, 

207 ) -> OrganizationPolicy: 

208 owner = repository.split("/", 1)[0] 

209 organization_repository = f"{owner}/{self.settings.org_config_repository}" 

210 org_text = await self.github.get_file_text( 

211 installation_id, 

212 organization_repository, 

213 self.settings.policy_path, 

214 ) 

215 return OrganizationPolicy() if org_text is None else OrganizationPolicy.from_toml(org_text) 

216 

217 async def _repository_policy_text( 

218 self, 

219 installation_id: int, 

220 repository: str, 

221 base_sha: str, 

222 ) -> str | None: 

223 return await self.github.get_file_text( 

224 installation_id, 

225 repository, 

226 self.settings.policy_path, 

227 ref=base_sha, 

228 ) 

229 

230 @staticmethod 

231 def _changed_files(values: list[dict[str, Any]]) -> tuple[ChangedFile, ...]: 

232 files: list[ChangedFile] = [] 

233 for value in values: 

234 status = _required_string(value.get("status"), "pull file status") 

235 previous = value.get("previous_filename") 

236 files.append( 

237 ChangedFile( 

238 path=_required_string(value.get("filename"), "pull file filename"), 

239 status=ChangedFileStatus(status), 

240 previous_path=previous if isinstance(previous, str) else None, 

241 ) 

242 ) 

243 return tuple(files) 

244 

245 async def _human_team_aliases( 

246 self, 

247 installation_id: int, 

248 repository: str, 

249 repository_owner: str, 

250 login: str, 

251 document: CodeownersDocument, 

252 ) -> frozenset[str]: 

253 team_owners = { 

254 owner 

255 for rule in document.rules 

256 for owner in rule.owners 

257 if "/" in owner 

258 and owner.split("/", 1)[0].removeprefix("@").lower() == repository_owner.lower() 

259 } 

260 semaphore = asyncio.Semaphore(10) 

261 

262 async def is_member(owner: str) -> bool: 

263 async with semaphore: 

264 team_slug = owner.split("/", 1)[1] 

265 member, can_own = await asyncio.gather( 

266 self.github.team_member( 

267 installation_id, 

268 repository_owner, 

269 team_slug, 

270 login, 

271 ), 

272 self.github.team_can_own_repository( 

273 installation_id, 

274 repository_owner, 

275 team_slug, 

276 repository, 

277 ), 

278 ) 

279 return member and can_own 

280 

281 membership = await asyncio.gather(*(is_member(owner) for owner in sorted(team_owners))) 

282 return frozenset( 

283 owner 

284 for owner, is_member in zip(sorted(team_owners), membership, strict=True) 

285 if is_member 

286 ) 

287 

288 async def _validated_apps( 

289 self, 

290 installation_id: int, 

291 organization: OrganizationPolicy, 

292 bot_user_ids: frozenset[int], 

293 ) -> dict[int, tuple[str, EnrolledApplication, int, str]]: 

294 """Bind configured bot users to independently fetched App identities.""" 

295 validated: dict[int, tuple[str, EnrolledApplication, int, str]] = {} 

296 for alias, app in organization.apps.items(): 

297 if app.bot_user_id not in bot_user_ids: 297 ↛ 298line 297 didn't jump to line 298 because the condition on line 297 was never true

298 continue 

299 metadata = await self.github.get_app(installation_id, app.slug) 

300 observed_id = metadata.get("id") 

301 observed_slug = metadata.get("slug") 

302 if observed_id != app.app_id or str(observed_slug).lower() != app.slug: 

303 log.warning( 

304 "enrolled_app_identity_mismatch", 

305 alias=alias, 

306 configured_app_id=app.app_id, 

307 observed_app_id=observed_id, 

308 configured_slug=app.slug, 

309 observed_slug=observed_slug, 

310 ) 

311 continue 

312 validated[app.bot_user_id] = (alias, app, int(observed_id), str(observed_slug)) 

313 return validated 

314 

315 async def _reviews( 

316 self, 

317 installation_id: int, 

318 repository: str, 

319 values: list[dict[str, Any]], 

320 organization: OrganizationPolicy, 

321 document: CodeownersDocument, 

322 head_sha: str, 

323 ) -> tuple[PullRequestReview, ...]: 

324 reviews: list[PullRequestReview] = [] 

325 repository_owner = repository.split("/", 1)[0] 

326 valid_states = {state.value for state in ReviewState} 

327 latest: dict[tuple[str, int], tuple[datetime, int, dict[str, Any]]] = {} 

328 for value in values: 

329 state_value = value.get("state") 

330 if state_value not in valid_states: 330 ↛ 331line 330 didn't jump to line 331 because the condition on line 330 was never true

331 msg = f"GitHub returned unknown review state {state_value!r}" 

332 raise GitHubError(msg) 

333 if state_value in {ReviewState.COMMENTED.value, ReviewState.PENDING.value}: 

334 continue 

335 user = value.get("user") 

336 if not isinstance(user, dict): 336 ↛ 337line 336 didn't jump to line 337 because the condition on line 336 was never true

337 raise GitHubError("opinionated review omitted its user") 

338 user_id = user.get("id") 

339 login = user.get("login") 

340 review_id = value.get("id") 

341 submitted_at = value.get("submitted_at") 

342 if ( 

343 isinstance(user_id, bool) 

344 or not isinstance(user_id, int) 

345 or not isinstance(login, str) 

346 or isinstance(review_id, bool) 

347 or not isinstance(review_id, int) 

348 or not isinstance(submitted_at, str) 

349 ): 

350 raise GitHubError("opinionated review omitted actor, ID, or submission time") 

351 actor_type = user.get("type") 

352 if actor_type not in {"Bot", "User"}: 352 ↛ 353line 352 didn't jump to line 353 because the condition on line 352 was never true

353 msg = f"opinionated review has unsupported actor type {actor_type!r}" 

354 raise GitHubError(msg) 

355 try: 

356 submitted = datetime.fromisoformat(submitted_at.replace("Z", "+00:00")) 

357 except ValueError as error: 

358 raise GitHubError("opinionated review has an invalid submission time") from error 

359 key = (str(actor_type), user_id) 

360 previous = latest.get(key) 

361 if previous is None or (submitted, review_id) > (previous[0], previous[1]): 361 ↛ 328line 361 didn't jump to line 328 because the condition on line 361 was always true

362 latest[key] = (submitted, review_id, value) 

363 

364 current = [ 

365 value 

366 for _, _, value in latest.values() 

367 if value.get("state") == ReviewState.APPROVED.value 

368 and value.get("commit_id") == head_sha 

369 ] 

370 team_owners = { 

371 owner 

372 for rule in document.rules 

373 for owner in rule.owners 

374 if "/" in owner 

375 and owner.split("/", 1)[0].removeprefix("@").lower() == repository_owner.lower() 

376 } 

377 human_approvals = sum( 

378 1 

379 for value in current 

380 if isinstance(value.get("user"), dict) and value["user"].get("type") == "User" 

381 ) 

382 if human_approvals * len(team_owners) > MAX_TEAM_MEMBERSHIP_LOOKUPS: 382 ↛ 383line 382 didn't jump to line 383 because the condition on line 382 was never true

383 raise EvidenceLimitError( 

384 "current approvals and CODEOWNERS teams exceed the membership lookup budget" 

385 ) 

386 current_bot_ids = frozenset( 

387 int(value["user"]["id"]) 

388 for value in current 

389 if isinstance(value.get("user"), dict) and value["user"].get("type") == "Bot" 

390 ) 

391 validated_apps = ( 

392 await self._validated_apps(installation_id, organization, current_bot_ids) 

393 if current_bot_ids 

394 else {} 

395 ) 

396 for value in current: 

397 user = _required_object(value.get("user"), "opinionated review user") 

398 user_id = int(value["user"]["id"]) 

399 login = str(user["login"]) 

400 review_id = int(value["id"]) 

401 submitted_at = str(value["submitted_at"]) 

402 if user.get("type") == "Bot": 

403 enrolled = validated_apps.get(user_id) 

404 if enrolled is None: 

405 continue 

406 _, app, observed_id, observed_slug = enrolled 

407 if login.lower() != f"{app.slug}[bot]": 407 ↛ 408line 407 didn't jump to line 408 because the condition on line 407 was never true

408 log.warning( 

409 "enrolled_app_bot_login_mismatch", 

410 configured_slug=app.slug, 

411 bot_user_id=user_id, 

412 observed_login=login, 

413 ) 

414 continue 

415 actor = ReviewActor( 

416 kind=ActorKind.APPLICATION, 

417 login=login, 

418 user_id=user_id, 

419 app_id=observed_id, 

420 app_slug=observed_slug, 

421 ) 

422 elif user.get("type") == "User": 422 ↛ 444line 422 didn't jump to line 444 because the condition on line 422 was always true

423 direct_owner_eligible, aliases = await asyncio.gather( 

424 self.github.user_can_own_repository( 

425 installation_id, 

426 repository, 

427 login, 

428 ), 

429 self._human_team_aliases( 

430 installation_id, 

431 repository, 

432 repository_owner, 

433 login, 

434 document, 

435 ), 

436 ) 

437 actor = ReviewActor( 

438 kind=ActorKind.HUMAN, 

439 login=login, 

440 user_id=user_id, 

441 owner_aliases=aliases, 

442 direct_owner_eligible=direct_owner_eligible, 

443 ) 

444 reviews.append( 

445 PullRequestReview( 

446 review_id=review_id, 

447 actor=actor, 

448 state=ReviewState.APPROVED, 

449 commit_sha=head_sha, 

450 submitted_at=datetime.fromisoformat(submitted_at.replace("Z", "+00:00")), 

451 ) 

452 ) 

453 return tuple(reviews) 

454 

455 async def _evaluate_current( 

456 self, 

457 job: ClaimedJob, 

458 pull: dict[str, Any], 

459 head_sha: str, 

460 base_sha: str, 

461 expected_changed_files: int, 

462 repository_policy: RepositoryPolicy, 

463 ) -> EvaluationResult: 

464 if not repository_policy.enabled: 

465 return evaluate( 

466 EvaluationInput( 

467 head_sha=head_sha, 

468 codeowners_text="", 

469 changed_files=(), 

470 organization_policy=OrganizationPolicy(), 

471 repository_policy=repository_policy, 

472 ) 

473 ) 

474 

475 if expected_changed_files >= MAX_PULL_FILES: 475 ↛ 476line 475 didn't jump to line 476 because the condition on line 475 was never true

476 return _failure( 

477 "pull_request_too_large", 

478 f"pull request has at least GitHub's {MAX_PULL_FILES:,}-file API limit", 

479 ) 

480 

481 try: 

482 organization = await self._load_organization_policy( 

483 job.installation_id, 

484 job.repository_full_name, 

485 ) 

486 except (ValidationError, ValueError) as error: 

487 return _failure("invalid_policy", str(error)) 

488 

489 codeowners = await self._find_codeowners( 

490 job.installation_id, job.repository_full_name, base_sha 

491 ) 

492 if codeowners is None: 

493 return _failure( 

494 "codeowners_missing", 

495 "enabled repository has no CODEOWNERS file in .github/, the root, or docs/", 

496 ) 

497 _, codeowners_text = codeowners 

498 try: 

499 document = parse_codeowners(codeowners_text) 

500 except ValueError: 

501 # The pure evaluator renders the parser's detailed line errors. 

502 document = CodeownersDocument(()) 

503 

504 changed_path_upper_bound = expected_changed_files * 2 

505 policy_pattern_count = ( 

506 sum(len(delegation.paths) for delegation in repository_policy.delegations) 

507 + len(organization.guardrails.non_delegable_paths) 

508 + (0 if self.settings.allow_insecure_changes else len(BUILTIN_NON_DELEGABLE_PATHS) + 1) 

509 ) 

510 if ( 510 ↛ 514line 510 didn't jump to line 514 because the condition on line 510 was never true

511 changed_path_upper_bound * (len(document.rules) + policy_pattern_count) 

512 > MAX_PATH_MATCH_OPERATIONS 

513 ): 

514 return _failure( 

515 "evaluation_complexity_exceeded", 

516 "changed paths and policy patterns exceed the bounded evaluation budget", 

517 ) 

518 

519 files_task = self.github.get_pull_files( 

520 job.installation_id, job.repository_full_name, job.pull_number 

521 ) 

522 reviews_task = self.github.get_reviews( 

523 job.installation_id, job.repository_full_name, job.pull_number 

524 ) 

525 errors_task = self.github.get_codeowners_errors( 

526 job.installation_id, job.repository_full_name, base_sha 

527 ) 

528 try: 

529 file_values, review_values, github_errors = await asyncio.gather( 

530 files_task, reviews_task, errors_task 

531 ) 

532 except PullRequestTooLargeError as error: 

533 return _failure("evidence_limit_exceeded", str(error)) 

534 if len(file_values) != expected_changed_files: 534 ↛ 535line 534 didn't jump to line 535 because the condition on line 534 was never true

535 return _failure( 

536 "incomplete_changed_files", 

537 "GitHub's pull files response did not match pull_request.changed_files", 

538 ) 

539 if github_errors: 

540 messages = [] 

541 for item in github_errors: 

542 line = item.get("line") 

543 message = item.get("message", "invalid CODEOWNERS entry") 

544 prefix = f"line {line}: " if isinstance(line, int) else "" 

545 messages.append(prefix + str(message)) 

546 return _failure("github_codeowners_error", "; ".join(messages)) 

547 

548 try: 

549 reviews = await self._reviews( 

550 job.installation_id, 

551 job.repository_full_name, 

552 review_values, 

553 organization, 

554 document, 

555 head_sha, 

556 ) 

557 changed_files = self._changed_files(file_values) 

558 except EvidenceLimitError as error: 

559 return _failure("evidence_limit_exceeded", str(error)) 

560 except (ValidationError, ValueError) as error: 

561 return _failure("malformed_github_evidence", str(error)) 

562 labels = _label_names(pull) 

563 return evaluate( 

564 EvaluationInput( 

565 head_sha=head_sha, 

566 codeowners_text=codeowners_text, 

567 changed_files=changed_files, 

568 reviews=reviews, 

569 labels=labels, 

570 organization_policy=organization, 

571 repository_policy=repository_policy, 

572 options=EvaluationOptions( 

573 exact_head_reviews=True, 

574 allow_insecure_changes=self.settings.allow_insecure_changes, 

575 repository_policy_path=self.settings.policy_path, 

576 ), 

577 ) 

578 ) 

579 

580 async def invalidate_for_trigger(self, job: JobRequest) -> bool: 

581 """Synchronously revoke a managed check before acknowledging a trigger.""" 

582 if self.settings.is_organization_config_repository(job.repository_full_name): 582 ↛ 583line 582 didn't jump to line 583 because the condition on line 582 was never true

583 return False 

584 pull = await self.github.get_pull( 

585 job.installation_id, 

586 job.repository_full_name, 

587 job.pull_number, 

588 ) 

589 head = _required_object(pull.get("head"), "pull_request.head") 

590 base = _required_object(pull.get("base"), "pull_request.base") 

591 head_sha = _required_string(head.get("sha"), "pull_request.head.sha") 

592 base_sha = _required_string(base.get("sha"), "pull_request.base.sha") 

593 pull_state = _required_string(pull.get("state"), "pull_request.state") 

594 if pull_state not in {"open", "closed"}: 594 ↛ 595line 594 didn't jump to line 595 because the condition on line 594 was never true

595 raise GitHubError(f"GitHub returned unknown pull request state {pull_state!r}") 

596 if pull_state == "closed": 596 ↛ 597line 596 didn't jump to line 597 because the condition on line 596 was never true

597 return False 

598 

599 managed_check = await self.github.has_check_run( 

600 job.installation_id, 

601 job.repository_full_name, 

602 head_sha, 

603 self.settings.check_name, 

604 ) 

605 if not managed_check: 

606 repository_text = await self._repository_policy_text( 

607 job.installation_id, 

608 job.repository_full_name, 

609 base_sha, 

610 ) 

611 if repository_text is None: 611 ↛ 612line 611 didn't jump to line 612 because the condition on line 611 was never true

612 return False 

613 

614 details_url = pull.get("html_url") if isinstance(pull.get("html_url"), str) else None 

615 async with self._check_write_guard(job.installation_id, head_sha): 

616 await self.github.upsert_check_run( 

617 job.installation_id, 

618 job.repository_full_name, 

619 head_sha, 

620 self.settings.check_name, 

621 status="in_progress", 

622 title="Re-evaluating CODEOWNER approvals", 

623 summary=( 

624 "New review or pull-request evidence arrived; approval is blocked pending " 

625 "re-evaluation." 

626 ), 

627 details_url=details_url, 

628 external_id=f"{job.repository_full_name}#{job.pull_number}@{head_sha}", 

629 ) 

630 return True 

631 

632 async def _head_is_unique_to_pull(self, job: ClaimedJob, head_sha: str) -> bool: 

633 associated = await self.github.list_commit_pulls( 

634 job.installation_id, 

635 job.repository_full_name, 

636 head_sha, 

637 ) 

638 open_head_pulls: set[int] = set() 

639 for associated_pull in associated: 

640 state_value = _required_string( 

641 associated_pull.get("state"), "associated pull_request.state" 

642 ) 

643 associated_head = _required_object( 

644 associated_pull.get("head"), "associated pull_request.head" 

645 ) 

646 associated_sha = _required_string( 

647 associated_head.get("sha"), "associated pull_request.head.sha" 

648 ) 

649 number_value = _required_nonnegative_int( 

650 associated_pull.get("number"), "associated pull_request.number" 

651 ) 

652 if state_value == "open" and associated_sha == head_sha: 652 ↛ 639line 652 didn't jump to line 639 because the condition on line 652 was always true

653 open_head_pulls.add(number_value) 

654 return open_head_pulls == {job.pull_number} 

655 

656 async def evaluate_job(self, job: ClaimedJob) -> None: 

657 """Evaluate a leased job and publish only against stable PR revisions.""" 

658 if self.settings.is_organization_config_repository(job.repository_full_name): 

659 return 

660 with EVALUATION_SECONDS.time(): 

661 pull = await self.github.get_pull( 

662 job.installation_id, job.repository_full_name, job.pull_number 

663 ) 

664 head = _required_object(pull.get("head"), "pull_request.head") 

665 base = _required_object(pull.get("base"), "pull_request.base") 

666 base_repository = _required_object(base.get("repo"), "pull_request.base.repo") 

667 canonical_repository = normalize_repository_full_name( 

668 _required_string( 

669 base_repository.get("full_name"), "pull_request.base.repo.full_name" 

670 ) 

671 ) 

672 if canonical_repository != job.repository_full_name: 

673 log.info( 

674 "stale_repository_alias_discarded", 

675 queued_repository=job.repository_full_name, 

676 canonical_repository=canonical_repository, 

677 pull_number=job.pull_number, 

678 ) 

679 return 

680 head_sha = _required_string(head.get("sha"), "pull_request.head.sha") 

681 base_sha = _required_string(base.get("sha"), "pull_request.base.sha") 

682 base_ref = _required_string(base.get("ref"), "pull_request.base.ref") 

683 pull_state = _required_string(pull.get("state"), "pull_request.state") 

684 if pull_state not in {"open", "closed"}: 684 ↛ 685line 684 didn't jump to line 685 because the condition on line 684 was never true

685 raise GitHubError(f"GitHub returned unknown pull request state {pull_state!r}") 

686 if pull_state == "closed": 686 ↛ 687line 686 didn't jump to line 687 because the condition on line 686 was never true

687 return 

688 

689 managed_check = await self.github.has_check_run( 

690 job.installation_id, 

691 job.repository_full_name, 

692 head_sha, 

693 self.settings.check_name, 

694 ) 

695 repository_text: str | None = None 

696 if not managed_check: 

697 repository_text = await self._repository_policy_text( 

698 job.installation_id, 

699 job.repository_full_name, 

700 base_sha, 

701 ) 

702 if repository_text is None: 702 ↛ 703line 702 didn't jump to line 703 because the condition on line 702 was never true

703 return 

704 details_url = pull.get("html_url") if isinstance(pull.get("html_url"), str) else None 

705 external_id = f"{job.repository_full_name}#{job.pull_number}@{head_sha}" 

706 

707 # Revoke any previous success before collecting mutable review and 

708 # label evidence. A retry remains blocking instead of leaving a 

709 # stale success visible while GitHub or the database is unavailable. 

710 async with self._check_write_guard(job.installation_id, head_sha): 

711 if not await asyncio.to_thread(self.store.is_current_claim, job): 711 ↛ 712line 711 didn't jump to line 712 because the condition on line 711 was never true

712 return 

713 await self.github.upsert_check_run( 

714 job.installation_id, 

715 job.repository_full_name, 

716 head_sha, 

717 self.settings.check_name, 

718 status="in_progress", 

719 title="Evaluating CODEOWNER approvals", 

720 summary=( 

721 "A current evaluation is in progress; approval is blocked until it " 

722 "completes." 

723 ), 

724 details_url=details_url, 

725 external_id=external_id, 

726 ) 

727 if job.last_delivery_id is not None: 727 ↛ 728line 727 didn't jump to line 728 because the condition on line 727 was never true

728 await asyncio.to_thread(self.store.mark_delivery_invalidated, job.last_delivery_id) 

729 if await asyncio.to_thread(self.store.has_blocking_authority, job, base_ref): 729 ↛ 730line 729 didn't jump to line 730 because the condition on line 729 was never true

730 raise AuthorityChangePendingError( 

731 "accepted authority change is still awaiting durable fan-out" 

732 ) 

733 

734 if managed_check: 

735 # Fetch only after revoking a prior success. Oversized, 

736 # malformed, or unavailable policy content must leave the 

737 # required check blocking while the durable job retries. 

738 repository_text = await self._repository_policy_text( 

739 job.installation_id, 

740 job.repository_full_name, 

741 base_sha, 

742 ) 

743 expected_changed_files = _required_nonnegative_int( 

744 pull.get("changed_files"), "pull_request.changed_files" 

745 ) 

746 

747 labels = _label_names(pull) 

748 try: 

749 repository_policy = ( 

750 RepositoryPolicy() 

751 if repository_text is None 

752 else RepositoryPolicy.from_toml(repository_text) 

753 ) 

754 except (ValidationError, ValueError) as error: 

755 result = _failure("invalid_policy", str(error)) 

756 else: 

757 result = await self._evaluate_current( 

758 job, 

759 pull, 

760 head_sha, 

761 base_sha, 

762 expected_changed_files, 

763 repository_policy, 

764 ) 

765 

766 # Close the check-after-race window: evidence collected for an old 

767 # head or base is never published as current. 

768 current = await self.github.get_pull( 

769 job.installation_id, job.repository_full_name, job.pull_number 

770 ) 

771 current_head = _required_object(current.get("head"), "pull_request.head") 

772 current_base = _required_object(current.get("base"), "pull_request.base") 

773 current_head_sha = _required_string(current_head.get("sha"), "pull_request.head.sha") 

774 current_base_sha = _required_string(current_base.get("sha"), "pull_request.base.sha") 

775 current_base_ref = _required_string(current_base.get("ref"), "pull_request.base.ref") 

776 current_state = _required_string(current.get("state"), "pull_request.state") 

777 if current_state not in {"open", "closed"}: 777 ↛ 778line 777 didn't jump to line 778 because the condition on line 777 was never true

778 raise GitHubError(f"GitHub returned unknown pull request state {current_state!r}") 

779 if current_state == "closed": 779 ↛ 780line 779 didn't jump to line 780 because the condition on line 779 was never true

780 return 

781 if ( 

782 current_head_sha != head_sha 

783 or current_base_sha != base_sha 

784 or current_base_ref != base_ref 

785 or _required_nonnegative_int( 

786 current.get("changed_files"), "pull_request.changed_files" 

787 ) 

788 != expected_changed_files 

789 or _label_names(current) != labels 

790 ): 

791 await asyncio.to_thread( 

792 self.store.enqueue, 

793 JobRequest( 

794 installation_id=job.installation_id, 

795 repository_full_name=job.repository_full_name, 

796 pull_number=job.pull_number, 

797 reason="pull_request_changed_during_evaluation", 

798 head_sha_hint=current_head_sha, 

799 ), 

800 ) 

801 return 

802 

803 # A review, label, or policy trigger can arrive without changing 

804 # either SHA. Never publish evidence collected before that trigger. 

805 title = { 

806 EvaluationConclusion.SUCCESS: "CODEOWNER approval requirement satisfied", 

807 EvaluationConclusion.FAILURE: "CODEOWNER approval required", 

808 }[result.conclusion] 

809 warning = "" 

810 if self.settings.allow_insecure_changes: 

811 warning = ( 

812 "\n\n> **Warning:** `EXTRA_CODEOWNERS_ALLOW_INSECURE_CHANGES=true` " 

813 "disabled built-in non-delegable paths. Organization guardrails still apply." 

814 ) 

815 # A shared installation authority guard lets normal evaluations 

816 # publish concurrently while ordering every final result against 

817 # the exclusive guard used by authority webhook acceptance. 

818 async with self._authority_publish_guard(job.installation_id): 

819 async with self._check_write_guard(job.installation_id, head_sha): 

820 # Check the generation while holding the same cross-process 

821 # writer lock used by webhook revocation. If a trigger arrived 

822 # before this lock, stale evidence is never published. If it 

823 # arrives while this lock is held, its revocation PATCH is 

824 # ordered after this completion, even if this process dies. 

825 if not await asyncio.to_thread(self.store.is_current_claim, job): 

826 return 

827 if await asyncio.to_thread(self.store.has_blocking_authority, job, base_ref): 827 ↛ 828line 827 didn't jump to line 828 because the condition on line 827 was never true

828 raise AuthorityChangePendingError( 

829 "accepted authority change arrived during evaluation" 

830 ) 

831 if ( 

832 result.conclusion is EvaluationConclusion.SUCCESS 

833 and not await self._head_is_unique_to_pull(job, head_sha) 

834 ): 

835 result = _failure( 

836 "shared_head_commit", 

837 "the head commit is shared by multiple open pull requests; push a " 

838 "distinct commit before approval", 

839 ) 

840 title = "CODEOWNER approval required" 

841 await self.github.upsert_check_run( 

842 job.installation_id, 

843 job.repository_full_name, 

844 head_sha, 

845 self.settings.check_name, 

846 status="completed", 

847 conclusion=result.conclusion.value, 

848 title=title, 

849 summary=result.summary + warning, 

850 text=result.check_output(), 

851 details_url=details_url, 

852 external_id=external_id, 

853 ) 

854 

855 # If a trigger committed while the completion request was in 

856 # flight, restore a blocking state ourselves. The shared writer 

857 # guard prevents this reset from overwriting a newer generation 

858 # that has already completed and removed its queue row. 

859 superseded = await asyncio.to_thread(self.store.has_superseding_job, job) 

860 authority_pending = await asyncio.to_thread( 

861 self.store.has_blocking_authority, job, base_ref 

862 ) 

863 if superseded or authority_pending: 

864 async with self._check_write_guard(job.installation_id, head_sha): 

865 superseded = await asyncio.to_thread(self.store.has_superseding_job, job) 

866 authority_pending = await asyncio.to_thread( 

867 self.store.has_blocking_authority, job, base_ref 

868 ) 

869 if superseded or authority_pending: 869 ↛ 890line 869 didn't jump to line 890

870 await self.github.upsert_check_run( 

871 job.installation_id, 

872 job.repository_full_name, 

873 head_sha, 

874 self.settings.check_name, 

875 status="in_progress", 

876 title="Re-evaluating CODEOWNER approvals", 

877 summary=( 

878 "New review, pull-request, or authority evidence arrived; " 

879 "approval is blocked pending re-evaluation." 

880 ), 

881 details_url=details_url, 

882 external_id=external_id, 

883 ) 

884 if authority_pending: 884 ↛ 885line 884 didn't jump to line 885 because the condition on line 884 was never true

885 raise AuthorityChangePendingError( 

886 "accepted authority change arrived during check publication" 

887 ) 

888 return 

889 

890 await asyncio.to_thread( 

891 self.store.record_audit, 

892 job.repository_full_name, 

893 job.pull_number, 

894 head_sha, 

895 result.conclusion.value, 

896 { 

897 **result.model_dump(mode="json"), 

898 "trigger": { 

899 "reason": job.reason, 

900 "delivery_id": job.last_delivery_id, 

901 }, 

902 }, 

903 ) 

904 EVALUATIONS.labels(result.conclusion.value).inc() 

905 

906 

907class Worker: 

908 """Lease and execute durable jobs until stopped.""" 

909 

910 def __init__( 

911 self, 

912 settings: Settings, 

913 store: QueueStore, 

914 evaluator: EvaluationService, 

915 owner: str, 

916 ) -> None: 

917 self.settings = settings 

918 self.store = store 

919 self.evaluator = evaluator 

920 self.owner = owner 

921 

922 async def _renew_lease(self, job: ClaimedJob, done: asyncio.Event) -> None: 

923 """Keep a live evaluation fenced to this worker until it finishes.""" 

924 interval = max(1.0, self.settings.worker_lease_seconds / 3) 

925 while not done.is_set(): 925 ↛ exitline 925 didn't return from function '_renew_lease' because the condition on line 925 was always true

926 try: 

927 await asyncio.wait_for(done.wait(), interval) 

928 return 

929 except TimeoutError: 

930 pass 

931 try: 

932 renewed = await asyncio.to_thread( 

933 self.store.renew_claim, 

934 job, 

935 self.settings.worker_lease_seconds, 

936 ) 

937 except Exception: 

938 log.exception( 

939 "evaluation_lease_renewal_failed", 

940 repository=job.repository_full_name, 

941 pull_number=job.pull_number, 

942 ) 

943 return 

944 if not renewed: 

945 log.warning( 

946 "evaluation_lease_lost", 

947 repository=job.repository_full_name, 

948 pull_number=job.pull_number, 

949 generation=job.generation, 

950 ) 

951 return 

952 

953 async def _process(self, job: ClaimedJob) -> None: 

954 done = asyncio.Event() 

955 heartbeat = asyncio.create_task(self._renew_lease(job, done), name=f"job-lease-{job.id}") 

956 try: 

957 await self.evaluator.evaluate_job(job) 

958 except GitHubRateLimitError as error: 

959 log.warning( 

960 "evaluation_rate_limited", 

961 repository=job.repository_full_name, 

962 pull_number=job.pull_number, 

963 retry_after_seconds=error.retry_after_seconds, 

964 ) 

965 await asyncio.to_thread( 

966 self.store.defer, 

967 job, 

968 self.owner, 

969 str(error), 

970 error.retry_after_seconds, 

971 ) 

972 except AuthorityChangePendingError as error: 

973 log.info( 

974 "evaluation_deferred_for_authority", 

975 repository=job.repository_full_name, 

976 pull_number=job.pull_number, 

977 ) 

978 await asyncio.to_thread( 

979 self.store.defer, 

980 job, 

981 self.owner, 

982 str(error), 

983 max(5, int(self.settings.worker_poll_seconds * 10)), 

984 ) 

985 except asyncio.CancelledError: 

986 raise 

987 except Exception as error: 

988 log.exception( 

989 "evaluation_failed", 

990 repository=job.repository_full_name, 

991 pull_number=job.pull_number, 

992 attempt=job.attempts, 

993 delivery_id=job.last_delivery_id, 

994 ) 

995 await asyncio.to_thread( 

996 self.store.fail, 

997 job, 

998 self.owner, 

999 str(error), 

1000 self.settings.worker_retry_max_seconds, 

1001 ) 

1002 else: 

1003 await asyncio.to_thread(self.store.complete, job, self.owner) 

1004 finally: 

1005 done.set() 

1006 await asyncio.gather(heartbeat) 

1007 

1008 async def _renew_authority_lease(self, job: ClaimedAuthorityJob, done: asyncio.Event) -> None: 

1009 interval = max(1.0, self.settings.worker_lease_seconds / 3) 

1010 while not done.is_set(): 1010 ↛ exitline 1010 didn't return from function '_renew_authority_lease' because the condition on line 1010 was always true

1011 try: 

1012 await asyncio.wait_for(done.wait(), interval) 

1013 return 

1014 except TimeoutError: 

1015 pass 

1016 renewed = await asyncio.to_thread( 

1017 self.store.renew_authority_claim, 

1018 job, 

1019 self.settings.worker_lease_seconds, 

1020 ) 

1021 if not renewed: 

1022 log.warning( 

1023 "authority_lease_lost", 

1024 installation_id=job.installation_id, 

1025 scope=job.repository_full_name or "installation", 

1026 ) 

1027 return 

1028 

1029 async def _execute_authority(self, job: ClaimedAuthorityJob) -> None: 

1030 if job.repository_full_name is None: 

1031 repositories = await self.evaluator.github.list_installation_repositories( 

1032 job.installation_id 

1033 ) 

1034 # Split broad work into independently retryable repository fences. 

1035 # The installation row continues to block every publication until 

1036 # all repository rows have been durably created. 

1037 for repository in repositories: 

1038 full_name = repository.get("full_name") 

1039 if not isinstance(full_name, str) or repository.get("archived") is True: 

1040 continue 

1041 if self.settings.is_organization_config_repository(full_name): 

1042 continue 

1043 await asyncio.to_thread( 

1044 self.store.enqueue_authority, 

1045 AuthorityRequest( 

1046 installation_id=job.installation_id, 

1047 repository_full_name=full_name, 

1048 base_ref=None, 

1049 reason=job.reason, 

1050 ), 

1051 ) 

1052 return 

1053 

1054 requests: list[JobRequest] = [] 

1055 full_name = job.repository_full_name 

1056 pulls = await self.evaluator.github.list_open_pulls(job.installation_id, full_name) 

1057 for pull in pulls: 

1058 number = pull.get("number") 

1059 head = pull.get("head") 

1060 base = pull.get("base") 

1061 if not isinstance(number, int) or isinstance(number, bool): 

1062 raise GitHubError("open pull response omitted its number") 

1063 if not isinstance(head, dict) or not isinstance(head.get("sha"), str): 1063 ↛ 1064line 1063 didn't jump to line 1064 because the condition on line 1063 was never true

1064 raise GitHubError("open pull response omitted its head SHA") 

1065 if job.base_ref is not None: 

1066 if not isinstance(base, dict) or not isinstance(base.get("ref"), str): 1066 ↛ 1067line 1066 didn't jump to line 1067 because the condition on line 1066 was never true

1067 raise GitHubError("open pull response omitted its base ref") 

1068 if base["ref"] != job.base_ref: 

1069 continue 

1070 request = JobRequest( 

1071 installation_id=job.installation_id, 

1072 repository_full_name=full_name, 

1073 pull_number=number, 

1074 reason=job.reason, 

1075 head_sha_hint=str(head["sha"]), 

1076 ) 

1077 await asyncio.to_thread(self.store.enqueue, request) 

1078 requests.append(request) 

1079 

1080 semaphore = asyncio.Semaphore(10) 

1081 

1082 async def revoke(request: JobRequest) -> None: 

1083 async with semaphore: 

1084 try: 

1085 invalidated = await self.evaluator.invalidate_for_trigger(request) 

1086 if invalidated: 

1087 await asyncio.to_thread(self.store.enqueue, request) 

1088 except GitHubRateLimitError: 

1089 raise 

1090 except Exception: 

1091 # Every PR was durably queued before this best-effort 

1092 # fast path. Its evaluation will revoke before collecting 

1093 # mutable authority evidence. 

1094 log.exception( 

1095 "authority_fast_revocation_deferred", 

1096 repository=request.repository_full_name, 

1097 pull_number=request.pull_number, 

1098 reason=job.reason, 

1099 ) 

1100 

1101 for offset in range(0, len(requests), 100): 

1102 outcomes = await asyncio.gather( 

1103 *(revoke(request) for request in requests[offset : offset + 100]), 

1104 return_exceptions=True, 

1105 ) 

1106 rate_limits = [ 

1107 outcome for outcome in outcomes if isinstance(outcome, GitHubRateLimitError) 

1108 ] 

1109 if rate_limits: 

1110 raise max(rate_limits, key=lambda error: error.retry_after_seconds) 

1111 

1112 async def _process_authority(self, job: ClaimedAuthorityJob) -> None: 

1113 done = asyncio.Event() 

1114 heartbeat = asyncio.create_task( 

1115 self._renew_authority_lease(job, done), 

1116 name=f"authority-lease-{job.id}", 

1117 ) 

1118 try: 

1119 await self._execute_authority(job) 

1120 except GitHubRateLimitError as error: 

1121 await asyncio.to_thread( 

1122 self.store.defer_authority, 

1123 job, 

1124 self.owner, 

1125 str(error), 

1126 error.retry_after_seconds, 

1127 ) 

1128 except asyncio.CancelledError: 

1129 raise 

1130 except Exception as error: 

1131 log.exception( 

1132 "authority_fanout_failed", 

1133 installation_id=job.installation_id, 

1134 scope=job.repository_full_name or "installation", 

1135 reason=job.reason, 

1136 ) 

1137 await asyncio.to_thread( 

1138 self.store.fail_authority, 

1139 job, 

1140 self.owner, 

1141 str(error), 

1142 self.settings.worker_retry_max_seconds, 

1143 ) 

1144 else: 

1145 await asyncio.to_thread(self.store.complete_authority, job, self.owner) 

1146 finally: 

1147 done.set() 

1148 await asyncio.gather(heartbeat) 

1149 

1150 async def run(self, stop: asyncio.Event) -> None: 

1151 """Run the worker loop.""" 

1152 while not stop.is_set(): 

1153 try: 

1154 QUEUE_DEPTH.set(await asyncio.to_thread(self.store.pending_count)) 

1155 DEAD_JOBS.set(await asyncio.to_thread(self.store.dead_count)) 

1156 authority_job = await asyncio.to_thread( 

1157 self.store.claim_authority, 

1158 self.owner, 

1159 self.settings.worker_lease_seconds, 

1160 ) 

1161 if authority_job is not None: 

1162 await self._process_authority(authority_job) 

1163 continue 

1164 job = await asyncio.to_thread( 

1165 self.store.claim, self.owner, self.settings.worker_lease_seconds 

1166 ) 

1167 if job is not None: 

1168 await self._process(job) 

1169 continue 

1170 except asyncio.CancelledError: 

1171 raise 

1172 except Exception: 

1173 # A transient database failure must not silently kill the task 

1174 # while process liveness continues to report healthy. 

1175 log.exception("worker_loop_failed") 

1176 

1177 if not stop.is_set(): 1177 ↛ 1178line 1177 didn't jump to line 1178 because the condition on line 1177 was never true

1178 with suppress(TimeoutError): 

1179 await asyncio.wait_for(stop.wait(), self.settings.worker_poll_seconds) 

1180 

1181 

1182class Reconciler: 

1183 """Periodically enqueue every open pull request to recover missed webhooks.""" 

1184 

1185 def __init__( 

1186 self, settings: Settings, github: GitHubClient, store: QueueStore, owner: str 

1187 ) -> None: 

1188 self.settings = settings 

1189 self.github = github 

1190 self.store = store 

1191 self.owner = owner 

1192 

1193 async def _renew_lease( 

1194 self, 

1195 done: asyncio.Event, 

1196 lost: asyncio.Event, 

1197 lease_seconds: int, 

1198 ) -> None: 

1199 interval = max(1.0, lease_seconds / 3) 

1200 while not done.is_set(): 1200 ↛ exitline 1200 didn't return from function '_renew_lease' because the condition on line 1200 was always true

1201 try: 

1202 await asyncio.wait_for(done.wait(), interval) 

1203 return 

1204 except TimeoutError: 

1205 pass 

1206 try: 

1207 renewed = await asyncio.to_thread( 

1208 self.store.acquire_service_lease, 

1209 "open-pr-reconciler", 

1210 self.owner, 

1211 lease_seconds, 

1212 ) 

1213 except Exception: 

1214 log.exception("reconciliation_lease_renewal_failed") 

1215 lost.set() 

1216 return 

1217 if not renewed: 

1218 log.warning("reconciliation_lease_lost") 

1219 lost.set() 

1220 return 

1221 

1222 async def reconcile_once(self) -> int: 

1223 """Discover and enqueue open pull requests across installations.""" 

1224 lease_seconds = max(300, self.settings.reconcile_interval_seconds * 2) 

1225 acquired = await asyncio.to_thread( 

1226 self.store.acquire_service_lease, "open-pr-reconciler", self.owner, lease_seconds 

1227 ) 

1228 if not acquired: 1228 ↛ 1229line 1228 didn't jump to line 1229 because the condition on line 1228 was never true

1229 return 0 

1230 done = asyncio.Event() 

1231 lost = asyncio.Event() 

1232 heartbeat = asyncio.create_task( 

1233 self._renew_lease(done, lost, lease_seconds), 

1234 name="reconciler-lease", 

1235 ) 

1236 try: 

1237 return await self._reconcile_owned(lost) 

1238 finally: 

1239 done.set() 

1240 await asyncio.gather(heartbeat) 

1241 

1242 async def _reconcile_owned(self, lost: asyncio.Event) -> int: 

1243 """Perform one reconciliation while the heartbeat owns the lease.""" 

1244 retention_boundary = datetime.now(UTC) - timedelta( 

1245 days=self.settings.webhook_delivery_retention_days 

1246 ) 

1247 pruned = await asyncio.to_thread(self.store.prune_deliveries, retention_boundary) 

1248 if pruned: 1248 ↛ 1249line 1248 didn't jump to line 1249 because the condition on line 1248 was never true

1249 log.info("webhook_deliveries_pruned", deliveries=pruned) 

1250 queued = 0 

1251 for installation in await self.github.list_installations(): 

1252 if lost.is_set(): 1252 ↛ 1253line 1252 didn't jump to line 1253 because the condition on line 1252 was never true

1253 return queued 

1254 installation_id = installation.get("id") 

1255 if not isinstance(installation_id, int) or installation.get("suspended_at") is not None: 1255 ↛ 1256line 1255 didn't jump to line 1256 because the condition on line 1255 was never true

1256 continue 

1257 try: 

1258 repositories = await self.github.list_installation_repositories(installation_id) 

1259 for repository in repositories: 

1260 if lost.is_set(): 1260 ↛ 1261line 1260 didn't jump to line 1261 because the condition on line 1260 was never true

1261 return queued 

1262 full_name = repository.get("full_name") 

1263 if not isinstance(full_name, str) or repository.get("archived") is True: 1263 ↛ 1264line 1263 didn't jump to line 1264 because the condition on line 1263 was never true

1264 continue 

1265 if self.settings.is_organization_config_repository(full_name): 1265 ↛ 1266line 1265 didn't jump to line 1266 because the condition on line 1265 was never true

1266 continue 

1267 for pull in await self.github.list_open_pulls(installation_id, full_name): 

1268 number = pull.get("number") 

1269 head = pull.get("head") 

1270 if not isinstance(number, int): 1270 ↛ 1271line 1270 didn't jump to line 1271 because the condition on line 1270 was never true

1271 continue 

1272 added = await asyncio.to_thread( 

1273 self.store.enqueue_if_absent, 

1274 JobRequest( 

1275 installation_id=installation_id, 

1276 repository_full_name=full_name, 

1277 pull_number=number, 

1278 reason="periodic_reconciliation", 

1279 head_sha_hint=( 

1280 head.get("sha") 

1281 if isinstance(head, dict) and isinstance(head.get("sha"), str) 

1282 else None 

1283 ), 

1284 ), 

1285 ) 

1286 queued += int(added) 

1287 except Exception: 

1288 log.exception("installation_reconciliation_failed", installation_id=installation_id) 

1289 return queued 

1290 

1291 async def run(self, stop: asyncio.Event) -> None: 

1292 """Reconcile immediately and then at the configured interval.""" 

1293 while not stop.is_set(): 

1294 try: 

1295 count = await self.reconcile_once() 

1296 RECONCILIATIONS.labels("success").inc() 

1297 RECONCILIATION_LAST_SUCCESS.set_to_current_time() 

1298 log.info("reconciliation_complete", pull_requests_queued=count) 

1299 except Exception: 

1300 RECONCILIATIONS.labels("failure").inc() 

1301 log.exception("reconciliation_failed") 

1302 with suppress(TimeoutError): 

1303 await asyncio.wait_for(stop.wait(), self.settings.reconcile_interval_seconds)