@openhands/extensions 0.18.0 → 0.19.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 +7 -0
- package/.github/dependabot.yml +24 -0
- package/.github/scripts/check_issue_readiness.py +272 -0
- package/.github/scripts/check_pr_description.py +226 -0
- package/.github/scripts/post-readiness-comment.mjs +184 -0
- package/.github/scripts/refresh_linked_pr_checks.py +171 -0
- package/.github/workflows/issue-readiness-check.yml +142 -0
- package/.github/workflows/pr-description-check.yml +37 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +16 -0
- package/README.md +10 -2
- package/automations/bundle-index.js +1 -1
- package/automations/catalog/github-agents-md-maintainer/manifest.json +5 -0
- package/automations/catalog/github-issue-to-pr/manifest.json +5 -0
- package/automations/catalog/github-pr-reviewer/manifest.json +5 -0
- package/automations/catalog/github-repo-monitor/manifest.json +5 -0
- package/automations/catalog/jira-issue-to-bitbucket-pr/manifest.json +23 -0
- package/automations/catalog/jira-issue-to-gitlab-mr/manifest.json +23 -0
- package/automations/catalog/linear-issue-to-bitbucket-pr/manifest.json +23 -0
- package/automations/catalog/linear-issue-to-github-pr/manifest.json +23 -0
- package/automations/catalog/linear-issue-to-gitlab-mr/manifest.json +23 -0
- package/automations/catalog/news-digest/manifest.json +5 -0
- package/automations/catalog/qa-changes/manifest.json +72 -0
- package/automations/catalog/slack-github-linear-daily-organization.json +27 -0
- package/automations/catalog-index.js +18 -6
- package/automations/catalog.schema.json +25 -0
- package/automations/index.d.ts +16 -0
- package/integrations/catalog/datadog.json +2 -2
- package/integrations/catalog/gitlab.json +41 -0
- package/integrations/catalog/notion.json +1 -1
- package/integrations/catalog-index.js +71 -69
- package/integrations/index.d.ts +1 -1
- package/marketplaces/openhands-extensions.json +21 -6
- package/package.json +1 -1
- package/plugins/pr-review/scripts/prompt.py +2 -0
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/skills/github-pr-reviewer/SKILL.md +2 -1
- package/skills/github-pr-reviewer/scripts/main.py +45 -4
- package/skills/github-pr-reviewer/tests/test_main.py +74 -0
- package/skills/index.js +11 -2
- package/skills/openhands-sdk/SKILL.md +1 -0
- package/skills/ticket-to-code-change/.plugin/plugin.json +21 -0
- package/skills/ticket-to-code-change/README.md +5 -0
- package/skills/ticket-to-code-change/SKILL.md +50 -0
- package/skills/ticket-to-code-change/commands/ticket-to-code-change-setup.md +8 -0
- package/tests/test_automation_setup.py +22 -0
- package/tests/test_catalogs.py +2 -2
- package/tests/test_check_issue_readiness.py +135 -0
- package/tests/test_check_pr_description.py +228 -0
- package/tests/test_pr_review_prompt.py +10 -0
- package/tests/test_refresh_linked_pr_checks.py +130 -0
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Re-run the PR Description Check for open PRs linked to an issue.
|
|
2
|
+
|
|
3
|
+
Invoked from the issue-readiness workflow when the `ready-for-dev` label is
|
|
4
|
+
added to or removed from an issue, so the PR gate reflects the settled
|
|
5
|
+
readiness state without waiting for the next PR event.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from check_pr_description import extract_linked_issue_numbers
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _run(args: list[str]) -> subprocess.CompletedProcess[str]:
|
|
20
|
+
return subprocess.run(args, capture_output=True, text=True, check=False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _linked_open_prs(repo: str, issue_number: int) -> list[dict]:
|
|
24
|
+
"""Return open PRs that cross-reference ``issue_number``."""
|
|
25
|
+
owner, name = repo.split("/", 1)
|
|
26
|
+
query = """
|
|
27
|
+
query($owner: String!, $name: String!, $num: Int!) {
|
|
28
|
+
repository(owner: $owner, name: $name) {
|
|
29
|
+
issue(number: $num) {
|
|
30
|
+
timelineItems(first: 100, itemTypes: [CROSS_REFERENCED_EVENT]) {
|
|
31
|
+
nodes {
|
|
32
|
+
__typename
|
|
33
|
+
... on CrossReferencedEvent {
|
|
34
|
+
source {
|
|
35
|
+
__typename
|
|
36
|
+
... on PullRequest { number headRefOid state }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
"""
|
|
45
|
+
result = _run(
|
|
46
|
+
[
|
|
47
|
+
"gh",
|
|
48
|
+
"api",
|
|
49
|
+
"graphql",
|
|
50
|
+
"-f",
|
|
51
|
+
f"query={query}",
|
|
52
|
+
"-F",
|
|
53
|
+
f"owner={owner}",
|
|
54
|
+
"-F",
|
|
55
|
+
f"name={name}",
|
|
56
|
+
"-F",
|
|
57
|
+
f"num={issue_number}",
|
|
58
|
+
"--jq",
|
|
59
|
+
".data.repository.issue.timelineItems.nodes",
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
if result.returncode != 0:
|
|
63
|
+
print(f"::warning::Could not query linked PRs: {result.stderr.strip()}")
|
|
64
|
+
return []
|
|
65
|
+
try:
|
|
66
|
+
nodes = json.loads(result.stdout)
|
|
67
|
+
except json.JSONDecodeError as exc:
|
|
68
|
+
print(f"::warning::Unexpected linked-PR query output: {exc}")
|
|
69
|
+
return []
|
|
70
|
+
prs = []
|
|
71
|
+
for node in nodes:
|
|
72
|
+
source = node.get("source") if isinstance(node, dict) else None
|
|
73
|
+
if not isinstance(source, dict):
|
|
74
|
+
continue
|
|
75
|
+
if source.get("__typename") != "PullRequest":
|
|
76
|
+
continue
|
|
77
|
+
if source.get("state") != "OPEN":
|
|
78
|
+
continue
|
|
79
|
+
prs.append(source)
|
|
80
|
+
return prs
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _rerun_pr_description_check(repo: str, head_sha: str) -> bool:
|
|
84
|
+
"""Re-run the latest PR Description Check for the given head SHA."""
|
|
85
|
+
runs_result = _run(
|
|
86
|
+
[
|
|
87
|
+
"gh",
|
|
88
|
+
"api",
|
|
89
|
+
"-X",
|
|
90
|
+
"GET",
|
|
91
|
+
f"repos/{repo}/actions/runs",
|
|
92
|
+
"-f",
|
|
93
|
+
f"head_sha={head_sha}",
|
|
94
|
+
"-f",
|
|
95
|
+
"per_page=100",
|
|
96
|
+
"--jq",
|
|
97
|
+
r'.workflow_runs[] | select(.name=="PR Description Check") | '
|
|
98
|
+
r'select(.event=="pull_request_target") | "\(.id) \(.created_at)"',
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
if runs_result.returncode != 0:
|
|
102
|
+
print(f"::warning::Could not list PR checks: {runs_result.stderr.strip()}")
|
|
103
|
+
return False
|
|
104
|
+
lines = [line.split() for line in runs_result.stdout.splitlines() if line.strip()]
|
|
105
|
+
if not lines:
|
|
106
|
+
return False
|
|
107
|
+
# Created-at is sortable; pick the most recent run for this commit.
|
|
108
|
+
latest = sorted(lines, key=lambda item: " ".join(item[1:]))[-1][0]
|
|
109
|
+
rerun = _run(
|
|
110
|
+
["gh", "api", "-X", "POST", f"repos/{repo}/actions/runs/{latest}/rerun"]
|
|
111
|
+
)
|
|
112
|
+
if rerun.returncode != 0:
|
|
113
|
+
print(
|
|
114
|
+
f"::warning::Could not re-run PR description check ({latest}): "
|
|
115
|
+
f"{rerun.stderr.strip()}"
|
|
116
|
+
)
|
|
117
|
+
return False
|
|
118
|
+
print(f"Re-ran PR description check (run {latest}) for linked open PR.")
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def main() -> int:
|
|
123
|
+
payload = json.loads(Path(os.environ["GITHUB_EVENT_PATH"]).read_text())
|
|
124
|
+
if {"action", "issue", "label", "repository"} - set(payload):
|
|
125
|
+
return 0
|
|
126
|
+
action = payload["action"]
|
|
127
|
+
raw_label = payload.get("label")
|
|
128
|
+
label = raw_label.get("name") if isinstance(raw_label, dict) else None
|
|
129
|
+
if action not in ("labeled", "unlabeled") or label != "ready-for-dev":
|
|
130
|
+
return 0
|
|
131
|
+
raw_repo = payload.get("repository")
|
|
132
|
+
repo = raw_repo.get("full_name") if isinstance(raw_repo, dict) else None
|
|
133
|
+
raw_issue = payload.get("issue")
|
|
134
|
+
issue_number = raw_issue.get("number") if isinstance(raw_issue, dict) else None
|
|
135
|
+
if not repo or not issue_number:
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
print(
|
|
139
|
+
f"ready-for-dev {action}: refreshing PR gates linked to issue #{issue_number}."
|
|
140
|
+
)
|
|
141
|
+
refreshed = 0
|
|
142
|
+
for pr in _linked_open_prs(repo, issue_number):
|
|
143
|
+
pr_number = pr["number"]
|
|
144
|
+
body_result = _run(
|
|
145
|
+
[
|
|
146
|
+
"gh",
|
|
147
|
+
"pr",
|
|
148
|
+
"view",
|
|
149
|
+
str(pr_number),
|
|
150
|
+
"--repo",
|
|
151
|
+
repo,
|
|
152
|
+
"--json",
|
|
153
|
+
"body",
|
|
154
|
+
"--jq",
|
|
155
|
+
".body",
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
if body_result.returncode != 0:
|
|
159
|
+
continue
|
|
160
|
+
if issue_number not in extract_linked_issue_numbers(body_result.stdout):
|
|
161
|
+
# Cross-referenced but not treated as a linked issue by the gate;
|
|
162
|
+
# nothing to refresh.
|
|
163
|
+
continue
|
|
164
|
+
if _rerun_pr_description_check(repo, pr["headRefOid"]):
|
|
165
|
+
refreshed += 1
|
|
166
|
+
print(f"Refreshed PR gates for {refreshed} linked PR(s).")
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
sys.exit(main())
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Issue Readiness Check
|
|
3
|
+
|
|
4
|
+
# Manages the `ready-for-dev` label based on type-specific readiness criteria
|
|
5
|
+
# tailored to extension contributions (skills, plugins, integrations,
|
|
6
|
+
# automations).
|
|
7
|
+
#
|
|
8
|
+
# Bug reports (label `bug`): the Actual Behavior section must reference a
|
|
9
|
+
# reproducible command (`uv run pytest`, `pytest`, `python`, `pip`, `npm run`,
|
|
10
|
+
# or `node`), plus an Acceptance Criteria section with at least one checklist
|
|
11
|
+
# item.
|
|
12
|
+
#
|
|
13
|
+
# Enhancements (label `enhancement`): the body must contain Desired Behavior and
|
|
14
|
+
# Acceptance Criteria sections, the latter with at least one checklist item.
|
|
15
|
+
#
|
|
16
|
+
# When criteria are met the label is added (idempotently); when they are not the
|
|
17
|
+
# label is removed. A feedback comment is posted (or updated) in three cases:
|
|
18
|
+
# 1. The issue is first opened or reopened - always comment so the author
|
|
19
|
+
# knows whether their issue is ready or what is missing.
|
|
20
|
+
# 2. The label is being added (issue became ready) - comment celebrating it.
|
|
21
|
+
# 3. The label is being removed (issue was ready but is no longer) - comment
|
|
22
|
+
# explaining what changed.
|
|
23
|
+
# Edits that do not change the label state do not produce a new comment. All
|
|
24
|
+
# comments are upserted by a hidden marker so there is at most one per issue.
|
|
25
|
+
#
|
|
26
|
+
# The readiness step uses `--json`, which always exits 0, so a not-ready result
|
|
27
|
+
# never aborts the workflow under `set -euo pipefail`; label and comment steps
|
|
28
|
+
# run for both outcomes.
|
|
29
|
+
|
|
30
|
+
on:
|
|
31
|
+
issues:
|
|
32
|
+
types: [opened, edited, reopened, labeled, unlabeled]
|
|
33
|
+
|
|
34
|
+
permissions:
|
|
35
|
+
issues: write
|
|
36
|
+
pull-requests: read
|
|
37
|
+
contents: read
|
|
38
|
+
|
|
39
|
+
concurrency:
|
|
40
|
+
group: issue-readiness-${{ github.event.issue.number }}
|
|
41
|
+
cancel-in-progress: false
|
|
42
|
+
|
|
43
|
+
jobs:
|
|
44
|
+
check:
|
|
45
|
+
if: github.event.issue.state == 'open' && github.event.issue.pull_request == null
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
timeout-minutes: 5
|
|
48
|
+
steps:
|
|
49
|
+
- name: Checkout trusted workflow scripts
|
|
50
|
+
uses: actions/checkout@v4
|
|
51
|
+
|
|
52
|
+
- name: Evaluate issue readiness
|
|
53
|
+
id: readiness
|
|
54
|
+
env:
|
|
55
|
+
GITHUB_EVENT_PATH: ${{ github.event_path }}
|
|
56
|
+
run: |
|
|
57
|
+
set -euo pipefail
|
|
58
|
+
python .github/scripts/check_issue_readiness.py --json > /tmp/result.json
|
|
59
|
+
echo "ready=$(jq -r '.ready' /tmp/result.json)" >> "$GITHUB_OUTPUT"
|
|
60
|
+
# Write reasons to a file so the comment step can read them
|
|
61
|
+
# without shell-escaping multiline text.
|
|
62
|
+
jq -r '.reasons[]?' /tmp/result.json > /tmp/reasons.txt || true
|
|
63
|
+
echo "reasons_file=/tmp/reasons.txt" >> "$GITHUB_OUTPUT"
|
|
64
|
+
|
|
65
|
+
- name: Add ready-for-dev label
|
|
66
|
+
if: steps.readiness.outputs.ready == 'true'
|
|
67
|
+
env:
|
|
68
|
+
GH_TOKEN: ${{ github.token }}
|
|
69
|
+
run: |
|
|
70
|
+
set -euo pipefail
|
|
71
|
+
# Capture stderr — tolerate "label already present" (exit 0
|
|
72
|
+
# from gh) but surface unexpected errors to the step summary.
|
|
73
|
+
if ! gh issue edit "${{ github.event.issue.number }}" \
|
|
74
|
+
--repo "${{ github.repository }}" \
|
|
75
|
+
--add-label "ready-for-dev" 2>/tmp/gh-err.txt; then
|
|
76
|
+
echo "::warning::Failed to add ready-for-dev label:" >> "$GITHUB_STEP_SUMMARY"
|
|
77
|
+
cat /tmp/gh-err.txt >> "$GITHUB_STEP_SUMMARY"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
- name: Remove ready-for-dev label
|
|
81
|
+
if: steps.readiness.outputs.ready != 'true' && contains(github.event.issue.labels.*.name, 'ready-for-dev')
|
|
82
|
+
env:
|
|
83
|
+
GH_TOKEN: ${{ github.token }}
|
|
84
|
+
run: |
|
|
85
|
+
set -euo pipefail
|
|
86
|
+
# Capture stderr — tolerate "label already removed" but
|
|
87
|
+
# surface unexpected errors.
|
|
88
|
+
if ! gh issue edit "${{ github.event.issue.number }}" \
|
|
89
|
+
--repo "${{ github.repository }}" \
|
|
90
|
+
--remove-label "ready-for-dev" 2>/tmp/gh-err.txt; then
|
|
91
|
+
echo "::warning::Failed to remove ready-for-dev label:" >> "$GITHUB_STEP_SUMMARY"
|
|
92
|
+
cat /tmp/gh-err.txt >> "$GITHUB_STEP_SUMMARY"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
- name: Post feedback comment
|
|
96
|
+
if: >-
|
|
97
|
+
github.event.action == 'opened'
|
|
98
|
+
|| github.event.action == 'reopened'
|
|
99
|
+
|| (steps.readiness.outputs.ready == 'true' && !contains(github.event.issue.labels.*.name, 'ready-for-dev'))
|
|
100
|
+
|| (steps.readiness.outputs.ready != 'true' && contains(github.event.issue.labels.*.name, 'ready-for-dev'))
|
|
101
|
+
env:
|
|
102
|
+
GH_TOKEN: ${{ github.token }}
|
|
103
|
+
run: |
|
|
104
|
+
set -euo pipefail
|
|
105
|
+
if [ "${{ steps.readiness.outputs.ready }}" = "true" ]; then
|
|
106
|
+
node .github/scripts/post-readiness-comment.mjs \
|
|
107
|
+
--issue-number "${{ github.event.issue.number }}" \
|
|
108
|
+
--repo "${{ github.repository }}" \
|
|
109
|
+
--ready
|
|
110
|
+
else
|
|
111
|
+
node .github/scripts/post-readiness-comment.mjs \
|
|
112
|
+
--issue-number "${{ github.event.issue.number }}" \
|
|
113
|
+
--repo "${{ github.repository }}" \
|
|
114
|
+
--reasons-file "${{ steps.readiness.outputs.reasons_file }}"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# When the ready-for-dev label is added or removed, re-run the PR
|
|
118
|
+
# description check for every open PR that links this issue, so the
|
|
119
|
+
# `ready-for-dev` gate does not go stale between PR events. Runs after
|
|
120
|
+
# `check` so the label reflects the settled readiness state.
|
|
121
|
+
refresh-linked-pr-gates:
|
|
122
|
+
name: Refresh PR gates for linked issues
|
|
123
|
+
needs: check
|
|
124
|
+
if: >-
|
|
125
|
+
github.event.issue.state == 'open'
|
|
126
|
+
&& github.event.issue.pull_request == null
|
|
127
|
+
&& (github.event.action == 'labeled' || github.event.action == 'unlabeled')
|
|
128
|
+
&& github.event.label.name == 'ready-for-dev'
|
|
129
|
+
runs-on: ubuntu-latest
|
|
130
|
+
timeout-minutes: 10
|
|
131
|
+
permissions:
|
|
132
|
+
contents: read
|
|
133
|
+
pull-requests: read
|
|
134
|
+
actions: write
|
|
135
|
+
steps:
|
|
136
|
+
- name: Checkout trusted workflow scripts
|
|
137
|
+
uses: actions/checkout@v4
|
|
138
|
+
|
|
139
|
+
- name: Re-run PR description checks for linked open PRs
|
|
140
|
+
env:
|
|
141
|
+
GH_TOKEN: ${{ github.token }}
|
|
142
|
+
run: python .github/scripts/refresh_linked_pr_checks.py
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: PR Description Check
|
|
3
|
+
|
|
4
|
+
# Use pull_request_target so fork PR descriptions can be checked, but only run
|
|
5
|
+
# trusted validation code from the base branch checkout below.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
pull_request_target:
|
|
9
|
+
types: [opened, edited, reopened, ready_for_review, synchronize]
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
pull-requests: read
|
|
14
|
+
issues: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
validate-pr-description:
|
|
18
|
+
name: Validate PR description
|
|
19
|
+
# Draft PRs may still have incomplete descriptions; validate when review starts.
|
|
20
|
+
# Release PRs are generated by the trusted prepare-release workflow without
|
|
21
|
+
# the standard human-authored PR template.
|
|
22
|
+
if: >-
|
|
23
|
+
github.event.pull_request.draft == false &&
|
|
24
|
+
github.event.pull_request.user.login != 'dependabot[bot]' &&
|
|
25
|
+
!(github.event.pull_request.head.repo.full_name == github.repository &&
|
|
26
|
+
startsWith(github.event.pull_request.head.ref, 'release-please-'))
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- name: Checkout trusted workflow scripts
|
|
30
|
+
uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
ref: ${{ github.event.pull_request.base.sha }}
|
|
33
|
+
|
|
34
|
+
- name: Validate PR template sections and linked-issue readiness
|
|
35
|
+
env:
|
|
36
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
37
|
+
run: python .github/scripts/check_pr_description.py
|
package/AGENTS.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
This repository (`OpenHands/extensions`) is the **public extensions registry** for OpenHands.
|
|
4
4
|
It contains **shareable skills and plugins** that can be loaded by OpenHands (CLI/GUI/Cloud) and by client code using the **Software Agent SDK**.
|
|
5
5
|
|
|
6
|
+
|
|
7
|
+
## Cross-Repository Boundaries
|
|
8
|
+
|
|
9
|
+
This repository owns the public registry of reusable OpenHands skills, plugins, automations, and integrations. These extensions are consumed by OpenHands applications and SDK-based clients.
|
|
10
|
+
|
|
11
|
+
Related repositories have distinct responsibilities:
|
|
12
|
+
|
|
13
|
+
- [`OpenHands/software-agent-sdk`](https://github.com/OpenHands/software-agent-sdk) owns the Python SDK, Agent Server, agent/tool behavior, conversations, workspaces, events, and canonical API.
|
|
14
|
+
- [`OpenHands/typescript-client`](https://github.com/OpenHands/typescript-client) owns the browser-compatible typed Agent Server client.
|
|
15
|
+
- [`OpenHands/OpenHands`](https://github.com/OpenHands/OpenHands) owns Agent Canvas UI and local-stack orchestration.
|
|
16
|
+
- [`OpenHands/automation`](https://github.com/OpenHands/automation) owns scheduling, webhooks, run history, dispatch, and sandbox lifecycle orchestration.
|
|
17
|
+
|
|
18
|
+
Put reusable skills, plugins, automations, and integrations here; put backend execution behavior in the SDK, typed API access in `typescript-client`, application UI in Agent Canvas, and scheduling/dispatch lifecycle code in `automation`. If a PR is opened in the wrong repository, explicitly recommend closing and moving it to the owning repository. PRs must follow this repository's applicable contribution and code-review guidance.
|
|
19
|
+
|
|
6
20
|
## What this repo contains
|
|
7
21
|
|
|
8
22
|
- `skills/` — a catalog of skills, **one directory per skill**.
|
|
@@ -110,6 +124,8 @@ When editing or adding skills in this repo, follow these rules (and add new skil
|
|
|
110
124
|
- `scripts/sync_extensions.py` keeps generated artifacts in sync: Claude Code command files, README catalog section, coverage checks, and vendor symlinks. Run `python scripts/sync_extensions.py --check` (or just push — CI runs it) to verify everything is consistent. Run without `--check` to auto-fix. The "Quick Start" section in `README.md` (OpenHands SDK, Claude Code, and Codex setup instructions) is **manually maintained** above the auto-generated catalog markers and is intentionally not generated by the sync script.
|
|
111
125
|
- The sync script uses PyYAML to parse SKILL.md frontmatter. If you add a skill with a slash trigger (e.g., `triggers: ["/mycommand"]`), the script auto-generates `commands/mycommand.md`. **Note:** Slash triggers in SKILL.md frontmatter are deprecated — prefer adding a `commands/command-name.md` file to the plugin's `commands/` directory instead. Keyword triggers (non-slash) remain the recommended way to activate skills by topic.
|
|
112
126
|
|
|
127
|
+
- The `ready-for-dev` gates live in `.github/workflows/issue-readiness-check.yml` and `.github/workflows/pr-description-check.yml`, backed by `.github/scripts/check_issue_readiness.py`, `check_pr_description.py`, `refresh_linked_pr_checks.py`, and `post-readiness-comment.mjs`. Issues labeled `bug` or `enhancement` must meet type-specific readiness criteria (reproducible command plus Acceptance Criteria checklist for bugs; Desired Behavior plus Acceptance Criteria checklist for enhancements) to receive the `ready-for-dev` label. Non-draft PRs must keep the `## Why`, `## Summary`, and `## How to Test` template sections filled, and any issue linked via a closing keyword or the `## Issue Number` section must carry `ready-for-dev` unless it predates the 2026-08-25 rollout. Label transitions on an issue re-run the PR Description Check for linked open PRs. Tests live in `tests/test_check_issue_readiness.py`, `tests/test_check_pr_description.py`, and `tests/test_refresh_linked_pr_checks.py`.
|
|
128
|
+
|
|
113
129
|
## OpenHands SDK documentation policy
|
|
114
130
|
|
|
115
131
|
- **Do NOT add SDK-specific or SDK-related documentation to this repo.** The canonical source of truth for SDK documentation is the [OpenHands docs site](https://docs.openhands.dev/sdk) and its structured index at <https://docs.openhands.dev/llms.txt>.
|
package/README.md
CHANGED
|
@@ -6,6 +6,13 @@ It contains reusable, shareable skills and plugins that customize agent behavior
|
|
|
6
6
|
- Skills overview docs: https://docs.openhands.dev/overview/skills
|
|
7
7
|
- SDK skill guide: https://docs.openhands.dev/sdk/guides/skill
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
## Repository boundaries
|
|
11
|
+
|
|
12
|
+
`OpenHands/extensions` is the public registry for reusable skills, plugins, automations, and integrations. [`OpenHands/software-agent-sdk`](https://github.com/OpenHands/software-agent-sdk) owns Agent Server execution and the canonical API, [`OpenHands/typescript-client`](https://github.com/OpenHands/typescript-client) owns typed browser access to that API, [`OpenHands/OpenHands`](https://github.com/OpenHands/OpenHands) owns Agent Canvas UI, and [`OpenHands/automation`](https://github.com/OpenHands/automation) owns scheduling, webhooks, run history, dispatch, and sandbox lifecycle orchestration.
|
|
13
|
+
|
|
14
|
+
Put reusable extension artifacts here rather than in application repositories. If a PR is opened in the wrong repository, close and move it to the repository that owns the change.
|
|
15
|
+
|
|
9
16
|
## Repository Layout
|
|
10
17
|
|
|
11
18
|
### Skills
|
|
@@ -89,7 +96,7 @@ The JS and Python versions are kept in lock-step by `release-please` and guarded
|
|
|
89
96
|
## Extensions Catalog
|
|
90
97
|
|
|
91
98
|
<!-- BEGIN AUTO-GENERATED CATALOG -->
|
|
92
|
-
This repository contains **2 marketplace(s)** with **
|
|
99
|
+
This repository contains **2 marketplace(s)** with **68 extensions** (58 skills, 10 plugins).
|
|
93
100
|
|
|
94
101
|
### large-codebase
|
|
95
102
|
|
|
@@ -108,7 +115,7 @@ OpenHands skills for interacting, improving, and refactoring large codebases
|
|
|
108
115
|
|
|
109
116
|
Official skills and plugins for OpenHands — the open-source AI software engineer.
|
|
110
117
|
|
|
111
|
-
**
|
|
118
|
+
**64 extensions** (56 skills, 8 plugins)
|
|
112
119
|
|
|
113
120
|
| Name | Type | Description | Commands |
|
|
114
121
|
|------|------|-------------|----------|
|
|
@@ -171,6 +178,7 @@ Official skills and plugins for OpenHands — the open-source AI software engine
|
|
|
171
178
|
| swift-linux | skill | Install and configure Swift programming language on Debian Linux for server-side development. Use when building Swift... | — |
|
|
172
179
|
| technical-writing | skill | Write and revise technical explanations in flowing, direct, conversational prose that stays concise without becoming ... | — |
|
|
173
180
|
| theme-factory | skill | Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc.... | — |
|
|
181
|
+
| ticket-to-code-change | skill | Set up Jira or Linear ticket-to-code-change automations for GitHub, GitLab, and Bitbucket. | `/ticket-to-code-change:setup` |
|
|
174
182
|
| upstream-fork-sync | skill | Keep a long-lived fork in sync with its upstream. Creates a cron automation that fetches upstream changes, rebases lo... | `/upstream-fork-sync:setup` |
|
|
175
183
|
| uv | skill | Common project, dependency, and environment operations using uv. | — |
|
|
176
184
|
| vercel | skill | Deploy and manage applications on Vercel, including preview deployments and deployment protection. | — |
|