@openhands/extensions 0.20.0 → 0.22.0

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 (305) hide show
  1. package/.claude-plugin/marketplace.json +881 -0
  2. package/.codex-plugin/marketplace.json +881 -0
  3. package/.github/workflows/check-extensions.yml +4 -0
  4. package/.github/workflows/npm-publish.yml +9 -3
  5. package/.github/workflows/pr-artifacts.yml +294 -0
  6. package/.github/workflows/pr-description-check.yml +4 -2
  7. package/.plugin/marketplace.json +881 -0
  8. package/.release-please-manifest.json +1 -1
  9. package/AGENTS.md +27 -3
  10. package/README.md +6 -2
  11. package/automations/bundle-index.js +22 -3
  12. package/automations/catalog/github-agents-md-maintainer/manifest.json +2 -1
  13. package/automations/catalog/github-delivery-watchdog/manifest.json +79 -0
  14. package/automations/catalog/github-issue-to-pr/manifest.json +24 -15
  15. package/automations/catalog/github-issue-triage/manifest.json +73 -0
  16. package/automations/catalog/github-pr-reviewer/manifest.json +23 -15
  17. package/automations/catalog/gitlab-issue-to-mr/manifest.json +124 -0
  18. package/automations/catalog-index.js +17 -11
  19. package/integrations/catalog/reportportal.json +76 -0
  20. package/integrations/catalog-index.js +6 -4
  21. package/marketplaces/openhands-extensions.json +42 -0
  22. package/package.json +1 -1
  23. package/plugins/city-weather/.claude-plugin/plugin.json +13 -0
  24. package/plugins/city-weather/.codex-plugin/plugin.json +13 -0
  25. package/plugins/cobol-modernization/.claude-plugin/plugin.json +19 -0
  26. package/plugins/cobol-modernization/.codex-plugin/plugin.json +19 -0
  27. package/plugins/issue-duplicate-checker/.claude-plugin/plugin.json +13 -0
  28. package/plugins/issue-duplicate-checker/.codex-plugin/plugin.json +13 -0
  29. package/plugins/magic-test/.claude-plugin/plugin.json +13 -0
  30. package/plugins/magic-test/.codex-plugin/plugin.json +13 -0
  31. package/plugins/migration-scoring/.claude-plugin/plugin.json +19 -0
  32. package/plugins/migration-scoring/.codex-plugin/plugin.json +19 -0
  33. package/plugins/onboarding/.claude-plugin/plugin.json +20 -0
  34. package/plugins/onboarding/.codex-plugin/plugin.json +20 -0
  35. package/plugins/onboarding/skills/agent-readiness-report/references/criteria.md +144 -0
  36. package/plugins/onboarding/skills/improve-agent-readiness/references/criteria.md +144 -0
  37. package/plugins/openhands/.claude-plugin/plugin.json +13 -0
  38. package/plugins/openhands/.codex-plugin/plugin.json +13 -0
  39. package/plugins/openhands/skills/openhands-api/.claude-plugin/plugin.json +22 -0
  40. package/plugins/openhands/skills/openhands-api/.codex-plugin/plugin.json +22 -0
  41. package/plugins/openhands/skills/openhands-api/.plugin/plugin.json +22 -0
  42. package/plugins/openhands/skills/openhands-api/README.md +52 -0
  43. package/plugins/openhands/skills/openhands-api/SKILL.md +469 -0
  44. package/plugins/openhands/skills/openhands-api/references/README.md +33 -0
  45. package/plugins/openhands/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
  46. package/plugins/openhands/skills/openhands-api/references/example_prompt.md +12 -0
  47. package/plugins/openhands/skills/openhands-api/references/windows.md +44 -0
  48. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.py +606 -0
  49. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.ts +252 -0
  50. package/plugins/openhands/skills/openhands-automation/.claude-plugin/plugin.json +19 -0
  51. package/plugins/openhands/skills/openhands-automation/.codex-plugin/plugin.json +19 -0
  52. package/plugins/openhands/skills/openhands-automation/.plugin/plugin.json +19 -0
  53. package/plugins/openhands/skills/openhands-automation/README.md +89 -0
  54. package/plugins/openhands/skills/openhands-automation/SKILL.md +899 -0
  55. package/plugins/openhands/skills/openhands-automation/commands/automation-create.md +8 -0
  56. package/plugins/openhands/skills/openhands-automation/references/ab-testing.md +185 -0
  57. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +832 -0
  58. package/plugins/openhands/skills/openhands-automation/references/security.md +84 -0
  59. package/plugins/openhands/skills/openhands-automation/references/windows.md +57 -0
  60. package/plugins/openhands/skills/openhands-sdk/.claude-plugin/plugin.json +20 -0
  61. package/plugins/openhands/skills/openhands-sdk/.codex-plugin/plugin.json +20 -0
  62. package/plugins/openhands/skills/openhands-sdk/.plugin/plugin.json +20 -0
  63. package/plugins/openhands/skills/openhands-sdk/README.md +22 -0
  64. package/plugins/openhands/skills/openhands-sdk/SKILL.md +245 -0
  65. package/plugins/openhands/skills/openhands-sdk/commands/sdk.md +8 -0
  66. package/plugins/pr-review/.claude-plugin/plugin.json +13 -0
  67. package/plugins/pr-review/.codex-plugin/plugin.json +13 -0
  68. package/plugins/pr-review/skills/code-review/.claude-plugin/plugin.json +19 -0
  69. package/plugins/pr-review/skills/code-review/.codex-plugin/plugin.json +19 -0
  70. package/plugins/pr-review/skills/code-review/.plugin/plugin.json +19 -0
  71. package/plugins/pr-review/skills/code-review/README.md +18 -0
  72. package/plugins/pr-review/skills/code-review/SKILL.md +209 -0
  73. package/plugins/pr-review/skills/code-review/commands/codereview-roasted.md +8 -0
  74. package/plugins/pr-review/skills/code-review/commands/codereview.md +8 -0
  75. package/plugins/pr-review/skills/code-review/references/risk-evaluation.md +41 -0
  76. package/plugins/pr-review/skills/code-review/references/supply-chain-security.md +31 -0
  77. package/plugins/pr-review/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
  78. package/plugins/pr-review/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
  79. package/plugins/pr-review/skills/github-pr-review/.plugin/plugin.json +18 -0
  80. package/plugins/pr-review/skills/github-pr-review/README.md +145 -0
  81. package/plugins/pr-review/skills/github-pr-review/SKILL.md +187 -0
  82. package/plugins/pr-review/skills/github-pr-review/commands/github-pr-review.md +8 -0
  83. package/plugins/pr-review/skills/github-pr-review/references/windows.md +47 -0
  84. package/plugins/qa-changes/.claude-plugin/plugin.json +11 -0
  85. package/plugins/qa-changes/.codex-plugin/plugin.json +11 -0
  86. package/plugins/qa-changes/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
  87. package/plugins/qa-changes/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
  88. package/plugins/qa-changes/skills/github-pr-review/.plugin/plugin.json +18 -0
  89. package/plugins/qa-changes/skills/github-pr-review/README.md +145 -0
  90. package/plugins/qa-changes/skills/github-pr-review/SKILL.md +187 -0
  91. package/plugins/qa-changes/skills/github-pr-review/commands/github-pr-review.md +8 -0
  92. package/plugins/qa-changes/skills/github-pr-review/references/windows.md +47 -0
  93. package/plugins/qa-changes/skills/qa-changes/README.md +18 -0
  94. package/plugins/qa-changes/skills/qa-changes/SKILL.md +229 -0
  95. package/plugins/qa-changes/skills/qa-changes/commands/qa-changes.md +8 -0
  96. package/plugins/release-notes/.claude-plugin/plugin.json +19 -0
  97. package/plugins/release-notes/.codex-plugin/plugin.json +19 -0
  98. package/plugins/release-notes/skills/release-notes/README.md +24 -0
  99. package/plugins/release-notes/skills/release-notes/SKILL.md +19 -0
  100. package/plugins/release-notes/skills/release-notes/commands/release-notes.md +8 -0
  101. package/plugins/vulnerability-remediation/.claude-plugin/plugin.json +19 -0
  102. package/plugins/vulnerability-remediation/.codex-plugin/plugin.json +19 -0
  103. package/plugins/vulnerability-remediation/skills/github/.claude-plugin/plugin.json +19 -0
  104. package/plugins/vulnerability-remediation/skills/github/.codex-plugin/plugin.json +19 -0
  105. package/plugins/vulnerability-remediation/skills/github/.plugin/plugin.json +19 -0
  106. package/plugins/vulnerability-remediation/skills/github/README.md +42 -0
  107. package/plugins/vulnerability-remediation/skills/github/SKILL.md +107 -0
  108. package/plugins/vulnerability-remediation/skills/github/references/windows.md +48 -0
  109. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +143 -0
  110. package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +230 -0
  111. package/plugins/vulnerability-remediation/skills/security/.claude-plugin/plugin.json +18 -0
  112. package/plugins/vulnerability-remediation/skills/security/.codex-plugin/plugin.json +18 -0
  113. package/plugins/vulnerability-remediation/skills/security/.plugin/plugin.json +18 -0
  114. package/plugins/vulnerability-remediation/skills/security/README.md +38 -0
  115. package/plugins/vulnerability-remediation/skills/security/SKILL.md +33 -0
  116. package/pyproject.toml +2 -2
  117. package/python/openhands_extensions/_version.py +1 -1
  118. package/skills/add-javadoc/.claude-plugin/plugin.json +18 -0
  119. package/skills/add-javadoc/.codex-plugin/plugin.json +18 -0
  120. package/skills/add-skill/.claude-plugin/plugin.json +18 -0
  121. package/skills/add-skill/.codex-plugin/plugin.json +18 -0
  122. package/skills/agent-canvas-environment/.claude-plugin/plugin.json +19 -0
  123. package/skills/agent-canvas-environment/.codex-plugin/plugin.json +19 -0
  124. package/skills/agent-creator/.claude-plugin/plugin.json +20 -0
  125. package/skills/agent-creator/.codex-plugin/plugin.json +20 -0
  126. package/skills/agent-memory/.claude-plugin/plugin.json +18 -0
  127. package/skills/agent-memory/.codex-plugin/plugin.json +18 -0
  128. package/skills/agent-sdk-builder/.claude-plugin/plugin.json +18 -0
  129. package/skills/agent-sdk-builder/.codex-plugin/plugin.json +18 -0
  130. package/skills/azure-devops/.claude-plugin/plugin.json +18 -0
  131. package/skills/azure-devops/.codex-plugin/plugin.json +18 -0
  132. package/skills/bitbucket/.claude-plugin/plugin.json +17 -0
  133. package/skills/bitbucket/.codex-plugin/plugin.json +17 -0
  134. package/skills/bitbucket-cloud/.claude-plugin/plugin.json +18 -0
  135. package/skills/bitbucket-cloud/.codex-plugin/plugin.json +18 -0
  136. package/skills/bitbucket-data-center/.claude-plugin/plugin.json +19 -0
  137. package/skills/bitbucket-data-center/.codex-plugin/plugin.json +19 -0
  138. package/skills/code-review/.claude-plugin/plugin.json +19 -0
  139. package/skills/code-review/.codex-plugin/plugin.json +19 -0
  140. package/skills/code-simplifier/.claude-plugin/plugin.json +21 -0
  141. package/skills/code-simplifier/.codex-plugin/plugin.json +21 -0
  142. package/skills/datadog/.claude-plugin/plugin.json +19 -0
  143. package/skills/datadog/.codex-plugin/plugin.json +19 -0
  144. package/skills/deno/.claude-plugin/plugin.json +18 -0
  145. package/skills/deno/.codex-plugin/plugin.json +18 -0
  146. package/skills/discord/.claude-plugin/plugin.json +18 -0
  147. package/skills/discord/.codex-plugin/plugin.json +18 -0
  148. package/skills/docker/.claude-plugin/plugin.json +17 -0
  149. package/skills/docker/.codex-plugin/plugin.json +17 -0
  150. package/skills/evidence-based-citations/.claude-plugin/plugin.json +20 -0
  151. package/skills/evidence-based-citations/.codex-plugin/plugin.json +20 -0
  152. package/skills/flarglebargle/.claude-plugin/plugin.json +16 -0
  153. package/skills/flarglebargle/.codex-plugin/plugin.json +16 -0
  154. package/skills/frontend-design/.claude-plugin/plugin.json +21 -0
  155. package/skills/frontend-design/.codex-plugin/plugin.json +21 -0
  156. package/skills/github/.claude-plugin/plugin.json +19 -0
  157. package/skills/github/.codex-plugin/plugin.json +19 -0
  158. package/skills/github/scripts/agent_conversation.py +143 -0
  159. package/skills/github/scripts/github_client.py +230 -0
  160. package/skills/github-actions/.claude-plugin/plugin.json +19 -0
  161. package/skills/github-actions/.codex-plugin/plugin.json +19 -0
  162. package/skills/github-agents-md-maintainer/.claude-plugin/plugin.json +19 -0
  163. package/skills/github-agents-md-maintainer/.codex-plugin/plugin.json +19 -0
  164. package/skills/github-agents-md-maintainer/SKILL.md +7 -1
  165. package/skills/github-agents-md-maintainer/scripts/github_client.py +230 -0
  166. package/skills/github-agents-md-maintainer/scripts/main.py +4 -40
  167. package/skills/github-delivery-watchdog/.claude-plugin/plugin.json +9 -0
  168. package/skills/github-delivery-watchdog/.codex-plugin/plugin.json +9 -0
  169. package/skills/github-delivery-watchdog/.plugin/plugin.json +9 -0
  170. package/skills/github-delivery-watchdog/README.md +10 -0
  171. package/skills/github-delivery-watchdog/SKILL.md +35 -0
  172. package/skills/github-delivery-watchdog/commands/github-delivery-watchdog.md +8 -0
  173. package/skills/github-delivery-watchdog/scripts/github_client.py +230 -0
  174. package/skills/github-delivery-watchdog/scripts/worker.py +96 -0
  175. package/skills/github-issue-to-pr/.claude-plugin/plugin.json +19 -0
  176. package/skills/github-issue-to-pr/.codex-plugin/plugin.json +19 -0
  177. package/skills/github-issue-to-pr/SKILL.md +15 -1
  178. package/skills/github-issue-to-pr/scripts/agent_conversation.py +143 -0
  179. package/skills/github-issue-to-pr/scripts/github_client.py +230 -0
  180. package/skills/github-issue-to-pr/scripts/main.py +19 -50
  181. package/skills/github-issue-to-pr/scripts/worker.py +172 -0
  182. package/skills/github-issue-triage/.claude-plugin/plugin.json +9 -0
  183. package/skills/github-issue-triage/.codex-plugin/plugin.json +9 -0
  184. package/skills/github-issue-triage/.plugin/plugin.json +9 -0
  185. package/skills/github-issue-triage/README.md +9 -0
  186. package/skills/github-issue-triage/SKILL.md +34 -0
  187. package/skills/github-issue-triage/commands/github-issue-triage.md +8 -0
  188. package/skills/github-issue-triage/scripts/agent_conversation.py +143 -0
  189. package/skills/github-issue-triage/scripts/github_client.py +230 -0
  190. package/skills/github-issue-triage/scripts/worker.py +106 -0
  191. package/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
  192. package/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
  193. package/skills/github-pr-reviewer/.claude-plugin/plugin.json +20 -0
  194. package/skills/github-pr-reviewer/.codex-plugin/plugin.json +20 -0
  195. package/skills/github-pr-reviewer/SKILL.md +15 -1
  196. package/skills/github-pr-reviewer/scripts/agent_conversation.py +143 -0
  197. package/skills/github-pr-reviewer/scripts/github_client.py +230 -0
  198. package/skills/github-pr-reviewer/scripts/main.py +23 -49
  199. package/skills/github-pr-reviewer/scripts/worker.py +94 -0
  200. package/skills/github-repo-monitor/.claude-plugin/plugin.json +22 -0
  201. package/skills/github-repo-monitor/.codex-plugin/plugin.json +22 -0
  202. package/skills/github-repo-monitor/SKILL.md +7 -1
  203. package/skills/github-repo-monitor/scripts/github_client.py +230 -0
  204. package/skills/github-repo-monitor/scripts/main.py +4 -46
  205. package/skills/gitlab/.claude-plugin/plugin.json +17 -0
  206. package/skills/gitlab/.codex-plugin/plugin.json +17 -0
  207. package/skills/gitlab-issue-to-mr/.claude-plugin/plugin.json +19 -0
  208. package/skills/gitlab-issue-to-mr/.codex-plugin/plugin.json +19 -0
  209. package/skills/gitlab-issue-to-mr/.plugin/plugin.json +19 -0
  210. package/skills/gitlab-issue-to-mr/README.md +92 -0
  211. package/skills/gitlab-issue-to-mr/SKILL.md +408 -0
  212. package/skills/gitlab-issue-to-mr/commands/issue-to-mr-setup.md +8 -0
  213. package/skills/gitlab-issue-to-mr/references/state-schema.md +140 -0
  214. package/skills/gitlab-issue-to-mr/scripts/main.py +1448 -0
  215. package/skills/incident-retrospective/.claude-plugin/plugin.json +21 -0
  216. package/skills/incident-retrospective/.codex-plugin/plugin.json +21 -0
  217. package/skills/index.js +52 -6
  218. package/skills/iterate/.claude-plugin/plugin.json +13 -0
  219. package/skills/iterate/.codex-plugin/plugin.json +13 -0
  220. package/skills/jira-issue-to-pr/.claude-plugin/plugin.json +19 -0
  221. package/skills/jira-issue-to-pr/.codex-plugin/plugin.json +19 -0
  222. package/skills/jupyter/.claude-plugin/plugin.json +18 -0
  223. package/skills/jupyter/.codex-plugin/plugin.json +18 -0
  224. package/skills/kubernetes/.claude-plugin/plugin.json +18 -0
  225. package/skills/kubernetes/.codex-plugin/plugin.json +18 -0
  226. package/skills/learn-from-code-review/.claude-plugin/plugin.json +19 -0
  227. package/skills/learn-from-code-review/.codex-plugin/plugin.json +19 -0
  228. package/skills/linear/.claude-plugin/plugin.json +19 -0
  229. package/skills/linear/.codex-plugin/plugin.json +19 -0
  230. package/skills/linear-triage/.claude-plugin/plugin.json +21 -0
  231. package/skills/linear-triage/.codex-plugin/plugin.json +21 -0
  232. package/skills/news-digest/.claude-plugin/plugin.json +19 -0
  233. package/skills/news-digest/.codex-plugin/plugin.json +19 -0
  234. package/skills/notion/.claude-plugin/plugin.json +17 -0
  235. package/skills/notion/.codex-plugin/plugin.json +17 -0
  236. package/skills/npm/.claude-plugin/plugin.json +17 -0
  237. package/skills/npm/.codex-plugin/plugin.json +17 -0
  238. package/skills/openhands-api/.claude-plugin/plugin.json +22 -0
  239. package/skills/openhands-api/.codex-plugin/plugin.json +22 -0
  240. package/skills/openhands-automation/.claude-plugin/plugin.json +19 -0
  241. package/skills/openhands-automation/.codex-plugin/plugin.json +19 -0
  242. package/skills/openhands-automation/README.md +1 -1
  243. package/skills/openhands-automation/SKILL.md +18 -8
  244. package/skills/openhands-enterprise-troubleshooting/.claude-plugin/plugin.json +20 -0
  245. package/skills/openhands-enterprise-troubleshooting/.codex-plugin/plugin.json +20 -0
  246. package/skills/openhands-enterprise-troubleshooting/.plugin/plugin.json +20 -0
  247. package/skills/openhands-enterprise-troubleshooting/README.md +79 -0
  248. package/skills/openhands-enterprise-troubleshooting/SKILL.md +339 -0
  249. package/skills/openhands-enterprise-troubleshooting/references/diagnostics.md +698 -0
  250. package/skills/openhands-enterprise-troubleshooting/references/support-bundle-analysis.md +570 -0
  251. package/skills/openhands-enterprise-troubleshooting/scripts/bundle_triage.py +532 -0
  252. package/skills/openhands-sdk/.claude-plugin/plugin.json +20 -0
  253. package/skills/openhands-sdk/.codex-plugin/plugin.json +20 -0
  254. package/skills/openhands-sdk/SKILL.md +2 -0
  255. package/skills/pdflatex/.claude-plugin/plugin.json +18 -0
  256. package/skills/pdflatex/.codex-plugin/plugin.json +18 -0
  257. package/skills/plain-english-content/.claude-plugin/plugin.json +15 -0
  258. package/skills/plain-english-content/.codex-plugin/plugin.json +15 -0
  259. package/skills/prd/.claude-plugin/plugin.json +19 -0
  260. package/skills/prd/.codex-plugin/plugin.json +19 -0
  261. package/skills/research-brief/.claude-plugin/plugin.json +20 -0
  262. package/skills/research-brief/.codex-plugin/plugin.json +20 -0
  263. package/skills/security/.claude-plugin/plugin.json +18 -0
  264. package/skills/security/.codex-plugin/plugin.json +18 -0
  265. package/skills/skill-creator/.claude-plugin/plugin.json +17 -0
  266. package/skills/skill-creator/.codex-plugin/plugin.json +17 -0
  267. package/skills/slack-channel-monitor/.claude-plugin/plugin.json +21 -0
  268. package/skills/slack-channel-monitor/.codex-plugin/plugin.json +21 -0
  269. package/skills/slack-standup-digest/.claude-plugin/plugin.json +21 -0
  270. package/skills/slack-standup-digest/.codex-plugin/plugin.json +21 -0
  271. package/skills/spark-version-upgrade/.claude-plugin/plugin.json +20 -0
  272. package/skills/spark-version-upgrade/.codex-plugin/plugin.json +20 -0
  273. package/skills/ssh/.claude-plugin/plugin.json +18 -0
  274. package/skills/ssh/.codex-plugin/plugin.json +18 -0
  275. package/skills/swift-linux/.claude-plugin/plugin.json +17 -0
  276. package/skills/swift-linux/.codex-plugin/plugin.json +17 -0
  277. package/skills/technical-writing/.claude-plugin/plugin.json +15 -0
  278. package/skills/technical-writing/.codex-plugin/plugin.json +15 -0
  279. package/skills/theme-factory/.claude-plugin/plugin.json +19 -0
  280. package/skills/theme-factory/.codex-plugin/plugin.json +19 -0
  281. package/skills/ticket-to-code-change/.claude-plugin/plugin.json +21 -0
  282. package/skills/ticket-to-code-change/.codex-plugin/plugin.json +21 -0
  283. package/skills/upstream-fork-sync/.claude-plugin/plugin.json +21 -0
  284. package/skills/upstream-fork-sync/.codex-plugin/plugin.json +21 -0
  285. package/skills/uv/.claude-plugin/plugin.json +18 -0
  286. package/skills/uv/.codex-plugin/plugin.json +18 -0
  287. package/skills/vercel/.claude-plugin/plugin.json +18 -0
  288. package/skills/vercel/.codex-plugin/plugin.json +18 -0
  289. package/tests/fixtures/automations/capabilities.json +6 -3
  290. package/tests/fixtures/automations/github-pr-reviewer.json +64 -43
  291. package/tests/github_automation_helpers.py +31 -0
  292. package/tests/test_agent_conversation_dispatch.py +180 -0
  293. package/tests/test_automation_setup.py +3 -3
  294. package/tests/test_github_automation_foundation.py +73 -0
  295. package/tests/test_github_developer_delivery.py +150 -0
  296. package/tests/test_github_reviewer_delivery.py +110 -0
  297. package/tests/test_github_skill_installation.py +63 -0
  298. package/tests/test_github_triage_delivery.py +90 -0
  299. package/tests/test_github_watchdog_delivery.py +233 -0
  300. package/tests/test_gitlab_issue_to_mr.py +823 -0
  301. package/.pr/custom-automation-event-trigger.png +0 -0
  302. package/.pr/custom-automation-plugin-fields.png +0 -0
  303. package/.pr/custom-automation-prompt-review.png +0 -0
  304. package/.pr/custom-automation-prompt-scheduled.png +0 -0
  305. package/.pr/custom-automation-upload-fields.png +0 -0
@@ -0,0 +1,150 @@
1
+ """Contract tests for independent developer delivery."""
2
+
3
+ from unittest.mock import Mock
4
+
5
+ from github_automation_helpers import worker
6
+
7
+
8
+ def _developer(tmp_path, monkeypatch):
9
+ module = worker("github-issue-to-pr", tmp_path, monkeypatch)
10
+ run = object.__new__(module.IssueToPR)
11
+ run.config = {
12
+ "trigger_label": "ready-for-dev",
13
+ "branch_prefix": "openhands/issue",
14
+ "review_label": "openhands-review",
15
+ }
16
+ run.repository = "owner/repo"
17
+ run.token_name = "FACTORY_GITHUB_DEVELOPER_TOKEN"
18
+ run.dispatcher = Mock()
19
+ run.dependencies_complete = lambda issue: True
20
+ run.statuses = lambda sha: {}
21
+ return module, run
22
+
23
+
24
+ def test_developer_fans_out_ready_issues_by_priority(tmp_path, monkeypatch):
25
+ _module, run = _developer(tmp_path, monkeypatch)
26
+ issues = [
27
+ {
28
+ "number": 2,
29
+ "title": "Normal",
30
+ "updated_at": "u2",
31
+ "labels": [{"name": "ready-for-dev"}],
32
+ },
33
+ {
34
+ "number": 1,
35
+ "title": "High",
36
+ "updated_at": "u1",
37
+ "labels": [{"name": "ready-for-dev"}, {"name": "priority:high"}],
38
+ },
39
+ ]
40
+ run.open_issues = lambda: issues
41
+ run.gh_pages = lambda path: []
42
+ run.gh = Mock(
43
+ side_effect=[{"id": 55, "default_branch": "main"}, {"object": {"sha": "base"}}]
44
+ )
45
+ submit = Mock(
46
+ side_effect=lambda **kwargs: {
47
+ "disposition": "created",
48
+ "conversation_id": kwargs["subject"],
49
+ }
50
+ )
51
+ run.dispatcher.deliver = submit
52
+
53
+ run.run()
54
+
55
+ assert [call.kwargs["subject"] for call in submit.call_args_list] == [
56
+ "55:issue:1",
57
+ "55:issue:2",
58
+ ]
59
+ prompt = submit.call_args_list[0].kwargs["prompt"]
60
+ assert "workspace starts empty" in prompt
61
+ assert "FACTORY_GITHUB_DEVELOPER_TOKEN" in prompt
62
+ assert "gh repo clone owner/repo ." in prompt
63
+
64
+
65
+ def test_developer_submits_failed_review_as_same_subject(tmp_path, monkeypatch):
66
+ _module, run = _developer(tmp_path, monkeypatch)
67
+ issue = {"number": 4, "title": "Feature", "updated_at": "u4", "labels": []}
68
+ pr = {
69
+ "number": 8,
70
+ "head": {"ref": "openhands/issue-4", "sha": "head"},
71
+ "base": {"ref": "main"},
72
+ "labels": [],
73
+ }
74
+ run.open_issues = lambda: [issue]
75
+ run.gh_pages = lambda path: [pr]
76
+ run.statuses = lambda sha: {"software-factory/review": "failure"}
77
+ run.gh = Mock(
78
+ side_effect=[{"id": 55, "default_branch": "main"}, {"object": {"sha": "base"}}]
79
+ )
80
+ submit = Mock(
81
+ return_value={"disposition": "created", "conversation_id": "conversation"}
82
+ )
83
+ run.dispatcher.deliver = submit
84
+
85
+ run.run()
86
+
87
+ assert submit.call_args.kwargs["subject"] == "55:issue:4"
88
+ assert submit.call_args.kwargs["delivery"] == "4:head"
89
+ prompt = submit.call_args.kwargs["prompt"]
90
+ assert "Revise existing PR #8 at exact head `head`" in prompt
91
+ assert "check out the existing remote branch `openhands/issue-4`" in prompt
92
+ assert "do not open another pull request" in prompt
93
+
94
+
95
+ def test_developer_continues_after_one_submission_fails(tmp_path, monkeypatch):
96
+ _module, run = _developer(tmp_path, monkeypatch)
97
+ issues = [
98
+ {
99
+ "number": 1,
100
+ "title": "First",
101
+ "updated_at": "u1",
102
+ "labels": [{"name": "ready-for-dev"}],
103
+ },
104
+ {
105
+ "number": 2,
106
+ "title": "Second",
107
+ "updated_at": "u2",
108
+ "labels": [{"name": "ready-for-dev"}],
109
+ },
110
+ ]
111
+ run.open_issues = lambda: issues
112
+ run.gh_pages = lambda path: []
113
+ run.gh = Mock(
114
+ side_effect=[{"id": 55, "default_branch": "main"}, {"object": {"sha": "base"}}]
115
+ )
116
+ submit = Mock(
117
+ side_effect=[
118
+ RuntimeError("unavailable"),
119
+ {"disposition": "created", "conversation_id": "second"},
120
+ ]
121
+ )
122
+ run.dispatcher.deliver = submit
123
+
124
+ run.run()
125
+
126
+ assert [call.kwargs["subject"] for call in submit.call_args_list] == [
127
+ "55:issue:1",
128
+ "55:issue:2",
129
+ ]
130
+
131
+
132
+ def test_developer_skips_open_pr_awaiting_review(tmp_path, monkeypatch):
133
+ _module, run = _developer(tmp_path, monkeypatch)
134
+ issue = {"number": 4, "title": "Feature", "labels": [{"name": "ready-for-dev"}]}
135
+ pr = {
136
+ "number": 8,
137
+ "head": {"ref": "openhands/issue-4", "sha": "head"},
138
+ "labels": [{"name": "openhands-review"}],
139
+ }
140
+ run.open_issues = lambda: [issue]
141
+ run.gh_pages = lambda path: [pr]
142
+ run.gh = Mock(
143
+ side_effect=[{"id": 55, "default_branch": "main"}, {"object": {"sha": "base"}}]
144
+ )
145
+ submit = Mock()
146
+ run.dispatcher.deliver = submit
147
+
148
+ run.run()
149
+
150
+ submit.assert_not_called()
@@ -0,0 +1,110 @@
1
+ """Contract tests for delegated GitHub PR review."""
2
+
3
+ from pathlib import Path
4
+ from unittest.mock import Mock
5
+
6
+ from github_automation_helpers import worker
7
+
8
+
9
+ def _reviewer(tmp_path, monkeypatch):
10
+ module = worker("github-pr-reviewer", tmp_path, monkeypatch)
11
+ run = object.__new__(module.PullRequestReviewer)
12
+ run.config = {"trigger_label": "openhands-review"}
13
+ run.repository = "owner/repo"
14
+ run.token = "token"
15
+ run.token_name = "FACTORY_GITHUB_REVIEWER_TOKEN"
16
+ run.dispatcher = Mock()
17
+ return module, run
18
+
19
+
20
+ def test_reviewer_submits_each_labeled_exact_head(tmp_path, monkeypatch):
21
+ module, run = _reviewer(tmp_path, monkeypatch)
22
+ prs = [
23
+ {
24
+ "number": 2,
25
+ "head": {"sha": "head-2"},
26
+ "labels": [{"name": "openhands-review"}],
27
+ },
28
+ {"number": 1, "head": {"sha": "head-1"}, "labels": []},
29
+ ]
30
+ run.gh_pages = lambda path: prs
31
+ run.gh = Mock(side_effect=[{"id": 99}, prs[0]])
32
+ monkeypatch.setattr(
33
+ module.workflow,
34
+ "_latest_trigger_label_event",
35
+ lambda *args: {"id": 7, "created_at": "now"},
36
+ )
37
+ submit = Mock(
38
+ return_value={"disposition": "created", "conversation_id": "conversation"}
39
+ )
40
+ run.dispatcher.deliver = submit
41
+
42
+ run.run()
43
+
44
+ assert submit.call_args.kwargs["subject"] == "99:pr:2"
45
+ assert submit.call_args.kwargs["delivery"] == "7:head-2"
46
+ prompt = submit.call_args.kwargs["prompt"]
47
+ assert "publish the review directly to GitHub" in prompt
48
+ assert "software-factory/review" in prompt
49
+ assert "software-factory/tests" in prompt
50
+ assert "FACTORY_GITHUB_REVIEWER_TOKEN" in prompt
51
+ assert "gh auth setup-git" in prompt
52
+ assert "GIT_TERMINAL_PROMPT=0" in prompt
53
+ assert "Never paste JSON artifacts" in prompt
54
+
55
+
56
+ def test_reviewer_ignores_unlabeled_prs(tmp_path, monkeypatch):
57
+ _module, run = _reviewer(tmp_path, monkeypatch)
58
+ run.gh_pages = lambda path: [{"number": 1, "labels": [], "head": {"sha": "head"}}]
59
+ run.gh = Mock(return_value={"id": 99})
60
+ submit = Mock()
61
+ run.dispatcher.deliver = submit
62
+
63
+ run.run()
64
+
65
+ submit.assert_not_called()
66
+
67
+
68
+ def test_reviewer_continues_after_one_submission_fails(tmp_path, monkeypatch):
69
+ module, run = _reviewer(tmp_path, monkeypatch)
70
+ prs = [
71
+ {
72
+ "number": number,
73
+ "labels": [{"name": "openhands-review"}],
74
+ "head": {"sha": f"head-{number}"},
75
+ }
76
+ for number in (1, 2)
77
+ ]
78
+ run.gh_pages = lambda path: prs
79
+ run.gh = Mock(side_effect=[{"id": 99}, *prs])
80
+ monkeypatch.setattr(
81
+ module.workflow,
82
+ "_latest_trigger_label_event",
83
+ lambda *args: {"id": 7, "created_at": "now"},
84
+ )
85
+ submit = Mock(
86
+ side_effect=[
87
+ RuntimeError("temporary failure"),
88
+ {"disposition": "created", "conversation_id": "conversation"},
89
+ ]
90
+ )
91
+ run.dispatcher.deliver = submit
92
+
93
+ run.run()
94
+
95
+ assert submit.call_count == 2
96
+
97
+
98
+ def test_reviewer_scanner_has_no_conversation_or_runtime_code():
99
+ source = (
100
+ Path(__file__).resolve().parents[1]
101
+ / "skills/github-pr-reviewer/scripts/worker.py"
102
+ ).read_text()
103
+ for forbidden in (
104
+ "RemoteConversation",
105
+ "RemoteWorkspace",
106
+ "AGENT_SERVER_URL",
107
+ "SESSION_API_KEY",
108
+ "AUTOMATION_CONVERSATION_ID",
109
+ ):
110
+ assert forbidden not in source
@@ -0,0 +1,63 @@
1
+ """Verify shared support is included in independently installed skills."""
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+ from openhands.sdk.skills import install_skill
10
+
11
+ ROOT = Path(__file__).resolve().parents[1]
12
+
13
+
14
+ def test_installed_triage_skill_contains_its_bundle(tmp_path):
15
+ name = "github-issue-triage"
16
+ install_skill(source=str(ROOT / "skills" / name), installed_dir=tmp_path)
17
+ scripts = tmp_path / name / "scripts"
18
+ manifest = json.loads(
19
+ (ROOT / f"automations/catalog/{name}/manifest.json").read_text()
20
+ )
21
+
22
+ for filename, source in manifest["setup"]["bundle"]["files"].items():
23
+ installed = scripts / filename
24
+ assert installed.is_file() and not installed.is_symlink()
25
+ assert installed.read_bytes() == (ROOT / source).read_bytes()
26
+
27
+ result = subprocess.run(
28
+ [sys.executable, "-c", "import worker"],
29
+ cwd=scripts,
30
+ text=True,
31
+ capture_output=True,
32
+ check=False,
33
+ )
34
+ assert result.returncode == 0, result.stderr
35
+
36
+
37
+ @pytest.mark.parametrize(
38
+ "name",
39
+ [
40
+ "github-repo-monitor",
41
+ "github-agents-md-maintainer",
42
+ "github-issue-to-pr",
43
+ "github-pr-reviewer",
44
+ ],
45
+ )
46
+ def test_installed_github_skill_contains_shared_client(tmp_path, name):
47
+ install_skill(source=str(ROOT / "skills" / name), installed_dir=tmp_path)
48
+ scripts = tmp_path / name / "scripts"
49
+ helper = scripts / "github_client.py"
50
+
51
+ assert helper.is_file() and not helper.is_symlink()
52
+ assert (
53
+ helper.read_bytes()
54
+ == (ROOT / "skills/github/scripts/github_client.py").read_bytes()
55
+ )
56
+ result = subprocess.run(
57
+ [sys.executable, "-c", "import main"],
58
+ cwd=scripts,
59
+ text=True,
60
+ capture_output=True,
61
+ check=False,
62
+ )
63
+ assert result.returncode == 0, result.stderr
@@ -0,0 +1,90 @@
1
+ """Contract tests for independent triage delivery."""
2
+
3
+ from unittest.mock import Mock
4
+
5
+ from github_automation_helpers import worker
6
+
7
+
8
+ def _triage(tmp_path, monkeypatch, issues):
9
+ module = worker("github-issue-triage", tmp_path, monkeypatch)
10
+ cls = module.IssueTriage
11
+ run = object.__new__(cls)
12
+ run.repository = "owner/repo"
13
+ run.token_name = "GITHUB_PERSONAL_ACCESS_TOKEN"
14
+ run.dispatcher = Mock()
15
+ run.open_issues = lambda: issues
16
+ run.dependencies_complete = lambda issue: True
17
+ run.gh = lambda method, path, body=None: {"id": 9876}
18
+ run.gh_pages = lambda path: []
19
+ return module, run
20
+
21
+
22
+ def test_triage_submits_each_changed_issue_as_agent_work(tmp_path, monkeypatch):
23
+ issues = [
24
+ {"number": 2, "title": "Second", "body": "B", "labels": []},
25
+ {"number": 1, "title": "First", "body": "A", "labels": []},
26
+ ]
27
+ _module, run = _triage(tmp_path, monkeypatch, issues)
28
+ submit = Mock(
29
+ side_effect=lambda **kwargs: {
30
+ "disposition": "created",
31
+ "conversation_id": kwargs["subject"],
32
+ }
33
+ )
34
+ run.dispatcher.deliver = submit
35
+
36
+ run.run()
37
+
38
+ assert [call.kwargs["subject"] for call in submit.call_args_list] == [
39
+ "9876:issue:1",
40
+ "9876:issue:2",
41
+ ]
42
+ assert "Do not implement code" in submit.call_args_list[0].kwargs["prompt"]
43
+ assert "GITHUB_PERSONAL_ACCESS_TOKEN" in submit.call_args_list[0].kwargs["prompt"]
44
+
45
+
46
+ def test_triage_continues_after_one_submission_fails(tmp_path, monkeypatch):
47
+ issues = [
48
+ {"number": 1, "title": "First", "body": "A", "labels": []},
49
+ {"number": 2, "title": "Second", "body": "B", "labels": []},
50
+ ]
51
+ _module, run = _triage(tmp_path, monkeypatch, issues)
52
+ submit = Mock(
53
+ side_effect=[
54
+ RuntimeError("unavailable"),
55
+ {"disposition": "created", "conversation_id": "second"},
56
+ ]
57
+ )
58
+ run.dispatcher.deliver = submit
59
+
60
+ run.run()
61
+
62
+ assert [call.kwargs["subject"] for call in submit.call_args_list] == [
63
+ "9876:issue:1",
64
+ "9876:issue:2",
65
+ ]
66
+
67
+
68
+ def test_triage_skips_ready_blocked_and_unchanged_issues(tmp_path, monkeypatch):
69
+ issues = [
70
+ {
71
+ "number": 1,
72
+ "title": "Ready",
73
+ "body": "",
74
+ "labels": [{"name": "ready-for-dev"}],
75
+ },
76
+ {"number": 2, "title": "Blocked", "body": "", "labels": []},
77
+ {"number": 3, "title": "Unchanged", "body": "", "labels": []},
78
+ ]
79
+ module, run = _triage(tmp_path, monkeypatch, issues)
80
+ run.dependencies_complete = lambda issue: issue["number"] != 2
81
+ marker = module.hashlib.sha256(
82
+ module.json.dumps(["Unchanged", "", []], sort_keys=True).encode()
83
+ ).hexdigest()
84
+ run.gh_pages = lambda path: [{"id": 10, "body": f"<!-- triage-source:{marker} -->"}]
85
+ submit = Mock()
86
+ run.dispatcher.deliver = submit
87
+
88
+ run.run()
89
+
90
+ submit.assert_not_called()
@@ -0,0 +1,233 @@
1
+ """Contract tests for independent watchdog delivery."""
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ from urllib.error import HTTPError
7
+
8
+ import pytest
9
+ from github_automation_helpers import ROOT, worker
10
+ from openhands.sdk.skills import install_skill
11
+
12
+
13
+ def test_installed_watchdog_skill_imports_worker(tmp_path):
14
+ name = "github-delivery-watchdog"
15
+ install_skill(source=str(ROOT / "skills" / name), installed_dir=tmp_path)
16
+ result = subprocess.run(
17
+ [sys.executable, "-c", "import worker"],
18
+ cwd=tmp_path / name / "scripts",
19
+ text=True,
20
+ capture_output=True,
21
+ check=False,
22
+ )
23
+ assert result.returncode == 0, result.stderr
24
+
25
+
26
+ @pytest.mark.parametrize(
27
+ "changes",
28
+ [
29
+ {"head": {"sha": "b" * 40, "ref": "openhands/issue-1"}},
30
+ {"draft": True},
31
+ {"mergeable": None},
32
+ {"state": "closed"},
33
+ {"base": {"sha": "c" * 40, "ref": "release"}},
34
+ ],
35
+ )
36
+ def test_watchdog_rejects_ineligible_pr_before_merge(tmp_path, monkeypatch, changes):
37
+ cls = worker("github-delivery-watchdog", tmp_path, monkeypatch).DeliveryWatchdog
38
+ run = object.__new__(cls)
39
+ run.config = {"base_branch": "main"}
40
+ pr = {
41
+ "state": "open",
42
+ "draft": False,
43
+ "mergeable": True,
44
+ "head": {"sha": "a" * 40, "ref": "openhands/issue-1"},
45
+ "base": {"ref": "main", "sha": "c" * 40},
46
+ **changes,
47
+ }
48
+ calls = []
49
+ run.gh = lambda *args: calls.append(args) or pr
50
+ assert run.merge(1, "a" * 40) is None
51
+ assert calls == [("GET", "/pulls/1")]
52
+
53
+
54
+ @pytest.mark.parametrize("base_branch", ["main", "release"])
55
+ @pytest.mark.parametrize(
56
+ "review,ci,comparison,merged",
57
+ [
58
+ ("success", True, "ahead", True),
59
+ ("failure", True, "ahead", False),
60
+ ("success", False, "ahead", False),
61
+ ("success", True, "behind", False),
62
+ ],
63
+ )
64
+ def test_watchdog_requires_exact_acceptance_ci_and_current_base(
65
+ tmp_path, monkeypatch, review, ci, comparison, merged, base_branch
66
+ ):
67
+ cls = worker("github-delivery-watchdog", tmp_path, monkeypatch).DeliveryWatchdog
68
+ run = object.__new__(cls)
69
+ run.config = {}
70
+ sha = "a" * 40
71
+ calls = []
72
+
73
+ def gh(method, path, body=None):
74
+ calls.append((method, path, body))
75
+ if path == "":
76
+ return {"default_branch": base_branch}
77
+ if path == "/pulls/1":
78
+ return {
79
+ "state": "open",
80
+ "draft": False,
81
+ "mergeable": True,
82
+ "head": {"sha": sha, "ref": "openhands/issue-1"},
83
+ "base": {"ref": base_branch, "sha": "c" * 40},
84
+ }
85
+ if "/compare/" in path:
86
+ return {"status": comparison}
87
+ return {"merged": True}
88
+
89
+ run.gh = gh
90
+ run.statuses = lambda sha: {
91
+ "software-factory/tests": "success",
92
+ "software-factory/review": review,
93
+ }
94
+ run.ci_passed = lambda sha: ci
95
+ assert bool(run.merge(1, sha)) is merged
96
+ if merged:
97
+ assert calls[-1] == (
98
+ "PUT",
99
+ "/pulls/1/merge",
100
+ {"sha": sha, "merge_method": "squash"},
101
+ )
102
+ else:
103
+ assert not any(c[0] == "PUT" for c in calls)
104
+
105
+
106
+ def test_watchdog_updates_accepted_branch_for_fresh_review(tmp_path, monkeypatch):
107
+ cls = worker("github-delivery-watchdog", tmp_path, monkeypatch).DeliveryWatchdog
108
+ run = object.__new__(cls)
109
+ run.config = {}
110
+ sha = "a" * 40
111
+ calls = []
112
+
113
+ def gh(method, path, body=None):
114
+ calls.append((method, path, body))
115
+ if path == "":
116
+ return {"default_branch": "main"}
117
+ if path == "/pulls/1":
118
+ return {
119
+ "state": "open",
120
+ "draft": False,
121
+ "mergeable": True,
122
+ "head": {"sha": sha, "ref": "openhands/issue-1"},
123
+ "base": {"ref": "main", "sha": "c" * 40},
124
+ }
125
+ if "/compare/" in path:
126
+ return {"status": "diverged", "behind_by": 1}
127
+ return {"message": "Updating pull request branch."}
128
+
129
+ run.gh = gh
130
+ run.statuses = lambda sha: {
131
+ "software-factory/tests": "success",
132
+ "software-factory/review": "success",
133
+ }
134
+ run.ci_passed = lambda sha: True
135
+
136
+ assert run.merge(1, sha) == {"message": "Updating pull request branch."}
137
+ assert calls[-2:] == [
138
+ ("POST", "/issues/1/labels", {"labels": ["openhands-review"]}),
139
+ ("PUT", "/pulls/1/update-branch", {"expected_head_sha": sha}),
140
+ ]
141
+
142
+
143
+ @pytest.mark.parametrize(
144
+ "error",
145
+ [
146
+ HTTPError("https://api.github.com", 409, "Conflict", {}, None),
147
+ TimeoutError("GitHub unavailable"),
148
+ json.JSONDecodeError("Invalid response", "", 0),
149
+ KeyError("head"),
150
+ ],
151
+ )
152
+ def test_watchdog_continues_after_a_pr_api_failure(tmp_path, monkeypatch, error):
153
+ module = worker("github-delivery-watchdog", tmp_path, monkeypatch)
154
+ run = object.__new__(module.DeliveryWatchdog)
155
+ run.gh_pages = lambda path: [
156
+ {"number": 1, "head": {"sha": "a"}},
157
+ {"number": 2, "head": {"sha": "b"}},
158
+ ]
159
+ visited = []
160
+
161
+ def merge(number, sha):
162
+ visited.append(number)
163
+ if number == 1:
164
+ raise error
165
+ return {"merged": True}
166
+
167
+ run.merge = merge
168
+ run.run()
169
+ assert visited == [1, 2]
170
+
171
+
172
+ @pytest.mark.parametrize("required,expected", [([], True), ([123], False)])
173
+ def test_actions_are_optional_but_configured_workflows_are_required(
174
+ tmp_path, monkeypatch, required, expected
175
+ ):
176
+ module = worker("github-delivery-watchdog", tmp_path, monkeypatch)
177
+ run = object.__new__(module.DeliveryWatchdog)
178
+ run.config = {"required_workflow_ids": required}
179
+ run.gh = lambda *args: {"workflow_runs": [], "total_count": 0}
180
+ assert run.ci_passed("a" * 40) is expected
181
+
182
+
183
+ @pytest.mark.parametrize(
184
+ "changes,expected",
185
+ [
186
+ ({}, True),
187
+ ({"conclusion": "failure"}, False),
188
+ ({"status": "in_progress"}, False),
189
+ ({"workflow_id": 2}, False),
190
+ ({"event": "push"}, False),
191
+ ({"head_branch": "other-branch"}, False),
192
+ ({"head_sha": "other-commit"}, False),
193
+ ],
194
+ )
195
+ def test_new_actions_execution_supersedes_only_its_own_trigger(
196
+ tmp_path, monkeypatch, changes, expected
197
+ ):
198
+ module = worker("github-delivery-watchdog", tmp_path, monkeypatch)
199
+ run = object.__new__(module.DeliveryWatchdog)
200
+ run.config = {"required_workflow_ids": [1]}
201
+ old = {
202
+ "id": 10,
203
+ "run_number": 1,
204
+ "workflow_id": 1,
205
+ "head_sha": "sha",
206
+ "event": "pull_request",
207
+ "head_branch": "feature",
208
+ "status": "completed",
209
+ "conclusion": "failure",
210
+ }
211
+ new = {**old, "id": 11, "run_number": 2, "conclusion": "success", **changes}
212
+ # Deliberately unsorted: correctness must not depend on API response order.
213
+ run.gh = lambda *args: {"workflow_runs": [old, new], "total_count": 2}
214
+ assert run.ci_passed("sha") is expected
215
+
216
+
217
+ def test_native_actions_rerun_keeps_its_run_identity(tmp_path, monkeypatch):
218
+ module = worker("github-delivery-watchdog", tmp_path, monkeypatch)
219
+ run = object.__new__(module.DeliveryWatchdog)
220
+ run.config = {"required_workflow_ids": [1]}
221
+ result = {
222
+ "id": 10,
223
+ "run_number": 1,
224
+ "run_attempt": 2,
225
+ "workflow_id": 1,
226
+ "head_sha": "sha",
227
+ "event": "pull_request",
228
+ "head_branch": "feature",
229
+ "status": "completed",
230
+ "conclusion": "success",
231
+ }
232
+ run.gh = lambda *args: {"workflow_runs": [result], "total_count": 1}
233
+ assert run.ci_passed("sha")