@openhands/extensions 0.22.1 → 0.24.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/.agents/skills/custom-codereview-guide.md +86 -17
- package/.claude-plugin/marketplace.json +14 -0
- package/.codex-plugin/marketplace.json +14 -0
- package/.github/workflows/check-extensions.yml +6 -6
- package/.github/workflows/deprecation-check.yml +1 -1
- package/.github/workflows/issue-readiness-check.yml +37 -106
- package/.github/workflows/npm-publish.yml +2 -2
- package/.github/workflows/pr-description-check.yml +1 -1
- package/.github/workflows/pypi-publish.yml +3 -3
- package/.github/workflows/tests.yml +2 -2
- package/.plugin/marketplace.json +14 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +32 -2
- package/README.md +4 -3
- package/automations/bundle-index.js +12 -11
- package/automations/catalog/github-issue-triage/manifest.json +2 -2
- package/automations/catalog/github-pr-reviewer/manifest.json +42 -5
- package/automations/catalog.schema.json +0 -4
- package/integrations/catalog/granola.json +38 -0
- package/integrations/catalog-index.js +16 -14
- package/marketplaces/openhands-extensions.json +14 -0
- package/package.json +1 -1
- package/plugins/openhands/skills/openhands-sdk/SKILL.md +3 -1
- package/plugins/pr-review/README.md +2 -1
- package/plugins/pr-review/scripts/agent_script.py +198 -81
- package/plugins/pr-review/scripts/prompt.py +35 -11
- package/plugins/pr-review/skills/code-review/SKILL.md +165 -200
- package/plugins/pr-review/skills/code-review/commands/codereview-roasted.md +1 -1
- package/plugins/pr-review/skills/code-review/commands/codereview.md +1 -1
- package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +8 -3
- package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +4 -0
- package/pyproject.toml +2 -2
- package/python/openhands_extensions/_version.py +1 -1
- package/skills/add-skill/SKILL.md +5 -2
- package/skills/canvas-extension-api/.claude-plugin/plugin.json +19 -0
- package/skills/canvas-extension-api/.codex-plugin/plugin.json +19 -0
- package/skills/canvas-extension-api/.plugin/plugin.json +19 -0
- package/skills/canvas-extension-api/README.md +33 -0
- package/skills/canvas-extension-api/SKILL.md +254 -0
- package/skills/canvas-extension-api/references/acceptance-checklist.md +20 -0
- package/skills/canvas-extension-api/references/backend-safety.md +9 -0
- package/skills/canvas-extension-api/references/connections.md +126 -0
- package/skills/canvas-extension-api/references/packaging-recipes.md +43 -0
- package/skills/canvas-extension-api/references/sidecar-pattern.md +49 -0
- package/skills/canvas-extension-api/references/testing-and-installation.md +286 -0
- package/skills/canvas-extension-api/references/v1-contract.md +250 -0
- package/skills/canvas-extension-api/scripts/validate-extension.mjs +92 -0
- package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +60 -0
- package/skills/code-review/SKILL.md +165 -200
- package/skills/code-review/commands/codereview-roasted.md +1 -1
- package/skills/code-review/commands/codereview.md +1 -1
- package/skills/github/scripts/agent_conversation.py +8 -3
- package/skills/github/scripts/github_client.py +4 -0
- package/skills/github-agents-md-maintainer/scripts/github_client.py +4 -0
- package/skills/github-delivery-watchdog/scripts/github_client.py +4 -0
- package/skills/github-issue-to-pr/scripts/agent_conversation.py +8 -3
- package/skills/github-issue-to-pr/scripts/github_client.py +4 -0
- package/skills/github-issue-triage/.claude-plugin/plugin.json +1 -1
- package/skills/github-issue-triage/.codex-plugin/plugin.json +1 -1
- package/skills/github-issue-triage/.plugin/plugin.json +1 -1
- package/skills/github-issue-triage/README.md +7 -0
- package/skills/github-issue-triage/SKILL.md +26 -3
- package/skills/github-issue-triage/scripts/agent_conversation.py +8 -3
- package/skills/github-issue-triage/scripts/github_client.py +4 -0
- package/skills/github-issue-triage/scripts/worker.py +142 -26
- package/skills/github-pr-reviewer/README.md +21 -10
- package/skills/github-pr-reviewer/SKILL.md +36 -10
- package/skills/github-pr-reviewer/commands/pr-reviewer-setup.md +1 -1
- package/skills/github-pr-reviewer/scripts/agent_conversation.py +8 -3
- package/skills/github-pr-reviewer/scripts/github_client.py +4 -0
- package/skills/github-pr-reviewer/scripts/main.py +63 -16
- package/skills/github-pr-reviewer/scripts/maintainer_handoff.py +151 -0
- package/skills/github-pr-reviewer/scripts/worker.py +218 -21
- package/skills/github-pr-reviewer/tests/test_main.py +85 -0
- package/skills/github-repo-monitor/scripts/github_client.py +4 -0
- package/skills/index.js +16 -8
- package/skills/openhands-sdk/SKILL.md +3 -1
- package/tests/fixtures/automations/github-pr-reviewer.json +120 -26
- package/tests/github_automation_helpers.py +1 -0
- package/tests/test_agent_conversation_dispatch.py +19 -4
- package/tests/test_automation_setup.py +15 -0
- package/tests/test_code_review_risk_evaluation.py +2 -2
- package/tests/test_github_reviewer_delivery.py +448 -5
- package/tests/test_github_triage_delivery.py +200 -6
- package/tests/test_maintainer_handoff.py +198 -0
- package/tests/test_pr_review_prompt.py +6 -2
- package/tests/test_pr_review_review_context.py +144 -0
- package/.github/scripts/check_issue_readiness.py +0 -272
- package/.github/scripts/post-readiness-comment.mjs +0 -184
- package/tests/test_check_issue_readiness.py +0 -135
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
"""Determine whether an issue meets the `ready-for-dev` readiness criteria.
|
|
2
|
-
|
|
3
|
-
The criteria are type-specific and tailored to extension contributions
|
|
4
|
-
(skills, plugins, integrations, and automations):
|
|
5
|
-
|
|
6
|
-
- Bug reports (labeled `bug`): the Actual Behavior section must describe a
|
|
7
|
-
reproducible run and include a supported validation command (`uv`,
|
|
8
|
-
`pytest`, `python`, `pip`, `npm`, or `node`), plus a non-empty Acceptance
|
|
9
|
-
Criteria section with at least one checklist item.
|
|
10
|
-
|
|
11
|
-
- Enhancements (labeled `enhancement`): the body must contain non-empty
|
|
12
|
-
Desired Behavior and Acceptance Criteria sections, the latter with at
|
|
13
|
-
least one checklist item.
|
|
14
|
-
|
|
15
|
-
GitHub issue forms render each field as an `### <Label>` (h3) heading followed
|
|
16
|
-
by the field text, with empty optional fields rendered as `_No response_`. This
|
|
17
|
-
parser splits the body on those headings so each criterion is checked against
|
|
18
|
-
the right field rather than the whole body.
|
|
19
|
-
|
|
20
|
-
Local usage:
|
|
21
|
-
|
|
22
|
-
python .github/scripts/check_issue_readiness.py --body-file /tmp/issue.md \
|
|
23
|
-
--labels bug
|
|
24
|
-
python .github/scripts/check_issue_readiness.py --event-path "$GITHUB_EVENT_PATH"
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
from __future__ import annotations
|
|
28
|
-
|
|
29
|
-
import argparse
|
|
30
|
-
import json
|
|
31
|
-
import os
|
|
32
|
-
import re
|
|
33
|
-
import sys
|
|
34
|
-
from dataclasses import dataclass, field
|
|
35
|
-
from pathlib import Path
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
BUG_LABEL = "bug"
|
|
39
|
-
ENHANCEMENT_LABEL = "enhancement"
|
|
40
|
-
|
|
41
|
-
# Issue-form fields render as `### Label` h3 headings. Match case-insensitively
|
|
42
|
-
# and tolerate trailing whitespace/colons. `^###\s+` is specific enough because
|
|
43
|
-
# h1/h2 are not produced by issue forms.
|
|
44
|
-
HEADING_RE = re.compile(r"(?m)^###\s+(.+?)\s*$")
|
|
45
|
-
|
|
46
|
-
# `_No response_` is what GitHub writes for an empty optional form field.
|
|
47
|
-
NO_RESPONSE = "_No response_"
|
|
48
|
-
|
|
49
|
-
# A reproducible command must appear in the Actual Behavior section. These are
|
|
50
|
-
# the commands a contributor can run in this repository: the Python test
|
|
51
|
-
# tooling (`uv run pytest`, `pytest`, `python`, `pip`) and the catalog build /
|
|
52
|
-
# validation tooling (`npm run build:*`, `node`).
|
|
53
|
-
RUN_METHOD_PATTERNS: tuple[re.Pattern[str], ...] = (
|
|
54
|
-
re.compile(r"\bpython\b", re.IGNORECASE),
|
|
55
|
-
re.compile(r"\bpytest\b", re.IGNORECASE),
|
|
56
|
-
re.compile(r"\buv\b", re.IGNORECASE),
|
|
57
|
-
re.compile(r"\bpip\b", re.IGNORECASE),
|
|
58
|
-
re.compile(r"\bnpm\b", re.IGNORECASE),
|
|
59
|
-
re.compile(r"\bnode\b", re.IGNORECASE),
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
# An Acceptance Criteria item is a markdown checklist bullet (`- [ ]` or
|
|
63
|
-
# `- [x]`). We require at least one so the section is verifiable.
|
|
64
|
-
CHECKLIST_ITEM_RE = re.compile(r"(?m)^\s*[-*]\s*\[[ xX]\]")
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@dataclass
|
|
68
|
-
class ReadinessResult:
|
|
69
|
-
"""Outcome of a readiness check."""
|
|
70
|
-
|
|
71
|
-
ready: bool
|
|
72
|
-
reasons: list[str] = field(default_factory=list)
|
|
73
|
-
|
|
74
|
-
def add(self, reason: str) -> None:
|
|
75
|
-
self.reasons.append(reason)
|
|
76
|
-
self.ready = False
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def visible_text(text: str) -> str:
|
|
80
|
-
"""Return field text with HTML comments stripped and emptiness normalized."""
|
|
81
|
-
cleaned = re.sub(r"<!--[\s\S]*?-->", "", text).strip()
|
|
82
|
-
if cleaned == NO_RESPONSE:
|
|
83
|
-
return ""
|
|
84
|
-
return cleaned
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def extract_sections(body: str) -> dict[str, str]:
|
|
88
|
-
"""Split the body into a {heading: text} map using `### <heading>` boundaries.
|
|
89
|
-
|
|
90
|
-
Issue forms render every field this way. Free-form issues (not created via a
|
|
91
|
-
form) may still use `###` headings; if they don't, the map is empty and the
|
|
92
|
-
caller falls back to whole-body checks.
|
|
93
|
-
"""
|
|
94
|
-
matches = list(HEADING_RE.finditer(body))
|
|
95
|
-
sections: dict[str, str] = {}
|
|
96
|
-
for index, match in enumerate(matches):
|
|
97
|
-
start = match.end()
|
|
98
|
-
end = matches[index + 1].start() if index + 1 < len(matches) else len(body)
|
|
99
|
-
sections[match.group(1).strip().lower()] = body[start:end]
|
|
100
|
-
return sections
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def find_section(sections: dict[str, str], *labels: str) -> str:
|
|
104
|
-
"""Return the first matching section text by case-insensitive label."""
|
|
105
|
-
for label in labels:
|
|
106
|
-
if label in sections:
|
|
107
|
-
return sections[label]
|
|
108
|
-
return ""
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def references_run_method(text: str) -> bool:
|
|
112
|
-
return any(pattern.search(text) for pattern in RUN_METHOD_PATTERNS)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def has_checklist_item(text: str) -> bool:
|
|
116
|
-
return bool(CHECKLIST_ITEM_RE.search(text))
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def check_bug(sections: dict[str, str]) -> ReadinessResult:
|
|
120
|
-
result = ReadinessResult(ready=True)
|
|
121
|
-
|
|
122
|
-
actual = visible_text(find_section(sections, "actual behavior", "actual"))
|
|
123
|
-
if not actual:
|
|
124
|
-
result.add(
|
|
125
|
-
"Fill in the `### Actual Behavior` section with reproducible steps "
|
|
126
|
-
"and the observed result."
|
|
127
|
-
)
|
|
128
|
-
elif not references_run_method(actual):
|
|
129
|
-
result.add(
|
|
130
|
-
"The Actual Behavior section must include a reproducible command "
|
|
131
|
-
"such as `uv run pytest`, `pytest`, `python`, `pip`, `npm run`, "
|
|
132
|
-
"or `node`."
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
acceptance = visible_text(
|
|
136
|
-
find_section(sections, "acceptance criteria", "acceptance")
|
|
137
|
-
)
|
|
138
|
-
if not acceptance:
|
|
139
|
-
result.add(
|
|
140
|
-
"Add an `### Acceptance Criteria` section with testable checklist items."
|
|
141
|
-
)
|
|
142
|
-
elif not has_checklist_item(acceptance):
|
|
143
|
-
result.add(
|
|
144
|
-
"The Acceptance Criteria section must contain at least one checklist item "
|
|
145
|
-
"(`- [ ] ...`)."
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
return result
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def check_enhancement(sections: dict[str, str]) -> ReadinessResult:
|
|
152
|
-
result = ReadinessResult(ready=True)
|
|
153
|
-
|
|
154
|
-
desired = visible_text(find_section(sections, "desired behavior", "desired"))
|
|
155
|
-
if not desired:
|
|
156
|
-
result.add(
|
|
157
|
-
"Add a `### Desired Behavior` section describing the behavior you want."
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
acceptance = visible_text(
|
|
161
|
-
find_section(sections, "acceptance criteria", "acceptance")
|
|
162
|
-
)
|
|
163
|
-
if not acceptance:
|
|
164
|
-
result.add(
|
|
165
|
-
"Add an `### Acceptance Criteria` section with testable checklist items."
|
|
166
|
-
)
|
|
167
|
-
elif not has_checklist_item(acceptance):
|
|
168
|
-
result.add(
|
|
169
|
-
"The Acceptance Criteria section must contain at least one checklist item "
|
|
170
|
-
"(`- [ ] ...`)."
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
return result
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def evaluate_readiness(body: str, labels: list[str]) -> ReadinessResult:
|
|
177
|
-
"""Return the readiness result for an issue body + label set.
|
|
178
|
-
|
|
179
|
-
An issue is only a candidate when it carries the `bug` or `enhancement`
|
|
180
|
-
label. If it has neither, it is treated as not-ready-for-dev (the gate does
|
|
181
|
-
not apply a label it cannot validate).
|
|
182
|
-
"""
|
|
183
|
-
label_set = {label.lower() for label in labels}
|
|
184
|
-
sections = extract_sections(body or "")
|
|
185
|
-
|
|
186
|
-
if BUG_LABEL in label_set:
|
|
187
|
-
return check_bug(sections)
|
|
188
|
-
if ENHANCEMENT_LABEL in label_set:
|
|
189
|
-
return check_enhancement(sections)
|
|
190
|
-
|
|
191
|
-
return ReadinessResult(
|
|
192
|
-
ready=False,
|
|
193
|
-
reasons=[
|
|
194
|
-
"The issue has neither the `bug` nor `enhancement` label, so its "
|
|
195
|
-
"readiness criteria cannot be evaluated. Add the appropriate label."
|
|
196
|
-
],
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def body_and_labels_from_event(event_path: Path) -> tuple[str, list[str]]:
|
|
201
|
-
payload = json.loads(event_path.read_text())
|
|
202
|
-
issue = payload.get("issue") or payload.get("pull_request")
|
|
203
|
-
if not isinstance(issue, dict):
|
|
204
|
-
raise ValueError("GitHub event payload does not contain an issue object")
|
|
205
|
-
body = issue.get("body")
|
|
206
|
-
body = body if isinstance(body, str) else ""
|
|
207
|
-
labels = [
|
|
208
|
-
label["name"] for label in issue.get("labels", []) if isinstance(label, dict)
|
|
209
|
-
]
|
|
210
|
-
return body, labels
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def parse_args() -> argparse.Namespace:
|
|
214
|
-
parser = argparse.ArgumentParser(
|
|
215
|
-
description="Evaluate whether an issue meets the ready-for-dev criteria."
|
|
216
|
-
)
|
|
217
|
-
parser.add_argument(
|
|
218
|
-
"--body-file", type=Path, help="Read the issue body from a file."
|
|
219
|
-
)
|
|
220
|
-
parser.add_argument(
|
|
221
|
-
"--labels",
|
|
222
|
-
help="Comma-separated issue labels (e.g. 'bug,skills').",
|
|
223
|
-
default="",
|
|
224
|
-
)
|
|
225
|
-
parser.add_argument(
|
|
226
|
-
"--event-path",
|
|
227
|
-
type=Path,
|
|
228
|
-
default=Path(os.environ["GITHUB_EVENT_PATH"])
|
|
229
|
-
if "GITHUB_EVENT_PATH" in os.environ
|
|
230
|
-
else None,
|
|
231
|
-
help="Read body and labels from a GitHub event payload.",
|
|
232
|
-
)
|
|
233
|
-
parser.add_argument(
|
|
234
|
-
"--json",
|
|
235
|
-
action="store_true",
|
|
236
|
-
help="Emit a JSON result instead of human-readable text.",
|
|
237
|
-
)
|
|
238
|
-
return parser.parse_args()
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def main() -> int:
|
|
242
|
-
args = parse_args()
|
|
243
|
-
|
|
244
|
-
if args.body_file is not None:
|
|
245
|
-
body = args.body_file.read_text()
|
|
246
|
-
labels = [label.strip() for label in args.labels.split(",") if label.strip()]
|
|
247
|
-
elif args.event_path is not None:
|
|
248
|
-
body, labels = body_and_labels_from_event(args.event_path)
|
|
249
|
-
else:
|
|
250
|
-
raise SystemExit("Pass --body-file or set GITHUB_EVENT_PATH.")
|
|
251
|
-
|
|
252
|
-
result = evaluate_readiness(body, labels)
|
|
253
|
-
|
|
254
|
-
if args.json:
|
|
255
|
-
print(json.dumps({"ready": result.ready, "reasons": result.reasons}))
|
|
256
|
-
# In --json mode the exit code is not meaningful: the result is consumed
|
|
257
|
-
# via the printed JSON, and the workflow must run to completion for both
|
|
258
|
-
# ready and not-ready issues (label add/remove, feedback comment).
|
|
259
|
-
return 0
|
|
260
|
-
|
|
261
|
-
if result.ready:
|
|
262
|
-
print("Issue meets ready-for-dev criteria.")
|
|
263
|
-
else:
|
|
264
|
-
print("Issue does not meet ready-for-dev criteria:")
|
|
265
|
-
for reason in result.reasons:
|
|
266
|
-
print(f" - {reason}")
|
|
267
|
-
|
|
268
|
-
return 0 if result.ready else 1
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if __name__ == "__main__":
|
|
272
|
-
sys.exit(main())
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Post (or update) a `ready-for-dev` readiness comment on an issue.
|
|
5
|
-
*
|
|
6
|
-
* Upserts by a hidden HTML marker so repeated runs update the same comment
|
|
7
|
-
* instead of spamming duplicates. Uses the GITHUB_TOKEN available to the
|
|
8
|
-
* workflow via the GH_TOKEN env var.
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* node post-readiness-comment.mjs \
|
|
12
|
-
* --issue-number 123 \
|
|
13
|
-
* --repo OpenHands/extensions \
|
|
14
|
-
* --reasons-file /tmp/reasons.txt \
|
|
15
|
-
* --ready # optional: post a "ready" message instead of "not ready"
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { readFileSync } from "node:fs";
|
|
19
|
-
|
|
20
|
-
const MARKER = "<!-- issue-readiness-check -->";
|
|
21
|
-
const API_ROOT = process.env.GITHUB_API_URL ?? "https://api.github.com";
|
|
22
|
-
|
|
23
|
-
function parseArgs(argv) {
|
|
24
|
-
const args = {};
|
|
25
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
26
|
-
const arg = argv[i];
|
|
27
|
-
if (!arg.startsWith("--")) continue;
|
|
28
|
-
const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
|
|
29
|
-
const key = rawKey.replaceAll("-", "_");
|
|
30
|
-
if (inlineValue !== undefined) {
|
|
31
|
-
args[key] = inlineValue;
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
const next = argv[i + 1];
|
|
35
|
-
if (next && !next.startsWith("--")) {
|
|
36
|
-
args[key] = next;
|
|
37
|
-
i += 1;
|
|
38
|
-
} else {
|
|
39
|
-
args[key] = "";
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return args;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function requireValue(name, value) {
|
|
46
|
-
if (!value) throw new Error(`Missing required value: ${name}`);
|
|
47
|
-
return value;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function listComments(repo, issueNumber, token) {
|
|
51
|
-
// Paginate through all comments so the marker is found even on issues
|
|
52
|
-
// with >100 comments.
|
|
53
|
-
const all = [];
|
|
54
|
-
let page = 1;
|
|
55
|
-
while (true) {
|
|
56
|
-
const url = `${API_ROOT}/repos/${repo}/issues/${issueNumber}/comments?per_page=100&page=${page}`;
|
|
57
|
-
const resp = await fetch(url, {
|
|
58
|
-
headers: {
|
|
59
|
-
Authorization: `Bearer ${token}`,
|
|
60
|
-
Accept: "application/vnd.github+json",
|
|
61
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
if (!resp.ok) {
|
|
65
|
-
throw new Error(`Failed to list comments: ${resp.status} ${await resp.text()}`);
|
|
66
|
-
}
|
|
67
|
-
const batch = await resp.json();
|
|
68
|
-
all.push(...batch);
|
|
69
|
-
if (batch.length < 100) break;
|
|
70
|
-
page += 1;
|
|
71
|
-
}
|
|
72
|
-
return all;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function createComment(repo, issueNumber, body, token) {
|
|
76
|
-
const url = `${API_ROOT}/repos/${repo}/issues/${issueNumber}/comments`;
|
|
77
|
-
const resp = await fetch(url, {
|
|
78
|
-
method: "POST",
|
|
79
|
-
headers: {
|
|
80
|
-
Authorization: `Bearer ${token}`,
|
|
81
|
-
Accept: "application/vnd.github+json",
|
|
82
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
83
|
-
"Content-Type": "application/json",
|
|
84
|
-
},
|
|
85
|
-
body: JSON.stringify({ body }),
|
|
86
|
-
});
|
|
87
|
-
if (!resp.ok) {
|
|
88
|
-
throw new Error(`Failed to create comment: ${resp.status} ${await resp.text()}`);
|
|
89
|
-
}
|
|
90
|
-
return resp.json();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function updateComment(repo, commentId, body, token) {
|
|
94
|
-
const url = `${API_ROOT}/repos/${repo}/issues/comments/${commentId}`;
|
|
95
|
-
const resp = await fetch(url, {
|
|
96
|
-
method: "PATCH",
|
|
97
|
-
headers: {
|
|
98
|
-
Authorization: `Bearer ${token}`,
|
|
99
|
-
Accept: "application/vnd.github+json",
|
|
100
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
101
|
-
"Content-Type": "application/json",
|
|
102
|
-
},
|
|
103
|
-
body: JSON.stringify({ body }),
|
|
104
|
-
});
|
|
105
|
-
if (!resp.ok) {
|
|
106
|
-
throw new Error(`Failed to update comment: ${resp.status} ${await resp.text()}`);
|
|
107
|
-
}
|
|
108
|
-
return resp.json();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function main() {
|
|
112
|
-
const args = parseArgs(process.argv.slice(2));
|
|
113
|
-
const issueNumber = requireValue("issue-number", args.issue_number);
|
|
114
|
-
const repo = requireValue("repo", args.repo);
|
|
115
|
-
const token = process.env.GH_TOKEN;
|
|
116
|
-
if (!token) throw new Error("GH_TOKEN env var is required");
|
|
117
|
-
|
|
118
|
-
const isReady = args.ready === "" || args.ready === "true";
|
|
119
|
-
const reasonsFile = args.reasons_file || args.reasons;
|
|
120
|
-
const reasons = reasonsFile
|
|
121
|
-
? readFileSync(reasonsFile, "utf8")
|
|
122
|
-
.split("\n")
|
|
123
|
-
.map((line) => line.trim())
|
|
124
|
-
.filter(Boolean)
|
|
125
|
-
: [];
|
|
126
|
-
|
|
127
|
-
const reasonList = reasons.length
|
|
128
|
-
? reasons.map((r) => `- ${r}`).join("\n")
|
|
129
|
-
: "- The issue does not meet the ready-for-dev criteria.";
|
|
130
|
-
|
|
131
|
-
const body = isReady
|
|
132
|
-
? [
|
|
133
|
-
MARKER,
|
|
134
|
-
"### \u2705 `ready-for-dev` label applied",
|
|
135
|
-
"",
|
|
136
|
-
"This issue meets the readiness criteria and is ready for contribution.",
|
|
137
|
-
"A contributor can now open a PR that links this issue (`Fixes #<number>`).",
|
|
138
|
-
"",
|
|
139
|
-
"_This is an automated readiness check posted by a workflow._",
|
|
140
|
-
].join("\n")
|
|
141
|
-
: [
|
|
142
|
-
MARKER,
|
|
143
|
-
"### \u26a0\ufe0f Not yet `ready-for-dev`",
|
|
144
|
-
"",
|
|
145
|
-
"This issue does not yet meet the readiness criteria, so the",
|
|
146
|
-
"`ready-for-dev` label has not been applied (or has been removed).",
|
|
147
|
-
"The criteria are type-specific:",
|
|
148
|
-
"",
|
|
149
|
-
"**Bug reports** (`bug` label):",
|
|
150
|
-
"- The `### Actual Behavior` section must describe reproducible steps",
|
|
151
|
-
" and include a command such as `uv run pytest`, `pytest`, `python`,",
|
|
152
|
-
" `pip`, `npm run`, or `node`.",
|
|
153
|
-
"- An `### Acceptance Criteria` section with at least one checklist item (`- [ ] ...`).",
|
|
154
|
-
"",
|
|
155
|
-
"**Enhancements** (`enhancement` label):",
|
|
156
|
-
"- A `### Desired Behavior` section.",
|
|
157
|
-
"- An `### Acceptance Criteria` section with at least one checklist item.",
|
|
158
|
-
"",
|
|
159
|
-
"What is missing:",
|
|
160
|
-
"",
|
|
161
|
-
reasonList,
|
|
162
|
-
"",
|
|
163
|
-
"Edit the issue to address the gaps and the `ready-for-dev` label will be",
|
|
164
|
-
"applied automatically.",
|
|
165
|
-
"",
|
|
166
|
-
"_This is an automated readiness check posted by a workflow._",
|
|
167
|
-
].join("\n");
|
|
168
|
-
|
|
169
|
-
const comments = await listComments(repo, issueNumber, token);
|
|
170
|
-
const existing = comments.find((comment) => comment.body?.includes(MARKER));
|
|
171
|
-
|
|
172
|
-
if (existing) {
|
|
173
|
-
await updateComment(repo, existing.id, body, token);
|
|
174
|
-
console.log(`Updated readiness comment ${existing.id} on issue #${issueNumber}`);
|
|
175
|
-
} else {
|
|
176
|
-
const created = await createComment(repo, issueNumber, body, token);
|
|
177
|
-
console.log(`Created readiness comment ${created.id} on issue #${issueNumber}`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
main().catch((error) => {
|
|
182
|
-
console.error(error);
|
|
183
|
-
process.exit(1);
|
|
184
|
-
});
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import importlib.util
|
|
4
|
-
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def _load_prod_module():
|
|
9
|
-
repo_root = Path(__file__).resolve().parents[1]
|
|
10
|
-
script_path = repo_root / ".github" / "scripts" / "check_issue_readiness.py"
|
|
11
|
-
name = "check_issue_readiness"
|
|
12
|
-
spec = importlib.util.spec_from_file_location(name, script_path)
|
|
13
|
-
assert spec and spec.loader
|
|
14
|
-
mod = importlib.util.module_from_spec(spec)
|
|
15
|
-
sys.modules[name] = mod
|
|
16
|
-
spec.loader.exec_module(mod)
|
|
17
|
-
return mod
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
_prod = _load_prod_module()
|
|
21
|
-
evaluate_readiness = _prod.evaluate_readiness
|
|
22
|
-
extract_sections = _prod.extract_sections
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
ENHANCEMENT_READY = """### Problem or Use Case
|
|
26
|
-
|
|
27
|
-
The integration catalog lacks a Linear entry.
|
|
28
|
-
|
|
29
|
-
### Desired Behavior
|
|
30
|
-
|
|
31
|
-
`integrations/catalog/linear.json` describes the Linear MCP server so the
|
|
32
|
-
catalog build picks it up.
|
|
33
|
-
|
|
34
|
-
### Acceptance Criteria
|
|
35
|
-
|
|
36
|
-
- [ ] `npm run build:integrations` succeeds with the new entry
|
|
37
|
-
- [ ] `getIntegrationCatalogEntry("linear")` returns the Linear catalog model
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
BUG_READY = """### Actual Behavior
|
|
41
|
-
|
|
42
|
-
Running `uv run --group test pytest tests/` fails in
|
|
43
|
-
`test_skills_catalog.py` with a KeyError when a skill is missing from the
|
|
44
|
-
marketplace.
|
|
45
|
-
|
|
46
|
-
### Acceptance Criteria
|
|
47
|
-
|
|
48
|
-
- [ ] `uv run --group test pytest tests/` passes with a clear error message
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def test_extract_sections_splits_on_headings():
|
|
53
|
-
sections = extract_sections("### Alpha\n\ntext\n\n### Beta\n\nmore\n")
|
|
54
|
-
assert sections["alpha"] == "\ntext\n\n"
|
|
55
|
-
assert sections["beta"] == "\nmore\n"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def test_enhancement_ready_passes():
|
|
59
|
-
result = evaluate_readiness(ENHANCEMENT_READY, ["enhancement"])
|
|
60
|
-
assert result.ready is True
|
|
61
|
-
assert result.reasons == []
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def test_enhancement_missing_acceptance_criteria_fails():
|
|
65
|
-
body = "### Desired Behavior\n\nSome desired change.\n"
|
|
66
|
-
result = evaluate_readiness(body, ["enhancement"])
|
|
67
|
-
assert result.ready is False
|
|
68
|
-
assert any("Acceptance Criteria" in r for r in result.reasons)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def test_enhancement_missing_desired_behavior_fails():
|
|
72
|
-
body = ENHANCEMENT_READY.replace(
|
|
73
|
-
"### Desired Behavior\n\n"
|
|
74
|
-
"`integrations/catalog/linear.json` describes the Linear MCP server so the\n"
|
|
75
|
-
"catalog build picks it up.\n\n",
|
|
76
|
-
"",
|
|
77
|
-
)
|
|
78
|
-
result = evaluate_readiness(body, ["enhancement"])
|
|
79
|
-
assert result.ready is False
|
|
80
|
-
assert any("Desired Behavior" in r for r in result.reasons)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def test_bug_ready_passes():
|
|
84
|
-
result = evaluate_readiness(BUG_READY, ["bug"])
|
|
85
|
-
assert result.ready is True
|
|
86
|
-
assert result.reasons == []
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def test_bug_missing_run_method_fails():
|
|
90
|
-
body = BUG_READY.replace(
|
|
91
|
-
"Running `uv run --group test pytest tests/` fails",
|
|
92
|
-
"Running the test suite fails",
|
|
93
|
-
)
|
|
94
|
-
result = evaluate_readiness(body, ["bug"])
|
|
95
|
-
assert result.ready is False
|
|
96
|
-
assert any("reproducible command" in r for r in result.reasons)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def test_bug_npm_build_command_is_a_valid_run_method():
|
|
100
|
-
body = BUG_READY.replace(
|
|
101
|
-
"Running `uv run --group test pytest tests/` fails in\n"
|
|
102
|
-
"`test_skills_catalog.py` with a KeyError when a skill is missing from the\n"
|
|
103
|
-
"marketplace.",
|
|
104
|
-
"Running `npm run build:skills` fails with a schema error.",
|
|
105
|
-
)
|
|
106
|
-
result = evaluate_readiness(body, ["bug"])
|
|
107
|
-
assert result.ready is True
|
|
108
|
-
assert result.reasons == []
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def test_bug_acceptance_needs_checklist_item():
|
|
112
|
-
body = BUG_READY.replace(
|
|
113
|
-
"- [ ] `uv run --group test pytest tests/` passes with a clear error message",
|
|
114
|
-
"Make the test error message clearer",
|
|
115
|
-
)
|
|
116
|
-
result = evaluate_readiness(body, ["bug"])
|
|
117
|
-
assert result.ready is False
|
|
118
|
-
assert any("checklist item" in r for r in result.reasons)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def test_no_response_field_counts_as_empty():
|
|
122
|
-
body = BUG_READY.replace(
|
|
123
|
-
"### Acceptance Criteria\n\n- [ ] `uv run --group test pytest tests/` "
|
|
124
|
-
"passes with a clear error message\n",
|
|
125
|
-
"### Acceptance Criteria\n\n_No response_\n",
|
|
126
|
-
)
|
|
127
|
-
result = evaluate_readiness(body, ["bug"])
|
|
128
|
-
assert result.ready is False
|
|
129
|
-
assert any("Acceptance Criteria" in r for r in result.reasons)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def test_no_bug_or_enhancement_label_not_ready():
|
|
133
|
-
result = evaluate_readiness(ENHANCEMENT_READY, [])
|
|
134
|
-
assert result.ready is False
|
|
135
|
-
assert any("bug" in r and "enhancement" in r for r in result.reasons)
|