@openhands/extensions 0.23.0 → 0.25.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 (75) hide show
  1. package/.agents/skills/custom-codereview-guide.md +14 -12
  2. package/.claude-plugin/marketplace.json +20 -0
  3. package/.codex-plugin/marketplace.json +20 -0
  4. package/.github/workflows/check-extensions.yml +6 -6
  5. package/.github/workflows/deprecation-check.yml +1 -1
  6. package/.github/workflows/issue-readiness-check.yml +37 -106
  7. package/.github/workflows/npm-publish.yml +2 -2
  8. package/.github/workflows/pr-description-check.yml +1 -1
  9. package/.github/workflows/pypi-publish.yml +3 -3
  10. package/.github/workflows/tests.yml +2 -2
  11. package/.plugin/marketplace.json +20 -0
  12. package/.release-please-manifest.json +1 -1
  13. package/AGENTS.md +10 -1
  14. package/README.md +4 -2
  15. package/automations/bundle-index.js +11 -7
  16. package/automations/catalog/github-pr-reviewer/manifest.json +19 -8
  17. package/automations/catalog/github-stale-ci-pr-closer/manifest.json +71 -0
  18. package/automations/catalog-index.js +2 -0
  19. package/integrations/catalog/granola.json +38 -0
  20. package/integrations/catalog-index.js +16 -14
  21. package/marketplaces/openhands-extensions.json +20 -0
  22. package/package.json +1 -1
  23. package/plugins/openhands/skills/openhands-sdk/SKILL.md +1 -0
  24. package/plugins/pr-review/skills/github-pr-review/SKILL.md +12 -2
  25. package/plugins/qa-changes/skills/github-pr-review/SKILL.md +12 -2
  26. package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +116 -0
  27. package/pyproject.toml +2 -2
  28. package/python/openhands_extensions/_version.py +1 -1
  29. package/skills/add-skill/SKILL.md +5 -2
  30. package/skills/canvas-extension-api/.claude-plugin/plugin.json +19 -0
  31. package/skills/canvas-extension-api/.codex-plugin/plugin.json +19 -0
  32. package/skills/canvas-extension-api/.plugin/plugin.json +19 -0
  33. package/skills/canvas-extension-api/README.md +33 -0
  34. package/skills/canvas-extension-api/SKILL.md +254 -0
  35. package/skills/canvas-extension-api/references/acceptance-checklist.md +20 -0
  36. package/skills/canvas-extension-api/references/backend-safety.md +9 -0
  37. package/skills/canvas-extension-api/references/connections.md +126 -0
  38. package/skills/canvas-extension-api/references/packaging-recipes.md +43 -0
  39. package/skills/canvas-extension-api/references/sidecar-pattern.md +49 -0
  40. package/skills/canvas-extension-api/references/testing-and-installation.md +286 -0
  41. package/skills/canvas-extension-api/references/v1-contract.md +250 -0
  42. package/skills/canvas-extension-api/scripts/validate-extension.mjs +92 -0
  43. package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +60 -0
  44. package/skills/github/scripts/github_client.py +116 -0
  45. package/skills/github-agents-md-maintainer/scripts/github_client.py +116 -0
  46. package/skills/github-delivery-watchdog/scripts/github_client.py +116 -0
  47. package/skills/github-issue-to-pr/scripts/github_client.py +116 -0
  48. package/skills/github-issue-triage/scripts/github_client.py +116 -0
  49. package/skills/github-pr-review/SKILL.md +12 -2
  50. package/skills/github-pr-reviewer/README.md +41 -1
  51. package/skills/github-pr-reviewer/SKILL.md +180 -5
  52. package/skills/github-pr-reviewer/references/state-schema.md +15 -0
  53. package/skills/github-pr-reviewer/scripts/github_client.py +116 -0
  54. package/skills/github-pr-reviewer/scripts/main.py +57 -12
  55. package/skills/github-pr-reviewer/scripts/worker.py +832 -61
  56. package/skills/github-pr-reviewer/tests/test_main.py +69 -0
  57. package/skills/github-repo-monitor/scripts/github_client.py +116 -0
  58. package/skills/github-repo-monitor/scripts/main.py +32 -1
  59. package/skills/github-stale-ci-pr-closer/.claude-plugin/plugin.json +9 -0
  60. package/skills/github-stale-ci-pr-closer/.codex-plugin/plugin.json +9 -0
  61. package/skills/github-stale-ci-pr-closer/.plugin/plugin.json +9 -0
  62. package/skills/github-stale-ci-pr-closer/README.md +9 -0
  63. package/skills/github-stale-ci-pr-closer/SKILL.md +24 -0
  64. package/skills/github-stale-ci-pr-closer/commands/github-stale-ci-pr-closer.md +8 -0
  65. package/skills/github-stale-ci-pr-closer/scripts/worker.py +335 -0
  66. package/skills/github-stale-ci-pr-closer/tests/test_worker.py +213 -0
  67. package/skills/index.js +22 -5
  68. package/skills/openhands-sdk/SKILL.md +1 -0
  69. package/tests/fixtures/automations/github-pr-reviewer.json +39 -24
  70. package/tests/test_github_automation_foundation.py +158 -0
  71. package/tests/test_github_repo_monitor.py +107 -0
  72. package/tests/test_github_reviewer_delivery.py +2236 -4
  73. package/.github/scripts/check_issue_readiness.py +0 -272
  74. package/.github/scripts/post-readiness-comment.mjs +0 -184
  75. package/tests/test_check_issue_readiness.py +0 -135
@@ -0,0 +1,213 @@
1
+ import sys
2
+ import unittest
3
+ from os import environ
4
+ from pathlib import Path
5
+ from unittest.mock import Mock, patch
6
+
7
+ sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
8
+ sys.path.insert(0, str(Path(__file__).parents[2] / "github" / "scripts"))
9
+ import worker
10
+
11
+ DAY = 24 * 60 * 60
12
+
13
+
14
+ class TestStateKey(unittest.TestCase):
15
+ def test_is_scoped_to_the_automation(self):
16
+ payload = '{"automation_id":"automation-123"}'
17
+ with patch.dict(environ, {"AUTOMATION_EVENT_PAYLOAD": payload}):
18
+ self.assertEqual(
19
+ worker._state_key("OpenHands/extensions"),
20
+ "github-stale-ci-pr-closer:automation-123:OpenHands__extensions",
21
+ )
22
+
23
+
24
+ class TestReconcile(unittest.TestCase):
25
+ def setUp(self):
26
+ self.closer = object.__new__(worker.StaleCIPullRequestCloser)
27
+ self.closer.required_checks = Mock(return_value=[{"context": "test"}])
28
+ self.closer.required_ci_state = Mock(return_value="failing")
29
+ self.closer.post_comment = Mock(return_value={"id": 99})
30
+ self.closer.comments = Mock(return_value=[])
31
+ self.closer.gh = Mock(return_value={})
32
+ self.pr = {
33
+ "number": 7,
34
+ "draft": False,
35
+ "head": {"sha": "abc"},
36
+ "base": {"ref": "main"},
37
+ "user": {"login": "author"},
38
+ }
39
+ self.records = {}
40
+
41
+ def reconcile(self, now):
42
+ return self.closer.reconcile(self.pr, self.records, now)
43
+
44
+ def test_warns_once_after_seven_days(self):
45
+ self.assertEqual(self.reconcile(0), "observing")
46
+ self.assertEqual(self.reconcile(7 * DAY), "warned")
47
+ self.assertEqual(self.reconcile(8 * DAY), "waiting")
48
+ self.closer.post_comment.assert_called_once()
49
+ self.assertIn(worker.WARNING_MARKER, self.closer.post_comment.call_args.args[1])
50
+
51
+ def test_recovers_a_persisted_warning_marker_without_duplication(self):
52
+ self.reconcile(0)
53
+ self.closer.comments.return_value = [
54
+ {
55
+ "id": 41,
56
+ "user": {"login": "all-hands-bot"},
57
+ "created_at": "1970-01-08T00:00:00Z",
58
+ "body": "<!-- openhands-stale-ci-warning head=abc -->",
59
+ }
60
+ ]
61
+ self.assertEqual(self.reconcile(8 * DAY), "waiting")
62
+ self.closer.post_comment.assert_not_called()
63
+ self.assertEqual(self.records["7"]["warning"]["comment_id"], 41)
64
+
65
+ def test_closes_after_seven_more_days_without_follow_up(self):
66
+ self.reconcile(0)
67
+ self.reconcile(7 * DAY)
68
+ self.assertEqual(self.reconcile(14 * DAY), "closed")
69
+ self.assertTrue(
70
+ any(
71
+ call.args[:2] == ("PATCH", "/pulls/7")
72
+ for call in self.closer.gh.call_args_list
73
+ )
74
+ )
75
+ self.assertNotIn("7", self.records)
76
+
77
+ def test_passing_or_pending_ci_cancels_the_lifecycle(self):
78
+ self.reconcile(0)
79
+ for state in ("passing", "pending"):
80
+ self.closer.required_ci_state.return_value = state
81
+ self.assertEqual(self.reconcile(DAY), state)
82
+ self.assertNotIn("7", self.records)
83
+ self.closer.required_ci_state.return_value = "failing"
84
+ self.reconcile(DAY)
85
+
86
+ def test_old_warning_is_not_reused_after_ci_recovers(self):
87
+ self.reconcile(0)
88
+ self.reconcile(7 * DAY)
89
+ self.closer.required_ci_state.return_value = "passing"
90
+ self.reconcile(8 * DAY)
91
+ self.closer.required_ci_state.return_value = "failing"
92
+ self.closer.comments.return_value = [
93
+ {
94
+ "id": 41,
95
+ "user": {"login": "all-hands-bot"},
96
+ "created_at": "1970-01-08T00:00:00Z",
97
+ "body": "<!-- openhands-stale-ci-warning head=abc -->",
98
+ }
99
+ ]
100
+ self.assertEqual(self.reconcile(8 * DAY), "observing")
101
+ self.assertEqual(self.reconcile(15 * DAY), "warned")
102
+ self.assertEqual(self.closer.post_comment.call_count, 2)
103
+ self.assertIn("warning", self.records["7"])
104
+
105
+ def test_author_comment_starts_a_fresh_window(self):
106
+ self.reconcile(0)
107
+ self.reconcile(7 * DAY)
108
+ self.closer.comments.return_value = [
109
+ {
110
+ "user": {"login": "author"},
111
+ "created_at": "1970-01-09T00:00:00Z",
112
+ "body": "working",
113
+ }
114
+ ]
115
+ self.assertEqual(self.reconcile(14 * DAY), "followed-up")
116
+ self.assertEqual(self.records["7"]["first_failed_at"], 14 * DAY)
117
+ self.assertNotIn("warning", self.records["7"])
118
+
119
+ def test_new_head_starts_a_fresh_window(self):
120
+ self.reconcile(0)
121
+ self.reconcile(7 * DAY)
122
+ self.pr["head"]["sha"] = "def"
123
+ self.assertEqual(self.reconcile(14 * DAY), "observing")
124
+ self.assertEqual(
125
+ self.records["7"], {"head_sha": "def", "first_failed_at": 14 * DAY}
126
+ )
127
+
128
+ def test_draft_cancels_the_lifecycle(self):
129
+ self.reconcile(0)
130
+ self.pr["draft"] = True
131
+ self.assertEqual(self.reconcile(DAY), "draft")
132
+ self.assertNotIn("7", self.records)
133
+
134
+
135
+ class TestRequiredCI(unittest.TestCase):
136
+ def setUp(self):
137
+ self.closer = object.__new__(worker.StaleCIPullRequestCloser)
138
+ self.closer.gh_pages = Mock(return_value=[])
139
+
140
+ def state(self, runs, required):
141
+ self.closer.gh = Mock(return_value={"check_runs": runs})
142
+ return self.closer.required_ci_state("sha", required)
143
+
144
+ def test_optional_failures_are_ignored(self):
145
+ runs = [
146
+ {
147
+ "id": 2,
148
+ "name": "required",
149
+ "app": {"id": 1},
150
+ "status": "completed",
151
+ "conclusion": "success",
152
+ },
153
+ {
154
+ "id": 1,
155
+ "name": "optional",
156
+ "app": {"id": 1},
157
+ "status": "completed",
158
+ "conclusion": "failure",
159
+ },
160
+ ]
161
+ self.assertEqual(
162
+ self.state(runs, [{"context": "required", "integration_id": 1}]), "passing"
163
+ )
164
+
165
+ def test_pending_required_check_wins_over_failure(self):
166
+ runs = [
167
+ {
168
+ "id": 2,
169
+ "name": "a",
170
+ "app": {"id": 1},
171
+ "status": "completed",
172
+ "conclusion": "failure",
173
+ },
174
+ {
175
+ "id": 1,
176
+ "name": "b",
177
+ "app": {"id": 1},
178
+ "status": "in_progress",
179
+ "conclusion": None,
180
+ },
181
+ ]
182
+ required = [
183
+ {"context": "a", "integration_id": 1},
184
+ {"context": "b", "integration_id": 1},
185
+ ]
186
+ self.assertEqual(self.state(runs, required), "pending")
187
+
188
+
189
+ class TestCandidateSearch(unittest.TestCase):
190
+ def setUp(self):
191
+ self.closer = object.__new__(worker.StaleCIPullRequestCloser)
192
+ self.closer.repository = "OpenHands/OpenHands"
193
+
194
+ def test_searches_only_inactive_failing_pull_requests(self):
195
+ self.closer.api = Mock(
196
+ return_value={
197
+ "incomplete_results": False,
198
+ "items": [{"number": 2}, {"number": 3}],
199
+ }
200
+ )
201
+ self.assertEqual(self.closer.stale_failure_numbers(14 * DAY), {2, 3})
202
+ params = self.closer.api.call_args.kwargs["params"]
203
+ self.assertIn("is:pr is:open draft:false status:failure", params["q"])
204
+ self.assertIn("updated:<1970-01-08", params["q"])
205
+
206
+ def test_rejects_incomplete_search_results(self):
207
+ self.closer.api = Mock(return_value={"incomplete_results": True, "items": []})
208
+ with self.assertRaisesRegex(RuntimeError, "incomplete"):
209
+ self.closer.stale_failure_numbers(14 * DAY)
210
+
211
+
212
+ if __name__ == "__main__":
213
+ unittest.main()