@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
@@ -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]
@@ -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` | `COMMENT`, `APPROVE`, or `REQUEST_CHANGES` |
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: post a short approval message with no inline comments
197
+ 8. If no issues: use `"event": "APPROVE"` with a short approval message and no inline comments