@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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "github-pr-reviewer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"name": "GitHub code review",
|
|
5
5
|
"category": "Code review",
|
|
6
|
-
"description": "Review requested pull-request heads, record exact-head decisions, stop early on out-of-scope changes, and optionally request a code-aware human maintainer after approval or a maintainer-decision scope stop.",
|
|
6
|
+
"description": "Review requested pull-request heads, resume an outstanding reviewer request on a schedule once its checks are green, review a bounded rotating window of open non-draft pull requests that nobody requested once their current head is green and unreviewed, drain the backlog oldest-first under a small per-scan new-conversation bound, record exact-head decisions, stop early on out-of-scope changes, and optionally request a code-aware human maintainer after approval or a maintainer-decision scope stop.",
|
|
7
7
|
"requires": {
|
|
8
8
|
"integrations": {},
|
|
9
9
|
"features": [
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"popularityRank": 100,
|
|
15
15
|
"estimatedSetupMinutes": 4,
|
|
16
|
-
"exampleImplementation": "A scheduled label
|
|
16
|
+
"exampleImplementation": "A scheduled scan of the trigger label, of open non-draft PRs holding an outstanding reviewer request, and of a bounded rotating window of open non-draft PRs nobody requested whose current head is green and unreviewed, or a GitHub reviewer-request event, selects a PR head and submits an idempotent subject turn. The unrequested window resumes from a per-repository position kept in the Automation KV store, so each scan reads a bounded number of pull requests while still covering the backlog over time, and explicit requests are always examined. The scheduled scan orders eligible heads with explicit requests first and then oldest-first, and starts at most a small configurable number of new conversations per scan across every repository. The profile-backed agent clones that exact head in its runtime, applies an early repository-ownership and product/architecture scope gate, then reuses the established review workflow, runs appropriate tests, and publishes a readable native review. A native approval, or a maintainer-decision scope stop, can then be handed to a code-aware human maintainer.",
|
|
17
17
|
"impact": {
|
|
18
18
|
"basis": "completed-runs",
|
|
19
19
|
"one": "1 PR review sweep completed",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"schedule": {
|
|
29
29
|
"type": "cron",
|
|
30
30
|
"label": "Check frequency",
|
|
31
|
-
"help": "How often to look for newly labelled pull requests.",
|
|
32
|
-
"default": "*/
|
|
31
|
+
"help": "How often to look for newly labelled pull requests, outstanding reviewer requests whose checks have not yet finished, and open non-draft pull requests nobody requested that still need a review.",
|
|
32
|
+
"default": "*/5 * * * *",
|
|
33
33
|
"required": true
|
|
34
34
|
},
|
|
35
35
|
"timezone": {
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"triggerLabel": {
|
|
69
69
|
"type": "text",
|
|
70
70
|
"label": "Trigger label",
|
|
71
|
-
"help": "
|
|
71
|
+
"help": "Pull requests carrying this label are reviewed. An open, non-draft pull request that nobody requested is also reviewed once its current head is green and carries no review yet, examined a bounded rotating window at a time so one scan reads only a slice of the backlog.",
|
|
72
72
|
"default": "openhands-review",
|
|
73
73
|
"required": true,
|
|
74
74
|
"constraints": {
|
|
@@ -87,6 +87,16 @@
|
|
|
87
87
|
"maxLength": 39
|
|
88
88
|
}
|
|
89
89
|
},
|
|
90
|
+
"maxNewPerRun": {
|
|
91
|
+
"type": "number",
|
|
92
|
+
"label": "New reviews per scan",
|
|
93
|
+
"help": "Most new review conversations one scheduled scan may start, counted across every repository. Keeps a first scan over a large backlog from starting an agent for every eligible pull request at once. Reviews that are already running are resumed without counting against it.",
|
|
94
|
+
"default": 2,
|
|
95
|
+
"required": true,
|
|
96
|
+
"constraints": {
|
|
97
|
+
"min": 1
|
|
98
|
+
}
|
|
99
|
+
},
|
|
90
100
|
"reviewTone": {
|
|
91
101
|
"type": "select",
|
|
92
102
|
"label": "Review tone",
|
|
@@ -125,7 +135,7 @@
|
|
|
125
135
|
}
|
|
126
136
|
},
|
|
127
137
|
"bundle": {
|
|
128
|
-
"version": "1.
|
|
138
|
+
"version": "1.8.0",
|
|
129
139
|
"entrypoint": "python3 worker.py",
|
|
130
140
|
"timeout": 300,
|
|
131
141
|
"files": {
|
|
@@ -139,12 +149,13 @@
|
|
|
139
149
|
"repos": "{{form.repositories}}",
|
|
140
150
|
"trigger_label": "{{form.triggerLabel}}",
|
|
141
151
|
"trigger_reviewer": "{{form.triggerReviewer}}",
|
|
152
|
+
"max_new_per_run": "{{form.maxNewPerRun}}",
|
|
142
153
|
"review_tone": "{{form.reviewTone}}",
|
|
143
154
|
"maintainers": "{{form.maintainers}}",
|
|
144
155
|
"github_token_secret": "{{form.githubTokenSecret}}"
|
|
145
156
|
}
|
|
146
157
|
},
|
|
147
158
|
"filter": "contains({{form.repositories}}, repository.full_name) && contains(keys(@), 'pull_request') && ((action == 'review_requested' && requested_reviewer.login == '{{form.triggerReviewer}}') || (action == 'submitted' && contains(keys(@), 'review') && review.user.login == '{{form.triggerReviewer}}'))",
|
|
148
|
-
"message": "Configure the repositories, trigger, review tone, optional maintainer roster, credential name, and agent profile."
|
|
159
|
+
"message": "Configure the repositories, trigger, review tone, optional maintainer roster, credential name, per-scan new-review bound, and agent profile."
|
|
149
160
|
}
|
|
150
161
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "github-stale-ci-pr-closer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"name": "GitHub stale CI cleanup",
|
|
5
|
+
"category": "Code review",
|
|
6
|
+
"description": "Warn and close abandoned pull requests whose required CI remains failing.",
|
|
7
|
+
"requires": {
|
|
8
|
+
"integrations": {},
|
|
9
|
+
"features": [
|
|
10
|
+
"customTarball"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"estimatedSetupMinutes": 3,
|
|
14
|
+
"exampleImplementation": "A deterministic scheduled scan reads the required checks that apply to each pull request's base branch. It warns after seven days of observed failure and closes after seven more days only when the author has neither pushed nor commented.",
|
|
15
|
+
"setup": {
|
|
16
|
+
"version": "1.0",
|
|
17
|
+
"mode": "direct",
|
|
18
|
+
"form": {
|
|
19
|
+
"triggers": {
|
|
20
|
+
"cron": {
|
|
21
|
+
"schedule": {
|
|
22
|
+
"type": "cron",
|
|
23
|
+
"label": "Check frequency",
|
|
24
|
+
"help": "How often to reconcile failing pull requests.",
|
|
25
|
+
"default": "0 * * * *",
|
|
26
|
+
"required": true
|
|
27
|
+
},
|
|
28
|
+
"timezone": {
|
|
29
|
+
"type": "timezone",
|
|
30
|
+
"label": "Timezone",
|
|
31
|
+
"help": "Timezone for the schedule.",
|
|
32
|
+
"default": "UTC",
|
|
33
|
+
"required": true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"args": {
|
|
38
|
+
"repositories": {
|
|
39
|
+
"type": "repo-picker",
|
|
40
|
+
"provider": "github",
|
|
41
|
+
"multiple": true,
|
|
42
|
+
"label": "Repositories",
|
|
43
|
+
"help": "Repositories whose open pull requests should be maintained.",
|
|
44
|
+
"required": true
|
|
45
|
+
},
|
|
46
|
+
"githubTokenSecret": {
|
|
47
|
+
"type": "text",
|
|
48
|
+
"label": "GitHub token secret",
|
|
49
|
+
"help": "Name of a saved GitHub token with rules and CI read access plus issue and pull-request write access.",
|
|
50
|
+
"default": "GITHUB_TOKEN_REVIEW",
|
|
51
|
+
"required": true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"bundle": {
|
|
56
|
+
"version": "1.0.0",
|
|
57
|
+
"entrypoint": "python3 worker.py",
|
|
58
|
+
"timeout": 300,
|
|
59
|
+
"files": {
|
|
60
|
+
"worker.py": "skills/github-stale-ci-pr-closer/scripts/worker.py",
|
|
61
|
+
"github_client.py": "skills/github/scripts/github_client.py"
|
|
62
|
+
},
|
|
63
|
+
"config": {
|
|
64
|
+
"repos": "{{form.repositories}}",
|
|
65
|
+
"github_token_secret": "{{form.githubTokenSecret}}"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"message": "Configure the repositories, credential name, and schedule."
|
|
69
|
+
},
|
|
70
|
+
"popularityRank": 77
|
|
71
|
+
}
|
|
@@ -24,6 +24,7 @@ import entry18 from "./catalog/github-issue-triage/manifest.json" with { type: "
|
|
|
24
24
|
import entry19 from "./catalog/upstream-fork-sync/manifest.json" with { type: "json" };
|
|
25
25
|
import entry20 from "./catalog/incident-retrospective-drafter/manifest.json" with { type: "json" };
|
|
26
26
|
import entry21 from "./catalog/news-digest/manifest.json" with { type: "json" };
|
|
27
|
+
import entry22 from "./catalog/github-stale-ci-pr-closer/manifest.json" with { type: "json" };
|
|
27
28
|
|
|
28
29
|
export const AUTOMATION_CATALOG_ENTRIES = [
|
|
29
30
|
entry0,
|
|
@@ -48,4 +49,5 @@ export const AUTOMATION_CATALOG_ENTRIES = [
|
|
|
48
49
|
entry19,
|
|
49
50
|
entry20,
|
|
50
51
|
entry21,
|
|
52
|
+
entry22,
|
|
51
53
|
];
|
|
@@ -890,6 +890,12 @@
|
|
|
890
890
|
"source": "./skills/github-delivery-watchdog",
|
|
891
891
|
"description": "Periodically check pull requests and merge only current heads with independent review, tests, and passing CI.",
|
|
892
892
|
"category": "automations"
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
"name": "github-stale-ci-pr-closer",
|
|
896
|
+
"source": "./skills/github-stale-ci-pr-closer",
|
|
897
|
+
"description": "Warn and close abandoned pull requests whose required CI remains failing.",
|
|
898
|
+
"category": "automations"
|
|
893
899
|
}
|
|
894
900
|
]
|
|
895
901
|
}
|
package/package.json
CHANGED
|
@@ -201,6 +201,7 @@ Source: [`examples/`](https://github.com/OpenHands/software-agent-sdk/tree/main/
|
|
|
201
201
|
- [`56_structured_output.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/56_structured_output.py)
|
|
202
202
|
- [`57_prompt_hooks`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/57_prompt_hooks)
|
|
203
203
|
- [`58_ask_oracle_tool`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/58_ask_oracle_tool)
|
|
204
|
+
- [`59_route_task_to_model.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/59_route_task_to_model.py)
|
|
204
205
|
|
|
205
206
|
### [`02_remote_agent_server/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server)
|
|
206
207
|
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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]
|
package/pyproject.toml
CHANGED
|
@@ -18,7 +18,7 @@ from importlib.metadata import PackageNotFoundError, version
|
|
|
18
18
|
#: Kept in lock-step with pyproject.toml/package.json by the version test.
|
|
19
19
|
#: release-please only rewrites a semver found on the annotated line itself, so
|
|
20
20
|
#: the annotation must stay on the assignment; on its own line it is a no-op.
|
|
21
|
-
_FALLBACK_VERSION = "0.
|
|
21
|
+
_FALLBACK_VERSION = "0.25.0" # x-release-please-version
|
|
22
22
|
|
|
23
23
|
try:
|
|
24
24
|
__version__: str = version("openhands-extensions")
|
|
@@ -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]
|