@openhands/extensions 0.24.0 → 0.26.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.
- package/.claude-plugin/marketplace.json +30 -0
- package/.codex-plugin/marketplace.json +30 -0
- package/.github/pull_request_template.md +17 -2
- package/.github/scripts/check_pr_description.py +64 -5
- package/.github/scripts/markdown_sections.py +95 -0
- package/.github/workflows/build-vscode-app.yml +30 -0
- package/.github/workflows/check-extensions.yml +3 -0
- package/.github/workflows/tests.yml +26 -0
- package/.plugin/marketplace.json +30 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +3 -2
- package/README.md +10 -3
- package/apps/README.md +20 -0
- package/apps/catalog.json +10 -0
- package/apps/index.d.ts +11 -0
- package/apps/index.js +5 -0
- package/apps/vscode/README.md +13 -0
- package/apps/vscode/canvas-extension.json +19 -0
- package/apps/vscode/extension.js +114 -0
- package/apps/vscode/scripts/package.mjs +84 -0
- package/automations/bundle-index.js +18 -14
- package/automations/catalog/github-agents-md-maintainer/manifest.json +1 -1
- package/automations/catalog/github-pr-reviewer/manifest.json +19 -8
- package/automations/catalog/github-stale-ci-pr-closer/manifest.json +71 -0
- package/automations/catalog/gitlab-issue-to-mr/manifest.json +1 -1
- package/automations/catalog/news-digest/manifest.json +2 -2
- package/automations/catalog-index.js +2 -0
- package/index.d.ts +2 -0
- package/index.js +1 -0
- package/marketplaces/openhands-extensions.json +30 -0
- package/package.json +9 -1
- package/plugins/openhands/skills/openhands-api/README.md +1 -1
- package/plugins/openhands/skills/openhands-api/SKILL.md +3 -3
- package/plugins/openhands/skills/openhands-api/scripts/openhands_api.py +13 -0
- package/plugins/openhands/skills/openhands-automation/SKILL.md +66 -4
- package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +21 -7
- package/plugins/openhands/skills/openhands-sdk/SKILL.md +1 -0
- package/plugins/pr-review/skills/github-pr-review/SKILL.md +12 -2
- package/plugins/qa-changes/skills/github-pr-review/SKILL.md +12 -2
- package/plugins/runtime-api-configs/.claude-plugin/plugin.json +20 -0
- package/plugins/runtime-api-configs/.codex-plugin/plugin.json +20 -0
- package/plugins/runtime-api-configs/.plugin/plugin.json +20 -0
- package/plugins/runtime-api-configs/README.md +59 -0
- package/plugins/runtime-api-configs/SKILL.md +240 -0
- package/plugins/runtime-api-configs/scripts/warm_runtime_configs.py +331 -0
- package/plugins/runtime-api-configs/tests/test_warm_runtime_configs.py +338 -0
- package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +223 -43
- package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +183 -15
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/scripts/check_plugin_resources.py +116 -0
- package/scripts/sync_extensions.py +32 -5
- package/skills/canvas-extension-api/SKILL.md +34 -3
- package/skills/canvas-extension-api/references/backend-safety.md +14 -0
- package/skills/canvas-extension-api/references/connections.md +24 -0
- package/skills/canvas-extension-api/references/sidecar-pattern.md +49 -10
- package/skills/canvas-extension-api/references/v1-contract.md +53 -3
- package/skills/canvas-extension-api/scripts/validate-extension.mjs +52 -0
- package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +192 -1
- package/skills/github/scripts/agent_conversation.py +223 -43
- package/skills/github/scripts/github_client.py +183 -15
- package/skills/github-agents-md-maintainer/README.md +7 -0
- package/skills/github-agents-md-maintainer/SKILL.md +13 -6
- package/skills/github-agents-md-maintainer/scripts/github_client.py +183 -15
- package/skills/github-agents-md-maintainer/scripts/main.py +199 -29
- package/skills/github-delivery-watchdog/scripts/github_client.py +183 -15
- package/skills/github-issue-to-pr/scripts/agent_conversation.py +223 -43
- package/skills/github-issue-to-pr/scripts/github_client.py +183 -15
- package/skills/github-issue-to-pr/scripts/worker.py +10 -1
- package/skills/github-issue-triage/scripts/agent_conversation.py +223 -43
- package/skills/github-issue-triage/scripts/github_client.py +183 -15
- package/skills/github-pr-review/SKILL.md +12 -2
- package/skills/github-pr-reviewer/README.md +36 -0
- package/skills/github-pr-reviewer/SKILL.md +147 -4
- package/skills/github-pr-reviewer/scripts/agent_conversation.py +223 -43
- package/skills/github-pr-reviewer/scripts/github_client.py +183 -15
- package/skills/github-pr-reviewer/scripts/main.py +17 -1
- package/skills/github-pr-reviewer/scripts/worker.py +842 -57
- package/skills/github-repo-monitor/scripts/github_client.py +183 -15
- package/skills/github-repo-monitor/scripts/main.py +32 -1
- package/skills/github-stale-ci-pr-closer/.claude-plugin/plugin.json +9 -0
- package/skills/github-stale-ci-pr-closer/.codex-plugin/plugin.json +9 -0
- package/skills/github-stale-ci-pr-closer/.plugin/plugin.json +9 -0
- package/skills/github-stale-ci-pr-closer/README.md +9 -0
- package/skills/github-stale-ci-pr-closer/SKILL.md +24 -0
- package/skills/github-stale-ci-pr-closer/commands/github-stale-ci-pr-closer.md +8 -0
- package/skills/github-stale-ci-pr-closer/scripts/worker.py +508 -0
- package/skills/github-stale-ci-pr-closer/tests/test_worker.py +353 -0
- package/skills/gitlab-issue-to-mr/README.md +12 -0
- package/skills/gitlab-issue-to-mr/SKILL.md +13 -6
- package/skills/gitlab-issue-to-mr/scripts/main.py +217 -42
- package/skills/incident-retrospective/SKILL.md +5 -5
- package/skills/index.js +34 -16
- package/skills/jira-issue-to-pr/README.md +12 -4
- package/skills/jira-issue-to-pr/SKILL.md +129 -18
- package/skills/jira-issue-to-pr/references/setup.md +8 -0
- package/skills/jira-issue-to-pr/scripts/main.py +181 -54
- package/skills/jira-issue-to-pr/scripts/setup_webhook.py +290 -0
- package/skills/linear-triage/SKILL.md +5 -5
- package/skills/news-digest/README.md +6 -0
- package/skills/news-digest/SKILL.md +19 -9
- package/skills/news-digest/commands/news-digest-setup.md +1 -1
- package/skills/news-digest/scripts/main.py +133 -15
- package/skills/openhands-api/README.md +1 -1
- package/skills/openhands-api/SKILL.md +3 -3
- package/skills/openhands-api/scripts/openhands_api.py +13 -0
- package/skills/openhands-automation/SKILL.md +66 -4
- package/skills/openhands-automation/references/custom-automation.md +21 -7
- package/skills/openhands-sdk/SKILL.md +1 -0
- package/skills/research-brief/SKILL.md +5 -5
- package/skills/slack-standup-digest/SKILL.md +5 -5
- package/skills/ticket-to-code-change/SKILL.md +6 -3
- package/skills/use-jev/.claude-plugin/plugin.json +8 -0
- package/skills/use-jev/.codex-plugin/plugin.json +8 -0
- package/skills/use-jev/.plugin/plugin.json +8 -0
- package/skills/use-jev/README.md +66 -0
- package/skills/use-jev/SKILL.md +66 -0
- package/tests/fixtures/automations/github-pr-reviewer.json +39 -24
- package/tests/test_agent_conversation_dispatch.py +212 -0
- package/tests/test_apps_catalog.py +26 -0
- package/tests/test_automation_setup.py +44 -0
- package/tests/test_check_pr_description.py +89 -6
- package/tests/test_github_automation_foundation.py +158 -0
- package/tests/test_github_repo_monitor.py +107 -0
- package/tests/test_github_reviewer_delivery.py +2247 -6
- package/tests/test_github_stale_ci_pr_closer.py +56 -0
- package/tests/test_jira_issue_to_pr.py +312 -0
- package/tests/test_jira_setup_webhook.py +309 -0
- package/tests/test_openhands_api_python.py +37 -0
- package/tests/test_skill_plugin_loading.py +25 -15
- package/tests/test_sync_extensions.py +102 -0
- package/tests/test_vscode_app.py +33 -0
- package/tests/vscode-app.test.mjs +43 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import unittest
|
|
3
|
+
from os import environ
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import Mock, patch
|
|
6
|
+
from urllib.error import HTTPError
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).parents[2] / "github" / "scripts"))
|
|
10
|
+
import worker
|
|
11
|
+
|
|
12
|
+
DAY = 24 * 60 * 60
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestStateKey(unittest.TestCase):
|
|
16
|
+
def test_is_scoped_to_the_automation(self):
|
|
17
|
+
payload = '{"automation_id":"automation-123"}'
|
|
18
|
+
with patch.dict(environ, {"AUTOMATION_EVENT_PAYLOAD": payload}):
|
|
19
|
+
self.assertEqual(
|
|
20
|
+
worker._state_key("OpenHands/extensions"),
|
|
21
|
+
"github-stale-ci-pr-closer:automation-123:OpenHands__extensions",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestReconcile(unittest.TestCase):
|
|
26
|
+
def setUp(self):
|
|
27
|
+
self.closer = object.__new__(worker.StaleCIPullRequestCloser)
|
|
28
|
+
self.closer.required_checks = Mock(return_value=[{"context": "test"}])
|
|
29
|
+
self.closer.required_ci_state = Mock(return_value="failing")
|
|
30
|
+
self.closer.post_comment = Mock(return_value={"id": 99})
|
|
31
|
+
self.closer.comments = Mock(return_value=[])
|
|
32
|
+
self.closer.gh = Mock(return_value={})
|
|
33
|
+
self.pr = {
|
|
34
|
+
"number": 7,
|
|
35
|
+
"draft": False,
|
|
36
|
+
"head": {"sha": "abc"},
|
|
37
|
+
"base": {"ref": "main"},
|
|
38
|
+
"user": {"login": "author"},
|
|
39
|
+
}
|
|
40
|
+
self.records = {}
|
|
41
|
+
|
|
42
|
+
def reconcile(self, now):
|
|
43
|
+
return self.closer.reconcile(self.pr, self.records, now)
|
|
44
|
+
|
|
45
|
+
def test_warns_once_after_seven_days(self):
|
|
46
|
+
self.assertEqual(self.reconcile(0), "observing")
|
|
47
|
+
self.assertEqual(self.reconcile(7 * DAY), "warned")
|
|
48
|
+
self.assertEqual(self.reconcile(8 * DAY), "waiting")
|
|
49
|
+
self.closer.post_comment.assert_called_once()
|
|
50
|
+
self.assertIn(worker.WARNING_MARKER, self.closer.post_comment.call_args.args[1])
|
|
51
|
+
|
|
52
|
+
def test_recovers_a_persisted_warning_marker_without_duplication(self):
|
|
53
|
+
self.reconcile(0)
|
|
54
|
+
self.closer.comments.return_value = [
|
|
55
|
+
{
|
|
56
|
+
"id": 41,
|
|
57
|
+
"user": {"login": "all-hands-bot"},
|
|
58
|
+
"created_at": "1970-01-08T00:00:00Z",
|
|
59
|
+
"body": "<!-- openhands-stale-ci-warning head=abc -->",
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
self.assertEqual(self.reconcile(8 * DAY), "waiting")
|
|
63
|
+
self.closer.post_comment.assert_not_called()
|
|
64
|
+
self.assertEqual(self.records["7"]["warning"]["comment_id"], 41)
|
|
65
|
+
|
|
66
|
+
def test_closes_after_seven_more_days_without_follow_up(self):
|
|
67
|
+
self.reconcile(0)
|
|
68
|
+
self.reconcile(7 * DAY)
|
|
69
|
+
self.assertEqual(self.reconcile(14 * DAY), "closed")
|
|
70
|
+
self.assertTrue(
|
|
71
|
+
any(
|
|
72
|
+
call.args[:2] == ("PATCH", "/pulls/7")
|
|
73
|
+
for call in self.closer.gh.call_args_list
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
self.assertNotIn("7", self.records)
|
|
77
|
+
|
|
78
|
+
def test_passing_or_pending_ci_cancels_the_lifecycle(self):
|
|
79
|
+
self.reconcile(0)
|
|
80
|
+
for state in ("passing", "pending"):
|
|
81
|
+
self.closer.required_ci_state.return_value = state
|
|
82
|
+
self.assertEqual(self.reconcile(DAY), state)
|
|
83
|
+
self.assertNotIn("7", self.records)
|
|
84
|
+
self.closer.required_ci_state.return_value = "failing"
|
|
85
|
+
self.reconcile(DAY)
|
|
86
|
+
|
|
87
|
+
def test_old_warning_is_not_reused_after_ci_recovers(self):
|
|
88
|
+
self.reconcile(0)
|
|
89
|
+
self.reconcile(7 * DAY)
|
|
90
|
+
self.closer.required_ci_state.return_value = "passing"
|
|
91
|
+
self.reconcile(8 * DAY)
|
|
92
|
+
self.closer.required_ci_state.return_value = "failing"
|
|
93
|
+
self.closer.comments.return_value = [
|
|
94
|
+
{
|
|
95
|
+
"id": 41,
|
|
96
|
+
"user": {"login": "all-hands-bot"},
|
|
97
|
+
"created_at": "1970-01-08T00:00:00Z",
|
|
98
|
+
"body": "<!-- openhands-stale-ci-warning head=abc -->",
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
self.assertEqual(self.reconcile(8 * DAY), "observing")
|
|
102
|
+
self.assertEqual(self.reconcile(15 * DAY), "warned")
|
|
103
|
+
self.assertEqual(self.closer.post_comment.call_count, 2)
|
|
104
|
+
self.assertIn("warning", self.records["7"])
|
|
105
|
+
|
|
106
|
+
def test_author_comment_starts_a_fresh_window(self):
|
|
107
|
+
self.reconcile(0)
|
|
108
|
+
self.reconcile(7 * DAY)
|
|
109
|
+
self.closer.comments.return_value = [
|
|
110
|
+
{
|
|
111
|
+
"user": {"login": "author"},
|
|
112
|
+
"created_at": "1970-01-09T00:00:00Z",
|
|
113
|
+
"body": "working",
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
self.assertEqual(self.reconcile(14 * DAY), "followed-up")
|
|
117
|
+
self.assertEqual(self.records["7"]["first_failed_at"], 14 * DAY)
|
|
118
|
+
self.assertNotIn("warning", self.records["7"])
|
|
119
|
+
|
|
120
|
+
def test_new_head_starts_a_fresh_window(self):
|
|
121
|
+
self.reconcile(0)
|
|
122
|
+
self.reconcile(7 * DAY)
|
|
123
|
+
self.pr["head"]["sha"] = "def"
|
|
124
|
+
self.assertEqual(self.reconcile(14 * DAY), "observing")
|
|
125
|
+
self.assertEqual(
|
|
126
|
+
self.records["7"], {"head_sha": "def", "first_failed_at": 14 * DAY}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def test_old_check_failure_warns_despite_recent_pr_activity(self):
|
|
130
|
+
self.pr["updated_at"] = "1970-01-08T00:00:00Z"
|
|
131
|
+
self.pr["_ci_state"] = "failing"
|
|
132
|
+
self.pr["_failed_at"] = 0
|
|
133
|
+
self.assertEqual(self.reconcile(7 * DAY), "warned")
|
|
134
|
+
|
|
135
|
+
def test_backfills_legacy_observation_time_from_check_failure(self):
|
|
136
|
+
self.records["7"] = {"head_sha": "abc", "first_failed_at": 6 * DAY}
|
|
137
|
+
self.pr["_ci_state"] = "failing"
|
|
138
|
+
self.pr["_failed_at"] = 0
|
|
139
|
+
self.assertEqual(self.reconcile(7 * DAY), "warned")
|
|
140
|
+
|
|
141
|
+
def test_backfill_leaves_a_legacy_follow_up_reset_alone(self):
|
|
142
|
+
# A pre-marker version cleared `warning` on author follow-up to open a
|
|
143
|
+
# fresh window. Re-aging that record to the check failure would collapse
|
|
144
|
+
# the new window and warn inside it.
|
|
145
|
+
self.records["7"] = {"head_sha": "abc", "first_failed_at": 13 * DAY}
|
|
146
|
+
self.pr["_ci_state"] = "failing"
|
|
147
|
+
self.pr["_failed_at"] = 0
|
|
148
|
+
self.closer.comments.return_value = [
|
|
149
|
+
{
|
|
150
|
+
"user": {"login": "author"},
|
|
151
|
+
"created_at": "1970-01-14T00:00:00Z",
|
|
152
|
+
"body": "working",
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
self.assertEqual(self.reconcile(14 * DAY), "observing")
|
|
156
|
+
self.assertEqual(self.records["7"]["first_failed_at"], 13 * DAY)
|
|
157
|
+
self.closer.post_comment.assert_not_called()
|
|
158
|
+
|
|
159
|
+
def test_legacy_follow_up_reset_still_warns_after_a_full_fresh_window(self):
|
|
160
|
+
self.records["7"] = {"head_sha": "abc", "first_failed_at": 13 * DAY}
|
|
161
|
+
self.pr["_ci_state"] = "failing"
|
|
162
|
+
self.pr["_failed_at"] = 0
|
|
163
|
+
self.closer.comments.return_value = [
|
|
164
|
+
{
|
|
165
|
+
"user": {"login": "author"},
|
|
166
|
+
"created_at": "1970-01-14T00:00:00Z",
|
|
167
|
+
"body": "working",
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
self.assertEqual(self.reconcile(20 * DAY), "warned")
|
|
171
|
+
|
|
172
|
+
def test_draft_cancels_the_lifecycle(self):
|
|
173
|
+
self.reconcile(0)
|
|
174
|
+
self.pr["draft"] = True
|
|
175
|
+
self.assertEqual(self.reconcile(DAY), "draft")
|
|
176
|
+
self.assertNotIn("7", self.records)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class TestRequiredCI(unittest.TestCase):
|
|
180
|
+
def setUp(self):
|
|
181
|
+
self.closer = object.__new__(worker.StaleCIPullRequestCloser)
|
|
182
|
+
self.closer.gh_pages = Mock(return_value=[])
|
|
183
|
+
|
|
184
|
+
def state(self, runs, required):
|
|
185
|
+
self.closer.gh = Mock(return_value={"check_runs": runs})
|
|
186
|
+
return self.closer.required_ci_state("sha", required)
|
|
187
|
+
|
|
188
|
+
def test_optional_failures_are_ignored(self):
|
|
189
|
+
runs = [
|
|
190
|
+
{
|
|
191
|
+
"id": 2,
|
|
192
|
+
"name": "required",
|
|
193
|
+
"app": {"id": 1},
|
|
194
|
+
"status": "completed",
|
|
195
|
+
"conclusion": "success",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"id": 1,
|
|
199
|
+
"name": "optional",
|
|
200
|
+
"app": {"id": 1},
|
|
201
|
+
"status": "completed",
|
|
202
|
+
"conclusion": "failure",
|
|
203
|
+
},
|
|
204
|
+
]
|
|
205
|
+
self.assertEqual(
|
|
206
|
+
self.state(runs, [{"context": "required", "integration_id": 1}]), "passing"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def test_pending_required_check_wins_over_failure(self):
|
|
210
|
+
runs = [
|
|
211
|
+
{
|
|
212
|
+
"id": 2,
|
|
213
|
+
"name": "a",
|
|
214
|
+
"app": {"id": 1},
|
|
215
|
+
"status": "completed",
|
|
216
|
+
"conclusion": "failure",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"id": 1,
|
|
220
|
+
"name": "b",
|
|
221
|
+
"app": {"id": 1},
|
|
222
|
+
"status": "in_progress",
|
|
223
|
+
"conclusion": None,
|
|
224
|
+
},
|
|
225
|
+
]
|
|
226
|
+
required = [
|
|
227
|
+
{"context": "a", "integration_id": 1},
|
|
228
|
+
{"context": "b", "integration_id": 1},
|
|
229
|
+
]
|
|
230
|
+
self.assertEqual(self.state(runs, required), "pending")
|
|
231
|
+
|
|
232
|
+
def test_newest_required_failure_sets_the_age(self):
|
|
233
|
+
contexts = [
|
|
234
|
+
{
|
|
235
|
+
"__typename": "CheckRun",
|
|
236
|
+
"databaseId": 2,
|
|
237
|
+
"name": "a",
|
|
238
|
+
"status": "COMPLETED",
|
|
239
|
+
"conclusion": "FAILURE",
|
|
240
|
+
"completedAt": "1970-01-01T00:00:00Z",
|
|
241
|
+
"checkSuite": {"app": {"databaseId": 1}},
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"__typename": "CheckRun",
|
|
245
|
+
"databaseId": 3,
|
|
246
|
+
"name": "b",
|
|
247
|
+
"status": "COMPLETED",
|
|
248
|
+
"conclusion": "FAILURE",
|
|
249
|
+
"completedAt": "1970-01-03T00:00:00Z",
|
|
250
|
+
"checkSuite": {"app": {"databaseId": 1}},
|
|
251
|
+
},
|
|
252
|
+
]
|
|
253
|
+
state, failed_at = self.closer._graphql_ci_result(
|
|
254
|
+
contexts,
|
|
255
|
+
[
|
|
256
|
+
{"context": "a", "integration_id": 1},
|
|
257
|
+
{"context": "b", "integration_id": 1},
|
|
258
|
+
],
|
|
259
|
+
)
|
|
260
|
+
self.assertEqual(state, "failing")
|
|
261
|
+
self.assertEqual(failed_at, 2 * DAY)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class TestPullRequestPagination(unittest.TestCase):
|
|
265
|
+
def setUp(self):
|
|
266
|
+
self.closer = object.__new__(worker.StaleCIPullRequestCloser)
|
|
267
|
+
self.closer.repository = "OpenHands/OpenHands"
|
|
268
|
+
|
|
269
|
+
@staticmethod
|
|
270
|
+
def page(number, has_next, cursor):
|
|
271
|
+
return {
|
|
272
|
+
"data": {
|
|
273
|
+
"repository": {
|
|
274
|
+
"pullRequests": {
|
|
275
|
+
"pageInfo": {
|
|
276
|
+
"hasNextPage": has_next,
|
|
277
|
+
"endCursor": cursor,
|
|
278
|
+
},
|
|
279
|
+
"nodes": [
|
|
280
|
+
{
|
|
281
|
+
"number": number,
|
|
282
|
+
"isDraft": False,
|
|
283
|
+
"baseRefName": "main",
|
|
284
|
+
"headRefOid": f"sha-{number}",
|
|
285
|
+
"author": {"login": "author"},
|
|
286
|
+
"commits": {
|
|
287
|
+
"nodes": [
|
|
288
|
+
{
|
|
289
|
+
"commit": {
|
|
290
|
+
"statusCheckRollup": {
|
|
291
|
+
"contexts": {
|
|
292
|
+
"totalCount": 0,
|
|
293
|
+
"nodes": [],
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
],
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
def test_paginates_every_open_pull_request(self):
|
|
308
|
+
self.closer.required_checks = Mock(return_value=[])
|
|
309
|
+
self.closer.api = Mock(
|
|
310
|
+
side_effect=[self.page(2, True, "next"), self.page(3, False, None)]
|
|
311
|
+
)
|
|
312
|
+
self.assertEqual(
|
|
313
|
+
[pr["number"] for pr in self.closer.open_pull_requests()], [2, 3]
|
|
314
|
+
)
|
|
315
|
+
second_variables = self.closer.api.call_args_list[1].kwargs["body"]["variables"]
|
|
316
|
+
self.assertEqual(second_variables["cursor"], "next")
|
|
317
|
+
|
|
318
|
+
def test_falls_back_to_rest_when_check_contexts_are_paginated(self):
|
|
319
|
+
page = self.page(2, False, None)
|
|
320
|
+
contexts = page["data"]["repository"]["pullRequests"]["nodes"][0]["commits"][
|
|
321
|
+
"nodes"
|
|
322
|
+
][0]["commit"]["statusCheckRollup"]["contexts"]
|
|
323
|
+
contexts["totalCount"] = 101
|
|
324
|
+
self.closer.required_checks = Mock(return_value=[{"context": "test"}])
|
|
325
|
+
self.closer.required_ci_result = Mock(return_value=("failing", 0))
|
|
326
|
+
self.closer.api = Mock(return_value=page)
|
|
327
|
+
|
|
328
|
+
pull_request = self.closer.open_pull_requests()[0]
|
|
329
|
+
|
|
330
|
+
self.assertEqual(pull_request["_ci_state"], "failing")
|
|
331
|
+
self.assertEqual(pull_request["_failed_at"], 0)
|
|
332
|
+
self.closer.required_ci_result.assert_called_once_with(
|
|
333
|
+
"sha-2", [{"context": "test"}]
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
@patch("worker.time.sleep")
|
|
337
|
+
def test_retries_a_transient_graphql_page_failure(self, sleep):
|
|
338
|
+
self.closer.required_checks = Mock(return_value=[])
|
|
339
|
+
self.closer.api = Mock(
|
|
340
|
+
side_effect=[
|
|
341
|
+
HTTPError(
|
|
342
|
+
"https://api.github.com/graphql", 502, "bad gateway", {}, None
|
|
343
|
+
),
|
|
344
|
+
self.page(2, False, None),
|
|
345
|
+
]
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
self.assertEqual(self.closer.open_pull_requests()[0]["number"], 2)
|
|
349
|
+
sleep.assert_called_once_with(1)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
if __name__ == "__main__":
|
|
353
|
+
unittest.main()
|
|
@@ -53,6 +53,18 @@ conversation never loses the work. `origin` carries no credential, so each GitLa
|
|
|
53
53
|
command has to name `GITLAB_TOKEN`, which the SDK injects only into a command
|
|
54
54
|
that mentions it and masks in the output.
|
|
55
55
|
|
|
56
|
+
On OpenHands Cloud and Enterprise the conversation is started through the
|
|
57
|
+
OpenHands API in a sandbox of its own, which changes three things:
|
|
58
|
+
|
|
59
|
+
- The agent clones the project itself; the script keeps no clone.
|
|
60
|
+
- The conversation runs with the user's own settings, so the secret allow-list
|
|
61
|
+
above does not apply: it holds the user's secrets and MCP servers.
|
|
62
|
+
- A conversation that ends without a merge request is reported on the issue. The
|
|
63
|
+
script cannot finish the work, because it has no copy of it.
|
|
64
|
+
|
|
65
|
+
A user with no `GITLAB_TOKEN` secret is served the token of their connected
|
|
66
|
+
GitLab integration instead.
|
|
67
|
+
|
|
56
68
|
## Two setup paths
|
|
57
69
|
|
|
58
70
|
The `/issue-to-mr:setup` conversation substitutes the constants at the top of
|
|
@@ -252,17 +252,24 @@ Fix any syntax errors before proceeding.
|
|
|
252
252
|
|
|
253
253
|
### Step 10 - Package and upload
|
|
254
254
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
- **
|
|
255
|
+
Set `OPENHANDS_HOST` and `AUTH_HEADER` for the commands below:
|
|
256
|
+
- **OpenHands Cloud or Enterprise** - a `<HOST>` value is in your system prompt:
|
|
257
|
+
use it as `OPENHANDS_HOST`, with `AUTH_HEADER="Authorization: Bearer $OPENHANDS_API_KEY"`
|
|
258
|
+
- **Local Agent Canvas** - an **Automation backend** is listed in the
|
|
259
|
+
`<RUNTIME_SERVICES>` block of your system context: use its `url_from_agent` as
|
|
260
|
+
`OPENHANDS_HOST`, with `AUTH_HEADER="X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY"`
|
|
261
|
+
|
|
262
|
+
On OpenHands Cloud or Enterprise each run starts in a fresh sandbox, so the
|
|
263
|
+
automation keeps its state in the KV store: check that `kvStore` is listed under
|
|
264
|
+
`features` in `GET ${OPENHANDS_HOST}/api/automation/v1/capabilities` before
|
|
265
|
+
deploying.
|
|
259
266
|
|
|
260
267
|
```bash
|
|
261
268
|
tar -czf /tmp/issue-to-mr.tar.gz -C /tmp/issue-to-mr-build .
|
|
262
269
|
|
|
263
270
|
TARBALL_PATH=$(curl -s -X POST \
|
|
264
271
|
"${OPENHANDS_HOST}/api/automation/v1/uploads?name=gitlab-issue-to-mr" \
|
|
265
|
-
-H "
|
|
272
|
+
-H "$AUTH_HEADER" \
|
|
266
273
|
-H "Content-Type: application/gzip" \
|
|
267
274
|
--data-binary @/tmp/issue-to-mr.tar.gz \
|
|
268
275
|
| python3 -c "import json,sys; print(json.load(sys.stdin)['tarball_path'])")
|
|
@@ -274,7 +281,7 @@ echo "Uploaded: $TARBALL_PATH"
|
|
|
274
281
|
|
|
275
282
|
```bash
|
|
276
283
|
curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1" \
|
|
277
|
-
-H "
|
|
284
|
+
-H "$AUTH_HEADER" \
|
|
278
285
|
-H "Content-Type: application/json" \
|
|
279
286
|
-d "{
|
|
280
287
|
\"name\": \"GitLab Issue to MR: {project_summary} label {trigger_label}\",
|