@openhands/extensions 0.24.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.
- package/.claude-plugin/marketplace.json +6 -0
- package/.codex-plugin/marketplace.json +6 -0
- package/.plugin/marketplace.json +6 -0
- package/.release-please-manifest.json +1 -1
- package/README.md +3 -2
- package/automations/bundle-index.js +11 -7
- 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-index.js +2 -0
- package/marketplaces/openhands-extensions.json +6 -0
- package/package.json +1 -1
- 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/vulnerability-remediation/skills/github/scripts/github_client.py +116 -0
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/skills/github/scripts/github_client.py +116 -0
- package/skills/github-agents-md-maintainer/scripts/github_client.py +116 -0
- package/skills/github-delivery-watchdog/scripts/github_client.py +116 -0
- package/skills/github-issue-to-pr/scripts/github_client.py +116 -0
- package/skills/github-issue-triage/scripts/github_client.py +116 -0
- package/skills/github-pr-review/SKILL.md +12 -2
- package/skills/github-pr-reviewer/README.md +35 -0
- package/skills/github-pr-reviewer/SKILL.md +156 -4
- package/skills/github-pr-reviewer/references/state-schema.md +15 -0
- package/skills/github-pr-reviewer/scripts/github_client.py +116 -0
- package/skills/github-pr-reviewer/scripts/main.py +17 -1
- package/skills/github-pr-reviewer/scripts/worker.py +820 -58
- package/skills/github-repo-monitor/scripts/github_client.py +116 -0
- 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 +335 -0
- package/skills/github-stale-ci-pr-closer/tests/test_worker.py +213 -0
- package/skills/index.js +12 -3
- package/skills/openhands-sdk/SKILL.md +1 -0
- package/tests/fixtures/automations/github-pr-reviewer.json +39 -24
- 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 +2108 -3
|
@@ -165,6 +165,122 @@ class GitHubRepository:
|
|
|
165
165
|
result.setdefault(item["context"], item["state"])
|
|
166
166
|
return result
|
|
167
167
|
|
|
168
|
+
def check_runs(self, sha):
|
|
169
|
+
"""Return every check run GitHub reported for one commit SHA.
|
|
170
|
+
|
|
171
|
+
Reading check runs needs no branch-protection or ruleset access, so this
|
|
172
|
+
is the head-eligibility signal the automation's own token can always
|
|
173
|
+
see. The endpoint answers with an object rather than a list, so it
|
|
174
|
+
paginates through ``gh`` instead of ``gh_pages``.
|
|
175
|
+
"""
|
|
176
|
+
runs = []
|
|
177
|
+
for page in range(1, 101):
|
|
178
|
+
data = self.gh(
|
|
179
|
+
"GET", f"/commits/{sha}/check-runs?per_page=100&page={page}"
|
|
180
|
+
)
|
|
181
|
+
batch = data.get("check_runs") or []
|
|
182
|
+
runs.extend(batch)
|
|
183
|
+
if len(runs) >= int(data.get("total_count") or 0) or not batch:
|
|
184
|
+
return runs
|
|
185
|
+
raise RuntimeError("GitHub check-run pagination exceeded limit")
|
|
186
|
+
|
|
187
|
+
def required_check_contexts(self, number):
|
|
188
|
+
"""Return the status contexts GitHub marks required on one pull request.
|
|
189
|
+
|
|
190
|
+
`isRequired` is the merge-policy source of truth and is PR-scoped, so it
|
|
191
|
+
is correct for a stacked PR whose symbolic base branch carries no branch
|
|
192
|
+
rules of its own. It is a field on each context in the head's status
|
|
193
|
+
check rollup and takes the pull request number, so a single GraphQL query
|
|
194
|
+
answers with the required check runs and any required commit statuses,
|
|
195
|
+
each already attributed to the exact head. Only contexts GitHub itself
|
|
196
|
+
reports as required are returned, so an optional workflow that fails
|
|
197
|
+
before creating any check run is not present.
|
|
198
|
+
"""
|
|
199
|
+
owner, name = self.repository.split("/", 1)
|
|
200
|
+
query = (
|
|
201
|
+
"query($owner:String!,$name:String!,$number:Int!){"
|
|
202
|
+
"repository(owner:$owner,name:$name){"
|
|
203
|
+
"pullRequest(number:$number){"
|
|
204
|
+
"commits(last:1){"
|
|
205
|
+
"nodes{"
|
|
206
|
+
"commit{"
|
|
207
|
+
"statusCheckRollup{"
|
|
208
|
+
"contexts(first:100){"
|
|
209
|
+
"pageInfo{hasNextPage}"
|
|
210
|
+
"nodes{"
|
|
211
|
+
"__typename"
|
|
212
|
+
" ... on CheckRun{name isRequired(pullRequestNumber:$number)}"
|
|
213
|
+
" ... on StatusContext{context isRequired(pullRequestNumber:$number)}"
|
|
214
|
+
"}"
|
|
215
|
+
"}"
|
|
216
|
+
"}"
|
|
217
|
+
"}"
|
|
218
|
+
"}"
|
|
219
|
+
"}"
|
|
220
|
+
"}"
|
|
221
|
+
"}"
|
|
222
|
+
"}"
|
|
223
|
+
)
|
|
224
|
+
data = self.api(
|
|
225
|
+
"POST",
|
|
226
|
+
"/graphql",
|
|
227
|
+
body={
|
|
228
|
+
"query": query,
|
|
229
|
+
"variables": {"owner": owner, "name": name, "number": number},
|
|
230
|
+
},
|
|
231
|
+
)
|
|
232
|
+
if data.get("errors"):
|
|
233
|
+
raise RuntimeError(
|
|
234
|
+
f"GitHub required-check query failed: {data['errors'][0].get('message')}"
|
|
235
|
+
)
|
|
236
|
+
nodes = (
|
|
237
|
+
(((data.get("data") or {}).get("repository") or {}).get("pullRequest") or {})
|
|
238
|
+
.get("commits", {})
|
|
239
|
+
.get("nodes")
|
|
240
|
+
) or []
|
|
241
|
+
contexts = (
|
|
242
|
+
((nodes[0].get("commit") or {}).get("statusCheckRollup") or {}).get(
|
|
243
|
+
"contexts", {}
|
|
244
|
+
)
|
|
245
|
+
if nodes
|
|
246
|
+
else {}
|
|
247
|
+
)
|
|
248
|
+
if (contexts.get("pageInfo") or {}).get("hasNextPage"):
|
|
249
|
+
raise RuntimeError(
|
|
250
|
+
"GitHub required-check rollup exceeds one page; the required "
|
|
251
|
+
"set may be incomplete"
|
|
252
|
+
)
|
|
253
|
+
return [
|
|
254
|
+
{
|
|
255
|
+
"name": node.get("name") or node.get("context"),
|
|
256
|
+
"kind": node.get("__typename"),
|
|
257
|
+
}
|
|
258
|
+
for node in (contexts.get("nodes") or [])
|
|
259
|
+
if node.get("isRequired") and (node.get("name") or node.get("context"))
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
def workflow_runs(self, sha):
|
|
263
|
+
"""Return every Actions workflow run GitHub reported for one commit SHA.
|
|
264
|
+
|
|
265
|
+
A workflow that fails before any job starts - a workflow-level error, or
|
|
266
|
+
a `pull_request` run whose jobs never materialize - still records a
|
|
267
|
+
failed check suite, but it contributes no check runs, so the commit's
|
|
268
|
+
check-run rollup and `gh pr checks` both report success. Reading the
|
|
269
|
+
workflow runs directly is the only way the gate can see that failure.
|
|
270
|
+
Like the check-run endpoint this answers with an object, so it paginates
|
|
271
|
+
manually.
|
|
272
|
+
"""
|
|
273
|
+
runs = []
|
|
274
|
+
for page in range(1, 101):
|
|
275
|
+
data = self.gh(
|
|
276
|
+
"GET", f"/actions/runs?head_sha={sha}&per_page=100&page={page}"
|
|
277
|
+
)
|
|
278
|
+
batch = data.get("workflow_runs") or []
|
|
279
|
+
runs.extend(batch)
|
|
280
|
+
if len(runs) >= int(data.get("total_count") or 0) or not batch:
|
|
281
|
+
return runs
|
|
282
|
+
raise RuntimeError("GitHub workflow-run pagination exceeded limit")
|
|
283
|
+
|
|
168
284
|
def completed_dependency(self, number):
|
|
169
285
|
if number in self._completed_dependencies:
|
|
170
286
|
return self._completed_dependencies[number]
|
|
@@ -51,6 +51,12 @@ REVIEW_STYLE_INSTRUCTIONS = ""
|
|
|
51
51
|
# than relying on the spawned agent's skill activation. Set to "" to disable.
|
|
52
52
|
REPO_REVIEW_GUIDE_PATH = ".agents/skills/custom-codereview-guide.md"
|
|
53
53
|
DEFAULT_OPENHANDS_URL = "http://localhost:8000"
|
|
54
|
+
# The most new review conversations one scheduled scan may start, counted across
|
|
55
|
+
# every configured repository rather than per repository. The scheduled scan
|
|
56
|
+
# drains outstanding reviewer requests in oldest-request order, so a small bound
|
|
57
|
+
# keeps a first scan over a large backlog from starting an agent for every
|
|
58
|
+
# eligible pull request at once.
|
|
59
|
+
MAX_NEW_PER_RUN = 2
|
|
54
60
|
|
|
55
61
|
# A review that ends with this marker is a scope stop: the reviewer found the
|
|
56
62
|
# change out of scope, or needing a product/architecture decision, before the
|
|
@@ -69,6 +75,7 @@ _CONFIG_TYPES: dict[str, type] = {
|
|
|
69
75
|
"review_tone": str,
|
|
70
76
|
"review_style_instructions": str,
|
|
71
77
|
"repo_review_guide_path": str,
|
|
78
|
+
"max_new_per_run": int,
|
|
72
79
|
"openhands_url": str,
|
|
73
80
|
}
|
|
74
81
|
|
|
@@ -95,7 +102,11 @@ def load_config(directory: Path | None = None) -> dict:
|
|
|
95
102
|
if key not in raw:
|
|
96
103
|
continue
|
|
97
104
|
value = raw[key]
|
|
98
|
-
|
|
105
|
+
# bool is an int in Python, so an unguarded int check would accept
|
|
106
|
+
# `"max_new_per_run": true` and then start `True` conversations.
|
|
107
|
+
if not isinstance(value, expected) or (
|
|
108
|
+
expected is int and isinstance(value, bool)
|
|
109
|
+
):
|
|
99
110
|
raise SystemExit(
|
|
100
111
|
f"{CONFIG_FILENAME}: {key} must be {expected.__name__}, "
|
|
101
112
|
f"got {type(value).__name__}"
|
|
@@ -106,6 +117,10 @@ def load_config(directory: Path | None = None) -> dict:
|
|
|
106
117
|
raise SystemExit(
|
|
107
118
|
f'{CONFIG_FILENAME}: repos must be a non-empty list of "owner/repo" strings'
|
|
108
119
|
)
|
|
120
|
+
if key == "max_new_per_run" and value < 1:
|
|
121
|
+
raise SystemExit(
|
|
122
|
+
f"{CONFIG_FILENAME}: max_new_per_run must be at least 1"
|
|
123
|
+
)
|
|
109
124
|
config[key] = value
|
|
110
125
|
return config
|
|
111
126
|
|
|
@@ -150,6 +165,7 @@ TRIGGER_LABEL = _CONFIG.get("trigger_label", TRIGGER_LABEL)
|
|
|
150
165
|
REVIEW_TONE = _CONFIG.get("review_tone", REVIEW_TONE)
|
|
151
166
|
REVIEW_STYLE_INSTRUCTIONS = _CONFIG.get("review_style_instructions", REVIEW_STYLE_INSTRUCTIONS)
|
|
152
167
|
REPO_REVIEW_GUIDE_PATH = _CONFIG.get("repo_review_guide_path", REPO_REVIEW_GUIDE_PATH)
|
|
168
|
+
MAX_NEW_PER_RUN = _CONFIG.get("max_new_per_run", MAX_NEW_PER_RUN)
|
|
153
169
|
DEFAULT_OPENHANDS_URL = _CONFIG.get("openhands_url", DEFAULT_OPENHANDS_URL)
|
|
154
170
|
|
|
155
171
|
DONE_DEBOUNCE = 15
|