imbi-api 2.32.2__tar.gz → 2.33.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. {imbi_api-2.32.2 → imbi_api-2.33.0}/PKG-INFO +2 -2
  2. {imbi_api-2.32.2 → imbi_api-2.33.0}/pyproject.toml +2 -2
  3. imbi_api-2.33.0/src/imbi/api/commit_pushed_at_repair.py +249 -0
  4. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/dashboard.py +26 -2
  5. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_deployments.py +98 -0
  6. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/projects.py +4 -2
  7. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/pull_requests.py +41 -18
  8. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/releases.py +198 -44
  9. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/maintenance/operations.py +79 -0
  10. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/maintenance/registry.py +35 -0
  11. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_dashboard.py +19 -0
  12. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_deployments.py +140 -0
  13. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_projects_helpers.py +2 -0
  14. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_pull_requests.py +93 -0
  15. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_releases.py +235 -8
  16. imbi_api-2.33.0/tests/test_commit_pushed_at_repair.py +292 -0
  17. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_maintenance_operations.py +97 -0
  18. {imbi_api-2.32.2 → imbi_api-2.33.0}/.gitignore +0 -0
  19. {imbi_api-2.32.2 → imbi_api-2.33.0}/README.md +0 -0
  20. {imbi_api-2.32.2 → imbi_api-2.33.0}/moon.yml +0 -0
  21. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/README.md +0 -0
  22. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/__init__.py +0 -0
  23. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/app.py +0 -0
  24. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/__init__.py +0 -0
  25. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/authorization_codes.py +0 -0
  26. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/autonomous.py +0 -0
  27. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/internal_services.py +0 -0
  28. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/local_auth.py +0 -0
  29. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/login_providers.py +0 -0
  30. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/membership.py +0 -0
  31. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/models.py +0 -0
  32. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/oauth.py +0 -0
  33. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/oauth_clients.py +0 -0
  34. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/password.py +0 -0
  35. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/permissions.py +0 -0
  36. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/principals.py +0 -0
  37. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/seed.py +0 -0
  38. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/sessions.py +0 -0
  39. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/tokens.py +0 -0
  40. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/totp.py +0 -0
  41. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/backfill_embeddings.py +0 -0
  42. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/blueprint_attributes.py +0 -0
  43. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/blueprint_compliance.py +0 -0
  44. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/commit_sync/__init__.py +0 -0
  45. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/commit_sync/queue.py +0 -0
  46. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/commit_sync/service.py +0 -0
  47. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/component_facts.py +0 -0
  48. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/deployment_status_repair.py +0 -0
  49. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/deployment_sweeper.py +0 -0
  50. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/deployment_sync/__init__.py +0 -0
  51. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/deployment_sync/queue.py +0 -0
  52. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/deployment_sync/service.py +0 -0
  53. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/documents/__init__.py +0 -0
  54. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/documents/read_sweeper.py +0 -0
  55. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/domain/__init__.py +0 -0
  56. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/domain/models.py +0 -0
  57. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/domain/scoring.py +0 -0
  58. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/drift.py +0 -0
  59. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/__init__.py +0 -0
  60. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/client.py +0 -0
  61. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/dependencies.py +0 -0
  62. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/models.py +0 -0
  63. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/templates/base.html +0 -0
  64. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/templates/base.txt +0 -0
  65. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/templates/password_reset.html +0 -0
  66. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/templates/password_reset.txt +0 -0
  67. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/templates/welcome.html +0 -0
  68. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/templates/welcome.txt +0 -0
  69. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/templates.py +0 -0
  70. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/__init__.py +0 -0
  71. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_credentials.py +0 -0
  72. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_document_events.py +0 -0
  73. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_document_history.py +0 -0
  74. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_document_reads.py +0 -0
  75. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_document_scope.py +0 -0
  76. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_helpers.py +0 -0
  77. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_json_fields.py +0 -0
  78. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_pagination.py +0 -0
  79. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_request_urls.py +0 -0
  80. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_search_index.py +0 -0
  81. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/admin.py +0 -0
  82. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/admin_plugins.py +0 -0
  83. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/ai_models.py +0 -0
  84. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/ai_providers.py +0 -0
  85. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/api_keys.py +0 -0
  86. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/auth.py +0 -0
  87. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/auth_providers.py +0 -0
  88. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/blueprints.py +0 -0
  89. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/client_credentials.py +0 -0
  90. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/comments.py +0 -0
  91. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/components.py +0 -0
  92. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/document_analytics.py +0 -0
  93. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/document_likes.py +0 -0
  94. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/document_presence.py +0 -0
  95. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/document_templates.py +0 -0
  96. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/document_versions.py +0 -0
  97. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/documents.py +0 -0
  98. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/environments.py +0 -0
  99. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/events.py +0 -0
  100. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/graph_query.py +0 -0
  101. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/iggy_connectors.py +0 -0
  102. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/integrations.py +0 -0
  103. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/link_definitions.py +0 -0
  104. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/local_auth.py +0 -0
  105. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/maintenance.py +0 -0
  106. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/mcp_servers.py +0 -0
  107. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/mfa.py +0 -0
  108. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/oauth_metadata.py +0 -0
  109. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/operations_log.py +0 -0
  110. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/organizations.py +0 -0
  111. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/plugin_edges.py +0 -0
  112. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/plugin_entities.py +0 -0
  113. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/plugins.py +0 -0
  114. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_analysis.py +0 -0
  115. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_commit_sync.py +0 -0
  116. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_configuration.py +0 -0
  117. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_incidents.py +0 -0
  118. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_integrations.py +0 -0
  119. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_lifecycle.py +0 -0
  120. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_logs.py +0 -0
  121. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_plugins.py +0 -0
  122. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_pr_sync.py +0 -0
  123. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_types.py +0 -0
  124. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/roles.py +0 -0
  125. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/sa_api_keys.py +0 -0
  126. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/scoring.py +0 -0
  127. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/scoring_policies.py +0 -0
  128. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/search.py +0 -0
  129. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/service_accounts.py +0 -0
  130. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/status.py +0 -0
  131. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/tags.py +0 -0
  132. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/teams.py +0 -0
  133. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/uploads.py +0 -0
  134. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/user_activity.py +0 -0
  135. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/users.py +0 -0
  136. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/webhooks.py +0 -0
  137. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/entrypoint.py +0 -0
  138. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/graph_sql.py +0 -0
  139. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/__init__.py +0 -0
  140. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/attribution.py +0 -0
  141. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/endpoints.py +0 -0
  142. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/errors.py +0 -0
  143. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/flows.py +0 -0
  144. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/host_integration.py +0 -0
  145. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/models.py +0 -0
  146. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/repository.py +0 -0
  147. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/resolution.py +0 -0
  148. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/state.py +0 -0
  149. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/sweeper.py +0 -0
  150. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/lifespans.py +0 -0
  151. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/llm/__init__.py +0 -0
  152. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/llm/dependencies.py +0 -0
  153. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/maintenance/__init__.py +0 -0
  154. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/maintenance/log.py +0 -0
  155. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/maintenance/state.py +0 -0
  156. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/maintenance/worker.py +0 -0
  157. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/mcp_test.py +0 -0
  158. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/middleware/__init__.py +0 -0
  159. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/middleware/rate_limit.py +0 -0
  160. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/models.py +0 -0
  161. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/openapi.py +0 -0
  162. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/orphan_releases.py +0 -0
  163. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/__init__.py +0 -0
  164. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/assignment_writer.py +0 -0
  165. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/assignments.py +0 -0
  166. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/credentials.py +0 -0
  167. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/installer.py +0 -0
  168. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/lifecycle.py +0 -0
  169. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/lifecycle_dispatch.py +0 -0
  170. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/reload.py +0 -0
  171. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/resolution.py +0 -0
  172. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/schemas.py +0 -0
  173. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/pr_sync/__init__.py +0 -0
  174. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/pr_sync/queue.py +0 -0
  175. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/pr_sync/service.py +0 -0
  176. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/prompts/__init__.py +0 -0
  177. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/prompts/release_notes_system.md +0 -0
  178. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/py.typed +0 -0
  179. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/relationships.py +0 -0
  180. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/release_promote/__init__.py +0 -0
  181. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/release_promote/queue.py +0 -0
  182. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/release_promote/service.py +0 -0
  183. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/sbom.py +0 -0
  184. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/scoring/__init__.py +0 -0
  185. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/scoring/queue.py +0 -0
  186. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/settings.py +0 -0
  187. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/storage/__init__.py +0 -0
  188. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/storage/client.py +0 -0
  189. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/storage/dependencies.py +0 -0
  190. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/storage/thumbnails.py +0 -0
  191. {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/storage/validation.py +0 -0
  192. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/__init__.py +0 -0
  193. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/__init__.py +0 -0
  194. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_authentication.py +0 -0
  195. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_authorization.py +0 -0
  196. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_encryption.py +0 -0
  197. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_internal_services.py +0 -0
  198. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_login_providers.py +0 -0
  199. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_membership.py +0 -0
  200. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_oauth.py +0 -0
  201. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_oauth_server.py +0 -0
  202. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_permissions.py +0 -0
  203. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_seed.py +0 -0
  204. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_sessions.py +0 -0
  205. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_totp.py +0 -0
  206. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/clickhouse/__init__.py +0 -0
  207. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/clickhouse/test_client.py +0 -0
  208. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/clickhouse/test_init.py +0 -0
  209. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/clickhouse/test_privacy.py +0 -0
  210. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/email/__init__.py +0 -0
  211. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/email/test_client.py +0 -0
  212. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/email/test_init.py +0 -0
  213. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/email/test_integration.py +0 -0
  214. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/email/test_models.py +0 -0
  215. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/email/test_templates.py +0 -0
  216. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/__init__.py +0 -0
  217. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_admin.py +0 -0
  218. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_admin_plugins.py +0 -0
  219. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_ai_models.py +0 -0
  220. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_ai_providers.py +0 -0
  221. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_api_keys.py +0 -0
  222. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_auth.py +0 -0
  223. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_auth_providers.py +0 -0
  224. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_blueprints.py +0 -0
  225. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_client_credentials.py +0 -0
  226. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_comments.py +0 -0
  227. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_components.py +0 -0
  228. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_credentials.py +0 -0
  229. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_document_analytics.py +0 -0
  230. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_document_likes.py +0 -0
  231. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_document_presence.py +0 -0
  232. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_document_reads.py +0 -0
  233. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_document_reads_sql.py +0 -0
  234. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_document_templates.py +0 -0
  235. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_document_versions.py +0 -0
  236. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_documents.py +0 -0
  237. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_environments.py +0 -0
  238. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_events.py +0 -0
  239. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_graph_query.py +0 -0
  240. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_iggy_connectors.py +0 -0
  241. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_init.py +0 -0
  242. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_integrations.py +0 -0
  243. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_json_fields.py +0 -0
  244. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_link_definitions.py +0 -0
  245. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_local_auth.py +0 -0
  246. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_maintenance.py +0 -0
  247. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_mcp_servers.py +0 -0
  248. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_mfa.py +0 -0
  249. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_operations_log.py +0 -0
  250. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_organizations.py +0 -0
  251. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_pagination.py +0 -0
  252. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_plugin_label_validation.py +0 -0
  253. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_plugins.py +0 -0
  254. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_analysis.py +0 -0
  255. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_commit_sync.py +0 -0
  256. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_configuration.py +0 -0
  257. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_deployments_autonomous.py +0 -0
  258. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_incidents.py +0 -0
  259. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_integrations.py +0 -0
  260. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_lifecycle.py +0 -0
  261. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_logs.py +0 -0
  262. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_plugins.py +0 -0
  263. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_response_surfacing.py +0 -0
  264. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_types.py +0 -0
  265. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_projects.py +0 -0
  266. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_request_urls.py +0 -0
  267. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_roles.py +0 -0
  268. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_sa_api_keys.py +0 -0
  269. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_scoring.py +0 -0
  270. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_scoring_policies.py +0 -0
  271. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_search.py +0 -0
  272. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_search_index.py +0 -0
  273. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_service_accounts.py +0 -0
  274. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_service_writeback.py +0 -0
  275. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_status.py +0 -0
  276. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_tags.py +0 -0
  277. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_teams.py +0 -0
  278. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_uploads.py +0 -0
  279. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_user_activity.py +0 -0
  280. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_users.py +0 -0
  281. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_webhooks.py +0 -0
  282. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/fixtures/sbom/npm-realistic.json +0 -0
  283. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/fixtures/sbom/pypi-realistic.json +0 -0
  284. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/fixtures/sbom/tiny.json +0 -0
  285. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/__init__.py +0 -0
  286. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_endpoints.py +0 -0
  287. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_errors.py +0 -0
  288. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_flows.py +0 -0
  289. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_host_integration.py +0 -0
  290. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_repository.py +0 -0
  291. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_resolution.py +0 -0
  292. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_state.py +0 -0
  293. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_sweeper.py +0 -0
  294. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/middleware/test_rate_limit.py +0 -0
  295. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/storage/__init__.py +0 -0
  296. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/storage/test_client.py +0 -0
  297. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/storage/test_init.py +0 -0
  298. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/storage/test_thumbnails.py +0 -0
  299. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/storage/test_validation.py +0 -0
  300. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/support.py +0 -0
  301. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_app.py +0 -0
  302. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_assignment_writer.py +0 -0
  303. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_backfill_embeddings.py +0 -0
  304. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_blueprint_attributes.py +0 -0
  305. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_blueprint_compliance.py +0 -0
  306. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_blueprints.py +0 -0
  307. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_commit_sync_queue.py +0 -0
  308. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_commit_sync_service.py +0 -0
  309. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_component_facts.py +0 -0
  310. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_deployment_status_repair.py +0 -0
  311. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_deployment_sweeper.py +0 -0
  312. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_deployment_sync_queue.py +0 -0
  313. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_deployment_sync_service.py +0 -0
  314. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_drift.py +0 -0
  315. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_entrypoint.py +0 -0
  316. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_graph_sql.py +0 -0
  317. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_identity_attribution.py +0 -0
  318. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_init.py +0 -0
  319. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_installer.py +0 -0
  320. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_lifecycle_dispatch.py +0 -0
  321. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_lifespans.py +0 -0
  322. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_maintenance_log.py +0 -0
  323. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_maintenance_state.py +0 -0
  324. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_maintenance_worker.py +0 -0
  325. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_models.py +0 -0
  326. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_openapi.py +0 -0
  327. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_orphan_releases.py +0 -0
  328. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_plugin_schemas.py +0 -0
  329. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_plugins.py +0 -0
  330. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_plugins_assignments.py +0 -0
  331. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_plugins_credentials.py +0 -0
  332. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_pr_sync_queue.py +0 -0
  333. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_pr_sync_service.py +0 -0
  334. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_projects_drift_ranges.py +0 -0
  335. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_release_promote_service.py +0 -0
  336. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_sbom.py +0 -0
  337. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_scoring_queue.py +0 -0
  338. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_scoring_triggers.py +0 -0
  339. {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: imbi-api
3
- Version: 2.32.2
3
+ Version: 2.33.0
4
4
  Summary: Imbi is a DevOps Service Management Platform designed to provide an efficient way to manage a large environment that contains many services and applications.
5
5
  Author-email: Alex Campbell <alexc@aweber.com>, Dave Shawley <daves@aweber.com>, "Gavin M. Roy" <gavinr@aweber.com>
6
6
  License-Expression: BSD-3-Clause
@@ -19,7 +19,7 @@ Requires-Dist: cyclonedx-python-lib<12,>=11.7
19
19
  Requires-Dist: fastapi
20
20
  Requires-Dist: filetype
21
21
  Requires-Dist: httpx<1,>=0.28.1
22
- Requires-Dist: imbi-common[databases,llm,sentry,server]==2.32.2
22
+ Requires-Dist: imbi-common[databases,llm,sentry,server]==2.33.0
23
23
  Requires-Dist: jinja2
24
24
  Requires-Dist: jsonpatch>=1.33
25
25
  Requires-Dist: mcp>=1.26.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "imbi-api"
3
- version = "2.32.2"
3
+ version = "2.33.0"
4
4
  description = "Imbi is a DevOps Service Management Platform designed to provide an efficient way to manage a large environment that contains many services and applications."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -28,7 +28,7 @@ dependencies = [
28
28
  "fastapi",
29
29
  "filetype",
30
30
  "httpx>=0.28.1,<1",
31
- "imbi-common[databases,llm,sentry,server]==2.32.2",
31
+ "imbi-common[databases,llm,sentry,server]==2.33.0",
32
32
  "jinja2",
33
33
  "jsonpatch>=1.33",
34
34
  "mcp>=1.26.0",
@@ -0,0 +1,249 @@
1
+ """Put back the push time commit re-syncs overwrote.
2
+
3
+ Every commit-sync path used to stamp ``commits.pushed_at`` with the
4
+ clock at ingest, and the table is a ``ReplacingMergeTree`` versioned on
5
+ ``recorded_at`` -- so any re-sync of a commit already stored replaced
6
+ its row with a fresh ``pushed_at``. Two producers did that at scale:
7
+
8
+ * the full backfill (``sync_all_history``, the ``commit-sync``
9
+ maintenance operation), which stamps every commit a project has with
10
+ the single instant the run started. Three runs in August 2026 left
11
+ roughly 19,000 commits across 600 projects sharing a handful of
12
+ timestamps, their push order collapsed onto author order;
13
+ * the ``workflow_run`` webhook rule, which re-syncs a run's head commit
14
+ when the run completes. A release workflow runs against the commit
15
+ a release was *cut from*, so that commit was re-stamped minutes after
16
+ the release commit itself and sorted above it (#308).
17
+
18
+ ``pushed_at`` orders the recent-commits feed, picks the commit-sync
19
+ gap-healing base, and picks the release-drift head, so the wrong values
20
+ mis-slice release ranges. The sync no longer re-stamps a stored commit,
21
+ but nothing that reads GitHub can put the old values back: the commits
22
+ API carries author and committer dates, never a push time.
23
+
24
+ The ``events`` table can. It holds every webhook delivery, and a
25
+ ``push`` delivery names the commits it carried (``payload.commits[]``,
26
+ plus ``payload.after`` for the head) and the branch it was for
27
+ (``payload.ref``). Its ``recorded_at`` is within about a second of the
28
+ ``pushed_at`` the sync originally wrote from it. So for each stored
29
+ commit, the earliest push delivery to the commit's own branch that
30
+ lists it is what its ``pushed_at`` was before anything overwrote it.
31
+
32
+ Deliberately conservative:
33
+
34
+ * only pushes to the branch the commit is stored under count. A pull
35
+ request's commits are also pushed to their feature branch, earlier,
36
+ and the sync's branch-gated rule never recorded those -- matching
37
+ them would order the commits by when their branch was pushed rather
38
+ than by when they reached the default branch, which is a different
39
+ wrong order.
40
+ * a row is rewritten only when its ``pushed_at`` is *later* than the
41
+ delivery by more than :data:`TOLERANCE`. Within tolerance it is the
42
+ original stamp; earlier means an even earlier sync recorded it and
43
+ that value is closer to the truth.
44
+ * a commit with no push delivery on record is left alone. Deliveries
45
+ only go back to June 2026, GitHub lists at most twenty commits per
46
+ push payload, and a compare-healed gap has no delivery of its own.
47
+ There is nothing to restore, and inventing a value would be worse
48
+ than the collapsed one.
49
+
50
+ The rewrite is a re-insert of the full row with the corrected
51
+ ``pushed_at`` and a fresh ``recorded_at`` -- the same read-modify-insert
52
+ ``opslog-backfill`` uses -- so ``FINAL`` readers see it at once and the
53
+ background merge collapses the superseded version. It composes with
54
+ the sync's carry-forward, which reads the *earliest* ``pushed_at``
55
+ across row versions: once the repaired row is on disk, every later
56
+ re-sync of that commit inherits the restored value.
57
+
58
+ There is no lock between this and a webhook re-sync, and the two are not
59
+ serialized: a re-sync that read the stored value *before* the repaired
60
+ row landed and inserted *after* it writes the stale value back with a
61
+ newer ``recorded_at``, and that version wins. The window is the few
62
+ milliseconds between the sync's lookup and its insert, it has to
63
+ coincide with a delivery for that exact commit, and the outcome is one
64
+ row back where it started rather than anything new being wrong. The
65
+ repair is idempotent, so the remedy is to run it again; the
66
+ ``commit-pushed-at-check`` dry run after a repair is what shows whether
67
+ anything did slip through. A shared lock would have to span the API
68
+ worker and the gateway's webhook handler for a race this narrow and
69
+ this cheaply undone, which is not worth the coupling.
70
+
71
+ Runs as the ``commit-pushed-at-repair`` maintenance operation, with
72
+ ``commit-pushed-at-check`` as its dry run.
73
+ """
74
+
75
+ from __future__ import annotations
76
+
77
+ import datetime
78
+ import logging
79
+ import typing
80
+
81
+ import pydantic
82
+
83
+ from imbi.common import clickhouse, models
84
+
85
+ LOGGER = logging.getLogger(__name__)
86
+
87
+ #: How far a stored ``pushed_at`` may trail its push delivery before it
88
+ #: counts as re-stamped. The webhook sync stamps ``now()`` while
89
+ #: handling the delivery, so the original value lands within a second
90
+ #: or two of ``events.recorded_at``; the smallest re-stamp -- a CI run
91
+ #: completing -- is minutes later.
92
+ TOLERANCE = datetime.timedelta(seconds=60)
93
+
94
+ _BRANCH_REF_PREFIX = 'refs/heads/'
95
+
96
+ #: Every column ``CommitRecord`` carries except ``recorded_at``, which
97
+ #: the re-insert regenerates. Listed rather than ``SELECT *`` so a
98
+ #: column added to the table later is a loud failure here, not a row
99
+ #: silently rewritten with a default.
100
+ _COMMIT_COLUMNS: typing.Final = (
101
+ 'project_id',
102
+ 'sha',
103
+ 'short_sha',
104
+ 'ref',
105
+ 'message',
106
+ 'author_name',
107
+ 'author_email',
108
+ 'author_login',
109
+ 'author_user',
110
+ 'committer_name',
111
+ 'ci_status',
112
+ 'authored_at',
113
+ 'committed_at',
114
+ 'url',
115
+ 'pushed_at',
116
+ )
117
+
118
+ # ``payload`` is a JSON column; ``payload.commits[]`` is its array
119
+ # subcolumn and ``c.id`` the per-element path. ``metadata.event_type``
120
+ # is the gateway's per-source label ('push'), distinct from the
121
+ # top-level ``type`` category ('webhook').
122
+ _DELIVERIES_SQL: typing.Final = (
123
+ 'SELECT recorded_at, '
124
+ 'toString(payload.ref) AS ref, '
125
+ 'toString(payload.after) AS after, '
126
+ 'arrayMap(c -> toString(c.id), payload.commits[]) AS commit_ids '
127
+ 'FROM events '
128
+ 'WHERE project_id = {project_id:String} '
129
+ "AND type = 'webhook' "
130
+ "AND toString(metadata.event_type) = 'push' "
131
+ "AND toString(payload.ref) LIKE 'refs/heads/%'"
132
+ )
133
+
134
+ # Column list is a module constant; values are bound parameters.
135
+ _COMMITS_SQL: typing.Final = (
136
+ 'SELECT ' + ', '.join(_COMMIT_COLUMNS) + ' FROM commits FINAL ' # noqa: S608
137
+ 'WHERE project_id = {project_id:String} '
138
+ 'AND sha IN {shas:Array(String)}'
139
+ )
140
+
141
+
142
+ class RepairSummary(typing.NamedTuple):
143
+ """What one project's repair did (or, dry run, would do)."""
144
+
145
+ #: Push deliveries to a branch found for the project.
146
+ deliveries: int = 0
147
+ #: Stored commits a delivery to their own branch accounts for.
148
+ examined: int = 0
149
+ #: Rows rewritten -- or, on a dry run, rows that would be.
150
+ repaired: int = 0
151
+ #: Largest correction applied, in seconds. Tells an operator
152
+ #: whether a project's damage was the backfill (days) or a
153
+ #: ``workflow_run`` re-stamp (minutes).
154
+ largest_shift_seconds: int = 0
155
+
156
+
157
+ async def _push_times(
158
+ project_id: str,
159
+ ) -> tuple[int, dict[tuple[str, str], datetime.datetime]]:
160
+ """Push deliveries read, and ``(branch, sha) -> earliest delivery``.
161
+
162
+ A commit pushed to several branches (a feature branch, then the
163
+ default branch on merge) gets one entry per branch, so the caller
164
+ can pick the delivery for the branch the commit is stored under.
165
+ """
166
+ rows = await clickhouse.query(_DELIVERIES_SQL, {'project_id': project_id})
167
+ earliest: dict[tuple[str, str], datetime.datetime] = {}
168
+ for row in rows:
169
+ recorded_at = clickhouse.as_utc_or_none(row.get('recorded_at'))
170
+ ref = str(row.get('ref') or '')
171
+ if recorded_at is None or not ref.startswith(_BRANCH_REF_PREFIX):
172
+ continue
173
+ branch = ref[len(_BRANCH_REF_PREFIX) :]
174
+ listed = typing.cast('list[object]', row.get('commit_ids') or [])
175
+ shas = {str(sha) for sha in listed if sha}
176
+ # ``after`` is the head the delivery attests to even when the
177
+ # payload's commit list is capped (GitHub sends at most twenty)
178
+ # or empty (a branch created from an existing commit).
179
+ after = str(row.get('after') or '')
180
+ if after and after != '0' * 40:
181
+ shas.add(after)
182
+ for sha in shas:
183
+ key = (branch, sha.lower())
184
+ if key not in earliest or recorded_at < earliest[key]:
185
+ earliest[key] = recorded_at
186
+ return len(rows), earliest
187
+
188
+
189
+ def _repaired_record(
190
+ row: dict[str, typing.Any], pushed_at: datetime.datetime
191
+ ) -> models.CommitRecord:
192
+ """The row as a ``CommitRecord`` carrying the restored push time.
193
+
194
+ ``recorded_at`` is left to the model's default so the re-insert
195
+ ranks newest under ``ReplacingMergeTree(recorded_at)``.
196
+ """
197
+ fields = {name: row.get(name) for name in _COMMIT_COLUMNS}
198
+ fields['authored_at'] = clickhouse.as_utc_or_none(row.get('authored_at'))
199
+ fields['committed_at'] = clickhouse.as_utc_or_none(row.get('committed_at'))
200
+ fields['pushed_at'] = pushed_at
201
+ return models.CommitRecord.model_validate(fields)
202
+
203
+
204
+ async def repair_project(
205
+ project_id: str, *, dry_run: bool = False
206
+ ) -> RepairSummary:
207
+ """Restore one project's re-stamped commit push times.
208
+
209
+ Idempotent: a repaired row sits within :data:`TOLERANCE` of its
210
+ delivery, so a second run finds nothing to rewrite.
211
+ """
212
+ deliveries, push_times = await _push_times(project_id)
213
+ if not push_times:
214
+ return RepairSummary(deliveries=deliveries)
215
+ shas = sorted({sha for _, sha in push_times})
216
+ rows = await clickhouse.query(
217
+ _COMMITS_SQL, {'project_id': project_id, 'shas': shas}
218
+ )
219
+ examined = 0
220
+ largest = datetime.timedelta(0)
221
+ repaired: list[pydantic.BaseModel] = []
222
+ for row in rows:
223
+ sha = str(row.get('sha') or '').lower()
224
+ branch = str(row.get('ref') or '')
225
+ delivered = push_times.get((branch, sha))
226
+ if delivered is None:
227
+ continue # only pushed to some other branch; see module doc
228
+ examined += 1
229
+ stored = clickhouse.as_utc_or_none(row.get('pushed_at'))
230
+ if stored is None:
231
+ continue
232
+ shift = stored - delivered
233
+ if shift <= TOLERANCE:
234
+ continue
235
+ largest = max(largest, shift)
236
+ repaired.append(_repaired_record(row, delivered))
237
+ if repaired and not dry_run:
238
+ await clickhouse.insert('commits', repaired)
239
+ LOGGER.info(
240
+ 'restored pushed_at on %d commit(s) for project %s',
241
+ len(repaired),
242
+ project_id,
243
+ )
244
+ return RepairSummary(
245
+ deliveries=deliveries,
246
+ examined=examined,
247
+ repaired=len(repaired),
248
+ largest_shift_seconds=int(largest.total_seconds()),
249
+ )
@@ -289,8 +289,24 @@ def _build_series(
289
289
  return MetricSeries(total=sum(daily), daily=daily)
290
290
 
291
291
 
292
+ async def _active_project_ids(db: graph.Graph) -> list[str]:
293
+ """IDs of every project that is not archived."""
294
+ query: typing.LiteralString = """
295
+ MATCH (p:Project)
296
+ WHERE coalesce(p.archived, false) = false
297
+ RETURN p.id AS id
298
+ """
299
+ records = await db.execute(query, {}, ['id'])
300
+ return [
301
+ graph.parse_agtype(r['id'])
302
+ for r in records
303
+ if graph.parse_agtype(r['id'])
304
+ ]
305
+
306
+
292
307
  @dashboard_router.get('/metrics', response_model=DashboardMetrics)
293
308
  async def get_dashboard_metrics(
309
+ db: graph.Pool,
294
310
  auth: typing.Annotated[
295
311
  permissions.AuthContext,
296
312
  fastapi.Depends(
@@ -298,7 +314,10 @@ async def get_dashboard_metrics(
298
314
  ),
299
315
  ],
300
316
  ) -> DashboardMetrics:
301
- """Return 7-day activity metrics with per-day counts for the tiles."""
317
+ """Return 7-day activity metrics with per-day counts for the tiles.
318
+
319
+ The pull request series skips drafts and archived projects.
320
+ """
302
321
  today = datetime.datetime.now(datetime.UTC).date()
303
322
  start_date = today - datetime.timedelta(days=_METRICS_WINDOW_DAYS - 1)
304
323
  since = datetime.datetime.combine(
@@ -308,7 +327,10 @@ async def get_dashboard_metrics(
308
327
  (start_date + datetime.timedelta(days=offset)).isoformat()
309
328
  for offset in range(_METRICS_WINDOW_DAYS)
310
329
  ]
311
- params: dict[str, typing.Any] = {'since': since}
330
+ params: dict[str, typing.Any] = {
331
+ 'since': since,
332
+ 'project_ids': await _active_project_ids(db),
333
+ }
312
334
  # One scan of the (FINAL, merge-on-read) deploy rows yields both the
313
335
  # per-day series and the per-environment rollup.
314
336
  deploys, events, ops, prs = await asyncio.gather(
@@ -339,6 +361,8 @@ async def get_dashboard_metrics(
339
361
  'SELECT toDate(created_at) AS day, count() AS c'
340
362
  ' FROM pull_requests FINAL'
341
363
  ' WHERE created_at >= {since:DateTime64(3)}'
364
+ ' AND NOT draft'
365
+ ' AND project_id IN {project_ids:Array(String)}'
342
366
  ' GROUP BY day',
343
367
  params,
344
368
  ),
@@ -1545,6 +1545,104 @@ async def fetch_release_notes_for_tag(
1545
1545
  )
1546
1546
 
1547
1547
 
1548
+ async def resolve_committish_for_tag(
1549
+ db: graph.Graph,
1550
+ *,
1551
+ org_slug: str,
1552
+ project_id: str,
1553
+ tag: str,
1554
+ auth: permissions.AuthContext,
1555
+ ) -> str | None:
1556
+ """Resolve ``tag`` to the short committish it points at.
1557
+
1558
+ The release-create path uses this when a caller supplies a tag but no
1559
+ commit -- a source host's ``release`` webhook names the tag and the
1560
+ branch it was cut from, never the SHA. The lookup goes through the
1561
+ deployment capability rather than the ClickHouse ``tags`` table
1562
+ because the two race: the ``release`` delivery lands before commit
1563
+ sync records the tag (32s apart on the delivery that motivated this),
1564
+ so the table is reliably empty at exactly the moment this is called.
1565
+ The remote, by contrast, has the tag by definition -- it is what it
1566
+ just announced.
1567
+
1568
+ ``None`` means the answer is *no commit*, and the caller may reject
1569
+ the release on it: the remote positively reported the ref does not
1570
+ exist (HTTP 404/422), no deployment capability is bound to the
1571
+ project at all, or the plugin does not implement
1572
+ ``resolve_committish``. Every other failure raises instead -- the
1573
+ same distinction ``resolve_remote_tags`` draws between ``'absent'``
1574
+ and ``'error'``, and for the same reason: a caller that refuses a
1575
+ release on ``None`` must never read unreachability as absence. A
1576
+ timed-out, rate-limited, unauthorized or uncredentialed lookup
1577
+ would otherwise turn a "retry shortly" into a terminal "your tag is
1578
+ invalid". Both the capability resolution and the call itself can
1579
+ fail either way, so both are sorted, not just the call.
1580
+ """
1581
+ try:
1582
+ resolved, ctx, credentials = await _resolve_and_context(
1583
+ db, org_slug, project_id, auth, best_effort_identity=True
1584
+ )
1585
+ except fastapi.HTTPException as exc:
1586
+ # Only 404 -- "no integration provides the deployment
1587
+ # capability" -- means this project has no way to name a
1588
+ # commit. The other answers here are failures to *ask*: a
1589
+ # missing service credential is a deliberate, retryable 503
1590
+ # (see ``_resolve_and_context``), a 403 is an authorization
1591
+ # problem, and a 400 means the capability is bound more than
1592
+ # once and needs ``?source=``. Reporting any of those as an
1593
+ # absent tag is the same mistake as swallowing the timeout.
1594
+ if exc.status_code == 404:
1595
+ return None
1596
+ raise
1597
+ handler = _handler(resolved)
1598
+ try:
1599
+ commit = await call_with_timeout(
1600
+ handler.resolve_committish(
1601
+ ctx, _resolve_credentials(ctx, credentials), tag
1602
+ )
1603
+ )
1604
+ except NotImplementedError:
1605
+ return None
1606
+ except plugin_errors.PluginRateLimited:
1607
+ raise
1608
+ except fastapi.HTTPException:
1609
+ # ``call_with_timeout`` raises 503 + ``Retry-After`` on timeout.
1610
+ raise
1611
+ except httpx.HTTPStatusError as exc:
1612
+ # 404 is the remote saying "no such ref"; GitHub answers 422 for
1613
+ # a ref it cannot even parse. Everything else (403, 5xx) is the
1614
+ # remote failing to answer, not answering "no".
1615
+ if exc.response.status_code in (404, 422):
1616
+ return None
1617
+ LOGGER.warning(
1618
+ 'committish lookup for project=%s tag=%s failed with %s',
1619
+ project_id,
1620
+ tag,
1621
+ exc.response.status_code,
1622
+ )
1623
+ raise fastapi.HTTPException(
1624
+ status_code=503,
1625
+ detail=f'Could not reach the source host to resolve {tag!r}',
1626
+ headers={'Retry-After': '5'},
1627
+ ) from exc
1628
+ except httpx.HTTPError as exc:
1629
+ # Transport-level: connect, read, protocol. Transient, so it
1630
+ # must not read as "the tag does not exist" either.
1631
+ LOGGER.warning(
1632
+ 'committish lookup for project=%s tag=%s failed: %r',
1633
+ project_id,
1634
+ tag,
1635
+ exc,
1636
+ )
1637
+ raise fastapi.HTTPException(
1638
+ status_code=503,
1639
+ detail=f'Could not reach the source host to resolve {tag!r}',
1640
+ headers={'Retry-After': '5'},
1641
+ ) from exc
1642
+ await persist_link_writeback(db, ctx)
1643
+ return versioning.short_committish(commit.sha)
1644
+
1645
+
1548
1646
  async def backfill_release_notes(
1549
1647
  db: graph.Graph,
1550
1648
  *,
@@ -553,8 +553,8 @@ async def _fetch_pr_counts(
553
553
  ) -> dict[str, tuple[int, int, int, int]]:
554
554
  """Return {project_id: (open, closed, viewer_open, viewer_closed)}.
555
555
 
556
- Errors are swallowed — PR counts are best-effort and should not
557
- fail the project list endpoint.
556
+ Draft PRs are not counted. Errors are swallowed — PR counts are
557
+ best-effort and should not fail the project list endpoint.
558
558
  """
559
559
  if not project_ids:
560
560
  return {}
@@ -570,6 +570,7 @@ async def _fetch_pr_counts(
570
570
  ' AS viewer_closed_count'
571
571
  ' FROM pull_requests FINAL'
572
572
  ' WHERE project_id IN {project_ids:Array(String)}'
573
+ ' AND NOT draft'
573
574
  ' GROUP BY project_id'
574
575
  )
575
576
  params['viewer'] = viewer
@@ -582,6 +583,7 @@ async def _fetch_pr_counts(
582
583
  ' 0 AS viewer_closed_count'
583
584
  ' FROM pull_requests FINAL'
584
585
  ' WHERE project_id IN {project_ids:Array(String)}'
586
+ ' AND NOT draft'
585
587
  ' GROUP BY project_id'
586
588
  )
587
589
  try:
@@ -111,13 +111,27 @@ def _row_to_response(row: dict[str, typing.Any]) -> dict[str, typing.Any]:
111
111
  async def _fetch_org_project_ids(
112
112
  db: graph.Pool,
113
113
  org_slug: str,
114
+ *,
115
+ include_archived: bool = False,
114
116
  ) -> list[str]:
115
- """Return all project IDs belonging to the org."""
116
- query: typing.LiteralString = """
117
+ """Return the project IDs belonging to the org.
118
+
119
+ Archived projects are excluded unless ``include_archived`` is set;
120
+ org-wide PR views should not count PRs nobody will act on.
121
+ """
122
+ archived_filter: typing.LiteralString = (
123
+ '' if include_archived else 'WHERE coalesce(p.archived, false) = false'
124
+ )
125
+ query: typing.LiteralString = (
126
+ """
117
127
  MATCH (p:Project)-[:OWNED_BY]->(:Team)
118
128
  -[:BELONGS_TO]->(o:Organization {{slug: {org_slug}}})
129
+ """
130
+ + archived_filter
131
+ + """
119
132
  RETURN p.id AS id
120
133
  """
134
+ )
121
135
  records = await db.execute(query, {'org_slug': org_slug}, ['id'])
122
136
  return [
123
137
  graph.parse_agtype(r['id'])
@@ -133,8 +147,12 @@ async def _list_prs(
133
147
  author: str | None = None,
134
148
  limit: int = DEFAULT_LIMIT,
135
149
  offset: int = 0,
150
+ include_drafts: bool = False,
136
151
  ) -> PullRequestListResponse:
137
- """Run the ClickHouse query and return a paginated response."""
152
+ """Run the ClickHouse query and return a paginated response.
153
+
154
+ Draft PRs are excluded unless ``include_drafts`` is set.
155
+ """
138
156
  if limit < 1 or limit > MAX_LIMIT:
139
157
  raise fastapi.HTTPException(
140
158
  status_code=400,
@@ -159,6 +177,9 @@ async def _list_prs(
159
177
  clauses.append('author = {author:String}')
160
178
  params['author'] = author
161
179
 
180
+ if not include_drafts:
181
+ clauses.append('NOT draft')
182
+
162
183
  where = ' AND '.join(clauses)
163
184
  count_sql = (
164
185
  'SELECT count() AS total,' # noqa: S608
@@ -212,9 +233,13 @@ async def list_project_pull_requests(
212
233
 
213
234
  Optional ``state`` filter accepts ``open`` or ``closed``.
214
235
  Optional ``author`` filter accepts a GitHub login.
215
- Results are ordered newest first.
236
+ Results are ordered newest first. Drafts are included and archived
237
+ projects are allowed: the project page filters drafts itself and
238
+ stays readable after the project is archived.
216
239
  """
217
- org_project_ids = await _fetch_org_project_ids(db, org_slug)
240
+ org_project_ids = await _fetch_org_project_ids(
241
+ db, org_slug, include_archived=True
242
+ )
218
243
  if project_id not in org_project_ids:
219
244
  raise fastapi.HTTPException(
220
245
  status_code=404,
@@ -229,6 +254,7 @@ async def list_project_pull_requests(
229
254
  author=author,
230
255
  limit=limit,
231
256
  offset=offset,
257
+ include_drafts=True,
232
258
  )
233
259
 
234
260
 
@@ -363,16 +389,13 @@ async def _fetch_pr_activity(
363
389
  ) -> list[dict[str, typing.Any]]:
364
390
  """Aggregate created/open/closed/merged counts per author.
365
391
 
366
- A PR counts toward ``created`` when created on/after ``since`` and
367
- toward ``merged`` when merged on/after ``since`` -- the two use
368
- different anchors, so a PR opened earlier but merged in-window still
369
- contributes to ``merged``.
370
-
371
- ``open`` and ``closed`` are current-state breakdowns of the PRs
372
- created in-window: ``open`` is still open, ``closed`` was closed
373
- without merging. Both anchor on ``created_at`` because the table
374
- carries no close timestamp -- only ``merged_at`` -- so a PR closed
375
- in-window but opened earlier cannot be attributed to the window.
392
+ Every column covers the PRs created on/after ``since``: ``open``,
393
+ ``closed`` (closed without merging) and ``merged`` are the current
394
+ state of that same set, so ``created == open + closed + merged`` on
395
+ every row. A PR opened before ``since`` is not counted anywhere,
396
+ even if it merged in-window; anchoring ``merged`` on ``merged_at``
397
+ instead made the columns disagree with ``created``. Draft PRs are
398
+ excluded from every count.
376
399
  """
377
400
  if not project_ids:
378
401
  return []
@@ -386,12 +409,12 @@ async def _fetch_pr_activity(
386
409
  ' AS open_count,'
387
410
  " countIf(state != 'open' AND NOT merged"
388
411
  ' AND created_at >= {since:DateTime64(3)}) AS closed_count,'
389
- ' countIf(merged AND merged_at >= {since:DateTime64(3)})'
412
+ ' countIf(merged AND created_at >= {since:DateTime64(3)})'
390
413
  ' AS merged_count'
391
414
  ' FROM pull_requests FINAL'
392
415
  ' WHERE project_id IN {project_ids:Array(String)}'
393
- ' AND (created_at >= {since:DateTime64(3)}'
394
- ' OR (merged AND merged_at >= {since:DateTime64(3)}))'
416
+ ' AND NOT draft'
417
+ ' AND created_at >= {since:DateTime64(3)}'
395
418
  ' GROUP BY author'
396
419
  )
397
420
  return await clickhouse.query(