@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]
|
|
@@ -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]
|
|
@@ -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]
|
|
@@ -57,12 +57,22 @@ gh api -X POST repos/{owner}/{repo}/pulls/{pr_number}/reviews --input /tmp/revie
|
|
|
57
57
|
| Parameter | Description |
|
|
58
58
|
|-----------|-------------|
|
|
59
59
|
| `commit_id` | Commit SHA to comment on (use `git rev-parse HEAD`) |
|
|
60
|
-
| `event` | `
|
|
60
|
+
| `event` | `APPROVE`, `COMMENT`, or `REQUEST_CHANGES` (see below) |
|
|
61
61
|
| `path` | File path as shown in the diff |
|
|
62
62
|
| `line` | Line number in the NEW version (right side of diff) |
|
|
63
63
|
| `side` | `RIGHT` for new/added lines, `LEFT` for deleted lines |
|
|
64
64
|
| `body` | Comment text with priority label |
|
|
65
65
|
|
|
66
|
+
### Choosing the `event` Value
|
|
67
|
+
|
|
68
|
+
| Event | When to use |
|
|
69
|
+
|-------|-------------|
|
|
70
|
+
| `APPROVE` | No issues found, or only 🟡 suggestions that are non-blocking |
|
|
71
|
+
| `COMMENT` | You have 🟠 Important feedback but it's not a hard blocker |
|
|
72
|
+
| `REQUEST_CHANGES` | There are 🔴 Critical issues that **must** be fixed before merge |
|
|
73
|
+
|
|
74
|
+
**Default to `APPROVE`** when the code is correct and merge-ready. Use `COMMENT` or `REQUEST_CHANGES` only when there are actionable issues that warrant it.
|
|
75
|
+
|
|
66
76
|
### Multi-Line Comments
|
|
67
77
|
|
|
68
78
|
For comments spanning multiple lines, add `start_line` to specify the range:
|
|
@@ -184,4 +194,4 @@ curl -X POST \
|
|
|
184
194
|
5. Do NOT post comments for code that is acceptable — only comment when action is needed
|
|
185
195
|
6. Use suggestion syntax for concrete code changes, but only after verifying the resulting code matches your description (see "How Suggestions Actually Work")
|
|
186
196
|
7. Keep the review body brief (details go in inline comments)
|
|
187
|
-
8. If no issues:
|
|
197
|
+
8. If no issues: use `"event": "APPROVE"` with a short approval message and no inline comments
|
|
@@ -12,7 +12,42 @@ This skill is activated by:
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
14
|
- Reviews PRs on demand from a GitHub reviewer request or label event
|
|
15
|
+
- Gates each scheduled review on the current head's GitHub-required checks
|
|
16
|
+
before starting an agent, read through the `isRequired` signal, so an optional
|
|
17
|
+
workflow that fails cannot block a mergeable head: a completed `failure`,
|
|
18
|
+
`cancelled`, or `timed_out` required check blocks the review, and a `queued`,
|
|
19
|
+
`in_progress`, or not-yet-reported required check exits with a clear
|
|
20
|
+
waiting-on-checks outcome instead of holding a slot. If the required-check
|
|
21
|
+
signal is unavailable the gate falls back to every current-head check run and
|
|
22
|
+
workflow run, including a workflow that fails before creating any check run
|
|
23
|
+
- Dispatches an explicit `all-hands-bot` review request even when required CI is
|
|
24
|
+
red or pending; the CI gate applies to scheduled discovery
|
|
25
|
+
- Ignores checks and workflow runs recorded for an obsolete head, so a stale
|
|
26
|
+
failure cannot block the push that fixed it
|
|
27
|
+
- Resumes an outstanding reviewer request on the next scheduled scan once the
|
|
28
|
+
requested head's checks are green, so a request that arrives during CI is not
|
|
29
|
+
lost; the explicit-request event path is kept for event-only deployments
|
|
30
|
+
- Reviews open, non-draft PRs that nobody requested on a scheduled scan once
|
|
31
|
+
their current head is green and carries no review yet, keyed by
|
|
32
|
+
repository/PR/head so a repeat scan never duplicates a conversation or review
|
|
33
|
+
and a changed head becomes eligible again
|
|
34
|
+
- Examines a bounded, rotating window of that unrequested backlog per scan
|
|
35
|
+
(10 PRs per repository), remembering its position in the Automation KV store so
|
|
36
|
+
the next scan resumes past it; explicit requests and trigger labels are always
|
|
37
|
+
examined, never skipped behind the window
|
|
38
|
+
- Posts no managed gate comment for an unrequested PR that is merely red or
|
|
39
|
+
pending - the comment answers an explicit request, so a scan over a large
|
|
40
|
+
backlog cannot storm the PRs with comments
|
|
41
|
+
- Names the retry the deployment actually has in the waiting comment: a
|
|
42
|
+
scheduled scan where a cron trigger exists, and removing and re-requesting the
|
|
43
|
+
bot where only the event trigger does
|
|
44
|
+
- Rewrites its own managed gate comment in place when the retry wording changes,
|
|
45
|
+
so switching a deployment from the event trigger to a cron scan updates the
|
|
46
|
+
outstanding-request explanation instead of leaving the old instruction
|
|
15
47
|
- Watches several repositories from a single automation, each with its own state
|
|
48
|
+
- Bounds a scheduled scan to a small, configurable number of new review
|
|
49
|
+
conversations across all repositories (default 2), draining the oldest
|
|
50
|
+
outstanding reviewer requests first and reaching the rest on later scans
|
|
16
51
|
- Processes each review request or label application idempotently
|
|
17
52
|
- Supports re-review by requesting the bot again or re-applying the label. Each
|
|
18
53
|
explicit request refreshes mutable GitHub state (head, PR body, reviews,
|
|
@@ -25,9 +25,110 @@ once per label event, and publishes the AI review to GitHub.
|
|
|
25
25
|
Windows PowerShell equivalents for the setup, packaging, upload, and API-check shell snippets are in `references/windows.md`.
|
|
26
26
|
|
|
27
27
|
The automation script is deterministic: PR discovery, label-event tracking,
|
|
28
|
-
state persistence, stale-result suppression, the repository
|
|
29
|
-
removal are all handled in Python. The LLM is invoked only for
|
|
30
|
-
itself.
|
|
28
|
+
head eligibility, state persistence, stale-result suppression, the repository
|
|
29
|
+
checkout, and its removal are all handled in Python. The LLM is invoked only for
|
|
30
|
+
the review itself.
|
|
31
|
+
|
|
32
|
+
Before any review conversation is created, a scheduled scan evaluates the
|
|
33
|
+
current head's GitHub-required checks, read through the GraphQL `isRequired`
|
|
34
|
+
signal for the pull request. That signal is the merge policy's own source of
|
|
35
|
+
truth and is pull-request-scoped, so it stays correct for a stacked PR whose
|
|
36
|
+
symbolic base branch carries no branch rules of its own:
|
|
37
|
+
|
|
38
|
+
- Only checks GitHub reports as required decide the scheduled gate. An optional
|
|
39
|
+
workflow that fails - including one that fails before creating any check run -
|
|
40
|
+
does not block a head whose required checks pass.
|
|
41
|
+
- A completed required run on the exact head whose conclusion is `failure`,
|
|
42
|
+
`cancelled`, or `timed_out` blocks the review. Any other unrecognized
|
|
43
|
+
conclusion fails closed as a block rather than silently approving.
|
|
44
|
+
- A completed required run whose conclusion is `success`, `neutral`, or
|
|
45
|
+
`skipped` does not block. A required `commit status` context is classified
|
|
46
|
+
alongside required check runs.
|
|
47
|
+
- A `queued` or `in_progress` required run on the exact head, and a required
|
|
48
|
+
context that has not reported a current-head check run at all, make the run
|
|
49
|
+
exit with a **waiting on checks** outcome. Unfulfilled is never approval. The
|
|
50
|
+
worker does not hold a slot polling; the next scheduled scan or explicit
|
|
51
|
+
review request retries.
|
|
52
|
+
- When the required-check signal cannot be read or is empty, the gate falls back
|
|
53
|
+
to every current-head check run and workflow run, so a red head still blocks.
|
|
54
|
+
In that fallback, workflow runs are read as well as check runs, because a
|
|
55
|
+
workflow can fail before creating any check run - a workflow-level error, or a
|
|
56
|
+
`pull_request` run whose jobs never start. Such a run leaves a failed check
|
|
57
|
+
suite with no check runs under it, so the commit's check-run rollup and
|
|
58
|
+
`gh pr checks` both report success and only the workflow run reveals the red
|
|
59
|
+
CI. A workflow run whose check suite already reported check runs is left to
|
|
60
|
+
those runs, so a workflow is never counted twice.
|
|
61
|
+
- A scheduled scan considers the trigger label **and** every open, non-draft PR
|
|
62
|
+
that still holds an outstanding `all-hands-bot` review request. That is what
|
|
63
|
+
resumes a request made while CI was running: the request is keyed by its own
|
|
64
|
+
`review_requested` event, so repeated scans reuse one conversation and one
|
|
65
|
+
review instead of creating duplicates, and a PR whose request was answered or
|
|
66
|
+
withdrawn simply drops out of `requested_reviewers`.
|
|
67
|
+
- The scheduled scan also reviews open, non-draft PRs that **nobody requested**
|
|
68
|
+
and that carry no trigger label, once their current head has no completed
|
|
69
|
+
review by this account. That is what keeps reviewing PRs after the outstanding
|
|
70
|
+
requests are exhausted. The delivery key is the stable
|
|
71
|
+
`scan:{repository}:{number}:{head}`, so repeated scans reuse one conversation
|
|
72
|
+
and one native review, and a changed head becomes eligible again under its new
|
|
73
|
+
SHA.
|
|
74
|
+
- The unrequested part of a scan is a **bounded, rotating window**, not the whole
|
|
75
|
+
backlog. Classifying one unrequested head costs a review read plus the
|
|
76
|
+
exact-head check and workflow reads, so examining every open PR in one scan
|
|
77
|
+
spent the run's API budget in the largest repository and announced every red or
|
|
78
|
+
pending head. A scan examines at most `SCAN_WINDOW` (10) unrequested PRs per
|
|
79
|
+
repository and stores where the window ended in the automation service's own
|
|
80
|
+
KV store, under a per-repository `review-scan:{owner}__{repo}` key, so the next
|
|
81
|
+
scan resumes past that point and the backlog is covered fairly over several
|
|
82
|
+
scans. The window wraps at the end of the backlog. When the KV store is not
|
|
83
|
+
available (a local run) the position is kept in memory, so the scan still
|
|
84
|
+
rotates within the run.
|
|
85
|
+
- An explicit `all-hands-bot` review request or a trigger label is **never**
|
|
86
|
+
subject to the window: every explicit candidate is examined on every scan,
|
|
87
|
+
whatever the stored position is, so a request is not delayed behind the
|
|
88
|
+
rotation.
|
|
89
|
+
- A gate stop on an **unrequested** head posts no managed comment. A PR nobody
|
|
90
|
+
asked about that is merely red or pending is simply skipped without starting an
|
|
91
|
+
agent, which is what keeps a scan over a large backlog from posting a gate
|
|
92
|
+
comment on every PR. The managed comment is the answer to an explicit request,
|
|
93
|
+
so a requested or labeled head that is red or pending still gets its
|
|
94
|
+
explanation.
|
|
95
|
+
- Workflow runs are read as well as check runs, because a workflow can fail
|
|
96
|
+
before creating any check run - a workflow-level error, or a `pull_request`
|
|
97
|
+
run whose jobs never start. Such a run leaves a failed check suite with no
|
|
98
|
+
check runs under it, so the commit's check-run rollup and `gh pr checks` both
|
|
99
|
+
report success and only the workflow run reveals the red CI. A workflow run
|
|
100
|
+
whose check suite already reported check runs is left to those runs, so a
|
|
101
|
+
workflow is never counted twice.
|
|
102
|
+
- Runs attributed to any other (obsolete) head SHA are ignored, so a stale
|
|
103
|
+
failure cannot block the push that fixed it. This applies to workflow runs
|
|
104
|
+
too.
|
|
105
|
+
- Only the latest run of each logical check or workflow counts. A logical check
|
|
106
|
+
is its name plus the reporting app identity, and a logical workflow is its
|
|
107
|
+
name plus workflow ID. The latest is chosen by the run ID (the reliable
|
|
108
|
+
creation sequence) with the start time as a tie-break, so a re-run that fixed
|
|
109
|
+
a check supersedes its earlier failure on the same SHA while a newer queued or
|
|
110
|
+
in-progress re-run supersedes an earlier success and makes the head wait.
|
|
111
|
+
Ordering by the run ID keeps a run whose start time is still absent in its
|
|
112
|
+
true creation position in both directions.
|
|
113
|
+
|
|
114
|
+
An explicit `all-hands-bot` review request is the intake-policy exception: the
|
|
115
|
+
caller asked for that head by name, so the request dispatches even when required
|
|
116
|
+
CI is red or pending, and the gate leaves no explanatory comment. Draft, scope,
|
|
117
|
+
exact-head, and delivery-deduplication safeguards still apply. A blocked or
|
|
118
|
+
waiting scheduled stop consumes no trigger: once the head's required checks are
|
|
119
|
+
non-blocking, the scheduled scan, or a new `all-hands-bot` review request,
|
|
120
|
+
starts the normal review.
|
|
121
|
+
|
|
122
|
+
The gate needs no configured list of check names. When it stops a review it
|
|
123
|
+
leaves one concise explanation on the PR, identified by a hidden marker carrying
|
|
124
|
+
the head SHA and gate category, so a later run for a different head updates that
|
|
125
|
+
comment instead of posting another. Only a marker this reviewer account authored
|
|
126
|
+
counts as its own comment; every other PR comment is untrusted and can neither
|
|
127
|
+
suppress the explanation nor be edited. If the repository's own workflow already
|
|
128
|
+
posted a deterministic remediation comment that names the same current-head
|
|
129
|
+
checks, the gate adds nothing; a disclosure about some other check does not
|
|
130
|
+
suppress it. The gate applies to scheduled label scans; an explicit
|
|
131
|
+
`all-hands-bot` review request bypasses it by design.
|
|
31
132
|
|
|
32
133
|
Each explicit review request - a re-applied trigger label, or a new reviewer
|
|
33
134
|
request in event mode - is a fresh review. The conversation for a PR is reused,
|
|
@@ -39,6 +140,52 @@ verdict, or label/priority claim that state no longer supports. Repository
|
|
|
39
140
|
analysis the conversation already did, such as reading `AGENTS.md`, stays
|
|
40
141
|
useful and is not repeated.
|
|
41
142
|
|
|
143
|
+
The waiting and blocked explanations name the retry the deployment actually has.
|
|
144
|
+
A scheduled run proves a scan is configured, so it says the next scan will
|
|
145
|
+
retry. An event-only run does not, so it tells the reader to remove the
|
|
146
|
+
outstanding `all-hands-bot` request and request `all-hands-bot` again instead of
|
|
147
|
+
promising a scan that does not exist: GitHub will not accept a second request
|
|
148
|
+
while the first is still outstanding.
|
|
149
|
+
|
|
150
|
+
The gate leaves only one explanation per head. When the same head keeps the same
|
|
151
|
+
hidden marker but the retry wording changes -- an event-only comment on a head
|
|
152
|
+
whose automation is later switched to a cron scan -- the scheduled run rewrites
|
|
153
|
+
that managed comment in place, so the comment always names the retry that is
|
|
154
|
+
actually deployed, and an unchanged body is left untouched.
|
|
155
|
+
|
|
156
|
+
## Bounded intake per scheduled scan
|
|
157
|
+
|
|
158
|
+
A scheduled scan drains outstanding reviewer requests fairly, but starting an
|
|
159
|
+
agent for every eligible pull request at once would overload the deployment, so
|
|
160
|
+
the scan starts at most `MAX_NEW_PER_RUN` new review conversations, counted
|
|
161
|
+
**across every configured repository** rather than per repository. The default
|
|
162
|
+
is `2`, and the rendered `config.json` overrides it through the
|
|
163
|
+
`max_new_per_run` key that `github-issue-to-pr` and `gitlab-issue-to-mr` already
|
|
164
|
+
use.
|
|
165
|
+
|
|
166
|
+
- Eligible candidates are ordered deterministically: an explicit `all-hands-bot`
|
|
167
|
+
request (or a trigger label) is ordered before an unrequested PR, then by the
|
|
168
|
+
oldest `all-hands-bot` `review_requested` event for a requested PR or the PR's
|
|
169
|
+
own creation time (oldest first) for an unrequested one, then by repository,
|
|
170
|
+
then by pull-request number, so the oldest work drains first and a later scan
|
|
171
|
+
reaches the remainder.
|
|
172
|
+
- The bound counts the conversations a scan **starts**. A delivery that only
|
|
173
|
+
deduplicates or reports an already-running conversation reuses a runtime and
|
|
174
|
+
consumes no slot, so repeated scans make progress on the backlog instead of
|
|
175
|
+
re-spending the bound on work already in flight.
|
|
176
|
+
- Reaching the bound never cuts the scan short. The scan still evaluates the
|
|
177
|
+
exact-head checks of the remaining candidates, reconciles completed reviews,
|
|
178
|
+
and runs the maintainer handoff. A candidate whose checks are pending or
|
|
179
|
+
failing starts no conversation and consumes no slot, so it cannot block a later
|
|
180
|
+
green candidate. Only an explicit request or a trigger label gets a waiting or
|
|
181
|
+
blocked gate comment; an unrequested head that is merely red or pending is
|
|
182
|
+
skipped silently.
|
|
183
|
+
- A dispatch that raises is reported and does not consume a slot or abort the
|
|
184
|
+
scan, so the candidates behind it are still considered.
|
|
185
|
+
- The explicit `review_requested` event path and the trigger-label scan are
|
|
186
|
+
unchanged: an explicit request still starts its conversation immediately, and
|
|
187
|
+
only the scheduled scan's new conversations are bounded.
|
|
188
|
+
|
|
42
189
|
The review prompt starts with a scope gate: using the repository's own guidance
|
|
43
190
|
(its scope categories and ownership boundaries, not a list of individual PR
|
|
44
191
|
numbers), the reviewer decides whether the change belongs in this repository
|
|
@@ -176,7 +323,7 @@ Record as `CRON_SCHEDULE`.
|
|
|
176
323
|
|
|
177
324
|
### Step 6 - Generate the automation script
|
|
178
325
|
|
|
179
|
-
Read `scripts/main.py` from this skill's directory. Apply exactly
|
|
326
|
+
Read `scripts/main.py` from this skill's directory. Apply exactly seven constant
|
|
180
327
|
substitutions near the top of the file:
|
|
181
328
|
|
|
182
329
|
> The script also reads a `config.json` shipped beside it, if there is one, over
|
|
@@ -192,6 +339,7 @@ substitutions near the top of the file:
|
|
|
192
339
|
| `REVIEW_TONE = "thorough"` | `REVIEW_TONE = "{review_tone}"` |
|
|
193
340
|
| `REVIEW_STYLE_INSTRUCTIONS = ""` | `REVIEW_STYLE_INSTRUCTIONS = "{style_instructions}"` |
|
|
194
341
|
| `REPO_REVIEW_GUIDE_PATH = ".agents/skills/custom-codereview-guide.md"` | leave unchanged to auto-load a repo review guide at this path, or set to `""` to disable |
|
|
342
|
+
| `MAX_NEW_PER_RUN = 2` | leave unchanged to bound a scheduled scan to two new review conversations across all repositories, or raise it if the deployment can hold more agents at once |
|
|
195
343
|
| `DEFAULT_OPENHANDS_URL = "http://localhost:8000"` | leave unchanged unless the user has a preference |
|
|
196
344
|
|
|
197
345
|
Use a safe string writer such as `json.dumps(value)` when inserting user-provided
|
|
@@ -348,6 +496,10 @@ The completion callback fires once for the whole run.
|
|
|
348
496
|
| 404 on repo access | Repo name wrong or no access | Re-check the entry in `REPOS` and the token's permissions |
|
|
349
497
|
| One repository is skipped, others work | That repository failed its access check | Read the `=== owner/repo ===` block in the run log |
|
|
350
498
|
| Same PR not reviewed after new commits | Label event was already processed | Remove and re-apply the trigger label |
|
|
499
|
+
| Review paused with a failing-check comment | A current-head required check reported `failure`, `cancelled`, or `timed_out` | Fix the named checks and push; the review starts on the new head, or request `all-hands-bot` to review immediately |
|
|
500
|
+
| Review reported waiting on checks | A current-head required check is `queued` or `in_progress`, or has not reported yet | No action; a later scan or a new review request retries |
|
|
501
|
+
| Optional workflow failed but no review was paused | The failed workflow is not required, so the required-only scheduled gate ignored it | No action; only GitHub-required checks gate scheduled discovery |
|
|
502
|
+
| Only a few reviews start on a large backlog | The per-scan `max_new_per_run` bound (default 2) reached | No action; later scans drain the remaining oldest requests and the bounded rotating window of unrequested PRs, or raise `max_new_per_run` if the deployment can hold more agents |
|
|
351
503
|
| Review result never posts | Conversation still running or stuck | Open the conversation link from the acknowledgement comment |
|
|
352
504
|
| Stale review suppressed | PR head SHA changed while the agent was reviewing | Re-apply the trigger label after the latest commit |
|
|
353
505
|
| Review arrives as a plain comment, not a review | Publishing failed, so the script posted the text as a fallback | Check that the token has Pull requests: Read and Write |
|
|
@@ -18,6 +18,21 @@ store under the key `state:{owner}__{repo}` — for example
|
|
|
18
18
|
`AUTOMATION_KV_TOKEN` is injected into the run environment. Each automation has
|
|
19
19
|
its own isolated namespace.
|
|
20
20
|
|
|
21
|
+
The scheduled worker (`worker.py`) uses the same store for one more document, the
|
|
22
|
+
**unrequested-scan cursor**, under the key `review-scan:{owner}__{repo}`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{ "cursor": 20 }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`cursor` is the position in the repository's unrequested-PR backlog at which the
|
|
29
|
+
next scheduled scan's bounded window starts. Each scan examines at most
|
|
30
|
+
`SCAN_WINDOW` (10) unrequested PRs from that position, then writes the position
|
|
31
|
+
after the window, so successive scans rotate through the whole backlog instead of
|
|
32
|
+
reading one pull request per open PR. A cursor past the end wraps to the start.
|
|
33
|
+
This document is separate from `main.py`'s per-repository review state; a missing
|
|
34
|
+
or unreadable cursor simply starts the window at the beginning.
|
|
35
|
+
|
|
21
36
|
**Fallback (local/dev):** When the KV store is not available, the state is
|
|
22
37
|
written to a local JSON file at:
|
|
23
38
|
|